Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* (C) Copyright IBM Corp. 2021, 2022
*
* SPDX-License-Identifier: Apache-2.0
*/
package com.ibm.fhir.persistence.jdbc.domain;
import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_SEARCH_ENABLE_LEGACY_WHOLE_SYSTEM_SEARCH_PARAMS;
import static com.ibm.fhir.database.utils.query.expression.ExpressionSupport.alias;
import static com.ibm.fhir.database.utils.query.expression.ExpressionSupport.bind;
import static com.ibm.fhir.database.utils.query.expression.ExpressionSupport.col;
import static com.ibm.fhir.database.utils.query.expression.ExpressionSupport.on;
import static com.ibm.fhir.database.utils.query.expression.ExpressionSupport.string;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.CODE_SYSTEM_ID;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.COMMON_TOKEN_VALUE_ID;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.DATE_START;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.DESCENDING;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.EQ;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.ESCAPE_PERCENT;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.ESCAPE_UNDERSCORE;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.IS_DELETED;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.LEFT_PAREN;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.MAX;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.MIN;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.NUMBER_VALUE;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.PARAMETER_NAME_ID;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.PERCENT_WILDCARD;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.QUANTITY_VALUE;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.RIGHT_PAREN;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.TOKEN_VALUE;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants.UNDERSCORE_WILDCARD;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants._LOGICAL_RESOURCES;
import static com.ibm.fhir.persistence.jdbc.JDBCConstants._RESOURCES;
import static com.ibm.fhir.search.SearchConstants.CANONICAL_COMPONENT_URI;
import static com.ibm.fhir.search.SearchConstants.CANONICAL_COMPONENT_VERSION;
import static com.ibm.fhir.search.SearchConstants.CANONICAL_SUFFIX;
import static com.ibm.fhir.search.SearchConstants.ID;
import static com.ibm.fhir.search.SearchConstants.LAST_UPDATED;
import static com.ibm.fhir.search.SearchConstants.PROFILE;
import static com.ibm.fhir.search.SearchConstants.SECURITY;
import static com.ibm.fhir.search.SearchConstants.TAG;
import static com.ibm.fhir.search.SearchConstants.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import com.ibm.fhir.config.FHIRConfigHelper;
import com.ibm.fhir.database.utils.api.IDatabaseTranslator;
import com.ibm.fhir.database.utils.common.DataDefinitionUtil;
import com.ibm.fhir.database.utils.query.Operator;
import com.ibm.fhir.database.utils.query.Select;
import com.ibm.fhir.database.utils.query.SelectAdapter;
import com.ibm.fhir.database.utils.query.WhereAdapter;
import com.ibm.fhir.database.utils.query.WhereFragment;
import com.ibm.fhir.database.utils.query.expression.ColumnExpNodeVisitor;
import com.ibm.fhir.database.utils.query.expression.StringExpNodeVisitor;
import com.ibm.fhir.database.utils.query.node.ExpNode;
import com.ibm.fhir.model.resource.CodeSystem;
import com.ibm.fhir.model.resource.Resource;
import com.ibm.fhir.model.type.Code;
import com.ibm.fhir.persistence.exception.FHIRPersistenceException;
import com.ibm.fhir.persistence.exception.FHIRPersistenceNotSupportedException;
import com.ibm.fhir.persistence.jdbc.JDBCConstants;
import com.ibm.fhir.persistence.jdbc.dao.api.JDBCIdentityCache;
import com.ibm.fhir.persistence.jdbc.dao.impl.ResourceProfileRec;
import com.ibm.fhir.persistence.jdbc.dto.CommonTokenValue;
import com.ibm.fhir.persistence.jdbc.util.CanonicalSupport;
import com.ibm.fhir.persistence.jdbc.util.CanonicalValue;
import com.ibm.fhir.persistence.jdbc.util.NewUriModifierUtil;
import com.ibm.fhir.persistence.jdbc.util.SqlParameterEncoder;
import com.ibm.fhir.persistence.jdbc.util.type.NewDateParmBehaviorUtil;
import com.ibm.fhir.persistence.jdbc.util.type.NewLastUpdatedParmBehaviorUtil;
import com.ibm.fhir.persistence.jdbc.util.type.NewLocationParmBehaviorUtil;
import com.ibm.fhir.persistence.jdbc.util.type.NewNumberParmBehaviorUtil;
import com.ibm.fhir.persistence.jdbc.util.type.NewQuantityParmBehaviorUtil;
import com.ibm.fhir.persistence.jdbc.util.type.OperatorUtil;
import com.ibm.fhir.search.SearchConstants;
import com.ibm.fhir.search.SearchConstants.Modifier;
import com.ibm.fhir.search.SearchConstants.Prefix;
import com.ibm.fhir.search.SearchConstants.Type;
import com.ibm.fhir.search.exception.FHIRSearchException;
import com.ibm.fhir.search.location.NearLocationHandler;
import com.ibm.fhir.search.location.bounding.Bounding;
import com.ibm.fhir.search.parameters.InclusionParameter;
import com.ibm.fhir.search.parameters.QueryParameter;
import com.ibm.fhir.search.parameters.QueryParameterValue;
import com.ibm.fhir.search.sort.Sort.Direction;
import com.ibm.fhir.search.util.SearchHelper;
import com.ibm.fhir.term.util.CodeSystemSupport;
import com.ibm.fhir.term.util.ValueSetSupport;
/**
* Used to render the domain model into a physical, executable query
* modeled as a Select statement. The domain model knows about resources
* and parameters. This class is used to translate the logical structure
* of the query into a physical one, using the correct table names, join
* predicates and filter expressions.
*/
public class SearchQueryRenderer implements SearchQueryVisitor {
private static final String CLASSNAME = SearchQueryRenderer.class.getName();
private static final Logger logger = Logger.getLogger(CLASSNAME);
private final static String STR_VALUE = "STR_VALUE";
private final static String STR_VALUE_LCASE = "STR_VALUE_LCASE";
private final static String LOGICAL_RESOURCES = "LOGICAL_RESOURCES";
// Database translator to handle SQL syntax variations among databases
private final IDatabaseTranslator translator;
// A cache providing access to various database reference ids
private final JDBCIdentityCache identityCache;
// pagination page number
private final int rowOffset;
// pagination page size
private final int rowsPerPage;
// Counter so we can allocate unique alias names
private int paramCounter = 0;
// Enable use of legacy whole-system search parameters for the search request
private final boolean legacyWholeSystemSearchParamsEnabled;
// Include DATA in the data fetch queries
private final boolean includeResourceData;
/**
* Public constructor
* @param translator
* @param identityCache
* @param rowOffset
* @param rowsPerPage
* @param includeResourceData
*/
public SearchQueryRenderer(IDatabaseTranslator translator, JDBCIdentityCache identityCache,
int rowOffset, int rowsPerPage, boolean includeResourceData) {
this.translator = translator;
this.identityCache = identityCache;
this.rowOffset = rowOffset;
this.rowsPerPage = rowsPerPage;
this.includeResourceData = includeResourceData;
this.legacyWholeSystemSearchParamsEnabled =
FHIRConfigHelper.getBooleanProperty(PROPERTY_SEARCH_ENABLE_LEGACY_WHOLE_SYSTEM_SEARCH_PARAMS, false);
}
/**
* Get the next index number to use as a parameter table alias
* @return
*/
protected int getNextAliasIndex() {
return ++paramCounter;
}
/**
* Get the table name for the xx_logical_resources table where xx is the
* resource type name
* @param resourceType
* @return the table name
*/
protected String resourceLogicalResources(String resourceType) {
if (isWholeSystemSearch(resourceType)) {
return LOGICAL_RESOURCES;
} else {
return resourceType + _LOGICAL_RESOURCES;
}
}
protected String resourceTypeField(String resourceType, int resourceTypeId) {
// If the query is a whole-system-search running at the global
// logical_resources level, we can get the resource type directly
// from the logical_resources table
if (isWholeSystemSearch(resourceType)) {
return "LR.RESOURCE_TYPE_ID";
} else {
// Use a literal value for the resource_type_id value
return Integer.toString(resourceTypeId);
}
}
/**
* Get the table name for the xx_resources table where xx is the resource type name
* @param resourceType
* @return
*/
protected String resourceResources(String resourceType) {
return resourceType + _RESOURCES;
}
/**
* Get the id for the given parameter name (cache lookup)
* @param parameterName
* @return
*/
protected int getParameterNameId(String parameterName) throws FHIRPersistenceException {
return this.identityCache.getParameterNameId(parameterName);
}
/**
* Get the common token value id matching the unique tuple {system, code}
* @param system
* @param code
* @return
* @throws FHIRPersistenceException
*/
protected Long getCommonTokenValueId(String system, String code) throws FHIRPersistenceException {
return this.identityCache.getCommonTokenValueId(system, code);
}
/**
* Get the common token value ids for the passed list of token values {system, code}.
* @param tokenValues
* @return
* @throws FHIRPersistenceException
*/
protected Set getCommonTokenValueIds(Collection tokenValues) throws FHIRPersistenceException {
return this.identityCache.getCommonTokenValueIds(tokenValues);
}
/**
* Get a list of common token values matching the given code
* @param code
* @return
* @throws FHIRPersistenceException
*/
protected List getCommonTokenValueIdList(String code) throws FHIRPersistenceException {
return this.identityCache.getCommonTokenValueIdList(code);
}
/**
* Get the id for the given code system name (cache lookup)
* @param codeSystemName
* @return
* @throws FHIRPersistenceException
*/
protected int getCodeSystemId(String codeSystemName) throws FHIRPersistenceException {
return this.identityCache.getCodeSystemId(codeSystemName);
}
/**
* Get the id for the given canonicalValue (cache lookup).
* @param canonicalValue
* @return the database id, or -1 if the value does not exist
* @throws FHIRPersistenceException
*/
protected int getCanonicalId(String canonicalValue) throws FHIRPersistenceException {
return this.identityCache.getCanonicalId(canonicalValue);
}
@Override
public QueryData countRoot(String rootResourceType) {
final int aliasIndex = 0;
final String xxLogicalResources = resourceLogicalResources(rootResourceType);
final String lrAliasName = getLRAlias(aliasIndex);
// The basic count query from the xx_LOGICAL_RESOURCES table. Query
// parameters are bolted on as exists statements in the WHERE clause.
// No need to join with xx_RESOURCES, because we only need to count
// undeleted logical resources, not individual resource versions
/*
SELECT COUNT(*) AS CNT
FROM Patient_LOGICAL_RESOURCES AS LR0
WHERE LR0.IS_DELETED = 'N'
AND EXISTS (
SELECT 1
FROM Patient_LOGICAL_RESOURCES AS LR1
INNER JOIN Patient_STR_VALUES AS P2 ON P2.LOGICAL_RESOURCE_ID = LR1.LOGICAL_RESOURCE_ID
AND P2.PARAMETER_NAME_ID = 1246
AND (P2.STR_VALUE = ?)
WHERE LR1.IS_DELETED = 'N'
AND LR1.LOGICAL_RESOURCE_ID = LR0.LOGICAL_RESOURCE_ID)
*/
SelectAdapter select = Select.select().addColumn(null, "COUNT(*)", alias("CNT"));
select.from(xxLogicalResources, alias(lrAliasName))
.where(lrAliasName, IS_DELETED).eq(string("N"));
return new QueryData(select, lrAliasName, null, rootResourceType, 0);
}
@Override
public QueryData dataRoot(String rootResourceType, int resourceTypeId) {
/*
// The data root query is formed as an inner select statement which we
// then inner join to the xx_RESOURCES table as a final step. This is
// crucial to enable the optimizer to generate the correct plan.
// The final query looks something like this:
SELECT R.RESOURCE_ID, R.LOGICAL_RESOURCE_ID, R.VERSION_ID, R.LAST_UPDATED, R.IS_DELETED, R.DATA, LR.LOGICAL_ID, R.RESOURCE_PAYLOAD_KEY
FROM (
SELECT LR0.LOGICAL_RESOURCE_ID, LR0.LOGICAL_ID, LR0.CURRENT_RESOURCE_ID
FROM Patient_LOGICAL_RESOURCES AS LR0
WHERE LR0.IS_DELETED = 'N'
AND EXISTS (
SELECT 1
FROM Patient_LOGICAL_RESOURCES AS LR1
INNER JOIN Patient_STR_VALUES AS P2 ON P2.LOGICAL_RESOURCE_ID = LR1.LOGICAL_RESOURCE_ID
AND P2.PARAMETER_NAME_ID = 1246
AND (P2.STR_VALUE = ?)
WHERE LR1.IS_DELETED = 'N'
AND LR1.LOGICAL_RESOURCE_ID = LR0.LOGICAL_RESOURCE_ID)) AS LR
INNER JOIN Patient_RESOURCES AS R ON LR.CURRENT_RESOURCE_ID = R.RESOURCE_ID
ORDER BY LR.LOGICAL_RESOURCE_ID
FETCH FIRST 10 ROWS ONLY
*/
final String xxLogicalResources = resourceLogicalResources(rootResourceType);
final String lrAliasName = "LR0";
// The core data query joining together the logical resources table. Query
// parameters are bolted on as exists statements in the WHERE clause. The final
// query is constructed when joinResources is called.
SelectAdapter select = Select.select("LR0.LOGICAL_RESOURCE_ID", "LR0.LOGICAL_ID", "LR0.CURRENT_RESOURCE_ID");
if (resourceTypeId >= 0) {
// needed for whole system search where the resource type is required
// in order to process the resource payload (which may be offloaded)
select.addColumn(Integer.toString(resourceTypeId), alias("RESOURCE_TYPE_ID"));
}
select.from(xxLogicalResources, alias(lrAliasName))
.where(lrAliasName, IS_DELETED).eq().literal("N");
return new QueryData(select, lrAliasName, null, rootResourceType, 0);
}
@Override
public QueryData getParameterBaseQuery(QueryData parent) {
final int aliasIndex = getNextAliasIndex();
final String xxLogicalResources = resourceLogicalResources(parent.getResourceType());
final String lrAlias = getLRAlias(aliasIndex);
final String parentLRAlias = parent.getLRAlias();
// SELECT 1 FROM xx_LOGICAL_RESOURCES LRn
// INNER JOIN ...
// INNER JOIN ...
// WHERE LRn.LOGICAL_RESOURCE_ID = LRp.LOGICAL_RESOURCE_ID
//
// Note: IS_DELETED is not checked in this sub-query - if necessary, that is
// the responsibility of the parent query.
SelectAdapter exists = Select.select("1");
exists.from(xxLogicalResources, alias(lrAlias))
.where(lrAlias, "LOGICAL_RESOURCE_ID").eq(parentLRAlias, "LOGICAL_RESOURCE_ID"); // correlate to parent query
// Add this exists to the parent query
parent.getQuery().from().where().and().exists(exists.build());
// This bit is important to understanding how this works. We return the
// sub-query here, not the main query. The sub-query is returned because
// it is the query to which we attach all the parameter table joins
return new QueryData(exists, lrAlias, null, parent.getResourceType(), 0);
}
@Override
public QueryData joinResources(QueryData queryData, boolean includeResourceTypeId) {
final SelectAdapter logicalResources = queryData.getQuery();
final String xxResources = resourceResources(queryData.getResourceType());
final String lrAliasName = "LR";
SelectAdapter select = Select.select("R.RESOURCE_ID", "R.LOGICAL_RESOURCE_ID", "R.VERSION_ID", "R.LAST_UPDATED",
"R.IS_DELETED", getDataCol(), "LR.LOGICAL_ID", "R.RESOURCE_PAYLOAD_KEY");
// Resource type id is used for whole-system-search cases where the query
// can return resources of different types (e.g. both Patient and Observation)
if (includeResourceTypeId) {
select.addColumn(lrAliasName, "RESOURCE_TYPE_ID", alias("RESOURCE_TYPE_ID"));
}
select.from(logicalResources.build(), alias(lrAliasName))
.innerJoin(xxResources, alias("R"), on(lrAliasName, "CURRENT_RESOURCE_ID").eq("R", "RESOURCE_ID"));
// The final query still needs ordering/pagination to be applied
return new QueryData(select, lrAliasName, null, queryData.getResourceType(), queryData.getChainDepth());
}
@Override
public QueryData includeRoot(String rootResourceType) {
/* Final query should like this:
SELECT R.RESOURCE_ID, R.LOGICAL_RESOURCE_ID, R.VERSION_ID, R.LAST_UPDATED, R.IS_DELETED, R.DATA, LR.LOGICAL_ID
FROM (
SELECT LR0.LOGICAL_RESOURCE_ID, LR0.LOGICAL_ID, LR0.CURRENT_RESOURCE_ID
FROM Patient_LOGICAL_RESOURCES AS LR0
WHERE LR0.IS_DELETED = 'N'
AND EXISTS (
SELECT 1
FROM Patient_LOGICAL_RESOURCES AS LR1
INNER JOIN Patient_STR_VALUES AS P2 ON P2.LOGICAL_RESOURCE_ID = LR1.LOGICAL_RESOURCE_ID
AND P2.PARAMETER_NAME_ID = 1246
AND (P2.STR_VALUE = ?)
WHERE LR1.IS_DELETED = 'N'
AND LR1.LOGICAL_RESOURCE_ID = LR0.LOGICAL_RESOURCE_ID)) AS LR
INNER JOIN Patient_RESOURCES AS R ON LR.CURRENT_RESOURCE_ID = R.RESOURCE_ID
ORDER BY LR.LOGICAL_RESOURCE_ID
FETCH FIRST 10 ROWS ONLY
*/
// The root query is just the inner distinct piece. The overall query is built by wrapInclude
final boolean distinct = true;
SelectAdapter select = Select.select(distinct, "R0.RESOURCE_ID", "R0.LOGICAL_RESOURCE_ID", "R0.VERSION_ID", "R0.LAST_UPDATED", "R0.IS_DELETED", "LR0.LOGICAL_ID");
return new QueryData(select, null, null, rootResourceType, 0);
}
@Override
public QueryData wrapInclude(QueryData query) {
// Need to join the RESOURCES table again to get the DATA column after the DISTINCT.
final String lrAlias = "LR";
final String rAlias = "R";
final String rTable = query.getResourceType() + "_RESOURCES";
SelectAdapter select = Select.select("LR.RESOURCE_ID", "LR.LOGICAL_RESOURCE_ID", "LR.VERSION_ID",
"LR.LAST_UPDATED", "LR.IS_DELETED", getDataCol(), "LR.LOGICAL_ID", "R.RESOURCE_PAYLOAD_KEY");
select.from(query.getQuery().build(), alias(lrAlias))
.innerJoin(rTable, alias(rAlias), on(lrAlias, "RESOURCE_ID").eq(rAlias, "RESOURCE_ID"));
return new QueryData(select, lrAlias, null, query.getResourceType(), 0);
}
@Override
public QueryData sortRoot(String rootResourceType) {
final String xxLogicalResources = resourceLogicalResources(rootResourceType);
final String lrAliasName = "LR0";
// The core data query joining together the logical resources table. Query
// parameters are bolted on as exists statements in the WHERE clause. The final
// query is constructed when joinResources is called.
SelectAdapter select = Select.select("LR0.CURRENT_RESOURCE_ID");
select.from(xxLogicalResources, alias(lrAliasName))
.where(lrAliasName, IS_DELETED).eq().literal("N");
// We need to group the sort parameters to address any duplicates
select.from().groupBy("LR0.CURRENT_RESOURCE_ID");
return new QueryData(select, lrAliasName, null, rootResourceType, 0);
}
@Override
public QueryData wholeSystemFilterRoot() {
/* Final query should look like this:
SELECT LR0.RESOURCE_TYPE_ID, LR0.LOGICAL_RESOURCE_ID
FROM LOGICAL_RESOURCES AS LR0
WHERE LR0.IS_DELETED = 'N'
AND EXISTS (
SELECT 1
FROM LOGICAL_RESOURCES AS LR1
INNER JOIN RESOURCE_TOKEN_REFS AS P2 ON P2.LOGICAL_RESOURCE_ID = LR1.LOGICAL_RESOURCE_ID
AND P2.PARAMETER_NAME_ID = 1008
AND ((P2.COMMON_TOKEN_VALUE_ID = 4))
WHERE LR1.IS_DELETED = 'N'
AND LR1.LOGICAL_RESOURCE_ID = LR0.LOGICAL_RESOURCE_ID)
ORDER BY LR0.LOGICAL_RESOURCE_ID
FETCH FIRST 10 ROWS ONLY
*/
final String lrAliasName = "LR0";
// The core data query joining together the logical resources table. Query
// parameters are bolted on as exists statements in the WHERE clause.
SelectAdapter select = Select.select("LR0.RESOURCE_TYPE_ID", "LR0.LOGICAL_RESOURCE_ID");
select.from(LOGICAL_RESOURCES, alias(lrAliasName))
.where(lrAliasName, IS_DELETED).eq().literal("N");
return new QueryData(select, lrAliasName, null, Resource.class.getSimpleName(), 0);
}
@Override
public QueryData wholeSystemDataRoot(String rootResourceType, int rootResourceTypeId) {
/* Final query should look something like this (where [RTFIELD] depends on the type of the
* whole system search):
SELECT R.RESOURCE_ID, R.LOGICAL_RESOURCE_ID, R.VERSION_ID, R.LAST_UPDATED, R.IS_DELETED, R.DATA, LR.LOGICAL_ID, R.RESOURCE_PAYLOAD_KEY, [RTFIELD] AS RESOURCE_TYPE_ID
FROM (
SELECT LR.LOGICAL_RESOURCE_ID, LR.LOGICAL_ID, LR.CURRENT_RESOURCE_ID
FROM Patient_LOGICAL_RESOURCES AS LR
WHERE LR.IS_DELETED = 'N') AS LR
AND LR.LOGICAL_RESOURCE_ID IN (2,4,6,10,12,14,20,24,26,29)) AS LR
INNER JOIN Patient_RESOURCES AS R ON LR.CURRENT_RESOURCE_ID = R.RESOURCE_ID
ORDER BY LR.LOGICAL_RESOURCE_ID
FETCH FIRST 10 ROWS ONLY
*/
final String xxLogicalResources = resourceLogicalResources(rootResourceType);
final String resourceTypeIdStr = Integer.toString(rootResourceTypeId);
final String lrAliasName = "LR";
// The core data query joining together the logical resources table. The final
// query is constructed when joinResources is called.
SelectAdapter select = Select.select("LR.LOGICAL_RESOURCE_ID", "LR.LOGICAL_ID", "LR.CURRENT_RESOURCE_ID");
select.addColumn(resourceTypeIdStr, alias("RESOURCE_TYPE_ID"));
select.from(xxLogicalResources, alias(lrAliasName))
.where(lrAliasName, IS_DELETED).eq().literal("N");
return new QueryData(select, lrAliasName, null, rootResourceType, 0);
}
@Override
public QueryData wrapWholeSystem(List queries, boolean isCountQuery) {
/* We need to either sum the counts of each individual count query or
aggregate the data of each individual data query.
Final query should look something like this for a count query:
SELECT SUM(CNT)
FROM (
UNION ALL
UNION ALL
...
UNION ALL
) AS COMBINED RESULTS
Final query should look something like this for a data query:
SELECT RESOURCE_ID, LOGICAL_RESOURCE_ID, VERSION_ID, LAST_UPDATED, IS_DELETED, DATA, LOGICAL_ID, RESOURCE_PAYLOAD_KEY, RESOURCE_TYPE_ID
FROM (
UNION ALL
UNION ALL
...
UNION ALL
) AS COMBINED RESULTS
ORDER BY COMBINED RESULTS.LOGICAL_RESOURCE_ID
FETCH FIRST 10 ROWS ONLY
*/
SelectAdapter select;
if (isCountQuery) {
select = Select.select("SUM(CNT)");
} else {
select = Select.select("RESOURCE_ID", "LOGICAL_RESOURCE_ID", "VERSION_ID", "LAST_UPDATED", "IS_DELETED", "DATA", "LOGICAL_ID", "RESOURCE_PAYLOAD_KEY", "RESOURCE_TYPE_ID");
}
SelectAdapter first = null;
SelectAdapter previous = null;
for (QueryData query : queries) {
SelectAdapter subSelect = query.getQuery();
if (previous == null) {
// Save head of UNION'd selects
first = subSelect;
} else {
// Add current select as union of previous
previous.unionAll(subSelect.getSelect());
}
previous = subSelect;
}
select.from(first.getSelect(), alias("COMBINED_RESULTS"));
return new QueryData(select, null, null, Resource.class.getSimpleName(), 0);
}
/**
* Get the filter predicate for the given token query parameter.
* @param queryParm the token query parameter
* @param paramAlias the alias used for the token values table
* @throws FHIRPersistenceException
*/
protected WhereFragment getTokenFilter(QueryParameter queryParm, String paramAlias) throws FHIRPersistenceException {
final Operator operator = getOperator(queryParm, EQ);
WhereFragment where = new WhereFragment();
boolean first = true;
where.leftParen();
// Append the suffix for :text modifier
String parameterName = queryParm.getCode();
if (Modifier.TEXT.equals(queryParm.getModifier())) {
parameterName += SearchConstants.TEXT_MODIFIER_SUFFIX;
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("getTokenFilter: '" + parameterName + "'" + ", Operator: " + operator + ", modifier: " + queryParm.getModifier());
}
for (QueryParameterValue value : queryParm.getValues()) {
// If multiple values are present, we need to OR them together.
if (first) {
first = false;
} else {
where.or();
}
// The expression may be complex, and we may need to OR them together. To avoid any
// precedence drama, we simply wrap everything in parens just to be safe
where.leftParen();
if (Modifier.IN.equals(queryParm.getModifier()) ||
Modifier.NOT_IN.equals(queryParm.getModifier()) ||
Modifier.ABOVE.equals(queryParm.getModifier()) ||
Modifier.BELOW.equals(queryParm.getModifier())) {
// IN and NOT_IN can follow the same code path here because of our assumption that
// if there's any matching parameter values then we'll also find a matching common_token_value_id
//
// Assume we have a valueset vs with codes like s1|c1 and s2|c2 and we have Basic resources `a` and `b`.
// Now the query comes like Basic?p:not-in=vs
//
// Scenario 1: `a` has the value `s1|c1` for the coding targeted by parameter p.
//
// WHERE NOT EXISTS(
// SELECT 1
// FROM Basic_RESOURCE_TOKEN_REFS AS P2
// WHERE P2.LOGICAL_RESOURCE_ID = LR1.LOGICAL_RESOURCE_ID
// AND P2.PARAMETER_NAME_ID = 20046
// AND ((P2.COMMON_TOKEN_VALUE_ID IN (1)))))) AS LR
//
// the select subquery selects 1 for `a`
// the select subquery selects empty for `b`
// so as a result it selects `b` and not `a`
//
//
// Scenario 2: none of the resources have any of the values from the valueset
//
// WHERE NOT EXISTS(
// SELECT 1
// FROM Basic_RESOURCE_TOKEN_REFS AS P2
// WHERE P2.LOGICAL_RESOURCE_ID = LR1.LOGICAL_RESOURCE_ID
// AND P2.PARAMETER_NAME_ID = 20046
// AND ((P2.COMMON_TOKEN_VALUE_ID IN (-1)))))) AS LR
//
// The select subquery will always return empty (we never have a -1 in the db)
// so as a result it selects `a` and `b` (and any other resources)
//
// This is what we want because it matches the semantics of `:not`; it select
// resources with other values AND resources with no value for this parameter at all
//
populateCodesSubSegment(where, queryParm.getModifier(), value, paramAlias);
} else {
String system = value.getValueSystem();
final String code = value.getValueCode();
// Determine code normalization based on code system case-sensitivity
String normalizedCode = null;
if (code != null) {
if (system != null) {
boolean codeSystemIsCaseSensitive = CodeSystemSupport.isCaseSensitive(system);
normalizedCode = SqlParameterEncoder.encode(codeSystemIsCaseSensitive ?
code : SearchHelper.normalizeForSearch(code));
} else {
normalizedCode = SqlParameterEncoder.encode(SearchHelper.normalizeForSearch(code));
}
}
// Replace an empty system with our default-token-system
if (system != null && system.isEmpty()) {
system = JDBCConstants.DEFAULT_TOKEN_SYSTEM;
}
// Include code
if (operator == Operator.EQ && code != null) {
if (system == null || system.equals("*")) {
// Even though we don't have a system, we can still use a list of
// common_token_value_ids matching the value-code, allowing a similar optimization
Set ctvs = new HashSet<>();
fetchCommonTokenValues(ctvs, SqlParameterEncoder.encode(code));
fetchCommonTokenValues(ctvs, SqlParameterEncoder.encode(SearchHelper.normalizeForSearch(code)));
addCommonTokenValueIdFilter(where, paramAlias, ctvs);
} else {
Long commonTokenValueId = identityCache.getCommonTokenValueId(system, normalizedCode);
where.col(paramAlias, COMMON_TOKEN_VALUE_ID).eq(nullCheck(commonTokenValueId));
}
} else {
// Traditional approach, using a join to xx_TOKEN_VALUES_V
// Include code if present
if (code != null) {
where.col(paramAlias, TOKEN_VALUE).operator(operator);
if (operator == Operator.LIKE) {
// Must escape special wildcard characters _ and % in the parameter value string
// as well as the escape character itself.
String textSearchString = normalizedCode
.replace("+", "++")
.replace(PERCENT_WILDCARD, ESCAPE_PERCENT)
.replace(UNDERSCORE_WILDCARD, ESCAPE_UNDERSCORE) + PERCENT_WILDCARD;
where.bind(SearchHelper.normalizeForSearch(textSearchString)).escape("+");
} else {
where.bind(normalizedCode);
}
}
// Include system if present
if (system != null) {
if (code != null) {
where.and();
}
// Filter on the code system for the given parameter
Integer codeSystemId = identityCache.getCodeSystemId(system);
where.col(paramAlias, CODE_SYSTEM_ID).eq().literal(nullCheck(codeSystemId));
}
}
}
where.rightParen();
}
where.rightParen();
return where;
}
/**
* Adds a filter predicate for COMMON_TOKEN_VALUE_ID. Fetches the list of possible matches (there's no code-system,
* so there could be multiple). If no match, then -1 is used to make sure the row isn't produced. If there is a
* single match, the predicate is COMMON_TOKEN_VALUE_ID = {n}. If there are multiple matches, the predicate is
* COMMON_TOKEN_VALUE_ID IN (1, 2, 3, ...).
* The query uses literal values not bind variables on purpose (better performance).
* @param where
* @param paramAlias
* @param searchValue
* @throws FHIRPersistenceException
*/
private void addCommonTokenValueIdFilter(WhereFragment where, String paramAlias, String searchValue) throws FHIRPersistenceException {
// grab the list of all matching common_token_value_id values
Set ctvs = new HashSet<>();
fetchCommonTokenValues(ctvs, searchValue);
// and add a filter expression paramAlias IN (...) for the values
addCommonTokenValueIdFilter(where, paramAlias, ctvs);
}
/**
* Add all common_token_value_id matching the given searchValue to the ctvs set.
* @param ctvs
* @param searchValue
* @throws FHIRPersistenceException
*/
private void fetchCommonTokenValues(Set ctvs, String searchValue) throws FHIRPersistenceException {
List ctvList = this.identityCache.getCommonTokenValueIdList(searchValue);
ctvs.addAll(ctvList);
}
/**
* Adds a filter predicate for COMMON_TOKEN_VALUE_ID. If the ctvs list is empty, then -1 is used to make
* sure the row isn't produced. If there is a single match, the predicate is COMMON_TOKEN_VALUE_ID = {n}.
* If there are multiple matches, the predicate is COMMON_TOKEN_VALUE_ID IN (1, 2, 3, ...).
* The query uses literal values not bind variables on purpose (better performance).
* @param where
* @param paramAlias
* @param ctvs
* @throws FHIRPersistenceException
*/
private void addCommonTokenValueIdFilter(WhereFragment where, String paramAlias, Collection ctvs) throws FHIRPersistenceException {
final List ctvList = new ArrayList<>(ctvs);
if (ctvList.isEmpty()) {
// use -1...resulting in no data
where.col(paramAlias, COMMON_TOKEN_VALUE_ID).eq(-1L);
} else if (ctvList.size() == 1) {
where.col(paramAlias, COMMON_TOKEN_VALUE_ID).eq(ctvList.get(0));
} else {
where.col(paramAlias, COMMON_TOKEN_VALUE_ID).inLiteralLong(ctvList);
}
}
/**
* Builds an SQL segment which populates an IN clause with codes for a token search parameter
* specifying the :in, :not-in, :above, or :below modifier.
*
* @param whereClauseSegment - the segment to which the sub-segment will be added
* @param modifier - the search parameter modifier (:in | :not-in | :above | :below)
* @param parameterValue - the search parameter value - a ValueSet URL or a CodeSystem URL + code
* @param parameterTableAlias - the alias for the parameter table e.g. CPx
* @throws FHIRPersistenceException
*/
private void populateCodesSubSegment(WhereFragment whereClauseSegment, Modifier modifier,
QueryParameterValue parameterValue, String parameterTableAlias) throws FHIRPersistenceException {
// Get the codes to populate the IN clause.
// Note: validation of the value set or the code system + code specified in parameterValue
// was done when the search parameter was parsed, so does not need to be done here.
Map> codeSetMap = null;
if (Modifier.IN.equals(modifier) || Modifier.NOT_IN.equals(modifier)) {
codeSetMap = ValueSetSupport.getCodeSetMap(ValueSetSupport.getValueSet(parameterValue.getValueCode()));
} else if (Modifier.ABOVE.equals(modifier) || Modifier.BELOW.equals(modifier)) {
CodeSystem codeSystem = CodeSystemSupport.getCodeSystem(parameterValue.getValueSystem());
Code code = Code.builder().value(parameterValue.getValueCode()).build();
Set codes;
if (Modifier.ABOVE.equals(modifier)) {
codes = CodeSystemSupport.getAncestorsAndSelf(codeSystem, code);
} else {
codes = CodeSystemSupport.getDescendantsAndSelf(codeSystem, code);
}
codeSetMap = Collections.singletonMap(parameterValue.getValueSystem(), codes);
}
List commonTokenValues = new ArrayList<>();
for (String codeSetUrl : codeSetMap.keySet()) {
// get the codes for this URL before stripping off the version part
Set codes = codeSetMap.get(codeSetUrl);
// Strip version from canonical codeSet URL. We don't store version in TOKEN_VALUES
// table so will just ignore it.
int index = codeSetUrl.lastIndexOf("|");
if (index != -1) {
codeSetUrl = codeSetUrl.substring(0, index);
}
Integer codeSystemId = identityCache.getCodeSystemId(codeSetUrl);
if (codeSystemId == null) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Skipping codes from system '" + codeSetUrl + "' as there are no such indexed values in the db");
}
continue;
}
for (String code : codes) {
commonTokenValues.add(new CommonTokenValue(codeSetUrl, codeSystemId, code));
}
}
Set commonTokenValueIds = getCommonTokenValueIds(commonTokenValues);
addCommonTokenValueIdFilter(whereClauseSegment, parameterTableAlias, commonTokenValueIds);
}
/**
* Add a filter expression to the given parameter sub-query (which is used as an EXISTS clause)
* @param paramExists the query statement to which we need to add the filter predicate
* @param queryParm the query parameter for which we need to compute and add the filter predicate
* @param paramAlias the alias for the query parameter table
* @return
* @throws FHIRPersistenceException
*/
protected WhereFragment getStringFilter(QueryParameter queryParm, String paramAlias) throws FHIRPersistenceException {
// Process the values from the queryParameter to produce
// the predicates we need to pass to the visitor (which is
// responsible for building the full query).
final Operator operator = getOperator(queryParm);
final String parameterName = queryParm.getCode();
if (logger.isLoggable(Level.FINE)) {
logger.fine("getStringFilter: " + parameterName + ", op=" + operator.name() + ", modifier=" + queryParm.getModifier());
}
WhereFragment whereFragment = new WhereFragment();
whereFragment.leftParen();
boolean multiple = false;
for (QueryParameterValue value : queryParm.getValues()) {
// Concatenate multiple matches with an OR
if (multiple) {
whereFragment.or();
} else {
multiple = true;
}
if (operator == Operator.LIKE) {
// Must escape special wildcard characters _ and % in the parameter value string
// as well as the escape character itself.
String tempSearchValue =
SqlParameterEncoder.encode(value.getValueString()
.replace("+", "++")
.replace(PERCENT_WILDCARD, ESCAPE_PERCENT)
.replace(UNDERSCORE_WILDCARD, ESCAPE_UNDERSCORE));
if (Modifier.CONTAINS.equals(queryParm.getModifier())) {
String searchValue = PERCENT_WILDCARD + tempSearchValue + PERCENT_WILDCARD;
searchValue = SearchHelper.normalizeForSearch(searchValue);
whereFragment.col(paramAlias, STR_VALUE_LCASE).like(bind(searchValue)).escape("+");
} else {
// If there is not a CONTAINS modifier on the query parm, construct
// a 'starts with' search value.
String searchValue = tempSearchValue + PERCENT_WILDCARD;
// Specific processing for
if (queryParm.getModifier() != null && queryParm.getType() == Type.URI) {
if (queryParm.getModifier() == Modifier.BELOW) {
searchValue = tempSearchValue + "/" + PERCENT_WILDCARD;
whereFragment.leftParen()
.col(paramAlias, STR_VALUE).eq(bind(tempSearchValue))
.or(paramAlias, STR_VALUE).like(bind(searchValue)).escape("+")
.rightParen();
} else if (queryParm.getModifier() == Modifier.ABOVE) {
NewUriModifierUtil.generateAboveValuesQuery(whereFragment, paramAlias, STR_VALUE, searchValue, operator);
} else {
// neither above nor below, so an exact match for URI
whereFragment.col(paramAlias, STR_VALUE).eq(bind(searchValue));
}
} else {
// Simple STARTS WITH
searchValue = SearchHelper.normalizeForSearch(searchValue);
logger.fine("LIKE: " + searchValue);
whereFragment.col(paramAlias, STR_VALUE_LCASE).like(bind(searchValue)).escape("+");
}
}
} else if (queryParm.getType() == Type.URI) {
// need to handle above/below modifier
if (queryParm.getModifier() == Modifier.BELOW) {
String tempSearchValue =
SqlParameterEncoder.encode(value.getValueString()
.replace("+", "++")
.replace(PERCENT_WILDCARD, ESCAPE_PERCENT)
.replace(UNDERSCORE_WILDCARD, ESCAPE_UNDERSCORE));
String searchValue = tempSearchValue + "/" + PERCENT_WILDCARD;
whereFragment.leftParen()
.col(paramAlias, STR_VALUE).eq(bind(tempSearchValue))
.or(paramAlias, STR_VALUE).like(bind(searchValue)).escape("+")
.rightParen();
} else if (queryParm.getModifier() == Modifier.ABOVE) {
String searchValue = SqlParameterEncoder.encode(value.getValueString());
NewUriModifierUtil.generateAboveValuesQuery(whereFragment, paramAlias, STR_VALUE, searchValue, operator);
} else {
// neither above nor below, so an exact match for URI
String searchValue = SqlParameterEncoder.encode(value.getValueString());
whereFragment.col(paramAlias, STR_VALUE).eq(bind(searchValue));
}
} else if (operator == Operator.EQ) {
// Exact match
String searchValue = SqlParameterEncoder.encode(value.getValueString());
whereFragment.col(paramAlias, STR_VALUE).eq(bind(searchValue));
} else {
// For anything other than an exact match, we search against the STR_VALUE_LCASE column in the
// Resource's string values table.
// Also, the search value is "normalized"; it has accents removed and is lower-cased. This enables a
// case-insensitive, accent-insensitive search.
// Build this piece: pX.str_value_lcase {operator} search-attribute-value
String searchValue = SqlParameterEncoder.encode(value.getValueString());
searchValue = SearchHelper.normalizeForSearch(searchValue);
whereFragment.col(paramAlias, STR_VALUE_LCASE).operator(operator).bind(searchValue);
addEscapeIfRequired(whereFragment, operator);
}
}
whereFragment.rightParen();
final ExpNode filter = whereFragment.getExpression();
if (logger.isLoggable(Level.FINE)) {
logger.fine("string filter[" + parameterName + "] := " + StringExpNodeVisitor.stringify(filter));
}
return whereFragment;
}
/**
* Add the ESCAPE modified to the LIKE clause if needed
* @param wf
* @param op
*/
private void addEscapeIfRequired(WhereFragment wf, Operator op) {
if (op == Operator.LIKE) {
wf.escape("+"); // adds ESCAPE '+'
}
}
@Override
public QueryData addSorting(QueryData queryData, String lrAlias) {
final String lrLogicalResourceId = DataDefinitionUtil.getQualifiedName(lrAlias, "LOGICAL_RESOURCE_ID");
queryData.getQuery().from().orderBy(lrLogicalResourceId);
return queryData;
}
@Override
public QueryData addWholeSystemSorting(QueryData queryData, List sortParms, String lrAlias) {
if (sortParms == null || sortParms.isEmpty()) {
return addSorting(queryData, lrAlias);
} else {
for (DomainSortParameter sortParm : sortParms) {
// for whole-system searches, sort parameters can only be _id or _lastUpdated
StringBuilder expression = new StringBuilder();
expression.append(ID.equals(sortParm.getSortParameter().getCode()) ?
DataDefinitionUtil.getQualifiedName(lrAlias, "LOGICAL_ID") :
DataDefinitionUtil.getQualifiedName(lrAlias, "LAST_UPDATED"))
.append(" ")
.append(Direction.DECREASING.equals(sortParm.getSortParameter().getDirection()) ? DESCENDING : "");
queryData.getQuery().from().orderBy(expression.toString());
}
}
return queryData;
}
@Override
public QueryData addPagination(QueryData queryData) {
queryData.getQuery().pagination(rowOffset, rowsPerPage);
return queryData;
}
/**
* Get the parameter values table name (e.g. Patient_STR_VALUES) for the
* given resource and parameter type. Note that this is now different from
* the original QuerySegmentAggregator implementation - it does not differentiate
* on chaining...that is left up to the building logic.
* @param resourceType
* @param paramType
* @return
*/
public String paramValuesTableName(String resourceType, QueryParameter queryParm) {
boolean wholeSystemSearch = isWholeSystemSearch(resourceType);
StringBuilder name = new StringBuilder(wholeSystemSearch ? "" : resourceType + "_");
switch (queryParm.getType()) {
case URI:
case STRING:
if (!this.legacyWholeSystemSearchParamsEnabled && PROFILE.equals(queryParm.getCode())) {
name.append(wholeSystemSearch ? "LOGICAL_RESOURCE_PROFILES" : "PROFILES");
} else {
name.append("STR_VALUES");
}
break;
case NUMBER:
name.append("NUMBER_VALUES");
break;
case QUANTITY:
name.append("QUANTITY_VALUES");
break;
case DATE:
name.append("DATE_VALUES");
break;
case SPECIAL:
name.append("LATLNG_VALUES");
break;
case REFERENCE:
case TOKEN:
if (!this.legacyWholeSystemSearchParamsEnabled && TAG.equals(queryParm.getCode())) {
name.append(wholeSystemSearch ? "LOGICAL_RESOURCE_TAGS" : "TAGS");
} else if (!this.legacyWholeSystemSearchParamsEnabled && SECURITY.equals(queryParm.getCode())) {
name.append(wholeSystemSearch ? "LOGICAL_RESOURCE_SECURITY" : "SECURITY");
} else {
name.append("RESOURCE_TOKEN_REFS"); // bypass the xx_TOKEN_VALUES_V for performance reasons
}
break;
case COMPOSITE:
name.append("LOGICAL_RESOURCES");
break;
}
return name.toString();
}
/**
* Get the column name to use for the given paramType
* @param paramType
* @return
*/
public String paramValuesColumnName(Type paramType) {
final String result;
switch (paramType) {
case URI:
case STRING:
result = "STR_VALUES";
break;
case NUMBER:
result = "NUMBER_VALUE";
break;
case QUANTITY:
result = "QUANTITY_VALUE";
break;
case DATE:
result = "DATE_VALUE";
break;
case SPECIAL:
result = "LATLNG_VALUES";
break;
case REFERENCE:
case TOKEN:
result = "TOKEN_VALUE";
break;
case COMPOSITE:
result = null;
break;
default:
result = null;
}
return result;
}
/**
* Get a simple filter predicate which can be used in the WHERE clause of a search query.
* This is used at the "leaf level" of parameter processing, where the queryParm relates
* to a single parameter (i.e. it is the caller's responsibility to handle chaining and
* other more complex behavior.
* @param queryData
* @param queryParm
* @return
* @throws FHIRPersistenceException
*/
protected WhereFragment getFilterPredicate(QueryData queryData, QueryParameter queryParm) throws FHIRPersistenceException {
WhereFragment filter = new WhereFragment();
final String code = queryParm.getCode();
final String parentAlias = queryData.getLRAlias();
if (ID.equals(code)) {
List values = queryParm.getValues().stream().map(p -> p.getValueCode()).collect(Collectors.toList());
if (values.size() == 1) {
filter.col(parentAlias, "LOGICAL_ID").eq().bind(values.get(0));
} else if (values.size() > 1) {
// the values are converted to bind-markers, so this is secure
filter.col(parentAlias, "LOGICAL_ID").in(values);
} else {
throw new FHIRPersistenceException("_id parameter value list is empty");
}
} else if (LAST_UPDATED.equals(code)) {
// Compute the _lastUpdated filter predicate for the given query parameter
NewLastUpdatedParmBehaviorUtil util = new NewLastUpdatedParmBehaviorUtil(parentAlias);
util.executeBehavior(filter, queryParm);
} else {
// A simple filter added as an exists clause to the current query
// AND EXISTS (SELECT 1
// FROM fhirdata.Patient_STR_VALUES AS P3 -- 'Patient string parameters'
// WHERE P3.LOGICAL_RESOURCE_ID = LR2.LOGICAL_RESOURCE_ID -- 'correlate to parent'
// AND P3.PARAMETER_NAME_ID = 123 -- 'name parameter'
// AND P3.STR_VALUE = 'Jones') -- 'name filter'
final int aliasIndex = getNextAliasIndex();
final String paramTable = paramValuesTableName(queryData.getResourceType(), queryParm);
final String paramAlias = getParamAlias(aliasIndex);
SelectAdapter exists = Select.select("1");
exists.from(paramTable, alias(paramAlias))
.where(paramAlias, "LOGICAL_RESOURCE_ID").eq(parentAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(code))
.and(paramFilter(queryParm, paramAlias).getExpression());
filter.exists(exists.build());
}
return filter;
}
/**
* Add a filter on the LOGICAL_ID for the given query parameter values
* @param queryData
* @param resourceType
* @param queryParm
*/
protected void addIdFilter(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
final SelectAdapter currentSubQuery = queryData.getQuery();
final String parentAlias = queryData.getLRAlias();
List values = queryParm.getValues().stream().map(p -> p.getValueCode()).collect(Collectors.toList());
if (values.size() == 1) {
currentSubQuery.from().where().and(parentAlias, "LOGICAL_ID").eq().bind(values.get(0));
} else if (values.size() > 1) {
// the values are converted to bind-markers, so this is secure
currentSubQuery.from().where().and(parentAlias, "LOGICAL_ID").in(values);
} else {
throw new FHIRPersistenceException("_id parameter value list is empty");
}
// If this is a whole-system search, add the following predicate in order to take
// advantage of index on the LOGICAL_RESOURCES table:
// AND .RESOURCE_TYPE_ID IN ()
if (isWholeSystemSearch(resourceType)) {
List resourceTypeIds = this.identityCache.getResourceTypeIds().stream().map(Long::valueOf).collect(Collectors.toList());
currentSubQuery.from().where().and(parentAlias, "RESOURCE_TYPE_ID").inLiteralLong(resourceTypeIds);
}
}
/**
* Get a filter predicate for the given number query parameter
* @param queryParm
* @param paramAlias
*/
protected WhereFragment getNumberFilter(QueryParameter queryParm, String paramAlias) throws FHIRPersistenceException {
WhereFragment where = new WhereFragment();
NewNumberParmBehaviorUtil behaviorUtil = new NewNumberParmBehaviorUtil();
behaviorUtil.executeBehavior(where, queryParm, paramAlias);
return where;
}
/**
* Add a filter predicate to the given exists sub-query
* @param queryParm
* @param paramAlias
*/
protected WhereFragment getQuantityFilter(QueryParameter queryParm, String paramAlias) throws FHIRPersistenceException {
WhereFragment where = new WhereFragment();
NewQuantityParmBehaviorUtil behaviorUtil = new NewQuantityParmBehaviorUtil(this.identityCache);
behaviorUtil.executeBehavior(where, queryParm, paramAlias);
return where;
}
/**
* Add a filter predicate to the given exists sub-query
* @param queryParm
* @param paramAlias
*/
protected WhereFragment getDateFilter(QueryParameter queryParm, String paramAlias) {
WhereFragment where = new WhereFragment();
NewDateParmBehaviorUtil util = new NewDateParmBehaviorUtil();
// It is possible that multiple date parameters could be chained together if there were
// multiple query parameters specified for the same date search parameter and we were
// able to consolidate them. Check specifically if we have a consolidated parameter which
// specifies a range, and if so, build a range filter.
if (queryParm.isChained() && queryParm.getChain().size() == 1) {
List lowerBoundPrefixes = Arrays.asList(Prefix.GT, Prefix.GE, Prefix.SA);
List upperBoundPrefixes = Arrays.asList(Prefix.LT, Prefix.LE, Prefix.EB);
Prefix prefix1 = queryParm.getValues().get(0).getPrefix();
Prefix prefix2 = queryParm.getNextParameter().getValues().get(0).getPrefix();
if (lowerBoundPrefixes.contains(prefix1) && upperBoundPrefixes.contains(prefix2)) {
// The consolidated parameter specifies a range, so build a date range filter
util.buildCustomRangeClause(where, paramAlias, queryParm, queryParm.getNextParameter());
return where;
} else if (lowerBoundPrefixes.contains(prefix2) && upperBoundPrefixes.contains(prefix1)) {
// The consolidated parameter specifies a range, so build a date range filter
util.buildCustomRangeClause(where, paramAlias, queryParm.getNextParameter(), queryParm);
return where;
}
}
boolean first = true;
while (queryParm != null) {
if (first) {
first = false;
} else {
where.and();
}
util.executeBehavior(where, queryParm, paramAlias);
queryParm = queryParm.getNextParameter();
}
return where;
}
/**
* Add a filter predicate to the given exists sub-query
* @param queryParm
* @param paramAlias
*/
protected WhereFragment getLocationFilter(QueryParameter queryParm, String paramAlias) throws FHIRPersistenceException {
WhereFragment where = new WhereFragment();
NearLocationHandler handler = new NearLocationHandler();
List boundingAreas;
try {
boundingAreas = handler.generateLocationPositionsFromParameters(Arrays.asList(queryParm));
} catch (FHIRSearchException e) {
throw new FHIRPersistenceException("input parameter is invalid bounding area, bad prefix, or bad units", e);
}
NewLocationParmBehaviorUtil behaviorUtil = new NewLocationParmBehaviorUtil();
behaviorUtil.buildLocationSearchQuery(where, boundingAreas, paramAlias);
return where;
}
/**
* Get the string to use as a parameter table alias for the given aliasIndex value
* @param aliasIndex
* @return
*/
protected String getParamAlias(int aliasIndex) {
return "P" + aliasIndex;
}
/**
* Get the string to use as a logical resource alias for the given aliasIndex value
* @param aliasIndex
* @return
*/
protected String getLRAlias(int aliasIndex) {
return "LR" + aliasIndex;
}
/**
* Compute the token parameter table name we want to use to join with. This method
* inspects the content of the given filter {@link ExpNode}. If the filter contains
* a reference to the TOKEN_VALUE column, the returned table name will be based
* on xx_TOKEN_VALUES_V, otherwise it will be based on xx_RESOURCE_TOKEN_REFS. The
* latter is preferable because it eliminates an unnecessary join, improves cardinality
* estimation and (usually) results in a better execution plan.
* @param filter
* @param resourceType
* @param paramAlias
* @return
*/
protected String getTokenParamTable(ExpNode filter, String resourceType, String paramAlias) {
ColumnExpNodeVisitor visitor = new ColumnExpNodeVisitor(); // gathers all columns used in the filter expression
Set columns = filter.visit(visitor);
boolean usesTokenValue = columns.contains(DataDefinitionUtil.getQualifiedName(paramAlias, TOKEN_VALUE)) ||
columns.contains(DataDefinitionUtil.getQualifiedName(paramAlias, CODE_SYSTEM_ID));
final String xxTokenValues;
if (usesTokenValue) {
// can't optimize because we filter on TOKEN_VALUE
xxTokenValues = resourceType + "_TOKEN_VALUES_V";
} else {
// only filters on COMMON_TOKEN_VALUE_ID so we can optimize
xxTokenValues = resourceType + "_RESOURCE_TOKEN_REFS";
}
return xxTokenValues;
}
/**
* Create a filter predicate for the given reference query parameter
* @param queryParm
* @param paramAlias
* @throws FHIRPersistenceException
*/
protected WhereFragment getReferenceFilter(QueryParameter queryParm, String paramAlias) throws FHIRPersistenceException {
WhereFragment whereClause = new WhereFragment();
whereClause.leftParen();
if (Modifier.IDENTIFIER.equals(queryParm.getModifier())) {
handleIdentifier(queryParm, paramAlias, whereClause);
whereClause.rightParen();
return whereClause;
}
List> resourceTypesAndIds = new ArrayList<>(queryParm.getValues().size());
for (QueryParameterValue value : queryParm.getValues()) {
resourceTypesAndIds.add(getResourceTypeAndId(queryParm, value));
}
List resourceReferenceTokenValues = new ArrayList<>(queryParm.getValues().size());
List ambiguousResourceReferenceTokenValues = new ArrayList<>();
for (Pair resourceTypeAndId : resourceTypesAndIds) {
String targetResourceType = resourceTypeAndId.getLeft();
String targetResourceId = resourceTypeAndId.getRight();
if (targetResourceType != null) {
Integer codeSystemIdForResourceType = getCodeSystemId(targetResourceType);
// targetResourceType is treated as the code-system for references
resourceReferenceTokenValues.add(new CommonTokenValue(targetResourceType, nullCheck(codeSystemIdForResourceType), targetResourceId));
} else {
ambiguousResourceReferenceTokenValues.add(targetResourceId);
}
}
// For unambiguous resource references, look up the common token value ids
Set resourceReferenceTokenIds = getCommonTokenValueIds(resourceReferenceTokenValues);
addCommonTokenValueIdFilter(whereClause, paramAlias, resourceReferenceTokenIds);
for (String targetResourceId : ambiguousResourceReferenceTokenValues) {
whereClause.or();
// grab the list of all matching common_token_value_id values
addCommonTokenValueIdFilter(whereClause, paramAlias, targetResourceId);
}
whereClause.rightParen();
return whereClause;
}
private Pair getResourceTypeAndId(QueryParameter queryParm, QueryParameterValue value) {
String targetResourceType = null;
String searchValue = SqlParameterEncoder.encode(value.getValueString());
// Make sure we split out the resource type if it is included in the search value
String[] parts = value.getValueString().split("/");
if (parts.length == 2) {
targetResourceType = parts[0];
searchValue = parts[1];
}
// Handle query parm representing this name/value pair construct:
// {name}:{Resource Type} = {resource-id}
if (queryParm.getModifier() != null && queryParm.getModifier().equals(Modifier.TYPE)) {
if (!SearchConstants.Type.REFERENCE.equals(queryParm.getType())) {
// Not a Reference
searchValue = queryParm.getModifierResourceTypeName() + "/"
+ SqlParameterEncoder.encode(value.getValueString());
} else {
// This is a Reference type.
if (parts.length != 2) {
// fallback to get the target resource type using the modifier
targetResourceType = queryParm.getModifierResourceTypeName();
}
}
}
return new ImmutablePair<>(targetResourceType, searchValue);
}
private void handleIdentifier(QueryParameter queryParm, String paramAlias, WhereFragment whereClause) throws FHIRPersistenceException {
boolean parmValueProcessed = false;
for (QueryParameterValue value : queryParm.getValues()) {
// If multiple values are present, we need to OR them together.
if (parmValueProcessed) {
whereClause.or();
} else {
parmValueProcessed = true;
}
// Determine code system case-sensitivity
boolean codeSystemIsCaseSensitive = false;
if (value.getValueSystem() != null && !value.getValueSystem().isEmpty()) {
// Normalize code if code system is not case-sensitive. Otherwise leave code as is.
codeSystemIsCaseSensitive = CodeSystemSupport.isCaseSensitive(value.getValueSystem());
final String searchValue = SqlParameterEncoder.encode(codeSystemIsCaseSensitive ?
value.getValueCode() : SearchHelper.normalizeForSearch(value.getValueCode()));
// We have a code-system and a code so we must have a common_token_value if the tuple exists
Long commonTokenValueId = getCommonTokenValueId(value.getValueSystem(), searchValue);
whereClause.col(paramAlias, COMMON_TOKEN_VALUE_ID).eq(nullCheck(commonTokenValueId)); // use literal
} else {
// No code system specified, search against both normalized code and unmodified code.
// Build equivalent of: pX.token_value IN (search-attribute-value, normalized-search-sttribute-value)
final String normalizedValue = SearchHelper.normalizeForSearch(value.getValueCode());
Set ctvs = new HashSet<>();
fetchCommonTokenValues(ctvs, value.getValueCode());
fetchCommonTokenValues(ctvs, normalizedValue);
addCommonTokenValueIdFilter(whereClause, paramAlias, ctvs);
}
}
}
/**
* Use -1 as a simple substitute for null literal ids because we know -1 will never exist
* as a value in the database (for fields populated by sequence values).
* @param value
* @return
*/
protected int nullCheck(Integer value) {
return value == null ? -1 : value;
}
/**
* Use -1 as a simple substitute for null literal ids because we know -1 will never exist
* as a value in the database (for fields populated by sequence values).
* @param value
* @return
*/
protected long nullCheck(Long value) {
return value == null ? -1L : value;
}
/**
* Get the operator we need to use for matching values for this parameter
* @param queryParameter
* @return
*/
protected Operator getOperator(QueryParameter queryParameter) {
return OperatorUtil.getOperator(queryParameter);
}
/**
* Map the Modifier in the passed Parameter to a supported query operator. If
* the mapping results in the default
* operator, override the default operator with the passed operator if the
* passed operator is not null.
*
* @param queryParm
* - A valid query Parameter.
* @param defaultOverride
* - An operator that should override the default
* operator.
* @return A supported operator.
*/
protected Operator getOperator(QueryParameter queryParm, String defaultOverride) {
return OperatorUtil.getOperator(queryParm, defaultOverride);
}
/**
* Get the filter predicate expression for the given query parameter taking into account its type,
* modifiers etc.
* @param paramTableAlias
* @param queryParm
* @return a valid expression
*/
protected WhereFragment paramFilter(QueryParameter queryParm, String paramTableAlias) throws FHIRPersistenceException {
final WhereFragment result;
switch (queryParm.getType()) {
case URI:
case STRING:
result = getStringFilter(queryParm, paramTableAlias);
break;
case NUMBER:
result = getNumberFilter(queryParm, paramTableAlias);
break;
case QUANTITY:
result = getQuantityFilter(queryParm, paramTableAlias);
break;
case DATE:
result = getDateFilter(queryParm, paramTableAlias);
break;
case SPECIAL:
result = getLocationFilter(queryParm, paramTableAlias);
break;
case REFERENCE:
result = getReferenceFilter(queryParm, paramTableAlias);
break;
case TOKEN:
result = getTokenFilter(queryParm, paramTableAlias);
break;
default:
result = null;
break;
}
if (result == null) {
throw new FHIRPersistenceException("Nested composite parameters are not supported");
}
return result;
}
@Override
public QueryData addLocationPosition(QueryData queryData, List queryParameters) throws FHIRPersistenceException {
// Special handling for location position extension logic
NearLocationHandler handler = new NearLocationHandler();
List boundingAreas;
try {
boundingAreas = handler.generateLocationPositionsFromParameters(queryParameters);
} catch (FHIRSearchException e) {
throw new FHIRPersistenceException("input parameter is invalid bounding area, bad prefix, or bad units", e);
}
if (!boundingAreas.isEmpty()) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("buildLocationQuerySegment no longer needed");
}
}
return null;
}
@Override
public QueryData addInclusionParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
QueryParameter currentParm = queryParm;
if (queryParm.getNextParameter() == null) {
// just a single inclusion parameter, so we can optimize and treat as a simple join
// to the main parameter filter block
addFilter(queryData, resourceType, queryParm);
} else {
// Attach a series of exists clauses to the parameter query block
final WhereAdapter where = queryData.getQuery().from().where();
where.and().leftParen();
while (currentParm != null) {
// Add an exists clause for the given parameter
WhereFragment filter = getFilterPredicate(queryData, queryParm);
where.filter(filter.getExpression());
currentParm = currentParm.getNextParameter();
if (currentParm != null) {
where.or();
}
}
where.rightParen();
}
return queryData;
}
@Override
public QueryData addIncludeFilter(QueryData queryData, InclusionParameter inclusionParm, List logicalResourceIds) throws FHIRPersistenceException {
// Build the entire join for the include query (everything after the FROM)
// Versioned reference support. From the spec:
// > If a resource has a reference that is versioned and _include is performed,
// > the specified version SHOULD be provided.
/*
SELECT R0.RESOURCE_ID, R0.LOGICAL_RESOURCE_ID, R0.VERSION_ID, R0.LAST_UPDATED, R0.IS_DELETED, R0.DATA, R0.RESOURCE_PAYLOAD_KEY, LR0.LOGICAL_ID
FROM fhirdata.ExplanationOfBenefit_TOKEN_VALUES_V AS P1
INNER JOIN fhirdata.Claim_LOGICAL_RESOURCES AS LR0
ON LR0.LOGICAL_ID = P1.TOKEN_VALUE
AND P1.PARAMETER_NAME_ID = 9263
AND P1.CODE_SYSTEM_ID = 341729359
AND P1.LOGICAL_RESOURCE_ID IN (135010606,135010540,135010498,135010412,135010428)
INNER JOIN fhirdata.Claim_RESOURCES AS R0
ON LR0.LOGICAL_RESOURCE_ID = R0.LOGICAL_RESOURCE_ID
AND COALESCE(P1.REF_VERSION_ID,LR0.VERSION_ID) = R0.VERSION_ID
AND R0.IS_DELETED = 'N'
*
*/
final String joinResourceType = inclusionParm.getJoinResourceType();
final String targetResourceType = inclusionParm.getSearchParameterTargetType();
final int aliasIndex = getNextAliasIndex();
final String xxLogicalResources = targetResourceType + "_LOGICAL_RESOURCES";
final String xxResources = targetResourceType + "_RESOURCES";
final String paramAlias = getParamAlias(aliasIndex);
final String lrAlias = "LR0";
final String rAlias = "R0";
SelectAdapter select = queryData.getQuery();
if (inclusionParm.isCanonical()) {
final String joinStrValues = joinResourceType + "_STR_VALUES";
final String targetStrValues = targetResourceType + "_STR_VALUES";
final String nextParamAlias = getParamAlias(getNextAliasIndex());
final String nextPlus1ParamAlias = getParamAlias(getNextAliasIndex());
final String nextPlus2ParamAlias = getParamAlias(getNextAliasIndex());
final String sourceUriCode = SearchHelper.makeCompositeSubCode(inclusionParm.getSearchParameter() +
CANONICAL_SUFFIX, CANONICAL_COMPONENT_URI);
final String sourceVersionCode = SearchHelper.makeCompositeSubCode(inclusionParm.getSearchParameter() +
CANONICAL_SUFFIX, CANONICAL_COMPONENT_VERSION);
final String targetUriCode = SearchHelper.makeCompositeSubCode(URL + CANONICAL_SUFFIX,
CANONICAL_COMPONENT_URI);
final String targetVersionCode = SearchHelper.makeCompositeSubCode(URL + CANONICAL_SUFFIX,
CANONICAL_COMPONENT_VERSION);
select.from(joinStrValues, alias(paramAlias))
.innerJoin(targetStrValues, alias(nextParamAlias),
on(nextParamAlias, "STR_VALUE").eq(paramAlias, "STR_VALUE")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(sourceUriCode))
.and(nextParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(targetUriCode))
.and(paramAlias, "LOGICAL_RESOURCE_ID").inLiteralLong(logicalResourceIds))
.innerJoin(joinStrValues, alias(nextPlus1ParamAlias),
on(nextPlus1ParamAlias, "LOGICAL_RESOURCE_ID").eq(paramAlias, "LOGICAL_RESOURCE_ID")
.and(nextPlus1ParamAlias, "COMPOSITE_ID").eq(paramAlias, "COMPOSITE_ID")
.and(nextPlus1ParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(sourceVersionCode)))
.innerJoin(targetStrValues, alias(nextPlus2ParamAlias),
on(nextPlus2ParamAlias, "LOGICAL_RESOURCE_ID").eq(nextParamAlias, "LOGICAL_RESOURCE_ID")
.and(nextPlus2ParamAlias, "COMPOSITE_ID").eq(nextParamAlias, "COMPOSITE_ID")
.and(nextPlus2ParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(targetVersionCode))
.and()
.leftParen().col(nextPlus1ParamAlias, "STR_VALUE").isNull()
.or().col(nextPlus2ParamAlias, "STR_VALUE").eq().col(nextPlus1ParamAlias, "STR_VALUE")
.rightParen())
.innerJoin(xxLogicalResources, alias(lrAlias),
on(nextParamAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID"))
.innerJoin(xxResources, alias(rAlias),
on(lrAlias, "LOGICAL_RESOURCE_ID").eq(rAlias, "LOGICAL_RESOURCE_ID")
.and(lrAlias, "VERSION_ID").eq(rAlias, "VERSION_ID")
.and(rAlias, IS_DELETED).eq().literal("N"));
} else {
final String tokenValues = joinResourceType + "_TOKEN_VALUES_V";
select.from(tokenValues, alias(paramAlias))
.innerJoin(xxLogicalResources, alias(lrAlias),
on(lrAlias, "LOGICAL_ID").eq(paramAlias, "TOKEN_VALUE")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(inclusionParm.getSearchParameter()))
.and(paramAlias, "CODE_SYSTEM_ID").eq(getCodeSystemId(targetResourceType))
.and(paramAlias, "LOGICAL_RESOURCE_ID").inLiteralLong(logicalResourceIds))
.innerJoin(xxResources, alias(rAlias),
on(lrAlias, "LOGICAL_RESOURCE_ID").eq(rAlias, "LOGICAL_RESOURCE_ID")
.and().coalesce(col(paramAlias, "REF_VERSION_ID"), col(lrAlias, "VERSION_ID")).eq(rAlias, "VERSION_ID")
.and(rAlias, IS_DELETED).eq().literal("N"));
}
return queryData;
}
@Override
public QueryData addRevIncludeFilter(QueryData queryData, InclusionParameter inclusionParm, List logicalResourceIds) throws FHIRPersistenceException {
/* old query
* EXISTS (SELECT 1 FROM
* (
* SELECT
* LOGICAL_ID, VERSION_ID
* FROM
* _LOGICAL_RESOURCES LR
* WHERE
* LR.LOGICAL_RESOURCE_ID IN ()
* ) REFS
* JOIN _TOKEN_VALUES_V P1
* ON REFS.LOGICAL_ID = P1.TOKEN_VALUE
* AND COALESCE(P1.REF_VERSION_ID, REFS.VERSION_ID) = REFS.VERSION_ID
* AND P1.PARAMETER_NAME_ID = {n}
* AND P1.CODE_SYSTEM_ID = {n}
* JOIN _LOGICAL_RESOURCES LR
* ON P1.LOGICAL_RESOURCE_ID = LR.LOGICAL_RESOURCE_ID
* AND LR.IS_DELETED = 'N'
*/
final String joinResourceType = inclusionParm.getJoinResourceType();
final String targetResourceType = inclusionParm.getSearchParameterTargetType();
final int aliasIndex = getNextAliasIndex();
final SelectAdapter query = queryData.getQuery();
final String targetLR = targetResourceType + "_LOGICAL_RESOURCES";
final String parentLR = joinResourceType +"_LOGICAL_RESOURCES";
final String parentR = joinResourceType + "_RESOURCES";
final String paramAlias = getParamAlias(aliasIndex);
final String parentLRAlias = "LR0";
final String rAlias = "R0";
final String lrAlias = getLRAlias(aliasIndex);
// parentLR <- token_values <- logical_resources IN (123,456)
query.from(parentLR, alias(parentLRAlias));
if (inclusionParm.isCanonical()) {
final String joinStrValues = joinResourceType + "_STR_VALUES";
final String targetStrValues = targetResourceType + "_STR_VALUES";
final String nextParamAlias = getParamAlias(getNextAliasIndex());
final String nextPlus1ParamAlias = getParamAlias(getNextAliasIndex());
final String nextPlus2ParamAlias = getParamAlias(getNextAliasIndex());
final String sourceUriCode = SearchHelper.makeCompositeSubCode(inclusionParm.getSearchParameter() +
CANONICAL_SUFFIX, CANONICAL_COMPONENT_URI);
final String sourceVersionCode = SearchHelper.makeCompositeSubCode(inclusionParm.getSearchParameter() +
CANONICAL_SUFFIX, CANONICAL_COMPONENT_VERSION);
final String targetUriCode = SearchHelper.makeCompositeSubCode(URL + CANONICAL_SUFFIX,
CANONICAL_COMPONENT_URI);
final String targetVersionCode = SearchHelper.makeCompositeSubCode(URL + CANONICAL_SUFFIX,
CANONICAL_COMPONENT_VERSION);
query.from()
.innerJoin(joinStrValues, alias(paramAlias),
on(parentLRAlias, "LOGICAL_RESOURCE_ID").eq(paramAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(sourceUriCode)))
.innerJoin(targetStrValues, alias(nextParamAlias),
on(nextParamAlias, "STR_VALUE").eq(paramAlias, "STR_VALUE")
.and(nextParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(targetUriCode))
.and(nextParamAlias, "LOGICAL_RESOURCE_ID").inLiteralLong(logicalResourceIds))
.innerJoin(targetStrValues, alias(nextPlus1ParamAlias),
on(nextPlus1ParamAlias, "LOGICAL_RESOURCE_ID").eq(nextParamAlias, "LOGICAL_RESOURCE_ID")
.and(nextPlus1ParamAlias, "COMPOSITE_ID").eq(nextParamAlias, "COMPOSITE_ID")
.and(nextPlus1ParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(targetVersionCode)))
.innerJoin(joinStrValues, alias(nextPlus2ParamAlias),
on(nextPlus2ParamAlias, "LOGICAL_RESOURCE_ID").eq(paramAlias, "LOGICAL_RESOURCE_ID")
.and(nextPlus2ParamAlias, "COMPOSITE_ID").eq(paramAlias, "COMPOSITE_ID")
.and(nextPlus2ParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(sourceVersionCode))
.and()
.leftParen().col(nextPlus2ParamAlias, "STR_VALUE").isNull()
.or().col(nextPlus2ParamAlias, "STR_VALUE").eq().col(nextPlus1ParamAlias, "STR_VALUE")
.rightParen());
} else {
final String tokenValues = joinResourceType + "_TOKEN_VALUES_V";
query.from()
.innerJoin(tokenValues, alias(paramAlias),
on(parentLRAlias, "LOGICAL_RESOURCE_ID").eq(paramAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(inclusionParm.getSearchParameter()))
.and(paramAlias, "CODE_SYSTEM_ID").eq(getCodeSystemId(targetResourceType)))
.innerJoin(targetLR, alias(lrAlias),
on(lrAlias, "LOGICAL_ID").eq(paramAlias, "TOKEN_VALUE")
.and().coalesce(col(paramAlias, "REF_VERSION_ID"), col(lrAlias, "VERSION_ID")).eq(lrAlias, "VERSION_ID")
.and(lrAlias, "LOGICAL_RESOURCE_ID").inLiteralLong(logicalResourceIds));
}
query.from().innerJoin(parentR, alias(rAlias), on(parentLRAlias, "CURRENT_RESOURCE_ID").eq(rAlias, "RESOURCE_ID"));
return queryData;
}
@Override
public QueryData addWholeSystemDataFilter(QueryData queryData, String resourceType, List logicalResourceIds) throws FHIRPersistenceException {
// Build the IN clause for the logical resource IDs.
SelectAdapter select = queryData.getQuery();
select.from().where().and("LR", "LOGICAL_RESOURCE_ID").inLiteralLong(logicalResourceIds);
return queryData;
}
@Override
public QueryData addWholeSystemResourceTypeFilter(QueryData queryData, List resourceTypeIds) throws FHIRPersistenceException {
// Build the IN clause for the resource type IDs.
List longResourceTypeIds = resourceTypeIds.stream().map(i -> i.longValue()).collect(Collectors.toList());
SelectAdapter select = queryData.getQuery();
select.from().where().and("LR0", "RESOURCE_TYPE_ID").inLiteralLong(longResourceTypeIds);
return queryData;
}
@Override
public QueryData addTokenParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
// Add a join to the query. The NOT/NOT_IN modifiers are trickier because
// they need to be handled as a NOT EXISTS clause.
final int aliasIndex = getNextAliasIndex();
final SelectAdapter query = queryData.getQuery();
final String paramAlias = getParamAlias(aliasIndex);
final String lrAlias = queryData.getLRAlias(); // join to LR at the same query level
final ExpNode filter;
filter = getTokenFilter(queryParm, paramAlias).getExpression();
// which table we join against depends on the fields used by the filter expression
final String xxTokenValues = getTokenParamTable(filter, resourceType, paramAlias);
String parameterName = queryParm.getCode();
// Append the suffix for :text modifier
if (Modifier.TEXT.equals(queryParm.getModifier())) {
parameterName += SearchConstants.TEXT_MODIFIER_SUFFIX;
}
if (queryParm.getModifier() == Modifier.NOT || queryParm.getModifier() == Modifier.NOT_IN) {
// Use a nested NOT EXISTS (...) instead of a simple join
SelectAdapter exists = Select.select("1");
exists.from(xxTokenValues, alias(paramAlias))
.where(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID") // correlate with the main query
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(parameterName));
// add the filter predicate to the exists where clause
exists.from().where().and(filter);
query.from().where().and().notExists(exists.build());
} else {
// Attach the parameter table to the single parameter exists join
query.from().innerJoin(xxTokenValues, alias(paramAlias), on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(parameterName))
.and(filter));
}
// We're not changing the level, so we return the same queryData we were given
return queryData;
}
@Override
public QueryData addTagParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
return addGlobalTokenParam(queryData, queryParm,
Modifier.TEXT.equals(queryParm.getModifier()) ? "RESOURCE_TOKEN_REFS" :
isWholeSystemSearch(resourceType) ? "LOGICAL_RESOURCE_TAGS" : resourceType + "_TAGS");
}
@Override
public QueryData addSecurityParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
return addGlobalTokenParam(queryData, queryParm,
Modifier.TEXT.equals(queryParm.getModifier()) ? "RESOURCE_TOKEN_REFS" :
isWholeSystemSearch(resourceType) ? "LOGICAL_RESOURCE_SECURITY" : resourceType + "_SECURITY");
}
private QueryData addGlobalTokenParam(QueryData queryData, QueryParameter queryParm, String parameterTable) throws FHIRPersistenceException {
// Add a join to the query. The NOT/NOT_IN modifiers are trickier because
// they need to be handled as a NOT EXISTS clause.
final int aliasIndex = getNextAliasIndex();
final SelectAdapter query = queryData.getQuery();
final String paramAlias = getParamAlias(aliasIndex);
final String lrAlias = queryData.getLRAlias(); // join to LR at the same query level
// Global tokens are stored in their own parameter table and therefore don't need a
// parameter_name_id but otherwise are just like any other token.
final ExpNode filter = getTokenFilter(queryParm, paramAlias).getExpression();
boolean tokenValueSearch = false;
String tokenValuesAlias = paramAlias;
if (Modifier.IN.equals(queryParm.getModifier()) || Modifier.NOT_IN.equals(queryParm.getModifier()) ||
Modifier.ABOVE.equals(queryParm.getModifier()) || Modifier.BELOW.equals(queryParm.getModifier()) ||
Modifier.TEXT.equals(queryParm.getModifier())) {
// For a search against a global search parameter where we need access to the search parameter value, we
// have to join the common token values table (which contains the parameter value) to the parameter table,
// which only contains a common_token_value_id.
// Since the filter is using paramAlias and the filter will get ANDed to the common token values join, we
// need to use paramAlias as the common token values alias and generate a new alias for the parameter table.
tokenValuesAlias = getParamAlias(getNextAliasIndex());
tokenValueSearch = true;
} else {
ColumnExpNodeVisitor visitor = new ColumnExpNodeVisitor(); // gathers all columns used in the filter expression
Set columns = filter.visit(visitor);
if (columns.contains(DataDefinitionUtil.getQualifiedName(paramAlias, CODE_SYSTEM_ID)) ||
columns.contains(DataDefinitionUtil.getQualifiedName(paramAlias, TOKEN_VALUE))) {
tokenValuesAlias = getParamAlias(getNextAliasIndex());
tokenValueSearch = true;
}
}
if (Modifier.NOT.equals(queryParm.getModifier()) || Modifier.NOT_IN.equals(queryParm.getModifier())) {
// Use a nested NOT EXISTS (...) instead of a simple join
SelectAdapter exists = Select.select("1");
exists.from(parameterTable, alias(tokenValuesAlias))
.where(tokenValuesAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID"); // correlate with the main query
if (tokenValueSearch) {
// Join to common token values table and add filter predicate to the common token values join on clause
exists.from().innerJoin("COMMON_TOKEN_VALUES", alias(paramAlias), on(paramAlias, "COMMON_TOKEN_VALUE_ID")
.eq(tokenValuesAlias, "COMMON_TOKEN_VALUE_ID")
.and(filter));
} else {
// Add the filter predicate to the exists where clause
exists.from().where().and(filter);
}
// Add as a not exists to the main query
query.from().where().and().notExists(exists.build());
} else {
if (tokenValueSearch) {
if (Modifier.TEXT.equals(queryParm.getModifier())) {
query.from().where().and(tokenValuesAlias, "PARAMETER_NAME_ID")
.eq(getParameterNameId(queryParm.getCode() + SearchConstants.TEXT_MODIFIER_SUFFIX));
}
// Join the xx_TAGS table and the common token values table to the exists
query.from().innerJoin(parameterTable, alias(tokenValuesAlias), on(tokenValuesAlias, "LOGICAL_RESOURCE_ID")
.eq(lrAlias, "LOGICAL_RESOURCE_ID"));
query.from().innerJoin("COMMON_TOKEN_VALUES", alias(paramAlias), on(paramAlias, "COMMON_TOKEN_VALUE_ID")
.eq(tokenValuesAlias, "COMMON_TOKEN_VALUE_ID")
.and(filter));
} else {
// Attach the parameter table to the single parameter exists join
query.from().innerJoin(parameterTable, alias(tokenValuesAlias), on(tokenValuesAlias, "LOGICAL_RESOURCE_ID")
.eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(filter));
}
}
// We're not changing the level, so we return the same queryData we were given
return queryData;
}
@Override
public QueryData addStringParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
// Join to the string parameter table
// Attach an exists clause to filter the result based on the string query parameter definition
final int aliasIndex = getNextAliasIndex();
final String lrAlias = queryData.getLRAlias();
final String paramTableName;
if (isWholeSystemSearch(resourceType)) {
paramTableName = "STR_VALUES";
} else {
paramTableName = resourceType + "_STR_VALUES";
}
final String paramAlias = getParamAlias(aliasIndex);
final String parameterName = queryParm.getCode();
// Add the (non-trivial) filter predicate for string parameters
ExpNode filter = getStringFilter(queryParm, paramAlias).getExpression();
SelectAdapter query = queryData.getQuery();
query.from().innerJoin(paramTableName, alias(paramAlias), on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(parameterName))
.and(filter));
return queryData;
}
@Override
public QueryData addCanonicalParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
String code = queryParm.getCode();
if (PROFILE.equals(code)) {
return addProfileParam(queryData, resourceType, queryParm);
}
// Convert all values into CanonicalValues
boolean versionNotSpecified = true;
List canonicalValues = new ArrayList<>();
for (QueryParameterValue parmValue : queryParm.getValues()) {
CanonicalValue canonicalValue = CanonicalSupport.createCanonicalValueFrom(parmValue.getValueString());
if (canonicalValue.getVersion() != null) {
versionNotSpecified = false;
}
canonicalValues.add(canonicalValue);
}
if ((versionNotSpecified && URL.equals(code))) {
// If no version specified and 'url' search parameter, we can process as a normal string parameter
return addStringParam(queryData, resourceType, queryParm);
} else {
// Process as a composite parameter
String compositeCode = code + CANONICAL_SUFFIX;
QueryParameter compositeParameter = new QueryParameter(Type.COMPOSITE, compositeCode, null, null);
// For each value in the original query parameter, build a value in the composite parameter
for (CanonicalValue canonicalValue : canonicalValues) {
QueryParameterValue compositeValue = new QueryParameterValue();
// Build query parameter for the uri value
QueryParameterValue uriParameterValue = new QueryParameterValue();
uriParameterValue.setValueString(canonicalValue.getUri());
QueryParameter uriParameter = new QueryParameter(Type.URI,
SearchHelper.makeCompositeSubCode(compositeCode, CANONICAL_COMPONENT_URI),
null, null, Collections.singletonList(uriParameterValue));
compositeValue.addComponent(uriParameter);
// Build query parameter for the version value if specified
if (canonicalValue.getVersion() != null) {
QueryParameterValue versionParameterValue = new QueryParameterValue();
versionParameterValue.setValueString(canonicalValue.getVersion());
QueryParameter versionParameter = new QueryParameter(Type.URI,
SearchHelper.makeCompositeSubCode(compositeCode, CANONICAL_COMPONENT_VERSION),
null, null, Collections.singletonList(versionParameterValue));
compositeValue.addComponent(versionParameter);
}
compositeParameter.getValues().add(compositeValue);
}
// Now process as a composite parameter
return addCompositeParam(queryData, compositeParameter);
}
}
private QueryData addProfileParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
// Join to the canonical parameter table...which in this case means the xx_profiles table
final int aliasIndex = getNextAliasIndex();
final String lrAlias = queryData.getLRAlias();
final String paramTableName;
if (isWholeSystemSearch(resourceType)) {
paramTableName = "LOGICAL_RESOURCE_PROFILES";
} else {
paramTableName = resourceType + "_PROFILES";
}
final String paramAlias = getParamAlias(aliasIndex);
// Build the filter predicate for the canonical values, handling the parse
WhereFragment whereFragment = new WhereFragment();
whereFragment.leftParen();
boolean multiple = false;
for (QueryParameterValue value : queryParm.getValues()) {
// Concatenate multiple matches with an OR
if (multiple) {
whereFragment.or();
} else {
multiple = true;
}
// Reuse the same CanonicalSupport code used for param extraction to parse the search value
ResourceProfileRec rpc = CanonicalSupport.makeResourceProfileRec(null, resourceType, -1, -1, value.getValueString(), false);
int canonicalId = getCanonicalId(rpc.getCanonicalValue());
whereFragment.col(paramAlias, "CANONICAL_ID").eq(canonicalId);
// TODO double-check semantics of ABOVE and BELOW in this context
if (rpc.getVersion() != null && !rpc.getVersion().isEmpty()) {
if (queryParm.getModifier() == Modifier.ABOVE) {
whereFragment.and(paramAlias, "VERSION").gte().bind(rpc.getVersion());
} else if (queryParm.getModifier() == Modifier.BELOW) {
whereFragment.and(paramAlias, "VERSION").lt().bind(rpc.getVersion());
} else {
whereFragment.and(paramAlias, "VERSION").eq().bind(rpc.getVersion());
}
}
if (rpc.getFragment() != null && !rpc.getFragment().isEmpty()) {
whereFragment.and(paramAlias, "FRAGMENT").eq().bind(rpc.getFragment());
}
}
whereFragment.rightParen();
SelectAdapter query = queryData.getQuery();
query.from().innerJoin(paramTableName, alias(paramAlias), on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(whereFragment.getExpression()));
return queryData;
}
@Override
public QueryData addMissingParam(QueryData queryData, QueryParameter queryParm, boolean isMissing) throws FHIRPersistenceException {
// note that there's no filter here to look for a specific value. We simply want to know
// whether or not the parameter exists for a given resource
final String parameterName = queryParm.getCode();
final int aliasIndex = getNextAliasIndex();
final String resourceType = queryData.getResourceType();
final String paramTableName = paramValuesTableName(resourceType, queryParm);
final String lrAlias = queryData.getLRAlias();
final String paramAlias = getParamAlias(aliasIndex);
SelectAdapter exists = Select.select("1");
exists.from(paramTableName, alias(paramAlias))
.where(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID"); // correlate with the main query
// Do not need PARAMETER_NAME_ID clause for _profile, _tag, or _security parameters since they have
// their own tables.
if (this.legacyWholeSystemSearchParamsEnabled ||
(!PROFILE.equals(parameterName) && !SECURITY.equals(parameterName) && !TAG.equals(parameterName))) {
exists.from().where().and(paramAlias, PARAMETER_NAME_ID).eq(getParameterNameId(parameterName));
}
// Add the exists to the where clause of the main query which already has a predicate
// so we need to AND the exists
SelectAdapter query = queryData.getQuery();
if (isMissing) {
// parameter should be missing, i.e. not exist
query.from().where().and().notExists(exists.build());
} else {
// parameter should be not missing...i.e. it exists
query.from().where().and().exists(exists.build());
}
return queryData;
}
@Override
public QueryData addChained(QueryData queryData, QueryParameter currentParm) throws FHIRPersistenceException {
logger.entering(CLASSNAME, "addChained");
// In this variant, each chained element is added as join to the current statement. We still need
// to add the EXISTS clause when depth == 0 (the first element in the chain)
// AND EXISTS (SELECT 1
// FROM fhirdata.Observation_TOKEN_VALUES_V AS P1 -- Observation references to
// INNER JOIN fhirdata.Device_LOGICAL_RESOURCES AS LR1 -- Device
// ON LR1.LOGICAL_ID = P1.TOKEN_VALUE -- Device.LOGICAL_ID = Observation.device
// AND P1.PARAMETER_NAME_ID = 1234 -- Observation.device reference param
// AND P1.CODE_SYSTEM_ID = 4321 -- code-system for Device
// AND LR1.IS_DELETED = 'N' -- referenced Device is not deleted
// WHERE P1.LOGICAL_RESOURCE_ID = LR0.LOGICAL_RESOURCE_ID -- correlate parameter to parent
final String sourceResourceType = queryData.getResourceType();
final SelectAdapter currentSubQuery = queryData.getQuery();
final int aliasIndex = getNextAliasIndex();
final String targetResourceType = currentParm.getModifierResourceTypeName();
final String xxLogicalResources = targetResourceType + "_LOGICAL_RESOURCES";
final String lrAlias = getLRAlias(aliasIndex);
String paramAlias = getParamAlias(aliasIndex);
// Add this chain element as a join to the current query. For forward chaining,
// we need to join logical-resources and token-values
if (currentParm.isCanonical()) {
// Chain via the 'url' and 'version' search parameter values
final String sourceStrValues = sourceResourceType + "_STR_VALUES";
final String targetStrValues = targetResourceType + "_STR_VALUES";
final String nextParamAlias = getParamAlias(getNextAliasIndex());
final String nextPlus1ParamAlias = getParamAlias(getNextAliasIndex());
final String nextPlus2ParamAlias = getParamAlias(getNextAliasIndex());
final String sourceUriCode = SearchHelper.makeCompositeSubCode(currentParm.getCode() + CANONICAL_SUFFIX,
CANONICAL_COMPONENT_URI);
final String sourceVersionCode = SearchHelper.makeCompositeSubCode(currentParm.getCode() + CANONICAL_SUFFIX,
CANONICAL_COMPONENT_VERSION);
final String targetUriCode = SearchHelper.makeCompositeSubCode(URL + CANONICAL_SUFFIX,
CANONICAL_COMPONENT_URI);
final String targetVersionCode = SearchHelper.makeCompositeSubCode(URL + CANONICAL_SUFFIX,
CANONICAL_COMPONENT_VERSION);
currentSubQuery.from()
.innerJoin(sourceStrValues, alias(paramAlias),
on(paramAlias, "LOGICAL_RESOURCE_ID").eq(queryData.getLRAlias(), "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(sourceUriCode)))
.innerJoin(targetStrValues, alias(nextParamAlias),
on(nextParamAlias, "STR_VALUE").eq(paramAlias, "STR_VALUE")
.and(nextParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(targetUriCode)))
.innerJoin(sourceStrValues, alias(nextPlus1ParamAlias),
on(nextPlus1ParamAlias, "LOGICAL_RESOURCE_ID").eq(paramAlias, "LOGICAL_RESOURCE_ID")
.and(nextPlus1ParamAlias, "COMPOSITE_ID").eq(paramAlias, "COMPOSITE_ID")
.and(nextPlus1ParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(sourceVersionCode)))
.innerJoin(targetStrValues, alias(nextPlus2ParamAlias),
on(nextPlus2ParamAlias, "LOGICAL_RESOURCE_ID").eq(nextParamAlias, "LOGICAL_RESOURCE_ID")
.and(nextPlus2ParamAlias, "COMPOSITE_ID").eq(nextParamAlias, "COMPOSITE_ID")
.and(nextPlus2ParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(targetVersionCode))
.and()
.leftParen().col(nextPlus1ParamAlias, "STR_VALUE").isNull()
.or().col(nextPlus2ParamAlias, "STR_VALUE").eq().col(nextPlus1ParamAlias, "STR_VALUE")
.rightParen())
.innerJoin(xxLogicalResources, alias(lrAlias),
on(lrAlias, "LOGICAL_RESOURCE_ID").eq(nextPlus2ParamAlias, "LOGICAL_RESOURCE_ID")
.and(lrAlias, "IS_DELETED").eq().literal("N"));
paramAlias = nextPlus2ParamAlias;
} else {
// Chain via the logical ID
final String tokenValues = sourceResourceType + "_TOKEN_VALUES_V"; // because we need TOKEN_VALUE
currentSubQuery.from()
.innerJoin(tokenValues, alias(paramAlias),
on(paramAlias, "LOGICAL_RESOURCE_ID").eq(queryData.getLRAlias(), "LOGICAL_RESOURCE_ID"))
.innerJoin(xxLogicalResources, alias(lrAlias),
on(lrAlias, "LOGICAL_ID").eq(paramAlias, "TOKEN_VALUE")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(currentParm.getCode()))
.and(paramAlias, "CODE_SYSTEM_ID").eq(nullCheck(getCodeSystemId(targetResourceType)))
.and(lrAlias, "IS_DELETED").eq().literal("N"));
}
logger.exiting(CLASSNAME, "addChained");
// Return details of the aliases needed for future chain elements
return new QueryData(currentSubQuery, lrAlias, paramAlias, targetResourceType, queryData.getChainDepth()+1);
}
@Override
public void addFilter(QueryData queryData, String resourceType, QueryParameter currentParm) throws FHIRPersistenceException {
// A variant where we just use a simple join instead of an exists (sub-select) to implement
// the parameter filter.
logger.fine("chainDepth: " + queryData.getChainDepth());
final SelectAdapter currentSubQuery = queryData.getQuery();
final String code = currentParm.getCode();
final String lrAlias = queryData.getLRAlias();
if (ID.equals(code)) {
addIdFilter(queryData, resourceType, currentParm);
} else if (LAST_UPDATED.equals(code)) {
// Compute the _lastUpdated filter predicate for the given query parameter
NewLastUpdatedParmBehaviorUtil util = new NewLastUpdatedParmBehaviorUtil(lrAlias);
WhereFragment filter = new WhereFragment();
util.executeBehavior(filter, currentParm);
// Add the filter predicate to the where clause of the base query
currentSubQuery.from().where().and(filter.getExpression());
} else {
// A simple filter added as an exists clause to the current query
// AND EXISTS (SELECT 1
// FROM fhirdata.Patient_STR_VALUES AS P3 -- 'Patient string parameters'
// WHERE P3.LOGICAL_RESOURCE_ID = LR2.LOGICAL_RESOURCE_ID -- 'correlate to parent'
// AND P3.PARAMETER_NAME_ID = 123 -- 'name parameter'
// AND P3.STR_VALUE = 'Jones') -- 'name filter'
final int aliasIndex = getNextAliasIndex();
final String paramAlias = getParamAlias(aliasIndex);
WhereFragment pf = paramFilter(currentParm, paramAlias);
final String paramTable;
if (Type.TOKEN.equals(currentParm.getType()) &&
!(TAG.equals(currentParm.getCode()) || SECURITY.equals(currentParm.getCode()))) {
paramTable = getTokenParamTable(pf.getExpression(), queryData.getResourceType(), paramAlias);
} else {
paramTable = paramValuesTableName(queryData.getResourceType(), currentParm);
}
if (currentParm.getModifier() == Modifier.NOT) {
// Needs to be handled as a NOT EXISTS correlated subquery
SelectAdapter exists = Select.select("1");
exists.from(paramTable, alias(paramAlias))
.where(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID"); // correlate to parent query
if (this.legacyWholeSystemSearchParamsEnabled ||
(!PROFILE.equals(code) && !SECURITY.equals(code) && !TAG.equals(code))) {
exists.from().where().and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(currentParm.getCode()));
}
exists.from().where().and(pf.getExpression());
// Add the sub-query as a NOT EXISTS filter to the main query
currentSubQuery.from().where().and().notExists(exists.build());
} else {
// Filter the query by adding a join
if (this.legacyWholeSystemSearchParamsEnabled ||
(!PROFILE.equals(code) && !SECURITY.equals(code) && !TAG.equals(code))) {
currentSubQuery.from()
.innerJoin(paramTable, alias(paramAlias),
on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(currentParm.getCode()))
.and(pf.getExpression()));
} else {
currentSubQuery.from()
.innerJoin(paramTable, alias(paramAlias),
on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(pf.getExpression()));
}
}
}
}
@Override
public QueryData addReverseChained(QueryData queryData, QueryParameter currentParm) throws FHIRPersistenceException {
logger.entering(CLASSNAME, "addReverseChained");
// For reverse chaining, we connect the token-value (reference)
// back to the parent query LOGICAL_ID and an xx_LOGICAL_RESOURCES
// to provide the LOGICAL_ID as the target for future chain elements
// INNER JOIN fhirdata.Observation_TOKEN_VALUES_V AS P1
// AND LR0.LOGICAL_ID = P1.TOKEN_VALUE -- 'Patient.LOGICAL_ID = Observation.patient'
// AND LR0.VERSION_ID = COALESCE(P1.REF_VERSION_ID, LR0.VERSION_ID)
// AND P1.PARAMETER_NAME_ID = 1246 -- 'Observation.patient'
// AND P1.CODE_SYSTEM_ID = 6 -- 'code system for Patient references'
// INNER JOIN fhirdata.Observation_LOGICAL_RESOURCES LR1
// ON LR1.LOGICAL_RESOURCE_ID = P1.LOGICAL_RESOURCE_ID
final String refResourceType = queryData.getResourceType();
final SelectAdapter currentSubQuery = queryData.getQuery();
final int aliasIndex = getNextAliasIndex();
final String resourceTypeName = currentParm.getModifierResourceTypeName();
final String xxLogicalResources = resourceTypeName + "_LOGICAL_RESOURCES";
final String lrAlias = getLRAlias(aliasIndex);
final String lrPrevAlias = queryData.getLRAlias();
String paramAlias = getParamAlias(aliasIndex);
if (currentParm.isCanonical()) {
final String strValues = resourceTypeName + "_STR_VALUES";
final String refStrValues = refResourceType + "_STR_VALUES";
final String nextParamAlias = getParamAlias(getNextAliasIndex());
final String nextPlus1ParamAlias = getParamAlias(getNextAliasIndex());
final String nextPlus2ParamAlias = getParamAlias(getNextAliasIndex());
final String sourceUriCode = SearchHelper.makeCompositeSubCode(currentParm.getCode() + CANONICAL_SUFFIX,
CANONICAL_COMPONENT_URI);
final String sourceVersionCode = SearchHelper.makeCompositeSubCode(currentParm.getCode() + SearchConstants.CANONICAL_SUFFIX,
CANONICAL_COMPONENT_VERSION);
final String targetUriCode = SearchHelper.makeCompositeSubCode(URL + CANONICAL_SUFFIX,
CANONICAL_COMPONENT_URI);
final String targetVersionCode = SearchHelper.makeCompositeSubCode(URL + CANONICAL_SUFFIX,
CANONICAL_COMPONENT_VERSION);
currentSubQuery.from()
.innerJoin(refStrValues, alias(paramAlias),
on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrPrevAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(targetUriCode)))
.innerJoin(strValues, alias(nextParamAlias),
on(nextParamAlias, "STR_VALUE").eq(paramAlias, "STR_VALUE")
.and(nextParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(sourceUriCode)))
.innerJoin(refStrValues, alias(nextPlus1ParamAlias),
on(nextPlus1ParamAlias, "LOGICAL_RESOURCE_ID").eq(paramAlias, "LOGICAL_RESOURCE_ID")
.and(nextPlus1ParamAlias, "COMPOSITE_ID").eq(paramAlias, "COMPOSITE_ID")
.and(nextPlus1ParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(targetVersionCode)))
.innerJoin(strValues, alias(nextPlus2ParamAlias),
on(nextPlus2ParamAlias, "LOGICAL_RESOURCE_ID").eq(nextParamAlias, "LOGICAL_RESOURCE_ID")
.and(nextPlus2ParamAlias, "COMPOSITE_ID").eq(nextParamAlias, "COMPOSITE_ID")
.and(nextPlus2ParamAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(sourceVersionCode))
.and()
.leftParen().col(nextPlus2ParamAlias, "STR_VALUE").isNull()
.or().col(nextPlus2ParamAlias, "STR_VALUE").eq().col(nextPlus1ParamAlias, "STR_VALUE")
.rightParen());
paramAlias = nextPlus2ParamAlias;
} else {
final String tokenValues = resourceTypeName + "_TOKEN_VALUES_V";
currentSubQuery.from()
.innerJoin(tokenValues, alias(paramAlias),
on(lrPrevAlias, "LOGICAL_ID").eq(paramAlias, "TOKEN_VALUE") // correlate with the main query
.and(lrPrevAlias, "VERSION_ID").eq().coalesce(col(paramAlias, "REF_VERSION_ID"), col(lrPrevAlias, "VERSION_ID"))
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(currentParm.getCode()))
.and(paramAlias, "CODE_SYSTEM_ID").eq(nullCheck(getCodeSystemId(refResourceType))));
}
currentSubQuery.from()
.innerJoin(xxLogicalResources, alias(lrAlias),
on(lrAlias, "LOGICAL_RESOURCE_ID").eq(paramAlias, "LOGICAL_RESOURCE_ID")
.and(lrAlias, "IS_DELETED").eq().literal("N"));
// Return a new QueryData with the aliases configured to use by the next element in the chain
logger.exiting(CLASSNAME, "addReverseChained");
return new QueryData(currentSubQuery, lrAlias, paramAlias, resourceTypeName, queryData.getChainDepth()+1);
}
@Override
public QueryData addNumberParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
// Attach an exists clause to the query
final String parameterName = queryParm.getCode();
final int aliasIndex = getNextAliasIndex();
final SelectAdapter query = queryData.getQuery();
final String paramTableName = resourceType + "_NUMBER_VALUES";
final String paramAlias = getParamAlias(aliasIndex);
final String lrAlias = queryData.getLRAlias();
ExpNode filter = getNumberFilter(queryParm, paramAlias).getExpression();
query.from().innerJoin(paramTableName, alias(paramAlias), on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(parameterName))
.and(filter));
return queryData;
}
@Override
public QueryData addQuantityParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
final String parameterName = queryParm.getCode();
final int aliasIndex = getNextAliasIndex();
final SelectAdapter query = queryData.getQuery();
final String paramTableName = resourceType + "_QUANTITY_VALUES";
final String paramAlias = getParamAlias(aliasIndex);
final String lrAlias = queryData.getLRAlias();
ExpNode filter = getQuantityFilter(queryParm, paramAlias).getExpression();
query.from().innerJoin(paramTableName, alias(paramAlias), on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(parameterName))
.and(filter));
return queryData;
}
@Override
public QueryData addDateParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
final String parameterName = queryParm.getCode();
final int aliasIndex = getNextAliasIndex();
final SelectAdapter query = queryData.getQuery();
final String paramTableName = resourceType + "_DATE_VALUES";
final String paramAlias = getParamAlias(aliasIndex);
final String lrAlias = queryData.getLRAlias();
ExpNode filter = getDateFilter(queryParm, paramAlias).getExpression();
query.from().innerJoin(paramTableName, alias(paramAlias), on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(parameterName))
.and(filter));
return queryData;
}
@Override
public QueryData addLocationParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
final String parameterName = queryParm.getCode();
final int aliasIndex = getNextAliasIndex();
final SelectAdapter query = queryData.getQuery();
final String paramTableName = resourceType + "_LATLNG_VALUES";
final String paramAlias = getParamAlias(aliasIndex);
final String lrAlias = queryData.getLRAlias();
ExpNode filter = getLocationFilter(queryParm, paramAlias).getExpression();
query.from().innerJoin(paramTableName, alias(paramAlias), on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(parameterName))
.and(filter));
return queryData;
}
@Override
public QueryData addReferenceParam(QueryData queryData, String resourceType, QueryParameter queryParm) throws FHIRPersistenceException {
final int aliasIndex = getNextAliasIndex();
final SelectAdapter query = queryData.getQuery();
final String paramAlias = getParamAlias(aliasIndex);
final String lrAlias = queryData.getLRAlias();
// Grab the filter expression first. We can then inspect the expression to
// look for use of the TOKEN_VALUE column. If use of this column isn't found,
// we can apply an optimization by joining against the RESOURCE_TOKEN_REFS
// table directly.
ExpNode filter = getReferenceFilter(queryParm, paramAlias).getExpression();
final String paramTableName = getTokenParamTable(filter, resourceType, paramAlias);
// Append the suffix for :identifier modifier
String queryParmCode = queryParm.getCode();
if (Modifier.IDENTIFIER.equals(queryParm.getModifier())) {
queryParmCode += SearchConstants.IDENTIFIER_MODIFIER_SUFFIX;
}
query.from().innerJoin(paramTableName, alias(paramAlias), on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(queryParmCode))
.and(filter));
return queryData;
}
@Override
public QueryData addCompositeParam(QueryData queryData, QueryParameter queryParm) throws FHIRPersistenceException {
final String lrAlias = queryData.getLRAlias();
final WhereAdapter where = queryData.getQuery().from().where();
// Each query parm value gets its own EXISTS OR'd together
if (queryParm.getValues().size() == 1) {
// Simple optimization. Only one composite value, so add
// as inner joins to the core parameter exists query
QueryParameterValue compositeValue = queryParm.getValues().get(0);
List components = compositeValue.getComponent();
int firstAliasIndex = -1;
for (int componentNum = 1; componentNum <= components.size(); componentNum++) {
QueryParameter component = components.get(componentNum - 1);
int aliasIndex = addCompositeParamTable(queryData.getQuery(), queryData.getResourceType(), lrAlias, component, componentNum, firstAliasIndex);
if (componentNum == 1) {
// Remember the alias we use for the first component so we can join subsequent
// component tables to the first
firstAliasIndex = aliasIndex;
}
}
} else {
// Each value gets its own EXISTS clause which we combine together
// with OR. The whole thing needs to be wrapped in parens to ensure
// the correct precedence.
// AND ( EXISTS (...) OR EXISTS (...) )
where.and().leftParen();
boolean first = true;
for (QueryParameterValue compositeValue : queryParm.getValues()) {
SelectAdapter exists = Select.select("1");
List components = compositeValue.getComponent();
for (int componentNum = 1; componentNum <= components.size(); componentNum++) {
QueryParameter component = components.get(componentNum - 1);
addParamTableToCompositeExists(exists, queryData.getResourceType(), lrAlias,
component, componentNum, true);
}
// Add the exists sub-query we just built to the where clause of the main query
if (first) {
first = false;
} else {
where.or();
}
where.exists(exists.build());
}
// AND ( EXISTS (...) OR EXISTS (...) ) <== close the paren
where.rightParen();
}
// The only thing we can return which makes any sense is the original query
return queryData;
}
/**
* Add a parameter table filter for a composite parameter
* @param query
* @param resourceType
* @param lrAlias
* @param component
* @param componentNum
* @param firstAliasIndex
* @return the parameter alias, so we can find the first composite param table alias
* @throws FHIRPersistenceException
*/
private int addCompositeParamTable(SelectAdapter query, String resourceType, String lrAlias, QueryParameter component, int componentNum,
int firstAliasIndex) throws FHIRPersistenceException {
final int aliasIndex = getNextAliasIndex();
String paramTableAlias = getParamAlias(aliasIndex);
String parameterName = component.getCode();
// Grab the parameter filter expression first so that we can see if it's safe to apply
// the COMMON_TOKEN_VALUES_ID optimization
final ExpNode filter = paramFilter(component, paramTableAlias).getExpression();
final String valuesTable;
if (component.getType() == Type.TOKEN && filter != null) {
// optimize token parameter joins if the expression lets us
valuesTable = getTokenParamTable(filter, resourceType, paramTableAlias);
} else {
valuesTable = paramValuesTableName(resourceType, component);
}
if (componentNum == 1) {
query.from().innerJoin(valuesTable, alias(paramTableAlias),
on(paramTableAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(paramTableAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(parameterName))
.and(filter));
} else {
// also join to the first parameter table
final String firstTableAlias = getParamAlias(firstAliasIndex);
query.from().innerJoin(valuesTable, alias(paramTableAlias),
on(paramTableAlias, "LOGICAL_RESOURCE_ID").eq(firstTableAlias, "LOGICAL_RESOURCE_ID")
.and(paramTableAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(parameterName))
.and(paramTableAlias, "COMPOSITE_ID").eq(firstTableAlias, "COMPOSITE_ID")
.and(filter));
}
return aliasIndex;
}
/**
* Build the composite join by adding the parameter table for the given
* component number in the composite definition.
* @param exists
* @param resourceType
* @param lrAlias
* @param component
* @param componentNum
* @param addParamFilter
* @throws FHIRPersistenceException
*/
private void addParamTableToCompositeExists(SelectAdapter exists, String resourceType, String lrAlias,
QueryParameter component, int componentNum, boolean addParamFilter) throws FHIRPersistenceException {
String componentTableAlias = "comp" + componentNum;
String parameterName = component.getCode();
// Grab the parameter filter expression first so that we can see if it's safe to apply
// the COMMON_TOKEN_VALUES_ID optimization
final ExpNode filter;
if (addParamFilter) {
filter = paramFilter(component, componentTableAlias).getExpression();
} else {
filter = null;
}
final String valuesTable;
if (component.getType() == Type.TOKEN && filter != null) {
// optimize token parameter joins if the expression lets us
valuesTable = getTokenParamTable(filter, resourceType, componentTableAlias);
} else {
valuesTable = paramValuesTableName(resourceType, component);
}
if (componentNum == 1) {
exists.from(valuesTable, alias(componentTableAlias))
.where(componentTableAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID") // correlate with the main query
.and(componentTableAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(parameterName));
// Parameter filter is skipped if this is coming from a missing/not missing search
if (addParamFilter) {
exists.from().where().and(filter);
}
} else {
// Join to the first parameter table
final String firstTableAlias = "comp1";
exists.from().innerJoin(valuesTable, alias(componentTableAlias),
on(componentTableAlias, "LOGICAL_RESOURCE_ID").eq(firstTableAlias, "LOGICAL_RESOURCE_ID")
.and(componentTableAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(parameterName))
.and(componentTableAlias, "COMPOSITE_ID").eq(firstTableAlias, "COMPOSITE_ID"));
// Parameter filter is skipped if this is coming from a missing/not missing search
if (addParamFilter) {
exists.from().where().and(filter);
}
}
}
@Override
public QueryData addCompositeParam(QueryData queryData, QueryParameter queryParm, boolean isMissing) throws FHIRPersistenceException {
final String lrAlias = queryData.getLRAlias();
// Each value gets its own EXISTS clause which we combine together
// with OR. The whole thing needs to be wrapped in parens to ensure
// the correct precedence.
// AND ( EXISTS (...) OR EXISTS (...)
final WhereAdapter where = queryData.getQuery().from().where();
where.and().leftParen();
boolean first = true;
// Each query parm value gets its own EXISTS OR'd together
for (QueryParameterValue compositeValue : queryParm.getValues()) {
SelectAdapter exists = Select.select("1");
List components = compositeValue.getComponent();
for (int componentNum = 1; componentNum <= components.size(); componentNum++) {
QueryParameter component = components.get(componentNum - 1);
addParamTableToCompositeExists(exists, queryData.getResourceType(), lrAlias,
component, componentNum, false); // do not add param filter expression
}
// Add the exists sub-query we just built to the where clause of the main query
if (first) {
first = false;
} else {
where.or();
}
if (isMissing) {
// parameter should be missing, i.e. not exist
where.notExists(exists.build());
} else {
// parameter should be not missing...i.e. it exists
where.exists(exists.build());
}
}
// AND ( EXISTS (...) OR EXISTS (...) ) <== close the paren
where.rightParen();
// The only thing we can return which makes any sense is the original query
return queryData;
}
@Override
public void addSortParam(QueryData queryData, String code, Type type, Direction direction) throws FHIRPersistenceException {
// Each sort parameter gets added as parameter table which is outer-joined to the
// core data query
SelectAdapter query = queryData.getQuery();
final int aliasIndex = getNextAliasIndex();
final String paramAlias = getParamAlias(aliasIndex);
addAggregateAndOrderByExpressions(queryData, code, type, direction, paramAlias);
// Now add the parameter table as an outer join
final String paramTable = getSortParameterTableName(queryData.getResourceType(), code, type);
final String lrAlias = queryData.getLRAlias();
if (ID.equals(code) || LAST_UPDATED.equals(code)) {
// No need to join parameter table - sort column is in LOGICAL_RESOURCES table
return;
} else if (!this.legacyWholeSystemSearchParamsEnabled &&
(PROFILE.equals(code) || SECURITY.equals(code) || TAG.equals(code))) {
// For a sort by _tag, _profile, or _security we need to join the parameter-specific token
// table with the common token values table.
String parameterTableAlias = getParamAlias(getNextAliasIndex());
query.from()
.leftOuterJoin(paramTable, alias(parameterTableAlias),
on(parameterTableAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID"));
query.from()
.innerJoin("COMMON_TOKEN_VALUES", alias(paramAlias),
on(paramAlias, "COMMON_TOKEN_VALUE_ID").eq(parameterTableAlias, "COMMON_TOKEN_VALUE_ID"));
} else {
query.from()
.leftOuterJoin(paramTable, alias(paramAlias),
on(paramAlias, "LOGICAL_RESOURCE_ID").eq(lrAlias, "LOGICAL_RESOURCE_ID")
.and(paramAlias, "PARAMETER_NAME_ID").eq(getParameterNameId(code)));
}
}
/**
* Returns the name of the database table corresponding to the code and type of the
* passed sort parameter.
*
* @param code A SortParameter code
* @param type A SortParameter type
* @return String - A database table name
* @throws FHIRPersistenceException
*/
protected String getSortParameterTableName(String resourceType, String code, Type type) throws FHIRPersistenceException {
final String METHODNAME = "getSortParameterTableName";
logger.entering(CLASSNAME, METHODNAME);
StringBuilder sortParameterTableName = new StringBuilder();
sortParameterTableName.append(resourceType).append("_");
switch (type) {
case URI:
case STRING:
if (!this.legacyWholeSystemSearchParamsEnabled && PROFILE.equals(code)) {
sortParameterTableName.append("PROFILES");
} else {
sortParameterTableName.append("STR_VALUES");
}
break;
case DATE:
sortParameterTableName.append("DATE_VALUES");
break;
case REFERENCE:
case TOKEN:
if (!this.legacyWholeSystemSearchParamsEnabled && TAG.equals(code)) {
sortParameterTableName.append("TAGS");
} else if (!this.legacyWholeSystemSearchParamsEnabled && SECURITY.equals(code)) {
sortParameterTableName.append("SECURITY");
} else {
sortParameterTableName.append("TOKEN_VALUES_V");
}
break;
case NUMBER:
sortParameterTableName.append("NUMBER_VALUES");
break;
case QUANTITY:
sortParameterTableName.append("QUANTITY_VALUES");
break;
default:
throw new FHIRPersistenceNotSupportedException("Parm type not supported: " + type.value());
}
logger.exiting(CLASSNAME, METHODNAME);
return sortParameterTableName.toString();
}
/**
* Add the min/max aggregate and sort expressions to the SORT query
* @param queryData
* @param code
* @param type
* @param direction
* @param parmAlias
* @throws FHIRPersistenceException
*/
private void addAggregateAndOrderByExpressions(QueryData queryData, String code, Type type, Direction direction, String parmAlias)
throws FHIRPersistenceException {
final String METHODNAME = "addAggregateAndOrderByExpressions";
logger.entering(CLASSNAME, METHODNAME);
SelectAdapter query = queryData.getQuery();
List valueAttributeNames;
if (!this.legacyWholeSystemSearchParamsEnabled &&
(PROFILE.equals(code) || SECURITY.equals(code) || TAG.equals(code))) {
valueAttributeNames = Collections.singletonList(TOKEN_VALUE);
} else {
valueAttributeNames = this.getValueAttributeNames(type);
}
for (String attributeName : valueAttributeNames) {
StringBuilder expression = new StringBuilder();
final String dirExp;
if (direction == Direction.INCREASING) {
expression.append(MIN);
dirExp = "ASC";
} else {
expression.append(MAX);
dirExp = "DESC";
}
expression.append(LEFT_PAREN);
if (LAST_UPDATED.equals(code)) {
expression.append(queryData.getLRAlias() + ".LAST_UPDATED");
} else if (ID.equals(code)) {
expression.append(queryData.getLRAlias() + ".LOGICAL_ID");
} else {
expression.append(parmAlias).append(".").append(attributeName);
}
expression.append(RIGHT_PAREN);
// add the aggregate column expression to the select list clause
query.addColumn(null, expression.toString(), null);
// Add the column to the order by clause
expression.append(" ").append(dirExp).append(" NULLS LAST");
query.from().orderBy(expression.toString());
}
logger.exiting(CLASSNAME, METHODNAME);
}
/**
* Returns the names of the Parameter attributes containing the values
* corresponding to the passed sort parameter type.
* @param type
* @throws FHIRPersistenceException
*/
private List getValueAttributeNames(Type type) throws FHIRPersistenceException {
final String METHODNAME = "getValueAttributeName";
logger.entering(CLASSNAME, METHODNAME);
List attributeNames = new ArrayList<>();
switch (type) {
case STRING:
attributeNames.add(STR_VALUE);
break;
case REFERENCE:
attributeNames.add(TOKEN_VALUE);
break;
case DATE:
attributeNames.add(DATE_START);
break;
case TOKEN:
attributeNames.add(TOKEN_VALUE);
break;
case NUMBER:
attributeNames.add(NUMBER_VALUE);
break;
case QUANTITY:
attributeNames.add(QUANTITY_VALUE);
break;
case URI:
attributeNames.add(STR_VALUE);
break;
default:
throw new FHIRPersistenceNotSupportedException("Parm type not supported: " + type.value());
}
logger.exiting(CLASSNAME, METHODNAME);
return attributeNames;
}
/**
* Check if whole-system search.
* @param resourceType
* @return true if whole-system search, false otherwise
*/
private boolean isWholeSystemSearch(String resourceType) {
return Resource.class.getSimpleName().equals(resourceType);
}
/**
* Get the select column entry for the resource data column. If
* the includeResourceData flag is false, the column is replaced
* with a literal NULL, cast to the appropriate type for the database.
* @return
*/
private String getDataCol() {
if (this.includeResourceData) {
return "R.DATA";
} else {
switch (translator.getType()) {
case DB2:
case DERBY:
return "CAST(NULL AS BLOB) AS DATA";
case POSTGRESQL:
return "NULL::TEXT AS DATA";
default:
throw new IllegalStateException("Database type not supported: " + translator.getType().name());
}
}
}
}