001/* 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.search.builder; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeSearchParam; 024import ca.uhn.fhir.exception.TokenParamFormatInvalidRequestException; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.model.RequestPartitionId; 027import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 028import ca.uhn.fhir.jpa.dao.BaseStorageDao; 029import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; 030import ca.uhn.fhir.jpa.model.config.PartitionSettings; 031import ca.uhn.fhir.jpa.model.dao.JpaPid; 032import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel; 033import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; 034import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; 035import ca.uhn.fhir.jpa.search.builder.models.MissingParameterQueryParams; 036import ca.uhn.fhir.jpa.search.builder.models.MissingQueryParameterPredicateParams; 037import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderCacheKey; 038import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderCacheLookupResult; 039import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderTypeEnum; 040import ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder; 041import ca.uhn.fhir.jpa.search.builder.predicate.BaseQuantityPredicateBuilder; 042import ca.uhn.fhir.jpa.search.builder.predicate.BaseSearchParamPredicateBuilder; 043import ca.uhn.fhir.jpa.search.builder.predicate.ComboNonUniqueSearchParameterPredicateBuilder; 044import ca.uhn.fhir.jpa.search.builder.predicate.ComboUniqueSearchParameterPredicateBuilder; 045import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder; 046import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder; 047import ca.uhn.fhir.jpa.search.builder.predicate.ICanMakeMissingParamPredicate; 048import ca.uhn.fhir.jpa.search.builder.predicate.ISourcePredicateBuilder; 049import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder; 050import ca.uhn.fhir.jpa.search.builder.predicate.ParsedLocationParam; 051import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder; 052import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder; 053import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder; 054import ca.uhn.fhir.jpa.search.builder.predicate.SearchParamPresentPredicateBuilder; 055import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder; 056import ca.uhn.fhir.jpa.search.builder.predicate.TagPredicateBuilder; 057import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder; 058import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder; 059import ca.uhn.fhir.jpa.search.builder.sql.ColumnTupleObject; 060import ca.uhn.fhir.jpa.search.builder.sql.PredicateBuilderFactory; 061import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; 062import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 063import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor; 064import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil; 065import ca.uhn.fhir.jpa.searchparam.util.SourceParam; 066import ca.uhn.fhir.model.api.IQueryParameterAnd; 067import ca.uhn.fhir.model.api.IQueryParameterOr; 068import ca.uhn.fhir.model.api.IQueryParameterType; 069import ca.uhn.fhir.parser.DataFormatException; 070import ca.uhn.fhir.rest.api.Constants; 071import ca.uhn.fhir.rest.api.QualifiedParamList; 072import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 073import ca.uhn.fhir.rest.api.server.RequestDetails; 074import ca.uhn.fhir.rest.param.CompositeParam; 075import ca.uhn.fhir.rest.param.DateParam; 076import ca.uhn.fhir.rest.param.DateRangeParam; 077import ca.uhn.fhir.rest.param.HasParam; 078import ca.uhn.fhir.rest.param.NumberParam; 079import ca.uhn.fhir.rest.param.QuantityParam; 080import ca.uhn.fhir.rest.param.ReferenceParam; 081import ca.uhn.fhir.rest.param.SpecialParam; 082import ca.uhn.fhir.rest.param.StringParam; 083import ca.uhn.fhir.rest.param.TokenParam; 084import ca.uhn.fhir.rest.param.TokenParamModifier; 085import ca.uhn.fhir.rest.param.UriParam; 086import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 087import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 088import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 089import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 090import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 091import com.google.common.collect.Lists; 092import com.google.common.collect.Maps; 093import com.google.common.collect.Sets; 094import com.healthmarketscience.sqlbuilder.BinaryCondition; 095import com.healthmarketscience.sqlbuilder.ComboCondition; 096import com.healthmarketscience.sqlbuilder.Condition; 097import com.healthmarketscience.sqlbuilder.Expression; 098import com.healthmarketscience.sqlbuilder.InCondition; 099import com.healthmarketscience.sqlbuilder.SelectQuery; 100import com.healthmarketscience.sqlbuilder.SetOperationQuery; 101import com.healthmarketscience.sqlbuilder.Subquery; 102import com.healthmarketscience.sqlbuilder.UnionQuery; 103import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; 104import jakarta.annotation.Nullable; 105import org.apache.commons.lang3.StringUtils; 106import org.apache.commons.lang3.tuple.Triple; 107import org.hl7.fhir.instance.model.api.IAnyResource; 108import org.slf4j.Logger; 109import org.slf4j.LoggerFactory; 110import org.springframework.util.CollectionUtils; 111 112import java.math.BigDecimal; 113import java.util.ArrayList; 114import java.util.Collection; 115import java.util.Collections; 116import java.util.EnumSet; 117import java.util.HashMap; 118import java.util.List; 119import java.util.Map; 120import java.util.Objects; 121import java.util.Optional; 122import java.util.Set; 123import java.util.function.Supplier; 124import java.util.regex.Pattern; 125import java.util.stream.Collectors; 126 127import static ca.uhn.fhir.jpa.search.builder.QueryStack.SearchForIdsParams.with; 128import static ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder.getResourceIdColumn; 129import static ca.uhn.fhir.jpa.util.QueryParameterUtils.fromOperation; 130import static ca.uhn.fhir.jpa.util.QueryParameterUtils.getChainedPart; 131import static ca.uhn.fhir.jpa.util.QueryParameterUtils.getParamNameWithPrefix; 132import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toAndPredicate; 133import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toEqualToOrInPredicate; 134import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOperation; 135import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOrPredicate; 136import static ca.uhn.fhir.rest.api.Constants.PARAM_HAS; 137import static ca.uhn.fhir.rest.api.Constants.PARAM_ID; 138import static org.apache.commons.lang3.StringUtils.isBlank; 139import static org.apache.commons.lang3.StringUtils.isNotBlank; 140import static org.apache.commons.lang3.StringUtils.split; 141 142public class QueryStack { 143 144 private static final Logger ourLog = LoggerFactory.getLogger(QueryStack.class); 145 public static final String LOCATION_POSITION = "Location.position"; 146 private static final Pattern PATTERN_DOT_AND_ALL_AFTER = Pattern.compile("\\..*"); 147 148 private final FhirContext myFhirContext; 149 private final SearchQueryBuilder mySqlBuilder; 150 private final SearchParameterMap mySearchParameters; 151 private final ISearchParamRegistry mySearchParamRegistry; 152 private final PartitionSettings myPartitionSettings; 153 private final JpaStorageSettings myStorageSettings; 154 private final EnumSet<PredicateBuilderTypeEnum> myReusePredicateBuilderTypes; 155 private final RequestDetails myRequestDetails; 156 private Map<PredicateBuilderCacheKey, BaseJoiningPredicateBuilder> myJoinMap; 157 private Map<String, BaseJoiningPredicateBuilder> myParamNameToPredicateBuilderMap; 158 // used for _offset queries with sort, should be removed once the fix is applied to the async path too. 159 private boolean myUseAggregate; 160 private boolean myGroupingAdded; 161 162 /** 163 * Constructor 164 */ 165 public QueryStack( 166 RequestDetails theRequestDetails, 167 SearchParameterMap theSearchParameters, 168 JpaStorageSettings theStorageSettings, 169 FhirContext theFhirContext, 170 SearchQueryBuilder theSqlBuilder, 171 ISearchParamRegistry theSearchParamRegistry, 172 PartitionSettings thePartitionSettings) { 173 this( 174 theRequestDetails, 175 theSearchParameters, 176 theStorageSettings, 177 theFhirContext, 178 theSqlBuilder, 179 theSearchParamRegistry, 180 thePartitionSettings, 181 EnumSet.of(PredicateBuilderTypeEnum.DATE)); 182 } 183 184 /** 185 * Constructor 186 */ 187 private QueryStack( 188 RequestDetails theRequestDetails, 189 SearchParameterMap theSearchParameters, 190 JpaStorageSettings theStorageSettings, 191 FhirContext theFhirContext, 192 SearchQueryBuilder theSqlBuilder, 193 ISearchParamRegistry theSearchParamRegistry, 194 PartitionSettings thePartitionSettings, 195 EnumSet<PredicateBuilderTypeEnum> theReusePredicateBuilderTypes) { 196 myRequestDetails = theRequestDetails; 197 myPartitionSettings = thePartitionSettings; 198 assert theSearchParameters != null; 199 assert theStorageSettings != null; 200 assert theFhirContext != null; 201 assert theSqlBuilder != null; 202 203 mySearchParameters = theSearchParameters; 204 myStorageSettings = theStorageSettings; 205 myFhirContext = theFhirContext; 206 mySqlBuilder = theSqlBuilder; 207 mySearchParamRegistry = theSearchParamRegistry; 208 myReusePredicateBuilderTypes = theReusePredicateBuilderTypes; 209 } 210 211 public void addSortOnCoordsNear(String theParamName, boolean theAscending, SearchParameterMap theParams) { 212 boolean handled = false; 213 if (myParamNameToPredicateBuilderMap != null) { 214 BaseJoiningPredicateBuilder builder = myParamNameToPredicateBuilderMap.get(theParamName); 215 if (builder instanceof CoordsPredicateBuilder) { 216 CoordsPredicateBuilder coordsBuilder = (CoordsPredicateBuilder) builder; 217 218 List<List<IQueryParameterType>> params = theParams.get(theParamName); 219 if (!params.isEmpty() && !params.get(0).isEmpty()) { 220 IQueryParameterType param = params.get(0).get(0); 221 ParsedLocationParam location = ParsedLocationParam.from(theParams, param); 222 double latitudeValue = location.getLatitudeValue(); 223 double longitudeValue = location.getLongitudeValue(); 224 mySqlBuilder.addSortCoordsNear(coordsBuilder, latitudeValue, longitudeValue, theAscending); 225 handled = true; 226 } 227 } 228 } 229 230 if (!handled) { 231 String msg = myFhirContext 232 .getLocalizer() 233 .getMessageSanitized(QueryStack.class, "cantSortOnCoordParamWithoutValues", theParamName); 234 throw new InvalidRequestException(Msg.code(2307) + msg); 235 } 236 } 237 238 public void addSortOnDate(String theResourceName, String theParamName, boolean theAscending) { 239 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 240 DatePredicateBuilder datePredicateBuilder = mySqlBuilder.createDatePredicateBuilder(); 241 242 Condition hashIdentityPredicate = 243 datePredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 244 245 addSortCustomJoin(firstPredicateBuilder, datePredicateBuilder, hashIdentityPredicate); 246 247 mySqlBuilder.addSortDate(datePredicateBuilder.getColumnValueLow(), theAscending, myUseAggregate); 248 } 249 250 public void addSortOnLastUpdated(boolean theAscending) { 251 ResourceTablePredicateBuilder resourceTablePredicateBuilder; 252 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 253 if (firstPredicateBuilder instanceof ResourceTablePredicateBuilder) { 254 resourceTablePredicateBuilder = (ResourceTablePredicateBuilder) firstPredicateBuilder; 255 } else { 256 resourceTablePredicateBuilder = 257 mySqlBuilder.addResourceTablePredicateBuilder(firstPredicateBuilder.getJoinColumns()); 258 } 259 mySqlBuilder.addSortDate(resourceTablePredicateBuilder.getColumnLastUpdated(), theAscending, myUseAggregate); 260 } 261 262 public void addSortOnNumber(String theResourceName, String theParamName, boolean theAscending) { 263 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 264 NumberPredicateBuilder numberPredicateBuilder = mySqlBuilder.createNumberPredicateBuilder(); 265 266 Condition hashIdentityPredicate = 267 numberPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 268 269 addSortCustomJoin(firstPredicateBuilder, numberPredicateBuilder, hashIdentityPredicate); 270 271 mySqlBuilder.addSortNumeric(numberPredicateBuilder.getColumnValue(), theAscending, myUseAggregate); 272 } 273 274 public void addSortOnQuantity(String theResourceName, String theParamName, boolean theAscending) { 275 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 276 277 BaseQuantityPredicateBuilder quantityPredicateBuilder = mySqlBuilder.createQuantityPredicateBuilder(); 278 279 Condition hashIdentityPredicate = 280 quantityPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 281 282 addSortCustomJoin(firstPredicateBuilder, quantityPredicateBuilder, hashIdentityPredicate); 283 284 mySqlBuilder.addSortNumeric(quantityPredicateBuilder.getColumnValue(), theAscending, myUseAggregate); 285 } 286 287 public void addSortOnResourceId(boolean theAscending) { 288 ResourceTablePredicateBuilder resourceTablePredicateBuilder; 289 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 290 if (firstPredicateBuilder instanceof ResourceTablePredicateBuilder) { 291 resourceTablePredicateBuilder = (ResourceTablePredicateBuilder) firstPredicateBuilder; 292 } else { 293 resourceTablePredicateBuilder = 294 mySqlBuilder.addResourceTablePredicateBuilder(firstPredicateBuilder.getJoinColumns()); 295 } 296 mySqlBuilder.addSortString(resourceTablePredicateBuilder.getColumnFhirId(), theAscending, myUseAggregate); 297 } 298 299 /** Sort on RES_ID -- used to break ties for reliable sort */ 300 public void addSortOnResourcePID(boolean theAscending) { 301 BaseJoiningPredicateBuilder predicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 302 mySqlBuilder.addSortString(predicateBuilder.getResourceIdColumn(), theAscending); 303 } 304 305 public void addSortOnResourceLink( 306 String theResourceName, 307 String theReferenceTargetType, 308 String theParamName, 309 String theChain, 310 boolean theAscending, 311 SearchParameterMap theParams) { 312 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 313 ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = mySqlBuilder.createReferencePredicateBuilder(this); 314 315 Condition pathPredicate = 316 resourceLinkPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName); 317 318 addSortCustomJoin(firstPredicateBuilder, resourceLinkPredicateBuilder, pathPredicate); 319 320 if (isBlank(theChain)) { 321 mySqlBuilder.addSortNumeric( 322 resourceLinkPredicateBuilder.getColumnTargetResourceId(), theAscending, myUseAggregate); 323 return; 324 } 325 326 String targetType = null; 327 RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam( 328 theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 329 if (theReferenceTargetType != null) { 330 targetType = theReferenceTargetType; 331 } else if (param.getTargets().size() > 1) { 332 throw new InvalidRequestException(Msg.code(2287) + "Unable to sort on a chained parameter from '" 333 + theParamName + "' as this parameter has multiple target types. Please specify the target type."); 334 } else if (param.getTargets().size() == 1) { 335 targetType = param.getTargets().iterator().next(); 336 } 337 338 if (isBlank(targetType)) { 339 throw new InvalidRequestException( 340 Msg.code(2288) + "Unable to sort on a chained parameter from '" + theParamName 341 + "' as this parameter as this parameter does not define a target type. Please specify the target type."); 342 } 343 344 RuntimeSearchParam targetSearchParameter = mySearchParamRegistry.getActiveSearchParam( 345 targetType, theChain, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 346 if (targetSearchParameter == null) { 347 Collection<String> validSearchParameterNames = mySearchParamRegistry 348 .getActiveSearchParams(targetType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH) 349 .values() 350 .stream() 351 .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.STRING 352 || t.getParamType() == RestSearchParameterTypeEnum.TOKEN 353 || t.getParamType() == RestSearchParameterTypeEnum.DATE) 354 .map(RuntimeSearchParam::getName) 355 .sorted() 356 .distinct() 357 .collect(Collectors.toList()); 358 String msg = myFhirContext 359 .getLocalizer() 360 .getMessageSanitized( 361 BaseStorageDao.class, 362 "invalidSortParameter", 363 theChain, 364 targetType, 365 validSearchParameterNames); 366 throw new InvalidRequestException(Msg.code(2289) + msg); 367 } 368 369 // add a left-outer join to a predicate for the target type, then sort on value columns(s). 370 switch (targetSearchParameter.getParamType()) { 371 case STRING: 372 StringPredicateBuilder stringPredicateBuilder = mySqlBuilder.createStringPredicateBuilder(); 373 addSortCustomJoin( 374 resourceLinkPredicateBuilder.getJoinColumnsForTarget(), 375 stringPredicateBuilder, 376 stringPredicateBuilder.createHashIdentityPredicate(targetType, theChain)); 377 378 mySqlBuilder.addSortString( 379 stringPredicateBuilder.getColumnValueNormalized(), theAscending, myUseAggregate); 380 return; 381 382 case TOKEN: 383 TokenPredicateBuilder tokenPredicateBuilder = mySqlBuilder.createTokenPredicateBuilder(); 384 addSortCustomJoin( 385 resourceLinkPredicateBuilder.getJoinColumnsForTarget(), 386 tokenPredicateBuilder, 387 tokenPredicateBuilder.createHashIdentityPredicate(targetType, theChain)); 388 389 mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnSystem(), theAscending, myUseAggregate); 390 mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnValue(), theAscending, myUseAggregate); 391 return; 392 393 case DATE: 394 DatePredicateBuilder datePredicateBuilder = mySqlBuilder.createDatePredicateBuilder(); 395 addSortCustomJoin( 396 resourceLinkPredicateBuilder.getJoinColumnsForTarget(), 397 datePredicateBuilder, 398 datePredicateBuilder.createHashIdentityPredicate(targetType, theChain)); 399 400 mySqlBuilder.addSortDate(datePredicateBuilder.getColumnValueLow(), theAscending, myUseAggregate); 401 return; 402 403 /* 404 * Note that many of the options below aren't implemented because they 405 * don't seem useful to me, but they could theoretically be implemented 406 * if someone ever needed them. I'm not sure why you'd want to do a chained 407 * sort on a target that was a reference or a quantity, but if someone needed 408 * that we could implement it here. 409 */ 410 case SPECIAL: { 411 if (LOCATION_POSITION.equals(targetSearchParameter.getPath())) { 412 List<List<IQueryParameterType>> params = theParams.get(theParamName); 413 if (params != null && !params.isEmpty() && !params.get(0).isEmpty()) { 414 IQueryParameterType locationParam = params.get(0).get(0); 415 final SpecialParam specialParam = 416 new SpecialParam().setValue(locationParam.getValueAsQueryToken(myFhirContext)); 417 ParsedLocationParam location = ParsedLocationParam.from(theParams, specialParam); 418 double latitudeValue = location.getLatitudeValue(); 419 double longitudeValue = location.getLongitudeValue(); 420 final CoordsPredicateBuilder coordsPredicateBuilder = mySqlBuilder.addCoordsPredicateBuilder( 421 resourceLinkPredicateBuilder.getJoinColumnsForTarget()); 422 mySqlBuilder.addSortCoordsNear( 423 coordsPredicateBuilder, latitudeValue, longitudeValue, theAscending); 424 } else { 425 String msg = myFhirContext 426 .getLocalizer() 427 .getMessageSanitized( 428 QueryStack.class, "cantSortOnCoordParamWithoutValues", theParamName); 429 throw new InvalidRequestException(Msg.code(2497) + msg); 430 } 431 return; 432 } 433 } 434 //noinspection fallthrough 435 case NUMBER: 436 case REFERENCE: 437 case COMPOSITE: 438 case QUANTITY: 439 case URI: 440 case HAS: 441 442 default: 443 throw new InvalidRequestException(Msg.code(2290) + "Unable to sort on a chained parameter " 444 + theParamName + "." + theChain + " as this parameter. Can not sort on chains of target type: " 445 + targetSearchParameter.getParamType().name()); 446 } 447 } 448 449 public void addSortOnString(String theResourceName, String theParamName, boolean theAscending) { 450 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 451 452 StringPredicateBuilder stringPredicateBuilder = mySqlBuilder.createStringPredicateBuilder(); 453 Condition hashIdentityPredicate = 454 stringPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 455 456 addSortCustomJoin(firstPredicateBuilder, stringPredicateBuilder, hashIdentityPredicate); 457 458 mySqlBuilder.addSortString(stringPredicateBuilder.getColumnValueNormalized(), theAscending, myUseAggregate); 459 } 460 461 public void addSortOnToken(String theResourceName, String theParamName, boolean theAscending) { 462 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 463 464 TokenPredicateBuilder tokenPredicateBuilder = mySqlBuilder.createTokenPredicateBuilder(); 465 Condition hashIdentityPredicate = 466 tokenPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 467 468 addSortCustomJoin(firstPredicateBuilder, tokenPredicateBuilder, hashIdentityPredicate); 469 470 mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnSystem(), theAscending, myUseAggregate); 471 mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnValue(), theAscending, myUseAggregate); 472 } 473 474 public void addSortOnUri(String theResourceName, String theParamName, boolean theAscending) { 475 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 476 477 UriPredicateBuilder uriPredicateBuilder = mySqlBuilder.createUriPredicateBuilder(); 478 Condition hashIdentityPredicate = 479 uriPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 480 481 addSortCustomJoin(firstPredicateBuilder, uriPredicateBuilder, hashIdentityPredicate); 482 483 mySqlBuilder.addSortString(uriPredicateBuilder.getColumnValue(), theAscending, myUseAggregate); 484 } 485 486 private void addSortCustomJoin( 487 BaseJoiningPredicateBuilder theFromJoiningPredicateBuilder, 488 BaseJoiningPredicateBuilder theToJoiningPredicateBuilder, 489 Condition theCondition) { 490 addSortCustomJoin(theFromJoiningPredicateBuilder.getJoinColumns(), theToJoiningPredicateBuilder, theCondition); 491 } 492 493 private void addSortCustomJoin( 494 DbColumn[] theFromDbColumn, 495 BaseJoiningPredicateBuilder theToJoiningPredicateBuilder, 496 Condition theCondition) { 497 498 ComboCondition onCondition = 499 mySqlBuilder.createOnCondition(theFromDbColumn, theToJoiningPredicateBuilder.getJoinColumns()); 500 501 if (theCondition != null) { 502 onCondition.addCondition(theCondition); 503 } 504 505 mySqlBuilder.addCustomJoin( 506 SelectQuery.JoinType.LEFT_OUTER, 507 theFromDbColumn[0].getTable(), 508 theToJoiningPredicateBuilder.getTable(), 509 onCondition); 510 } 511 512 public void setUseAggregate(boolean theUseAggregate) { 513 myUseAggregate = theUseAggregate; 514 } 515 516 @SuppressWarnings("unchecked") 517 private <T extends BaseJoiningPredicateBuilder> PredicateBuilderCacheLookupResult<T> createOrReusePredicateBuilder( 518 PredicateBuilderTypeEnum theType, 519 DbColumn[] theSourceJoinColumn, 520 String theParamName, 521 Supplier<T> theFactoryMethod) { 522 boolean cacheHit = false; 523 BaseJoiningPredicateBuilder retVal; 524 if (myReusePredicateBuilderTypes.contains(theType)) { 525 PredicateBuilderCacheKey key = new PredicateBuilderCacheKey(theSourceJoinColumn, theType, theParamName); 526 if (myJoinMap == null) { 527 myJoinMap = new HashMap<>(); 528 } 529 retVal = myJoinMap.get(key); 530 if (retVal != null) { 531 cacheHit = true; 532 } else { 533 retVal = theFactoryMethod.get(); 534 myJoinMap.put(key, retVal); 535 } 536 } else { 537 retVal = theFactoryMethod.get(); 538 } 539 540 if (theType == PredicateBuilderTypeEnum.COORDS) { 541 if (myParamNameToPredicateBuilderMap == null) { 542 myParamNameToPredicateBuilderMap = new HashMap<>(); 543 } 544 myParamNameToPredicateBuilderMap.put(theParamName, retVal); 545 } 546 547 return new PredicateBuilderCacheLookupResult<>(cacheHit, (T) retVal); 548 } 549 550 private Condition createPredicateComposite( 551 @Nullable DbColumn[] theSourceJoinColumn, 552 String theResourceName, 553 String theSPNamePrefix, 554 RuntimeSearchParam theParamDef, 555 List<? extends IQueryParameterType> theNextAnd, 556 RequestPartitionId theRequestPartitionId) { 557 return createPredicateComposite( 558 theSourceJoinColumn, 559 theResourceName, 560 theSPNamePrefix, 561 theParamDef, 562 theNextAnd, 563 theRequestPartitionId, 564 mySqlBuilder); 565 } 566 567 private Condition createPredicateComposite( 568 @Nullable DbColumn[] theSourceJoinColumn, 569 String theResourceName, 570 String theSpnamePrefix, 571 RuntimeSearchParam theParamDef, 572 List<? extends IQueryParameterType> theNextAnd, 573 RequestPartitionId theRequestPartitionId, 574 SearchQueryBuilder theSqlBuilder) { 575 576 Condition orCondidtion = null; 577 for (IQueryParameterType next : theNextAnd) { 578 579 if (!(next instanceof CompositeParam<?, ?>)) { 580 throw new InvalidRequestException(Msg.code(1203) + "Invalid type for composite param (must be " 581 + CompositeParam.class.getSimpleName() + ": " + next.getClass()); 582 } 583 CompositeParam<?, ?> cp = (CompositeParam<?, ?>) next; 584 585 List<RuntimeSearchParam> componentParams = 586 JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParamDef); 587 RuntimeSearchParam left = componentParams.get(0); 588 IQueryParameterType leftValue = cp.getLeftValue(); 589 Condition leftPredicate = createPredicateCompositePart( 590 theSourceJoinColumn, 591 theResourceName, 592 theSpnamePrefix, 593 left, 594 leftValue, 595 theRequestPartitionId, 596 theSqlBuilder); 597 598 RuntimeSearchParam right = componentParams.get(1); 599 IQueryParameterType rightValue = cp.getRightValue(); 600 Condition rightPredicate = createPredicateCompositePart( 601 theSourceJoinColumn, 602 theResourceName, 603 theSpnamePrefix, 604 right, 605 rightValue, 606 theRequestPartitionId, 607 theSqlBuilder); 608 609 Condition andCondition = toAndPredicate(leftPredicate, rightPredicate); 610 611 if (orCondidtion == null) { 612 orCondidtion = toOrPredicate(andCondition); 613 } else { 614 orCondidtion = toOrPredicate(orCondidtion, andCondition); 615 } 616 } 617 618 return orCondidtion; 619 } 620 621 private Condition createPredicateCompositePart( 622 @Nullable DbColumn[] theSourceJoinColumn, 623 String theResourceName, 624 String theSpnamePrefix, 625 RuntimeSearchParam theParam, 626 IQueryParameterType theParamValue, 627 RequestPartitionId theRequestPartitionId, 628 SearchQueryBuilder theSqlBuilder) { 629 630 switch (theParam.getParamType()) { 631 case STRING: { 632 return createPredicateString( 633 theSourceJoinColumn, 634 theResourceName, 635 theSpnamePrefix, 636 theParam, 637 Collections.singletonList(theParamValue), 638 null, 639 theRequestPartitionId, 640 theSqlBuilder); 641 } 642 case TOKEN: { 643 return createPredicateToken( 644 theSourceJoinColumn, 645 theResourceName, 646 theSpnamePrefix, 647 theParam, 648 Collections.singletonList(theParamValue), 649 null, 650 theRequestPartitionId, 651 theSqlBuilder); 652 } 653 case DATE: { 654 return createPredicateDate( 655 theSourceJoinColumn, 656 theResourceName, 657 theSpnamePrefix, 658 theParam, 659 Collections.singletonList(theParamValue), 660 toOperation(((DateParam) theParamValue).getPrefix()), 661 theRequestPartitionId, 662 theSqlBuilder); 663 } 664 case QUANTITY: { 665 return createPredicateQuantity( 666 theSourceJoinColumn, 667 theResourceName, 668 theSpnamePrefix, 669 theParam, 670 Collections.singletonList(theParamValue), 671 null, 672 theRequestPartitionId, 673 theSqlBuilder); 674 } 675 case NUMBER: 676 case REFERENCE: 677 case COMPOSITE: 678 case URI: 679 case HAS: 680 case SPECIAL: 681 default: 682 throw new InvalidRequestException(Msg.code(1204) 683 + "Don't know how to handle composite parameter with type of " + theParam.getParamType()); 684 } 685 } 686 687 private Condition createMissingParameterQuery(MissingParameterQueryParams theParams) { 688 if (theParams.getParamType() == RestSearchParameterTypeEnum.COMPOSITE) { 689 ourLog.error("Cannot create missing parameter query for a composite parameter."); 690 return null; 691 } else if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { 692 if (isEligibleForEmbeddedChainedResourceSearch( 693 theParams.getResourceType(), theParams.getParamName(), theParams.getQueryParameterTypes()) 694 .supportsUplifted()) { 695 ourLog.error("Cannot construct missing query parameter search for ContainedResource REFERENCE search."); 696 return null; 697 } 698 } 699 700 // TODO - Change this when we have HFJ_SPIDX_MISSING table 701 /* 702 * How we search depends on if the 703 * {@link JpaStorageSettings#getIndexMissingFields()} property 704 * is Enabled or Disabled. 705 * 706 * If it is, we will use the SP_MISSING values set into the various 707 * SP_INDX_X tables and search on those ("old" search). 708 * 709 * If it is not set, however, we will try and construct a query that 710 * looks for missing SearchParameters in the SP_IDX_* tables ("new" search). 711 * 712 * You cannot mix and match, however (SP_MISSING is not in HASH_IDENTITY information). 713 * So setting (or unsetting) the IndexMissingFields 714 * property should always be followed up with a /$reindex call. 715 * 716 * --- 717 * 718 * Current limitations: 719 * Checking if a row exists ("new" search) for a given missing field in an SP_INDX_* table 720 * (ie, :missing=true) is slow when there are many resources in the table. (Defaults to 721 * a table scan, since HASH_IDENTITY isn't part of the index). 722 * 723 * However, the "old" search method was slow for the reverse: when looking for resources 724 * that do not have a missing field (:missing=false) for much the same reason. 725 */ 726 SearchQueryBuilder sqlBuilder = theParams.getSqlBuilder(); 727 if (myStorageSettings.getIndexMissingFields() == JpaStorageSettings.IndexEnabledEnum.DISABLED) { 728 // new search 729 return createMissingPredicateForUnindexedMissingFields(theParams, sqlBuilder); 730 } else { 731 // old search 732 return createMissingPredicateForIndexedMissingFields(theParams, sqlBuilder); 733 } 734 } 735 736 /** 737 * Old way of searching. 738 * Missing values must be indexed! 739 */ 740 private Condition createMissingPredicateForIndexedMissingFields( 741 MissingParameterQueryParams theParams, SearchQueryBuilder sqlBuilder) { 742 PredicateBuilderTypeEnum predicateType = null; 743 Supplier<? extends BaseJoiningPredicateBuilder> supplier = null; 744 switch (theParams.getParamType()) { 745 case STRING: 746 predicateType = PredicateBuilderTypeEnum.STRING; 747 supplier = () -> sqlBuilder.addStringPredicateBuilder(theParams.getSourceJoinColumn()); 748 break; 749 case NUMBER: 750 predicateType = PredicateBuilderTypeEnum.NUMBER; 751 supplier = () -> sqlBuilder.addNumberPredicateBuilder(theParams.getSourceJoinColumn()); 752 break; 753 case DATE: 754 predicateType = PredicateBuilderTypeEnum.DATE; 755 supplier = () -> sqlBuilder.addDatePredicateBuilder(theParams.getSourceJoinColumn()); 756 break; 757 case TOKEN: 758 predicateType = PredicateBuilderTypeEnum.TOKEN; 759 supplier = () -> sqlBuilder.addTokenPredicateBuilder(theParams.getSourceJoinColumn()); 760 break; 761 case QUANTITY: 762 predicateType = PredicateBuilderTypeEnum.QUANTITY; 763 supplier = () -> sqlBuilder.addQuantityPredicateBuilder(theParams.getSourceJoinColumn()); 764 break; 765 case REFERENCE: 766 case URI: 767 // we expect these values, but the pattern is slightly different; 768 // see below 769 break; 770 case HAS: 771 case SPECIAL: 772 predicateType = PredicateBuilderTypeEnum.COORDS; 773 supplier = () -> sqlBuilder.addCoordsPredicateBuilder(theParams.getSourceJoinColumn()); 774 break; 775 case COMPOSITE: 776 default: 777 break; 778 } 779 780 if (supplier != null) { 781 BaseSearchParamPredicateBuilder join = (BaseSearchParamPredicateBuilder) createOrReusePredicateBuilder( 782 predicateType, theParams.getSourceJoinColumn(), theParams.getParamName(), supplier) 783 .getResult(); 784 785 return join.createPredicateParamMissingForNonReference( 786 theParams.getResourceType(), 787 theParams.getParamName(), 788 theParams.isMissing(), 789 theParams.getRequestPartitionId()); 790 } else { 791 if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { 792 SearchParamPresentPredicateBuilder join = 793 sqlBuilder.addSearchParamPresentPredicateBuilder(theParams.getSourceJoinColumn()); 794 return join.createPredicateParamMissingForReference( 795 theParams.getResourceType(), 796 theParams.getParamName(), 797 theParams.isMissing(), 798 theParams.getRequestPartitionId()); 799 } else if (theParams.getParamType() == RestSearchParameterTypeEnum.URI) { 800 UriPredicateBuilder join = sqlBuilder.addUriPredicateBuilder(theParams.getSourceJoinColumn()); 801 return join.createPredicateParamMissingForNonReference( 802 theParams.getResourceType(), 803 theParams.getParamName(), 804 theParams.isMissing(), 805 theParams.getRequestPartitionId()); 806 } else { 807 // we don't expect to see this 808 ourLog.error("Invalid param type " + theParams.getParamType().name()); 809 return null; 810 } 811 } 812 } 813 814 /** 815 * New way of searching for missing fields. 816 * Missing values must not indexed! 817 */ 818 private Condition createMissingPredicateForUnindexedMissingFields( 819 MissingParameterQueryParams theParams, SearchQueryBuilder sqlBuilder) { 820 ResourceTablePredicateBuilder table = sqlBuilder.getOrCreateResourceTablePredicateBuilder(); 821 822 ICanMakeMissingParamPredicate innerQuery = PredicateBuilderFactory.createPredicateBuilderForParamType( 823 theParams.getParamType(), theParams.getSqlBuilder(), this); 824 return innerQuery.createPredicateParamMissingValue(new MissingQueryParameterPredicateParams( 825 table, theParams.isMissing(), theParams.getParamName(), theParams.getRequestPartitionId())); 826 } 827 828 public Condition createPredicateCoords( 829 @Nullable DbColumn[] theSourceJoinColumn, 830 String theResourceName, 831 String theSpnamePrefix, 832 RuntimeSearchParam theSearchParam, 833 List<? extends IQueryParameterType> theList, 834 RequestPartitionId theRequestPartitionId, 835 SearchQueryBuilder theSqlBuilder) { 836 Boolean isMissing = theList.get(0).getMissing(); 837 if (isMissing != null) { 838 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 839 840 return createMissingParameterQuery(new MissingParameterQueryParams( 841 theSqlBuilder, 842 theSearchParam.getParamType(), 843 theList, 844 paramName, 845 theResourceName, 846 theSourceJoinColumn, 847 theRequestPartitionId)); 848 } else { 849 CoordsPredicateBuilder predicateBuilder = createOrReusePredicateBuilder( 850 PredicateBuilderTypeEnum.COORDS, 851 theSourceJoinColumn, 852 theSearchParam.getName(), 853 () -> mySqlBuilder.addCoordsPredicateBuilder(theSourceJoinColumn)) 854 .getResult(); 855 856 List<Condition> codePredicates = new ArrayList<>(); 857 for (IQueryParameterType nextOr : theList) { 858 Condition singleCode = predicateBuilder.createPredicateCoords( 859 mySearchParameters, 860 nextOr, 861 theResourceName, 862 theSearchParam, 863 predicateBuilder, 864 theRequestPartitionId); 865 codePredicates.add(singleCode); 866 } 867 868 return predicateBuilder.combineWithRequestPartitionIdPredicate( 869 theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); 870 } 871 } 872 873 public Condition createPredicateDate( 874 @Nullable DbColumn[] theSourceJoinColumn, 875 String theResourceName, 876 String theSpnamePrefix, 877 RuntimeSearchParam theSearchParam, 878 List<? extends IQueryParameterType> theList, 879 SearchFilterParser.CompareOperation theOperation, 880 RequestPartitionId theRequestPartitionId) { 881 return createPredicateDate( 882 theSourceJoinColumn, 883 theResourceName, 884 theSpnamePrefix, 885 theSearchParam, 886 theList, 887 theOperation, 888 theRequestPartitionId, 889 mySqlBuilder); 890 } 891 892 public Condition createPredicateDate( 893 @Nullable DbColumn[] theSourceJoinColumn, 894 String theResourceName, 895 String theSpnamePrefix, 896 RuntimeSearchParam theSearchParam, 897 List<? extends IQueryParameterType> theList, 898 SearchFilterParser.CompareOperation theOperation, 899 RequestPartitionId theRequestPartitionId, 900 SearchQueryBuilder theSqlBuilder) { 901 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 902 903 Boolean isMissing = theList.get(0).getMissing(); 904 if (isMissing != null) { 905 return createMissingParameterQuery(new MissingParameterQueryParams( 906 theSqlBuilder, 907 theSearchParam.getParamType(), 908 theList, 909 paramName, 910 theResourceName, 911 theSourceJoinColumn, 912 theRequestPartitionId)); 913 } else { 914 PredicateBuilderCacheLookupResult<DatePredicateBuilder> predicateBuilderLookupResult = 915 createOrReusePredicateBuilder( 916 PredicateBuilderTypeEnum.DATE, 917 theSourceJoinColumn, 918 paramName, 919 () -> theSqlBuilder.addDatePredicateBuilder(theSourceJoinColumn)); 920 DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult(); 921 boolean cacheHit = predicateBuilderLookupResult.isCacheHit(); 922 923 List<Condition> codePredicates = new ArrayList<>(); 924 925 for (IQueryParameterType nextOr : theList) { 926 Condition p = predicateBuilder.createPredicateDateWithoutIdentityPredicate(nextOr, theOperation); 927 codePredicates.add(p); 928 } 929 930 Condition predicate = toOrPredicate(codePredicates); 931 932 if (!cacheHit) { 933 predicate = predicateBuilder.combineWithHashIdentityPredicate(theResourceName, paramName, predicate); 934 predicate = predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); 935 } 936 937 return predicate; 938 } 939 } 940 941 private Condition createPredicateFilter( 942 QueryStack theQueryStack3, 943 SearchFilterParser.BaseFilter theFilter, 944 String theResourceName, 945 RequestPartitionId theRequestPartitionId) { 946 947 if (theFilter instanceof SearchFilterParser.FilterParameter) { 948 return createPredicateFilter( 949 theQueryStack3, 950 (SearchFilterParser.FilterParameter) theFilter, 951 theResourceName, 952 theRequestPartitionId); 953 } else if (theFilter instanceof SearchFilterParser.FilterLogical) { 954 // Left side 955 Condition xPredicate = createPredicateFilter( 956 theQueryStack3, 957 ((SearchFilterParser.FilterLogical) theFilter).getFilter1(), 958 theResourceName, 959 theRequestPartitionId); 960 961 // Right side 962 Condition yPredicate = createPredicateFilter( 963 theQueryStack3, 964 ((SearchFilterParser.FilterLogical) theFilter).getFilter2(), 965 theResourceName, 966 theRequestPartitionId); 967 968 if (((SearchFilterParser.FilterLogical) theFilter).getOperation() 969 == SearchFilterParser.FilterLogicalOperation.and) { 970 return ComboCondition.and(xPredicate, yPredicate); 971 } else if (((SearchFilterParser.FilterLogical) theFilter).getOperation() 972 == SearchFilterParser.FilterLogicalOperation.or) { 973 return ComboCondition.or(xPredicate, yPredicate); 974 } else { 975 // Shouldn't happen 976 throw new InvalidRequestException(Msg.code(1205) + "Don't know how to handle operation " 977 + ((SearchFilterParser.FilterLogical) theFilter).getOperation()); 978 } 979 } else { 980 return createPredicateFilter( 981 theQueryStack3, 982 ((SearchFilterParser.FilterParameterGroup) theFilter).getContained(), 983 theResourceName, 984 theRequestPartitionId); 985 } 986 } 987 988 private Condition createPredicateFilter( 989 QueryStack theQueryStack3, 990 SearchFilterParser.FilterParameter theFilter, 991 String theResourceName, 992 RequestPartitionId theRequestPartitionId) { 993 994 String paramName = theFilter.getParamPath().getName(); 995 996 switch (paramName) { 997 case IAnyResource.SP_RES_ID: { 998 TokenParam param = new TokenParam(); 999 param.setValueAsQueryToken(null, null, null, theFilter.getValue()); 1000 return theQueryStack3.createPredicateResourceId( 1001 null, 1002 Collections.singletonList(Collections.singletonList(param)), 1003 theResourceName, 1004 theFilter.getOperation(), 1005 theRequestPartitionId); 1006 } 1007 case Constants.PARAM_SOURCE: { 1008 TokenParam param = new TokenParam(); 1009 param.setValueAsQueryToken(null, null, null, theFilter.getValue()); 1010 return createPredicateSource(null, Collections.singletonList(param)); 1011 } 1012 default: 1013 RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam( 1014 theResourceName, paramName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 1015 if (searchParam == null) { 1016 Collection<String> validNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta( 1017 theResourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 1018 String msg = myFhirContext 1019 .getLocalizer() 1020 .getMessageSanitized( 1021 BaseStorageDao.class, 1022 "invalidSearchParameter", 1023 paramName, 1024 theResourceName, 1025 validNames); 1026 throw new InvalidRequestException(Msg.code(1206) + msg); 1027 } 1028 RestSearchParameterTypeEnum typeEnum = searchParam.getParamType(); 1029 if (typeEnum == RestSearchParameterTypeEnum.URI) { 1030 return theQueryStack3.createPredicateUri( 1031 null, 1032 theResourceName, 1033 null, 1034 searchParam, 1035 Collections.singletonList(new UriParam(theFilter.getValue())), 1036 theFilter.getOperation(), 1037 theRequestPartitionId); 1038 } else if (typeEnum == RestSearchParameterTypeEnum.STRING) { 1039 return theQueryStack3.createPredicateString( 1040 null, 1041 theResourceName, 1042 null, 1043 searchParam, 1044 Collections.singletonList(new StringParam(theFilter.getValue())), 1045 theFilter.getOperation(), 1046 theRequestPartitionId); 1047 } else if (typeEnum == RestSearchParameterTypeEnum.DATE) { 1048 return theQueryStack3.createPredicateDate( 1049 null, 1050 theResourceName, 1051 null, 1052 searchParam, 1053 Collections.singletonList( 1054 new DateParam(fromOperation(theFilter.getOperation()), theFilter.getValue())), 1055 theFilter.getOperation(), 1056 theRequestPartitionId); 1057 } else if (typeEnum == RestSearchParameterTypeEnum.NUMBER) { 1058 return theQueryStack3.createPredicateNumber( 1059 null, 1060 theResourceName, 1061 null, 1062 searchParam, 1063 Collections.singletonList(new NumberParam(theFilter.getValue())), 1064 theFilter.getOperation(), 1065 theRequestPartitionId); 1066 } else if (typeEnum == RestSearchParameterTypeEnum.REFERENCE) { 1067 SearchFilterParser.CompareOperation operation = theFilter.getOperation(); 1068 String resourceType = 1069 null; // The value can either have (Patient/123) or not have (123) a resource type, either 1070 // way it's not needed here 1071 String chain = (theFilter.getParamPath().getNext() != null) 1072 ? theFilter.getParamPath().getNext().toString() 1073 : null; 1074 String value = theFilter.getValue(); 1075 ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value); 1076 return theQueryStack3.createPredicateReference( 1077 null, 1078 theResourceName, 1079 paramName, 1080 new ArrayList<>(), 1081 Collections.singletonList(referenceParam), 1082 operation, 1083 theRequestPartitionId); 1084 } else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) { 1085 return theQueryStack3.createPredicateQuantity( 1086 null, 1087 theResourceName, 1088 null, 1089 searchParam, 1090 Collections.singletonList(new QuantityParam(theFilter.getValue())), 1091 theFilter.getOperation(), 1092 theRequestPartitionId); 1093 } else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) { 1094 throw new InvalidRequestException(Msg.code(1207) 1095 + "Composite search parameters not currently supported with _filter clauses"); 1096 } else if (typeEnum == RestSearchParameterTypeEnum.TOKEN) { 1097 TokenParam param = new TokenParam(); 1098 param.setValueAsQueryToken(null, null, null, theFilter.getValue()); 1099 return theQueryStack3.createPredicateToken( 1100 null, 1101 theResourceName, 1102 null, 1103 searchParam, 1104 Collections.singletonList(param), 1105 theFilter.getOperation(), 1106 theRequestPartitionId); 1107 } 1108 break; 1109 } 1110 return null; 1111 } 1112 1113 private Condition createPredicateHas( 1114 @Nullable DbColumn[] theSourceJoinColumn, 1115 String theResourceType, 1116 List<List<IQueryParameterType>> theHasParameters, 1117 RequestDetails theRequest, 1118 RequestPartitionId theRequestPartitionId) { 1119 1120 List<Condition> andPredicates = new ArrayList<>(); 1121 for (List<? extends IQueryParameterType> nextOrList : theHasParameters) { 1122 1123 String targetResourceType = null; 1124 String paramReference = null; 1125 String parameterName = null; 1126 1127 String paramName = null; 1128 List<QualifiedParamList> parameters = new ArrayList<>(); 1129 for (IQueryParameterType nextParam : nextOrList) { 1130 HasParam next = (HasParam) nextParam; 1131 targetResourceType = next.getTargetResourceType(); 1132 paramReference = next.getReferenceFieldName(); 1133 parameterName = next.getParameterName(); 1134 paramName = PATTERN_DOT_AND_ALL_AFTER.matcher(parameterName).replaceAll(""); 1135 parameters.add(QualifiedParamList.singleton(null, next.getValueAsQueryToken(myFhirContext))); 1136 } 1137 1138 if (paramName == null) { 1139 continue; 1140 } 1141 1142 try { 1143 myFhirContext.getResourceDefinition(targetResourceType); 1144 } catch (DataFormatException e) { 1145 throw new InvalidRequestException(Msg.code(1208) + "Invalid resource type: " + targetResourceType); 1146 } 1147 1148 ArrayList<IQueryParameterType> orValues = Lists.newArrayList(); 1149 1150 if (paramName.startsWith("_has:")) { 1151 1152 ourLog.trace("Handling double _has query: {}", paramName); 1153 1154 String qualifier = paramName.substring(4); 1155 for (IQueryParameterType next : nextOrList) { 1156 HasParam nextHasParam = new HasParam(); 1157 nextHasParam.setValueAsQueryToken( 1158 myFhirContext, PARAM_HAS, qualifier, next.getValueAsQueryToken(myFhirContext)); 1159 orValues.add(nextHasParam); 1160 } 1161 1162 } else if (paramName.equals(PARAM_ID)) { 1163 1164 for (IQueryParameterType next : nextOrList) { 1165 orValues.add(new TokenParam(next.getValueAsQueryToken(myFhirContext))); 1166 } 1167 1168 } else { 1169 1170 // Ensure that the name of the search param 1171 // (e.g. the `code` in Patient?_has:Observation:subject:code=sys|val) 1172 // exists on the target resource type. 1173 RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getRuntimeSearchParam( 1174 targetResourceType, paramName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 1175 1176 // Ensure that the name of the back-referenced search param on the target (e.g. the `subject` in 1177 // Patient?_has:Observation:subject:code=sys|val) 1178 // exists on the target resource, or in the top-level Resource resource. 1179 mySearchParamRegistry.getRuntimeSearchParam( 1180 targetResourceType, paramReference, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 1181 1182 IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams( 1183 mySearchParamRegistry, myFhirContext, owningParameterDef, paramName, parameters); 1184 1185 for (IQueryParameterOr<?> next : parsedParam.getValuesAsQueryTokens()) { 1186 orValues.addAll(next.getValuesAsQueryTokens()); 1187 } 1188 } 1189 1190 // Handle internal chain inside the has. 1191 if (parameterName.contains(".")) { 1192 // Previously, for some unknown reason, we were calling getChainedPart() twice. This broke the _has 1193 // then chain, then _has use case by effectively cutting off the second part of the chain and 1194 // missing one iteration of the recursive call to build the query. 1195 // So, for example, for 1196 // Practitioner?_has:ExplanationOfBenefit:care-team:coverage.payor._has:List:item:_id=list1 1197 // instead of passing " payor._has:List:item:_id=list1" to the next recursion, the second call to 1198 // getChainedPart() was wrongly removing "payor." and passing down "_has:List:item:_id=list1" instead. 1199 // This resulted in running incorrect SQL with nonsensical join that resulted in 0 results. 1200 // However, after running the pipeline, I've concluded there's no use case at all for the 1201 // double call to "getChainedPart()", which is why there's no conditional logic at all to make a double 1202 // call to getChainedPart(). 1203 final String chainedPart = getChainedPart(parameterName); 1204 1205 orValues.stream() 1206 .filter(qp -> qp instanceof ReferenceParam) 1207 .map(qp -> (ReferenceParam) qp) 1208 .forEach(rp -> rp.setChain(chainedPart)); 1209 1210 parameterName = parameterName.substring(0, parameterName.indexOf('.')); 1211 } 1212 1213 int colonIndex = parameterName.indexOf(':'); 1214 if (colonIndex != -1) { 1215 parameterName = parameterName.substring(0, colonIndex); 1216 } 1217 1218 ResourceLinkPredicateBuilder resourceLinkTableJoin = 1219 mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn); 1220 1221 List<String> paths = resourceLinkTableJoin.createResourceLinkPaths( 1222 targetResourceType, paramReference, new ArrayList<>()); 1223 if (CollectionUtils.isEmpty(paths)) { 1224 throw new InvalidRequestException(Msg.code(2305) + "Reference field does not exist: " + paramReference); 1225 } 1226 1227 Condition typePredicate = BinaryCondition.equalTo( 1228 resourceLinkTableJoin.getColumnTargetResourceType(), 1229 mySqlBuilder.generatePlaceholder(theResourceType)); 1230 Condition pathPredicate = toEqualToOrInPredicate( 1231 resourceLinkTableJoin.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths)); 1232 1233 Condition linkedPredicate = 1234 searchForIdsWithAndOr(with().setSourceJoinColumn(resourceLinkTableJoin.getJoinColumnsForSource()) 1235 .setResourceName(targetResourceType) 1236 .setParamName(parameterName) 1237 .setAndOrParams(Collections.singletonList(orValues)) 1238 .setRequest(theRequest) 1239 .setRequestPartitionId(theRequestPartitionId)); 1240 1241 if (myPartitionSettings.isDatabasePartitionMode()) { 1242 andPredicates.add(toAndPredicate(pathPredicate, typePredicate, linkedPredicate)); 1243 } else { 1244 Condition partitionPredicate = resourceLinkTableJoin.createPartitionIdPredicate(theRequestPartitionId); 1245 andPredicates.add(toAndPredicate(partitionPredicate, pathPredicate, typePredicate, linkedPredicate)); 1246 } 1247 } 1248 1249 return toAndPredicate(andPredicates); 1250 } 1251 1252 public Condition createPredicateNumber( 1253 @Nullable DbColumn[] theSourceJoinColumn, 1254 String theResourceName, 1255 String theSpnamePrefix, 1256 RuntimeSearchParam theSearchParam, 1257 List<? extends IQueryParameterType> theList, 1258 SearchFilterParser.CompareOperation theOperation, 1259 RequestPartitionId theRequestPartitionId) { 1260 return createPredicateNumber( 1261 theSourceJoinColumn, 1262 theResourceName, 1263 theSpnamePrefix, 1264 theSearchParam, 1265 theList, 1266 theOperation, 1267 theRequestPartitionId, 1268 mySqlBuilder); 1269 } 1270 1271 public Condition createPredicateNumber( 1272 @Nullable DbColumn[] theSourceJoinColumn, 1273 String theResourceName, 1274 String theSpnamePrefix, 1275 RuntimeSearchParam theSearchParam, 1276 List<? extends IQueryParameterType> theList, 1277 SearchFilterParser.CompareOperation theOperation, 1278 RequestPartitionId theRequestPartitionId, 1279 SearchQueryBuilder theSqlBuilder) { 1280 1281 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 1282 1283 Boolean isMissing = theList.get(0).getMissing(); 1284 if (isMissing != null) { 1285 return createMissingParameterQuery(new MissingParameterQueryParams( 1286 theSqlBuilder, 1287 theSearchParam.getParamType(), 1288 theList, 1289 paramName, 1290 theResourceName, 1291 theSourceJoinColumn, 1292 theRequestPartitionId)); 1293 } else { 1294 NumberPredicateBuilder join = createOrReusePredicateBuilder( 1295 PredicateBuilderTypeEnum.NUMBER, 1296 theSourceJoinColumn, 1297 paramName, 1298 () -> theSqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn)) 1299 .getResult(); 1300 1301 List<Condition> codePredicates = new ArrayList<>(); 1302 for (IQueryParameterType nextOr : theList) { 1303 1304 if (nextOr instanceof NumberParam) { 1305 NumberParam param = (NumberParam) nextOr; 1306 1307 BigDecimal value = param.getValue(); 1308 if (value == null) { 1309 continue; 1310 } 1311 1312 SearchFilterParser.CompareOperation operation = theOperation; 1313 if (operation == null) { 1314 operation = toOperation(param.getPrefix()); 1315 } 1316 1317 Condition predicate = join.createPredicateNumeric( 1318 theResourceName, paramName, operation, value, theRequestPartitionId, nextOr); 1319 codePredicates.add(predicate); 1320 1321 } else { 1322 throw new IllegalArgumentException(Msg.code(1211) + "Invalid token type: " + nextOr.getClass()); 1323 } 1324 } 1325 1326 return join.combineWithRequestPartitionIdPredicate( 1327 theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); 1328 } 1329 } 1330 1331 public Condition createPredicateQuantity( 1332 @Nullable DbColumn[] theSourceJoinColumn, 1333 String theResourceName, 1334 String theSpnamePrefix, 1335 RuntimeSearchParam theSearchParam, 1336 List<? extends IQueryParameterType> theList, 1337 SearchFilterParser.CompareOperation theOperation, 1338 RequestPartitionId theRequestPartitionId) { 1339 return createPredicateQuantity( 1340 theSourceJoinColumn, 1341 theResourceName, 1342 theSpnamePrefix, 1343 theSearchParam, 1344 theList, 1345 theOperation, 1346 theRequestPartitionId, 1347 mySqlBuilder); 1348 } 1349 1350 public Condition createPredicateQuantity( 1351 @Nullable DbColumn[] theSourceJoinColumn, 1352 String theResourceName, 1353 String theSpnamePrefix, 1354 RuntimeSearchParam theSearchParam, 1355 List<? extends IQueryParameterType> theList, 1356 SearchFilterParser.CompareOperation theOperation, 1357 RequestPartitionId theRequestPartitionId, 1358 SearchQueryBuilder theSqlBuilder) { 1359 1360 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 1361 1362 Boolean isMissing = theList.get(0).getMissing(); 1363 if (isMissing != null) { 1364 return createMissingParameterQuery(new MissingParameterQueryParams( 1365 theSqlBuilder, 1366 theSearchParam.getParamType(), 1367 theList, 1368 paramName, 1369 theResourceName, 1370 theSourceJoinColumn, 1371 theRequestPartitionId)); 1372 } else { 1373 List<QuantityParam> quantityParams = 1374 theList.stream().map(QuantityParam::toQuantityParam).collect(Collectors.toList()); 1375 1376 BaseQuantityPredicateBuilder join = null; 1377 boolean normalizedSearchEnabled = myStorageSettings 1378 .getNormalizedQuantitySearchLevel() 1379 .equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED); 1380 if (normalizedSearchEnabled) { 1381 List<QuantityParam> normalizedQuantityParams = quantityParams.stream() 1382 .map(UcumServiceUtil::toCanonicalQuantityOrNull) 1383 .filter(Objects::nonNull) 1384 .collect(Collectors.toList()); 1385 1386 if (normalizedQuantityParams.size() == quantityParams.size()) { 1387 join = createOrReusePredicateBuilder( 1388 PredicateBuilderTypeEnum.QUANTITY, 1389 theSourceJoinColumn, 1390 paramName, 1391 () -> theSqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)) 1392 .getResult(); 1393 quantityParams = normalizedQuantityParams; 1394 } 1395 } 1396 1397 if (join == null) { 1398 join = createOrReusePredicateBuilder( 1399 PredicateBuilderTypeEnum.QUANTITY, 1400 theSourceJoinColumn, 1401 paramName, 1402 () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)) 1403 .getResult(); 1404 } 1405 1406 List<Condition> codePredicates = new ArrayList<>(); 1407 for (QuantityParam nextOr : quantityParams) { 1408 Condition singleCode = join.createPredicateQuantity( 1409 nextOr, theResourceName, paramName, null, join, theOperation, theRequestPartitionId); 1410 codePredicates.add(singleCode); 1411 } 1412 1413 return join.combineWithRequestPartitionIdPredicate( 1414 theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); 1415 } 1416 } 1417 1418 public Condition createPredicateReference( 1419 @Nullable DbColumn[] theSourceJoinColumn, 1420 String theResourceName, 1421 String theParamName, 1422 List<String> theQualifiers, 1423 List<? extends IQueryParameterType> theList, 1424 SearchFilterParser.CompareOperation theOperation, 1425 RequestPartitionId theRequestPartitionId) { 1426 return createPredicateReference( 1427 theSourceJoinColumn, 1428 theResourceName, 1429 theParamName, 1430 theQualifiers, 1431 theList, 1432 theOperation, 1433 theRequestPartitionId, 1434 mySqlBuilder); 1435 } 1436 1437 public Condition createPredicateReference( 1438 @Nullable DbColumn[] theSourceJoinColumn, 1439 String theResourceName, 1440 String theParamName, 1441 List<String> theQualifiers, 1442 List<? extends IQueryParameterType> theList, 1443 SearchFilterParser.CompareOperation theOperation, 1444 RequestPartitionId theRequestPartitionId, 1445 SearchQueryBuilder theSqlBuilder) { 1446 1447 if ((theOperation != null) 1448 && (theOperation != SearchFilterParser.CompareOperation.eq) 1449 && (theOperation != SearchFilterParser.CompareOperation.ne)) { 1450 throw new InvalidRequestException( 1451 Msg.code(1212) 1452 + "Invalid operator specified for reference predicate. Supported operators for reference predicate are \"eq\" and \"ne\"."); 1453 } 1454 1455 Boolean isMissing = theList.get(0).getMissing(); 1456 if (isMissing != null) { 1457 return createMissingParameterQuery(new MissingParameterQueryParams( 1458 theSqlBuilder, 1459 RestSearchParameterTypeEnum.REFERENCE, 1460 theList, 1461 theParamName, 1462 theResourceName, 1463 theSourceJoinColumn, 1464 theRequestPartitionId)); 1465 } else { 1466 ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder( 1467 PredicateBuilderTypeEnum.REFERENCE, 1468 theSourceJoinColumn, 1469 theParamName, 1470 () -> theSqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)) 1471 .getResult(); 1472 return predicateBuilder.createPredicate( 1473 myRequestDetails, 1474 theResourceName, 1475 theParamName, 1476 theQualifiers, 1477 theList, 1478 theOperation, 1479 theRequestPartitionId); 1480 } 1481 } 1482 1483 public void addGrouping() { 1484 if (!myGroupingAdded) { 1485 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 1486 1487 /* 1488 * Postgres and Oracle don't like it if we are doing a SELECT DISTINCT 1489 * with multiple selected columns but no GROUP BY clause. 1490 */ 1491 if (mySqlBuilder.isSelectPartitionId()) { 1492 mySqlBuilder 1493 .getSelect() 1494 .addGroupings( 1495 firstPredicateBuilder.getPartitionIdColumn(), 1496 firstPredicateBuilder.getResourceIdColumn()); 1497 } else { 1498 mySqlBuilder.getSelect().addGroupings(firstPredicateBuilder.getJoinColumns()); 1499 } 1500 myGroupingAdded = true; 1501 } 1502 } 1503 1504 public void addOrdering() { 1505 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 1506 mySqlBuilder.getSelect().addOrderings(firstPredicateBuilder.getJoinColumns()); 1507 } 1508 1509 public Condition createPredicateReferenceForEmbeddedChainedSearchResource( 1510 @Nullable DbColumn[] theSourceJoinColumn, 1511 String theResourceName, 1512 RuntimeSearchParam theSearchParam, 1513 List<? extends IQueryParameterType> theList, 1514 SearchFilterParser.CompareOperation theOperation, 1515 RequestPartitionId theRequestPartitionId, 1516 EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) { 1517 1518 boolean wantChainedAndNormal = 1519 theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN; 1520 1521 // A bit of a hack, but we need to turn off cache reuse while in this method so that we don't try to reuse 1522 // builders across different subselects 1523 EnumSet<PredicateBuilderTypeEnum> cachedReusePredicateBuilderTypes = 1524 EnumSet.copyOf(myReusePredicateBuilderTypes); 1525 if (wantChainedAndNormal) { 1526 myReusePredicateBuilderTypes.clear(); 1527 } 1528 1529 ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor(); 1530 chainExtractor.deriveChains(theResourceName, theSearchParam, theList); 1531 Map<List<ChainElement>, Set<LeafNodeDefinition>> chains = chainExtractor.getChains(); 1532 1533 Map<List<String>, Set<LeafNodeDefinition>> referenceLinks = Maps.newHashMap(); 1534 for (List<ChainElement> nextChain : chains.keySet()) { 1535 Set<LeafNodeDefinition> leafNodes = chains.get(nextChain); 1536 1537 collateChainedSearchOptions(referenceLinks, nextChain, leafNodes, theEmbeddedChainedSearchModeEnum); 1538 } 1539 1540 UnionQuery union = null; 1541 if (wantChainedAndNormal) { 1542 union = new UnionQuery(SetOperationQuery.Type.UNION_ALL); 1543 } 1544 1545 List<Condition> predicates = new ArrayList<>(); 1546 for (List<String> nextReferenceLink : referenceLinks.keySet()) { 1547 for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) { 1548 SearchQueryBuilder builder; 1549 if (wantChainedAndNormal) { 1550 builder = mySqlBuilder.newChildSqlBuilder(mySqlBuilder.isIncludePartitionIdInJoins()); 1551 } else { 1552 builder = mySqlBuilder; 1553 } 1554 1555 DbColumn[] previousJoinColumn = null; 1556 1557 // Create a reference link predicates to the subselect for every link but the last one 1558 for (String nextLink : nextReferenceLink) { 1559 // We don't want to call createPredicateReference() here, because the whole point is to avoid the 1560 // recursion. 1561 // TODO: Are we missing any important business logic from that method? All tests are passing. 1562 ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = 1563 builder.addReferencePredicateBuilder(this, previousJoinColumn); 1564 builder.addPredicate( 1565 resourceLinkPredicateBuilder.createPredicateSourcePaths(Lists.newArrayList(nextLink))); 1566 previousJoinColumn = resourceLinkPredicateBuilder.getJoinColumnsForTarget(); 1567 } 1568 1569 Condition containedCondition = createIndexPredicate( 1570 previousJoinColumn, 1571 leafNodeDefinition.getLeafTarget(), 1572 leafNodeDefinition.getLeafPathPrefix(), 1573 leafNodeDefinition.getLeafParamName(), 1574 leafNodeDefinition.getParamDefinition(), 1575 leafNodeDefinition.getOrValues(), 1576 theOperation, 1577 leafNodeDefinition.getQualifiers(), 1578 theRequestPartitionId, 1579 builder); 1580 1581 if (wantChainedAndNormal) { 1582 builder.addPredicate(containedCondition); 1583 union.addQueries(builder.getSelect()); 1584 } else { 1585 predicates.add(containedCondition); 1586 } 1587 } 1588 } 1589 1590 Condition retVal; 1591 if (wantChainedAndNormal) { 1592 1593 if (theSourceJoinColumn == null) { 1594 BaseJoiningPredicateBuilder root = mySqlBuilder.getOrCreateFirstPredicateBuilder(false); 1595 DbColumn[] joinColumns = root.getJoinColumns(); 1596 Object joinColumnObject; 1597 if (joinColumns.length == 1) { 1598 joinColumnObject = joinColumns[0]; 1599 } else { 1600 joinColumnObject = ColumnTupleObject.from(joinColumns); 1601 } 1602 retVal = new InCondition(joinColumnObject, union); 1603 } else { 1604 // -- for the resource link, need join with target_resource_id 1605 retVal = new InCondition(theSourceJoinColumn, union); 1606 } 1607 1608 } else { 1609 1610 retVal = toOrPredicate(predicates); 1611 } 1612 1613 // restore the state of this collection to turn caching back on before we exit 1614 myReusePredicateBuilderTypes.addAll(cachedReusePredicateBuilderTypes); 1615 return retVal; 1616 } 1617 1618 private void collateChainedSearchOptions( 1619 Map<List<String>, Set<LeafNodeDefinition>> referenceLinks, 1620 List<ChainElement> nextChain, 1621 Set<LeafNodeDefinition> leafNodes, 1622 EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) { 1623 // Manually collapse the chain using all possible variants of contained resource patterns. 1624 // This is a bit excruciating to extend beyond three references. Do we want to find a way to automate this 1625 // someday? 1626 // Note: the first element in each chain is assumed to be discrete. This may need to change when we add proper 1627 // support for `_contained` 1628 if (nextChain.size() == 1) { 1629 // discrete -> discrete 1630 if (theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN) { 1631 // If !theWantChainedAndNormal that means we're only processing refchains 1632 // so the discrete -> contained case is the only one that applies 1633 updateMapOfReferenceLinks( 1634 referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), leafNodes); 1635 } 1636 1637 // discrete -> contained 1638 RuntimeSearchParam firstParamDefinition = 1639 leafNodes.iterator().next().getParamDefinition(); 1640 updateMapOfReferenceLinks( 1641 referenceLinks, 1642 Lists.newArrayList(), 1643 leafNodes.stream() 1644 .map(t -> t.withPathPrefix( 1645 nextChain.get(0).getResourceType(), 1646 nextChain.get(0).getSearchParameterName())) 1647 // When we're handling discrete->contained the differences between search 1648 // parameters don't matter. E.g. if we're processing "subject.name=foo" 1649 // the name could be Patient:name or Group:name but it doesn't actually 1650 // matter that these are different since in this case both of these end 1651 // up being an identical search in the string table for "subject.name". 1652 .map(t -> t.withParam(firstParamDefinition)) 1653 .collect(Collectors.toSet())); 1654 } else if (nextChain.size() == 2) { 1655 // discrete -> discrete -> discrete 1656 updateMapOfReferenceLinks( 1657 referenceLinks, 1658 Lists.newArrayList( 1659 nextChain.get(0).getPath(), nextChain.get(1).getPath()), 1660 leafNodes); 1661 // discrete -> discrete -> contained 1662 updateMapOfReferenceLinks( 1663 referenceLinks, 1664 Lists.newArrayList(nextChain.get(0).getPath()), 1665 leafNodes.stream() 1666 .map(t -> t.withPathPrefix( 1667 nextChain.get(1).getResourceType(), 1668 nextChain.get(1).getSearchParameterName())) 1669 .collect(Collectors.toSet())); 1670 // discrete -> contained -> discrete 1671 updateMapOfReferenceLinks( 1672 referenceLinks, 1673 Lists.newArrayList(mergePaths( 1674 nextChain.get(0).getPath(), nextChain.get(1).getPath())), 1675 leafNodes); 1676 if (myStorageSettings.isIndexOnContainedResourcesRecursively()) { 1677 // discrete -> contained -> contained 1678 updateMapOfReferenceLinks( 1679 referenceLinks, 1680 Lists.newArrayList(), 1681 leafNodes.stream() 1682 .map(t -> t.withPathPrefix( 1683 nextChain.get(0).getResourceType(), 1684 nextChain.get(0).getSearchParameterName() + "." 1685 + nextChain.get(1).getSearchParameterName())) 1686 .collect(Collectors.toSet())); 1687 } 1688 } else if (nextChain.size() == 3) { 1689 // discrete -> discrete -> discrete -> discrete 1690 updateMapOfReferenceLinks( 1691 referenceLinks, 1692 Lists.newArrayList( 1693 nextChain.get(0).getPath(), 1694 nextChain.get(1).getPath(), 1695 nextChain.get(2).getPath()), 1696 leafNodes); 1697 // discrete -> discrete -> discrete -> contained 1698 updateMapOfReferenceLinks( 1699 referenceLinks, 1700 Lists.newArrayList( 1701 nextChain.get(0).getPath(), nextChain.get(1).getPath()), 1702 leafNodes.stream() 1703 .map(t -> t.withPathPrefix( 1704 nextChain.get(2).getResourceType(), 1705 nextChain.get(2).getSearchParameterName())) 1706 .collect(Collectors.toSet())); 1707 // discrete -> discrete -> contained -> discrete 1708 updateMapOfReferenceLinks( 1709 referenceLinks, 1710 Lists.newArrayList( 1711 nextChain.get(0).getPath(), 1712 mergePaths( 1713 nextChain.get(1).getPath(), nextChain.get(2).getPath())), 1714 leafNodes); 1715 // discrete -> contained -> discrete -> discrete 1716 updateMapOfReferenceLinks( 1717 referenceLinks, 1718 Lists.newArrayList( 1719 mergePaths( 1720 nextChain.get(0).getPath(), nextChain.get(1).getPath()), 1721 nextChain.get(2).getPath()), 1722 leafNodes); 1723 // discrete -> contained -> discrete -> contained 1724 updateMapOfReferenceLinks( 1725 referenceLinks, 1726 Lists.newArrayList(mergePaths( 1727 nextChain.get(0).getPath(), nextChain.get(1).getPath())), 1728 leafNodes.stream() 1729 .map(t -> t.withPathPrefix( 1730 nextChain.get(2).getResourceType(), 1731 nextChain.get(2).getSearchParameterName())) 1732 .collect(Collectors.toSet())); 1733 if (myStorageSettings.isIndexOnContainedResourcesRecursively()) { 1734 // discrete -> contained -> contained -> discrete 1735 updateMapOfReferenceLinks( 1736 referenceLinks, 1737 Lists.newArrayList(mergePaths( 1738 nextChain.get(0).getPath(), 1739 nextChain.get(1).getPath(), 1740 nextChain.get(2).getPath())), 1741 leafNodes); 1742 // discrete -> discrete -> contained -> contained 1743 updateMapOfReferenceLinks( 1744 referenceLinks, 1745 Lists.newArrayList(nextChain.get(0).getPath()), 1746 leafNodes.stream() 1747 .map(t -> t.withPathPrefix( 1748 nextChain.get(1).getResourceType(), 1749 nextChain.get(1).getSearchParameterName() + "." 1750 + nextChain.get(2).getSearchParameterName())) 1751 .collect(Collectors.toSet())); 1752 // discrete -> contained -> contained -> contained 1753 updateMapOfReferenceLinks( 1754 referenceLinks, 1755 Lists.newArrayList(), 1756 leafNodes.stream() 1757 .map(t -> t.withPathPrefix( 1758 nextChain.get(0).getResourceType(), 1759 nextChain.get(0).getSearchParameterName() + "." 1760 + nextChain.get(1).getSearchParameterName() + "." 1761 + nextChain.get(2).getSearchParameterName())) 1762 .collect(Collectors.toSet())); 1763 } 1764 } else { 1765 // TODO: the chain is too long, it isn't practical to hard-code all the possible patterns. If anyone ever 1766 // needs this, we should revisit the approach 1767 throw new InvalidRequestException(Msg.code(2011) 1768 + "The search chain is too long. Only chains of up to three references are supported."); 1769 } 1770 } 1771 1772 private void updateMapOfReferenceLinks( 1773 Map<List<String>, Set<LeafNodeDefinition>> theReferenceLinksMap, 1774 ArrayList<String> thePath, 1775 Set<LeafNodeDefinition> theLeafNodesToAdd) { 1776 Set<LeafNodeDefinition> leafNodes = theReferenceLinksMap.computeIfAbsent(thePath, k -> Sets.newHashSet()); 1777 leafNodes.addAll(theLeafNodesToAdd); 1778 } 1779 1780 private String mergePaths(String... paths) { 1781 String result = ""; 1782 for (String nextPath : paths) { 1783 int separatorIndex = nextPath.indexOf('.'); 1784 if (StringUtils.isEmpty(result)) { 1785 result = nextPath; 1786 } else { 1787 result = result + nextPath.substring(separatorIndex); 1788 } 1789 } 1790 return result; 1791 } 1792 1793 private Condition createIndexPredicate( 1794 DbColumn[] theSourceJoinColumn, 1795 String theResourceName, 1796 String theSpnamePrefix, 1797 String theParamName, 1798 RuntimeSearchParam theParamDefinition, 1799 ArrayList<IQueryParameterType> theOrValues, 1800 SearchFilterParser.CompareOperation theOperation, 1801 List<String> theQualifiers, 1802 RequestPartitionId theRequestPartitionId, 1803 SearchQueryBuilder theSqlBuilder) { 1804 Condition containedCondition; 1805 1806 switch (theParamDefinition.getParamType()) { 1807 case DATE: 1808 containedCondition = createPredicateDate( 1809 theSourceJoinColumn, 1810 theResourceName, 1811 theSpnamePrefix, 1812 theParamDefinition, 1813 theOrValues, 1814 theOperation, 1815 theRequestPartitionId, 1816 theSqlBuilder); 1817 break; 1818 case NUMBER: 1819 containedCondition = createPredicateNumber( 1820 theSourceJoinColumn, 1821 theResourceName, 1822 theSpnamePrefix, 1823 theParamDefinition, 1824 theOrValues, 1825 theOperation, 1826 theRequestPartitionId, 1827 theSqlBuilder); 1828 break; 1829 case QUANTITY: 1830 containedCondition = createPredicateQuantity( 1831 theSourceJoinColumn, 1832 theResourceName, 1833 theSpnamePrefix, 1834 theParamDefinition, 1835 theOrValues, 1836 theOperation, 1837 theRequestPartitionId, 1838 theSqlBuilder); 1839 break; 1840 case STRING: 1841 containedCondition = createPredicateString( 1842 theSourceJoinColumn, 1843 theResourceName, 1844 theSpnamePrefix, 1845 theParamDefinition, 1846 theOrValues, 1847 theOperation, 1848 theRequestPartitionId, 1849 theSqlBuilder); 1850 break; 1851 case TOKEN: 1852 containedCondition = createPredicateToken( 1853 theSourceJoinColumn, 1854 theResourceName, 1855 theSpnamePrefix, 1856 theParamDefinition, 1857 theOrValues, 1858 theOperation, 1859 theRequestPartitionId, 1860 theSqlBuilder); 1861 break; 1862 case COMPOSITE: 1863 containedCondition = createPredicateComposite( 1864 theSourceJoinColumn, 1865 theResourceName, 1866 theSpnamePrefix, 1867 theParamDefinition, 1868 theOrValues, 1869 theRequestPartitionId, 1870 theSqlBuilder); 1871 break; 1872 case URI: 1873 containedCondition = createPredicateUri( 1874 theSourceJoinColumn, 1875 theResourceName, 1876 theSpnamePrefix, 1877 theParamDefinition, 1878 theOrValues, 1879 theOperation, 1880 theRequestPartitionId, 1881 theSqlBuilder); 1882 break; 1883 case REFERENCE: 1884 containedCondition = createPredicateReference( 1885 theSourceJoinColumn, 1886 theResourceName, 1887 isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName, 1888 theQualifiers, 1889 theOrValues, 1890 theOperation, 1891 theRequestPartitionId, 1892 theSqlBuilder); 1893 break; 1894 case HAS: 1895 case SPECIAL: 1896 default: 1897 throw new InvalidRequestException( 1898 Msg.code(1215) + "The search type:" + theParamDefinition.getParamType() + " is not supported."); 1899 } 1900 return containedCondition; 1901 } 1902 1903 @Nullable 1904 public Condition createPredicateResourceId( 1905 @Nullable DbColumn[] theSourceJoinColumn, 1906 List<List<IQueryParameterType>> theValues, 1907 String theResourceName, 1908 SearchFilterParser.CompareOperation theOperation, 1909 RequestPartitionId theRequestPartitionId) { 1910 ResourceIdPredicateBuilder builder = mySqlBuilder.newResourceIdBuilder(); 1911 return builder.createPredicateResourceId( 1912 theSourceJoinColumn, theResourceName, theValues, theOperation, theRequestPartitionId); 1913 } 1914 1915 private Condition createPredicateSourceForAndList( 1916 @Nullable DbColumn[] theSourceJoinColumn, List<List<IQueryParameterType>> theAndOrParams) { 1917 mySqlBuilder.getOrCreateFirstPredicateBuilder(); 1918 1919 List<Condition> andPredicates = new ArrayList<>(theAndOrParams.size()); 1920 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1921 andPredicates.add(createPredicateSource(theSourceJoinColumn, nextAnd)); 1922 } 1923 return toAndPredicate(andPredicates); 1924 } 1925 1926 private Condition createPredicateSource( 1927 @Nullable DbColumn[] theSourceJoinColumn, List<? extends IQueryParameterType> theList) { 1928 if (myStorageSettings.getStoreMetaSourceInformation() 1929 == JpaStorageSettings.StoreMetaSourceInformationEnum.NONE) { 1930 String msg = myFhirContext.getLocalizer().getMessage(QueryStack.class, "sourceParamDisabled"); 1931 throw new InvalidRequestException(Msg.code(1216) + msg); 1932 } 1933 1934 List<Condition> orPredicates = new ArrayList<>(); 1935 1936 // :missing=true modifier processing requires "LEFT JOIN" with HFJ_RESOURCE table to return correct results 1937 // if both sourceUri and requestId are not populated for the resource 1938 Optional<? extends IQueryParameterType> isMissingSourceOptional = theList.stream() 1939 .filter(nextParameter -> nextParameter.getMissing() != null && nextParameter.getMissing()) 1940 .findFirst(); 1941 1942 if (isMissingSourceOptional.isPresent()) { 1943 ISourcePredicateBuilder join = 1944 getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.LEFT_OUTER); 1945 orPredicates.add(join.createPredicateMissingSourceUri()); 1946 return toOrPredicate(orPredicates); 1947 } 1948 // for all other cases we use "INNER JOIN" to match search parameters 1949 ISourcePredicateBuilder join = getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.INNER); 1950 1951 for (IQueryParameterType nextParameter : theList) { 1952 SourceParam sourceParameter = new SourceParam(nextParameter.getValueAsQueryToken(myFhirContext)); 1953 String sourceUri = sourceParameter.getSourceUri(); 1954 String requestId = sourceParameter.getRequestId(); 1955 if (isNotBlank(sourceUri) && isNotBlank(requestId)) { 1956 orPredicates.add(toAndPredicate( 1957 join.createPredicateSourceUri(sourceUri), join.createPredicateRequestId(requestId))); 1958 } else if (isNotBlank(sourceUri)) { 1959 orPredicates.add( 1960 join.createPredicateSourceUriWithModifiers(nextParameter, myStorageSettings, sourceUri)); 1961 } else if (isNotBlank(requestId)) { 1962 orPredicates.add(join.createPredicateRequestId(requestId)); 1963 } 1964 } 1965 1966 return toOrPredicate(orPredicates); 1967 } 1968 1969 private ISourcePredicateBuilder getSourcePredicateBuilder( 1970 @Nullable DbColumn[] theSourceJoinColumn, SelectQuery.JoinType theJoinType) { 1971 if (myStorageSettings.isAccessMetaSourceInformationFromProvenanceTable()) { 1972 return createOrReusePredicateBuilder( 1973 PredicateBuilderTypeEnum.SOURCE, 1974 theSourceJoinColumn, 1975 Constants.PARAM_SOURCE, 1976 () -> mySqlBuilder.addResourceHistoryProvenancePredicateBuilder( 1977 theSourceJoinColumn, theJoinType)) 1978 .getResult(); 1979 } 1980 return createOrReusePredicateBuilder( 1981 PredicateBuilderTypeEnum.SOURCE, 1982 theSourceJoinColumn, 1983 Constants.PARAM_SOURCE, 1984 () -> mySqlBuilder.addResourceHistoryPredicateBuilder(theSourceJoinColumn, theJoinType)) 1985 .getResult(); 1986 } 1987 1988 public Condition createPredicateString( 1989 @Nullable DbColumn[] theSourceJoinColumn, 1990 String theResourceName, 1991 String theSpnamePrefix, 1992 RuntimeSearchParam theSearchParam, 1993 List<? extends IQueryParameterType> theList, 1994 SearchFilterParser.CompareOperation theOperation, 1995 RequestPartitionId theRequestPartitionId) { 1996 return createPredicateString( 1997 theSourceJoinColumn, 1998 theResourceName, 1999 theSpnamePrefix, 2000 theSearchParam, 2001 theList, 2002 theOperation, 2003 theRequestPartitionId, 2004 mySqlBuilder); 2005 } 2006 2007 public Condition createPredicateString( 2008 @Nullable DbColumn[] theSourceJoinColumn, 2009 String theResourceName, 2010 String theSpnamePrefix, 2011 RuntimeSearchParam theSearchParam, 2012 List<? extends IQueryParameterType> theList, 2013 SearchFilterParser.CompareOperation theOperation, 2014 RequestPartitionId theRequestPartitionId, 2015 SearchQueryBuilder theSqlBuilder) { 2016 Boolean isMissing = theList.get(0).getMissing(); 2017 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 2018 2019 if (isMissing != null) { 2020 return createMissingParameterQuery(new MissingParameterQueryParams( 2021 theSqlBuilder, 2022 theSearchParam.getParamType(), 2023 theList, 2024 paramName, 2025 theResourceName, 2026 theSourceJoinColumn, 2027 theRequestPartitionId)); 2028 } 2029 2030 StringPredicateBuilder join = createOrReusePredicateBuilder( 2031 PredicateBuilderTypeEnum.STRING, 2032 theSourceJoinColumn, 2033 paramName, 2034 () -> theSqlBuilder.addStringPredicateBuilder(theSourceJoinColumn)) 2035 .getResult(); 2036 2037 List<Condition> codePredicates = new ArrayList<>(); 2038 for (IQueryParameterType nextOr : theList) { 2039 Condition singleCode = join.createPredicateString( 2040 nextOr, theResourceName, theSpnamePrefix, theSearchParam, join, theOperation); 2041 codePredicates.add(singleCode); 2042 } 2043 2044 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, toOrPredicate(codePredicates)); 2045 } 2046 2047 public Condition createPredicateTag( 2048 @Nullable DbColumn[] theSourceJoinColumn, 2049 List<List<IQueryParameterType>> theList, 2050 String theParamName, 2051 RequestPartitionId theRequestPartitionId) { 2052 TagTypeEnum tagType; 2053 if (Constants.PARAM_TAG.equals(theParamName)) { 2054 tagType = TagTypeEnum.TAG; 2055 } else if (Constants.PARAM_PROFILE.equals(theParamName)) { 2056 tagType = TagTypeEnum.PROFILE; 2057 } else if (Constants.PARAM_SECURITY.equals(theParamName)) { 2058 tagType = TagTypeEnum.SECURITY_LABEL; 2059 } else { 2060 throw new IllegalArgumentException(Msg.code(1217) + "Param name: " + theParamName); // shouldn't happen 2061 } 2062 2063 List<Condition> andPredicates = new ArrayList<>(); 2064 for (List<? extends IQueryParameterType> nextAndParams : theList) { 2065 if (!checkHaveTags(nextAndParams, theParamName)) { 2066 continue; 2067 } 2068 2069 List<Triple<String, String, String>> tokens = Lists.newArrayList(); 2070 boolean paramInverted = populateTokens(tokens, nextAndParams); 2071 if (tokens.isEmpty()) { 2072 continue; 2073 } 2074 2075 Condition tagPredicate; 2076 BaseJoiningPredicateBuilder join; 2077 if (paramInverted) { 2078 2079 boolean selectPartitionId = myPartitionSettings.isDatabasePartitionMode(); 2080 SearchQueryBuilder sqlBuilder = mySqlBuilder.newChildSqlBuilder(selectPartitionId); 2081 TagPredicateBuilder tagSelector = sqlBuilder.addTagPredicateBuilder(null); 2082 sqlBuilder.addPredicate( 2083 tagSelector.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId)); 2084 SelectQuery sql = sqlBuilder.getSelect(); 2085 2086 join = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 2087 Expression subSelect = new Subquery(sql); 2088 2089 Object left; 2090 if (selectPartitionId) { 2091 left = new ColumnTupleObject(join.getJoinColumns()); 2092 } else { 2093 left = join.getResourceIdColumn(); 2094 } 2095 tagPredicate = new InCondition(left, subSelect).setNegate(true); 2096 2097 } else { 2098 // Tag table can't be a query root because it will include deleted resources, and can't select by 2099 // resource type 2100 mySqlBuilder.getOrCreateFirstPredicateBuilder(); 2101 2102 TagPredicateBuilder tagJoin = createOrReusePredicateBuilder( 2103 PredicateBuilderTypeEnum.TAG, 2104 theSourceJoinColumn, 2105 theParamName, 2106 () -> mySqlBuilder.addTagPredicateBuilder(theSourceJoinColumn)) 2107 .getResult(); 2108 tagPredicate = tagJoin.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId); 2109 join = tagJoin; 2110 } 2111 2112 andPredicates.add(join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, tagPredicate)); 2113 } 2114 2115 return toAndPredicate(andPredicates); 2116 } 2117 2118 private boolean populateTokens( 2119 List<Triple<String, String, String>> theTokens, List<? extends IQueryParameterType> theAndParams) { 2120 boolean paramInverted = false; 2121 2122 for (IQueryParameterType nextOrParam : theAndParams) { 2123 String code; 2124 String system; 2125 if (nextOrParam instanceof TokenParam) { 2126 TokenParam nextParam = (TokenParam) nextOrParam; 2127 code = nextParam.getValue(); 2128 system = nextParam.getSystem(); 2129 if (nextParam.getModifier() == TokenParamModifier.NOT) { 2130 paramInverted = true; 2131 } 2132 } else if (nextOrParam instanceof ReferenceParam) { 2133 ReferenceParam nextParam = (ReferenceParam) nextOrParam; 2134 code = nextParam.getValue(); 2135 system = null; 2136 } else { 2137 UriParam nextParam = (UriParam) nextOrParam; 2138 code = nextParam.getValue(); 2139 system = null; 2140 } 2141 2142 if (isNotBlank(code)) { 2143 theTokens.add(Triple.of(system, nextOrParam.getQueryParameterQualifier(), code)); 2144 } 2145 } 2146 return paramInverted; 2147 } 2148 2149 private boolean checkHaveTags(List<? extends IQueryParameterType> theParams, String theParamName) { 2150 for (IQueryParameterType nextParamUncasted : theParams) { 2151 if (nextParamUncasted instanceof TokenParam) { 2152 TokenParam nextParam = (TokenParam) nextParamUncasted; 2153 if (isNotBlank(nextParam.getValue())) { 2154 return true; 2155 } 2156 if (isNotBlank(nextParam.getSystem())) { 2157 throw new TokenParamFormatInvalidRequestException( 2158 Msg.code(1218), theParamName, nextParam.getValueAsQueryToken(myFhirContext)); 2159 } 2160 } 2161 2162 if (nextParamUncasted instanceof ReferenceParam 2163 && isNotBlank(((ReferenceParam) nextParamUncasted).getValue())) { 2164 return true; 2165 } else if (nextParamUncasted instanceof UriParam && isNotBlank(((UriParam) nextParamUncasted).getValue())) { 2166 return true; 2167 } 2168 } 2169 2170 return false; 2171 } 2172 2173 public Condition createPredicateToken( 2174 @Nullable DbColumn[] theSourceJoinColumn, 2175 String theResourceName, 2176 String theSpnamePrefix, 2177 RuntimeSearchParam theSearchParam, 2178 List<? extends IQueryParameterType> theList, 2179 SearchFilterParser.CompareOperation theOperation, 2180 RequestPartitionId theRequestPartitionId) { 2181 return createPredicateToken( 2182 theSourceJoinColumn, 2183 theResourceName, 2184 theSpnamePrefix, 2185 theSearchParam, 2186 theList, 2187 theOperation, 2188 theRequestPartitionId, 2189 mySqlBuilder); 2190 } 2191 2192 public Condition createPredicateToken( 2193 @Nullable DbColumn[] theSourceJoinColumn, 2194 String theResourceName, 2195 String theSpnamePrefix, 2196 RuntimeSearchParam theSearchParam, 2197 List<? extends IQueryParameterType> theList, 2198 SearchFilterParser.CompareOperation theOperation, 2199 RequestPartitionId theRequestPartitionId, 2200 SearchQueryBuilder theSqlBuilder) { 2201 2202 List<IQueryParameterType> tokens = new ArrayList<>(); 2203 2204 boolean paramInverted = false; 2205 TokenParamModifier modifier; 2206 2207 for (IQueryParameterType nextOr : theList) { 2208 if (nextOr instanceof TokenParam) { 2209 if (!((TokenParam) nextOr).isEmpty()) { 2210 TokenParam id = (TokenParam) nextOr; 2211 if (id.isText()) { 2212 2213 // Check whether the :text modifier is actually enabled here 2214 boolean tokenTextIndexingEnabled = 2215 BaseSearchParamExtractor.tokenTextIndexingEnabledForSearchParam( 2216 myStorageSettings, theSearchParam); 2217 if (!tokenTextIndexingEnabled) { 2218 String msg; 2219 if (myStorageSettings.isSuppressStringIndexingInTokens()) { 2220 msg = myFhirContext 2221 .getLocalizer() 2222 .getMessage(QueryStack.class, "textModifierDisabledForServer"); 2223 } else { 2224 msg = myFhirContext 2225 .getLocalizer() 2226 .getMessage(QueryStack.class, "textModifierDisabledForSearchParam"); 2227 } 2228 throw new MethodNotAllowedException(Msg.code(1219) + msg); 2229 } 2230 return createPredicateString( 2231 theSourceJoinColumn, 2232 theResourceName, 2233 theSpnamePrefix, 2234 theSearchParam, 2235 theList, 2236 null, 2237 theRequestPartitionId, 2238 theSqlBuilder); 2239 } 2240 2241 modifier = id.getModifier(); 2242 // for :not modifier, create a token and remove the :not modifier 2243 if (modifier == TokenParamModifier.NOT) { 2244 tokens.add(new TokenParam(((TokenParam) nextOr).getSystem(), ((TokenParam) nextOr).getValue())); 2245 paramInverted = true; 2246 } else { 2247 tokens.add(nextOr); 2248 } 2249 } 2250 } else { 2251 tokens.add(nextOr); 2252 } 2253 } 2254 2255 if (tokens.isEmpty()) { 2256 return null; 2257 } 2258 2259 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 2260 Condition predicate; 2261 BaseJoiningPredicateBuilder join; 2262 2263 if (paramInverted) { 2264 boolean selectPartitionId = myPartitionSettings.isDatabasePartitionMode(); 2265 SearchQueryBuilder sqlBuilder = theSqlBuilder.newChildSqlBuilder(selectPartitionId); 2266 TokenPredicateBuilder tokenSelector = sqlBuilder.addTokenPredicateBuilder(null); 2267 sqlBuilder.addPredicate(tokenSelector.createPredicateToken( 2268 tokens, theResourceName, theSpnamePrefix, theSearchParam, theRequestPartitionId)); 2269 SelectQuery sql = sqlBuilder.getSelect(); 2270 Expression subSelect = new Subquery(sql); 2271 2272 join = theSqlBuilder.getOrCreateFirstPredicateBuilder(); 2273 2274 DbColumn[] leftColumns; 2275 if (theSourceJoinColumn == null) { 2276 leftColumns = join.getJoinColumns(); 2277 } else { 2278 leftColumns = theSourceJoinColumn; 2279 } 2280 2281 Object left = new ColumnTupleObject(leftColumns); 2282 predicate = new InCondition(left, subSelect).setNegate(true); 2283 2284 } else { 2285 Boolean isMissing = theList.get(0).getMissing(); 2286 if (isMissing != null) { 2287 return createMissingParameterQuery(new MissingParameterQueryParams( 2288 theSqlBuilder, 2289 theSearchParam.getParamType(), 2290 theList, 2291 paramName, 2292 theResourceName, 2293 theSourceJoinColumn, 2294 theRequestPartitionId)); 2295 } 2296 2297 TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder( 2298 PredicateBuilderTypeEnum.TOKEN, 2299 theSourceJoinColumn, 2300 paramName, 2301 () -> theSqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)) 2302 .getResult(); 2303 2304 predicate = tokenJoin.createPredicateToken( 2305 tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId); 2306 join = tokenJoin; 2307 } 2308 2309 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); 2310 } 2311 2312 public Condition createPredicateUri( 2313 @Nullable DbColumn[] theSourceJoinColumn, 2314 String theResourceName, 2315 String theSpnamePrefix, 2316 RuntimeSearchParam theSearchParam, 2317 List<? extends IQueryParameterType> theList, 2318 SearchFilterParser.CompareOperation theOperation, 2319 RequestPartitionId theRequestPartitionId) { 2320 return createPredicateUri( 2321 theSourceJoinColumn, 2322 theResourceName, 2323 theSpnamePrefix, 2324 theSearchParam, 2325 theList, 2326 theOperation, 2327 theRequestPartitionId, 2328 mySqlBuilder); 2329 } 2330 2331 public Condition createPredicateUri( 2332 @Nullable DbColumn[] theSourceJoinColumn, 2333 String theResourceName, 2334 String theSpnamePrefix, 2335 RuntimeSearchParam theSearchParam, 2336 List<? extends IQueryParameterType> theList, 2337 SearchFilterParser.CompareOperation theOperation, 2338 RequestPartitionId theRequestPartitionId, 2339 SearchQueryBuilder theSqlBuilder) { 2340 2341 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 2342 2343 Boolean isMissing = theList.get(0).getMissing(); 2344 if (isMissing != null) { 2345 return createMissingParameterQuery(new MissingParameterQueryParams( 2346 theSqlBuilder, 2347 theSearchParam.getParamType(), 2348 theList, 2349 paramName, 2350 theResourceName, 2351 theSourceJoinColumn, 2352 theRequestPartitionId)); 2353 } else { 2354 UriPredicateBuilder join = theSqlBuilder.addUriPredicateBuilder(theSourceJoinColumn); 2355 2356 Condition predicate = join.addPredicate(theList, paramName, theOperation, myRequestDetails); 2357 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); 2358 } 2359 } 2360 2361 public QueryStack newChildQueryFactoryWithFullBuilderReuse() { 2362 return new QueryStack( 2363 myRequestDetails, 2364 mySearchParameters, 2365 myStorageSettings, 2366 myFhirContext, 2367 mySqlBuilder, 2368 mySearchParamRegistry, 2369 myPartitionSettings, 2370 EnumSet.allOf(PredicateBuilderTypeEnum.class)); 2371 } 2372 2373 @Nullable 2374 public Condition searchForIdsWithAndOr(SearchForIdsParams theSearchForIdsParams) { 2375 2376 if (theSearchForIdsParams.myAndOrParams.isEmpty()) { 2377 return null; 2378 } 2379 2380 switch (theSearchForIdsParams.myParamName) { 2381 case IAnyResource.SP_RES_ID: 2382 return createPredicateResourceId( 2383 theSearchForIdsParams.mySourceJoinColumn, 2384 theSearchForIdsParams.myAndOrParams, 2385 theSearchForIdsParams.myResourceName, 2386 null, 2387 theSearchForIdsParams.myRequestPartitionId); 2388 2389 case Constants.PARAM_PID: 2390 return createPredicateResourcePID( 2391 theSearchForIdsParams.mySourceJoinColumn, theSearchForIdsParams.myAndOrParams); 2392 2393 case PARAM_HAS: 2394 return createPredicateHas( 2395 theSearchForIdsParams.mySourceJoinColumn, 2396 theSearchForIdsParams.myResourceName, 2397 theSearchForIdsParams.myAndOrParams, 2398 theSearchForIdsParams.myRequest, 2399 theSearchForIdsParams.myRequestPartitionId); 2400 2401 case Constants.PARAM_TAG: 2402 case Constants.PARAM_PROFILE: 2403 case Constants.PARAM_SECURITY: 2404 if (myStorageSettings.getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.INLINE) { 2405 return createPredicateSearchParameter( 2406 theSearchForIdsParams.mySourceJoinColumn, 2407 theSearchForIdsParams.myResourceName, 2408 theSearchForIdsParams.myParamName, 2409 theSearchForIdsParams.myAndOrParams, 2410 theSearchForIdsParams.myRequestPartitionId); 2411 } else { 2412 return createPredicateTag( 2413 theSearchForIdsParams.mySourceJoinColumn, 2414 theSearchForIdsParams.myAndOrParams, 2415 theSearchForIdsParams.myParamName, 2416 theSearchForIdsParams.myRequestPartitionId); 2417 } 2418 2419 case Constants.PARAM_SOURCE: 2420 return createPredicateSourceForAndList( 2421 theSearchForIdsParams.mySourceJoinColumn, theSearchForIdsParams.myAndOrParams); 2422 2423 case Constants.PARAM_LASTUPDATED: 2424 // this case statement handles a _lastUpdated query as part of a reverse search 2425 // only (/Patient?_has:Encounter:patient:_lastUpdated=ge2023-10-24). 2426 // performing a _lastUpdated query on a resource (/Patient?_lastUpdated=eq2023-10-24) 2427 // is handled in {@link SearchBuilder#createChunkedQuery}. 2428 return createReverseSearchPredicateLastUpdated( 2429 theSearchForIdsParams.myAndOrParams, theSearchForIdsParams.mySourceJoinColumn); 2430 2431 default: 2432 return createPredicateSearchParameter( 2433 theSearchForIdsParams.mySourceJoinColumn, 2434 theSearchForIdsParams.myResourceName, 2435 theSearchForIdsParams.myParamName, 2436 theSearchForIdsParams.myAndOrParams, 2437 theSearchForIdsParams.myRequestPartitionId); 2438 } 2439 } 2440 2441 /** 2442 * Raw match on RES_ID 2443 */ 2444 private Condition createPredicateResourcePID( 2445 DbColumn[] theSourceJoinColumn, List<List<IQueryParameterType>> theAndOrParams) { 2446 DbColumn pidColumn = getResourceIdColumn(theSourceJoinColumn); 2447 2448 if (pidColumn == null) { 2449 BaseJoiningPredicateBuilder predicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 2450 pidColumn = predicateBuilder.getResourceIdColumn(); 2451 } 2452 2453 // we don't support any modifiers for now 2454 Set<Long> pids = theAndOrParams.stream() 2455 .map(orList -> orList.stream() 2456 .map(v -> v.getValueAsQueryToken(myFhirContext)) 2457 .map(Long::valueOf) 2458 .collect(Collectors.toSet())) 2459 .reduce(Sets::intersection) 2460 .orElse(Set.of()); 2461 2462 if (pids.isEmpty()) { 2463 mySqlBuilder.setMatchNothing(); 2464 return null; 2465 } 2466 2467 return toEqualToOrInPredicate(pidColumn, mySqlBuilder.generatePlaceholders(pids)); 2468 } 2469 2470 private Condition createReverseSearchPredicateLastUpdated( 2471 List<List<IQueryParameterType>> theAndOrParams, DbColumn[] theSourceColumn) { 2472 2473 ResourceTablePredicateBuilder resourceTableJoin = 2474 mySqlBuilder.addResourceTablePredicateBuilder(theSourceColumn); 2475 2476 List<Condition> andPredicates = new ArrayList<>(theAndOrParams.size()); 2477 2478 for (List<IQueryParameterType> aList : theAndOrParams) { 2479 if (!aList.isEmpty()) { 2480 DateParam dateParam = (DateParam) aList.get(0); 2481 DateRangeParam dateRangeParam = new DateRangeParam(dateParam); 2482 Condition aCondition = mySqlBuilder.addPredicateLastUpdated(dateRangeParam, resourceTableJoin); 2483 andPredicates.add(aCondition); 2484 } 2485 } 2486 2487 return toAndPredicate(andPredicates); 2488 } 2489 2490 @Nullable 2491 private Condition createPredicateSearchParameter( 2492 @Nullable DbColumn[] theSourceJoinColumn, 2493 String theResourceName, 2494 String theParamName, 2495 List<List<IQueryParameterType>> theAndOrParams, 2496 RequestPartitionId theRequestPartitionId) { 2497 List<Condition> andPredicates = new ArrayList<>(); 2498 RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam( 2499 theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 2500 if (nextParamDef != null) { 2501 2502 if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.isIncludePartitionInSearchHashes()) { 2503 if (theRequestPartitionId.isAllPartitions()) { 2504 throw new PreconditionFailedException( 2505 Msg.code(1220) + "This server is not configured to support search against all partitions"); 2506 } 2507 } 2508 2509 switch (nextParamDef.getParamType()) { 2510 case DATE: 2511 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2512 // FT: 2021-01-18 use operation 'gt', 'ge', 'le' or 'lt' 2513 // to create the predicateDate instead of generic one with operation = null 2514 SearchFilterParser.CompareOperation operation = null; 2515 if (!nextAnd.isEmpty()) { 2516 DateParam param = (DateParam) nextAnd.get(0); 2517 operation = toOperation(param.getPrefix()); 2518 } 2519 andPredicates.add(createPredicateDate( 2520 theSourceJoinColumn, 2521 theResourceName, 2522 null, 2523 nextParamDef, 2524 nextAnd, 2525 operation, 2526 theRequestPartitionId)); 2527 } 2528 break; 2529 case QUANTITY: 2530 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2531 SearchFilterParser.CompareOperation operation = null; 2532 if (!nextAnd.isEmpty()) { 2533 QuantityParam param = (QuantityParam) nextAnd.get(0); 2534 operation = toOperation(param.getPrefix()); 2535 } 2536 andPredicates.add(createPredicateQuantity( 2537 theSourceJoinColumn, 2538 theResourceName, 2539 null, 2540 nextParamDef, 2541 nextAnd, 2542 operation, 2543 theRequestPartitionId)); 2544 } 2545 break; 2546 case REFERENCE: 2547 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2548 2549 // Handle Search Parameters where the name is a full chain 2550 // (e.g. SearchParameter with name=composition.patient.identifier) 2551 if (handleFullyChainedParameter( 2552 theSourceJoinColumn, 2553 theResourceName, 2554 theParamName, 2555 theRequestPartitionId, 2556 andPredicates, 2557 nextAnd)) { 2558 continue; 2559 } 2560 2561 EmbeddedChainedSearchModeEnum embeddedChainedSearchModeEnum = 2562 isEligibleForEmbeddedChainedResourceSearch(theResourceName, theParamName, nextAnd); 2563 if (embeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY) { 2564 andPredicates.add(createPredicateReference( 2565 theSourceJoinColumn, 2566 theResourceName, 2567 theParamName, 2568 new ArrayList<>(), 2569 nextAnd, 2570 null, 2571 theRequestPartitionId)); 2572 } else { 2573 andPredicates.add(createPredicateReferenceForEmbeddedChainedSearchResource( 2574 theSourceJoinColumn, 2575 theResourceName, 2576 nextParamDef, 2577 nextAnd, 2578 null, 2579 theRequestPartitionId, 2580 embeddedChainedSearchModeEnum)); 2581 } 2582 } 2583 break; 2584 case STRING: 2585 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2586 andPredicates.add(createPredicateString( 2587 theSourceJoinColumn, 2588 theResourceName, 2589 null, 2590 nextParamDef, 2591 nextAnd, 2592 SearchFilterParser.CompareOperation.sw, 2593 theRequestPartitionId)); 2594 } 2595 break; 2596 case TOKEN: 2597 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2598 if (LOCATION_POSITION.equals(nextParamDef.getPath())) { 2599 andPredicates.add(createPredicateCoords( 2600 theSourceJoinColumn, 2601 theResourceName, 2602 null, 2603 nextParamDef, 2604 nextAnd, 2605 theRequestPartitionId, 2606 mySqlBuilder)); 2607 } else { 2608 andPredicates.add(createPredicateToken( 2609 theSourceJoinColumn, 2610 theResourceName, 2611 null, 2612 nextParamDef, 2613 nextAnd, 2614 null, 2615 theRequestPartitionId)); 2616 } 2617 } 2618 break; 2619 case NUMBER: 2620 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2621 andPredicates.add(createPredicateNumber( 2622 theSourceJoinColumn, 2623 theResourceName, 2624 null, 2625 nextParamDef, 2626 nextAnd, 2627 null, 2628 theRequestPartitionId)); 2629 } 2630 break; 2631 case COMPOSITE: 2632 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2633 andPredicates.add(createPredicateComposite( 2634 theSourceJoinColumn, 2635 theResourceName, 2636 null, 2637 nextParamDef, 2638 nextAnd, 2639 theRequestPartitionId)); 2640 } 2641 break; 2642 case URI: 2643 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2644 andPredicates.add(createPredicateUri( 2645 theSourceJoinColumn, 2646 theResourceName, 2647 null, 2648 nextParamDef, 2649 nextAnd, 2650 SearchFilterParser.CompareOperation.eq, 2651 theRequestPartitionId)); 2652 } 2653 break; 2654 case HAS: 2655 case SPECIAL: 2656 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2657 if (LOCATION_POSITION.equals(nextParamDef.getPath())) { 2658 andPredicates.add(createPredicateCoords( 2659 theSourceJoinColumn, 2660 theResourceName, 2661 null, 2662 nextParamDef, 2663 nextAnd, 2664 theRequestPartitionId, 2665 mySqlBuilder)); 2666 } 2667 } 2668 break; 2669 } 2670 } else { 2671 // These are handled later 2672 if (!Constants.PARAM_CONTENT.equals(theParamName) && !Constants.PARAM_TEXT.equals(theParamName)) { 2673 if (Constants.PARAM_FILTER.equals(theParamName)) { 2674 2675 // Parse the predicates enumerated in the _filter separated by AND or OR... 2676 if (theAndOrParams.get(0).get(0) instanceof StringParam) { 2677 String filterString = 2678 ((StringParam) theAndOrParams.get(0).get(0)).getValue(); 2679 SearchFilterParser.BaseFilter filter; 2680 try { 2681 filter = SearchFilterParser.parse(filterString); 2682 } catch (SearchFilterParser.FilterSyntaxException theE) { 2683 throw new InvalidRequestException( 2684 Msg.code(1221) + "Error parsing _filter syntax: " + theE.getMessage()); 2685 } 2686 if (filter != null) { 2687 2688 if (!myStorageSettings.isFilterParameterEnabled()) { 2689 throw new InvalidRequestException(Msg.code(1222) + Constants.PARAM_FILTER 2690 + " parameter is disabled on this server"); 2691 } 2692 2693 Condition predicate = 2694 createPredicateFilter(this, filter, theResourceName, theRequestPartitionId); 2695 if (predicate != null) { 2696 mySqlBuilder.addPredicate(predicate); 2697 } 2698 } 2699 } 2700 2701 } else { 2702 RuntimeSearchParam notEnabledForSearchParam = mySearchParamRegistry.getActiveSearchParam( 2703 theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.ALL); 2704 if (notEnabledForSearchParam == null) { 2705 String msg = myFhirContext 2706 .getLocalizer() 2707 .getMessageSanitized( 2708 BaseStorageDao.class, 2709 "invalidSearchParameter", 2710 theParamName, 2711 theResourceName, 2712 mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta( 2713 theResourceName, 2714 ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)); 2715 throw new InvalidRequestException(Msg.code(1223) + msg); 2716 } else { 2717 String msg = myFhirContext 2718 .getLocalizer() 2719 .getMessageSanitized( 2720 BaseStorageDao.class, 2721 "invalidSearchParameterNotEnabledForSearch", 2722 theParamName, 2723 theResourceName, 2724 mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta( 2725 theResourceName, 2726 ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)); 2727 throw new InvalidRequestException(Msg.code(2540) + msg); 2728 } 2729 } 2730 } 2731 } 2732 2733 return toAndPredicate(andPredicates); 2734 } 2735 2736 /** 2737 * This method handles the case of Search Parameters where the name/code 2738 * in the SP is a full chain expression. Normally to handle an expression 2739 * like <code>Observation?subject.name=foo</code> are handled by a SP 2740 * with a type of REFERENCE where the name is "subject". That is not 2741 * handled here. On the other hand, if the SP has a name value containing 2742 * the full chain (e.g. "subject.name") we handle that here. 2743 * 2744 * @return Returns {@literal true} if the search parameter was handled 2745 * by this method 2746 */ 2747 private boolean handleFullyChainedParameter( 2748 @Nullable DbColumn[] theSourceJoinColumn, 2749 String theResourceName, 2750 String theParamName, 2751 RequestPartitionId theRequestPartitionId, 2752 List<Condition> andPredicates, 2753 List<? extends IQueryParameterType> nextAnd) { 2754 if (!nextAnd.isEmpty() && nextAnd.get(0) instanceof ReferenceParam) { 2755 ReferenceParam param = (ReferenceParam) nextAnd.get(0); 2756 if (isNotBlank(param.getChain())) { 2757 String fullName = theParamName + "." + param.getChain(); 2758 RuntimeSearchParam fullChainParam = mySearchParamRegistry.getActiveSearchParam( 2759 theResourceName, fullName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 2760 if (fullChainParam != null) { 2761 List<IQueryParameterType> swappedParamTypes = nextAnd.stream() 2762 .map(t -> newParameterInstance(fullChainParam, null, t.getValueAsQueryToken(myFhirContext))) 2763 .collect(Collectors.toList()); 2764 List<List<IQueryParameterType>> params = List.of(swappedParamTypes); 2765 Condition predicate = createPredicateSearchParameter( 2766 theSourceJoinColumn, theResourceName, fullName, params, theRequestPartitionId); 2767 andPredicates.add(predicate); 2768 return true; 2769 } 2770 } 2771 } 2772 return false; 2773 } 2774 2775 /** 2776 * When searching using a chained search expression (e.g. "Patient?organization.name=foo") 2777 * we have a few options: 2778 * <ul> 2779 * <li> 2780 * A. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceLink} for 2781 * paramName="organization" with a join on {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString} 2782 * with paramName="name", that's {@link EmbeddedChainedSearchModeEnum#REF_JOIN_ONLY} 2783 * which is the standard searching case. Let's guess that 99.9% of all searches work 2784 * this way. 2785 * </ul> 2786 * <li> 2787 * B. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString} 2788 * with paramName="organization.name", that's {@link EmbeddedChainedSearchModeEnum#UPLIFTED_ONLY}. 2789 * We only do this if there is an uplifted refchain declared on the "organization" 2790 * search parameter for the "name" search parameter, and contained indexing is disabled. 2791 * This kind of index can come from indexing normal references where the search parameter 2792 * has an uplifted refchain declared, and it can also come from indexing contained resources. 2793 * For both of these cases, the actual index in the database is identical. But the important 2794 * difference is that when you're searching for contained resources you also want to 2795 * search for normal references. When you're searching for explicit refchains, no normal 2796 * indexes matter because they'd be a duplicate of the uplifted refchain. 2797 * </li> 2798 * <li> 2799 * C. We can also do both and return a union of the two, using 2800 * {@link EmbeddedChainedSearchModeEnum#UPLIFTED_AND_REF_JOIN}. We do that if contained 2801 * resource indexing is enabled since we have to assume there may be indexes 2802 * on "organization" for both contained and non-contained Organization. 2803 * resources. 2804 * </li> 2805 */ 2806 private EmbeddedChainedSearchModeEnum isEligibleForEmbeddedChainedResourceSearch( 2807 String theResourceType, String theParameterName, List<? extends IQueryParameterType> theParameter) { 2808 boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources(); 2809 boolean indexOnUpliftedRefchains = myStorageSettings.isIndexOnUpliftedRefchains(); 2810 2811 if (!indexOnContainedResources && !indexOnUpliftedRefchains) { 2812 return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY; 2813 } 2814 2815 boolean haveUpliftCandidates = theParameter.stream() 2816 .filter(t -> t instanceof ReferenceParam) 2817 .map(t -> ((ReferenceParam) t).getChain()) 2818 .filter(StringUtils::isNotBlank) 2819 // Chains on _has can't be indexed for contained searches - At least not yet. It's not clear to me if we 2820 // ever want to support this, it would be really hard to do. 2821 .filter(t -> !t.startsWith(PARAM_HAS + ":")) 2822 .anyMatch(t -> { 2823 if (indexOnContainedResources) { 2824 return true; 2825 } 2826 RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam( 2827 theResourceType, 2828 theParameterName, 2829 ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 2830 return param != null && param.hasUpliftRefchain(t); 2831 }); 2832 2833 if (haveUpliftCandidates) { 2834 if (indexOnContainedResources) { 2835 return EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN; 2836 } 2837 ourLog.debug("Search on {}.{} uses an uplifted refchain.", theResourceType, theParameterName); 2838 return EmbeddedChainedSearchModeEnum.UPLIFTED_ONLY; 2839 } else { 2840 return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY; 2841 } 2842 } 2843 2844 public void addPredicateCompositeUnique(List<String> theIndexStrings, RequestPartitionId theRequestPartitionId) { 2845 ComboUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboUniquePredicateBuilder(); 2846 Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexStrings); 2847 mySqlBuilder.addPredicate(predicate); 2848 } 2849 2850 public void addPredicateCompositeNonUnique(List<String> theIndexStrings, RequestPartitionId theRequestPartitionId) { 2851 ComboNonUniqueSearchParameterPredicateBuilder predicateBuilder = 2852 mySqlBuilder.addComboNonUniquePredicateBuilder(); 2853 Condition predicate = predicateBuilder.createPredicateHashComplete(theRequestPartitionId, theIndexStrings); 2854 mySqlBuilder.addPredicate(predicate); 2855 } 2856 2857 // expand out the pids 2858 public void addPredicateEverythingOperation( 2859 String theResourceName, List<String> theTypeSourceResourceNames, JpaPid... theTargetPids) { 2860 ResourceLinkPredicateBuilder table = mySqlBuilder.addReferencePredicateBuilder(this, null); 2861 Condition predicate = 2862 table.createEverythingPredicate(theResourceName, theTypeSourceResourceNames, theTargetPids); 2863 mySqlBuilder.addPredicate(predicate); 2864 mySqlBuilder.getSelect().setIsDistinct(true); 2865 addGrouping(); 2866 } 2867 2868 public IQueryParameterType newParameterInstance( 2869 RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) { 2870 IQueryParameterType qp = newParameterInstance(theParam); 2871 2872 qp.setValueAsQueryToken(myFhirContext, theParam.getName(), theQualifier, theValueAsQueryToken); 2873 return qp; 2874 } 2875 2876 private IQueryParameterType newParameterInstance(RuntimeSearchParam theParam) { 2877 2878 IQueryParameterType qp; 2879 switch (theParam.getParamType()) { 2880 case DATE: 2881 qp = new DateParam(); 2882 break; 2883 case NUMBER: 2884 qp = new NumberParam(); 2885 break; 2886 case QUANTITY: 2887 qp = new QuantityParam(); 2888 break; 2889 case STRING: 2890 qp = new StringParam(); 2891 break; 2892 case TOKEN: 2893 qp = new TokenParam(); 2894 break; 2895 case COMPOSITE: 2896 List<RuntimeSearchParam> compositeOf = 2897 JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParam); 2898 if (compositeOf.size() != 2) { 2899 throw new InternalErrorException(Msg.code(1224) + "Parameter " + theParam.getName() + " has " 2900 + compositeOf.size() + " composite parts. Don't know how handlt this."); 2901 } 2902 IQueryParameterType leftParam = newParameterInstance(compositeOf.get(0)); 2903 IQueryParameterType rightParam = newParameterInstance(compositeOf.get(1)); 2904 qp = new CompositeParam<>(leftParam, rightParam); 2905 break; 2906 case URI: 2907 qp = new UriParam(); 2908 break; 2909 case REFERENCE: 2910 qp = new ReferenceParam(); 2911 break; 2912 case SPECIAL: 2913 qp = new SpecialParam(); 2914 break; 2915 case HAS: 2916 default: 2917 throw new InvalidRequestException( 2918 Msg.code(1225) + "The search type: " + theParam.getParamType() + " is not supported."); 2919 } 2920 return qp; 2921 } 2922 2923 /** 2924 * @see #isEligibleForEmbeddedChainedResourceSearch(String, String, List) for an explanation of the values in this enum 2925 */ 2926 public enum EmbeddedChainedSearchModeEnum { 2927 UPLIFTED_ONLY(true), 2928 UPLIFTED_AND_REF_JOIN(true), 2929 REF_JOIN_ONLY(false); 2930 2931 private final boolean mySupportsUplifted; 2932 2933 EmbeddedChainedSearchModeEnum(boolean theSupportsUplifted) { 2934 mySupportsUplifted = theSupportsUplifted; 2935 } 2936 2937 public boolean supportsUplifted() { 2938 return mySupportsUplifted; 2939 } 2940 } 2941 2942 private static final class ChainElement { 2943 private final String myResourceType; 2944 private final String mySearchParameterName; 2945 private final String myPath; 2946 2947 public ChainElement(String theResourceType, String theSearchParameterName, String thePath) { 2948 this.myResourceType = theResourceType; 2949 this.mySearchParameterName = theSearchParameterName; 2950 this.myPath = thePath; 2951 } 2952 2953 public String getResourceType() { 2954 return myResourceType; 2955 } 2956 2957 public String getPath() { 2958 return myPath; 2959 } 2960 2961 public String getSearchParameterName() { 2962 return mySearchParameterName; 2963 } 2964 2965 @Override 2966 public boolean equals(Object o) { 2967 if (this == o) return true; 2968 if (o == null || getClass() != o.getClass()) return false; 2969 ChainElement that = (ChainElement) o; 2970 return myResourceType.equals(that.myResourceType) 2971 && mySearchParameterName.equals(that.mySearchParameterName) 2972 && myPath.equals(that.myPath); 2973 } 2974 2975 @Override 2976 public int hashCode() { 2977 return Objects.hash(myResourceType, mySearchParameterName, myPath); 2978 } 2979 } 2980 2981 private class ReferenceChainExtractor { 2982 private final Map<List<ChainElement>, Set<LeafNodeDefinition>> myChains = Maps.newHashMap(); 2983 2984 public Map<List<ChainElement>, Set<LeafNodeDefinition>> getChains() { 2985 return myChains; 2986 } 2987 2988 private boolean isReferenceParamValid(ReferenceParam theReferenceParam) { 2989 return split(theReferenceParam.getChain(), '.').length <= 3; 2990 } 2991 2992 private List<String> extractPaths(String theResourceType, RuntimeSearchParam theSearchParam) { 2993 List<String> pathsForType = theSearchParam.getPathsSplit().stream() 2994 .map(String::trim) 2995 .filter(t -> (t.startsWith(theResourceType) || t.startsWith("(" + theResourceType))) 2996 .collect(Collectors.toList()); 2997 if (pathsForType.isEmpty()) { 2998 ourLog.warn( 2999 "Search parameter {} does not have a path for resource type {}.", 3000 theSearchParam.getName(), 3001 theResourceType); 3002 } 3003 3004 return pathsForType; 3005 } 3006 3007 public void deriveChains( 3008 String theResourceType, 3009 RuntimeSearchParam theSearchParam, 3010 List<? extends IQueryParameterType> theList) { 3011 List<String> paths = extractPaths(theResourceType, theSearchParam); 3012 for (String path : paths) { 3013 List<ChainElement> searchParams = Lists.newArrayList(); 3014 searchParams.add(new ChainElement(theResourceType, theSearchParam.getName(), path)); 3015 for (IQueryParameterType nextOr : theList) { 3016 String targetValue = nextOr.getValueAsQueryToken(myFhirContext); 3017 if (nextOr instanceof ReferenceParam) { 3018 ReferenceParam referenceParam = (ReferenceParam) nextOr; 3019 if (!isReferenceParamValid(referenceParam)) { 3020 throw new InvalidRequestException(Msg.code(2007) + "The search chain " 3021 + theSearchParam.getName() + "." + referenceParam.getChain() 3022 + " is too long. Only chains up to three references are supported."); 3023 } 3024 3025 String targetChain = referenceParam.getChain(); 3026 List<String> qualifiers = Lists.newArrayList(referenceParam.getResourceType()); 3027 3028 processNextLinkInChain( 3029 searchParams, 3030 theSearchParam, 3031 targetChain, 3032 targetValue, 3033 qualifiers, 3034 referenceParam.getResourceType()); 3035 } 3036 } 3037 } 3038 } 3039 3040 private void processNextLinkInChain( 3041 List<ChainElement> theSearchParams, 3042 RuntimeSearchParam thePreviousSearchParam, 3043 String theChain, 3044 String theTargetValue, 3045 List<String> theQualifiers, 3046 String theResourceType) { 3047 3048 String nextParamName = theChain; 3049 String nextChain = null; 3050 String nextQualifier = null; 3051 int linkIndex = theChain.indexOf('.'); 3052 if (linkIndex != -1) { 3053 nextParamName = theChain.substring(0, linkIndex); 3054 nextChain = theChain.substring(linkIndex + 1); 3055 } 3056 3057 int qualifierIndex = nextParamName.indexOf(':'); 3058 if (qualifierIndex != -1) { 3059 nextParamName = nextParamName.substring(0, qualifierIndex); 3060 nextQualifier = nextParamName.substring(qualifierIndex); 3061 } 3062 3063 List<String> qualifiersBranch = Lists.newArrayList(); 3064 qualifiersBranch.addAll(theQualifiers); 3065 qualifiersBranch.add(nextQualifier); 3066 3067 boolean searchParamFound = false; 3068 for (String nextTarget : thePreviousSearchParam.getTargets()) { 3069 RuntimeSearchParam nextSearchParam = null; 3070 if (isBlank(theResourceType) || theResourceType.equals(nextTarget)) { 3071 nextSearchParam = mySearchParamRegistry.getActiveSearchParam( 3072 nextTarget, nextParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 3073 } 3074 if (nextSearchParam != null) { 3075 searchParamFound = true; 3076 // If we find a search param on this resource type for this parameter name, keep iterating 3077 // Otherwise, abandon this branch and carry on to the next one 3078 if (StringUtils.isEmpty(nextChain)) { 3079 // We've reached the end of the chain 3080 ArrayList<IQueryParameterType> orValues = Lists.newArrayList(); 3081 3082 if (RestSearchParameterTypeEnum.REFERENCE.equals(nextSearchParam.getParamType())) { 3083 orValues.add(new ReferenceParam(nextQualifier, "", theTargetValue)); 3084 } else { 3085 IQueryParameterType qp = newParameterInstance(nextSearchParam); 3086 qp.setValueAsQueryToken(myFhirContext, nextSearchParam.getName(), null, theTargetValue); 3087 orValues.add(qp); 3088 } 3089 3090 Set<LeafNodeDefinition> leafNodes = 3091 myChains.computeIfAbsent(theSearchParams, k -> Sets.newHashSet()); 3092 leafNodes.add(new LeafNodeDefinition( 3093 nextSearchParam, orValues, nextTarget, nextParamName, "", qualifiersBranch)); 3094 } else { 3095 List<String> nextPaths = extractPaths(nextTarget, nextSearchParam); 3096 for (String nextPath : nextPaths) { 3097 List<ChainElement> searchParamBranch = Lists.newArrayList(); 3098 searchParamBranch.addAll(theSearchParams); 3099 3100 searchParamBranch.add(new ChainElement(nextTarget, nextSearchParam.getName(), nextPath)); 3101 processNextLinkInChain( 3102 searchParamBranch, 3103 nextSearchParam, 3104 nextChain, 3105 theTargetValue, 3106 qualifiersBranch, 3107 nextQualifier); 3108 } 3109 } 3110 } 3111 } 3112 if (!searchParamFound) { 3113 throw new InvalidRequestException(Msg.code(1214) 3114 + myFhirContext 3115 .getLocalizer() 3116 .getMessage( 3117 BaseStorageDao.class, 3118 "invalidParameterChain", 3119 thePreviousSearchParam.getName() + '.' + theChain)); 3120 } 3121 } 3122 } 3123 3124 private static class LeafNodeDefinition { 3125 private final RuntimeSearchParam myParamDefinition; 3126 private final ArrayList<IQueryParameterType> myOrValues; 3127 private final String myLeafTarget; 3128 private final String myLeafParamName; 3129 private final String myLeafPathPrefix; 3130 private final List<String> myQualifiers; 3131 3132 public LeafNodeDefinition( 3133 RuntimeSearchParam theParamDefinition, 3134 ArrayList<IQueryParameterType> theOrValues, 3135 String theLeafTarget, 3136 String theLeafParamName, 3137 String theLeafPathPrefix, 3138 List<String> theQualifiers) { 3139 myParamDefinition = theParamDefinition; 3140 myOrValues = theOrValues; 3141 myLeafTarget = theLeafTarget; 3142 myLeafParamName = theLeafParamName; 3143 myLeafPathPrefix = theLeafPathPrefix; 3144 myQualifiers = theQualifiers; 3145 } 3146 3147 public RuntimeSearchParam getParamDefinition() { 3148 return myParamDefinition; 3149 } 3150 3151 public ArrayList<IQueryParameterType> getOrValues() { 3152 return myOrValues; 3153 } 3154 3155 public String getLeafTarget() { 3156 return myLeafTarget; 3157 } 3158 3159 public String getLeafParamName() { 3160 return myLeafParamName; 3161 } 3162 3163 public String getLeafPathPrefix() { 3164 return myLeafPathPrefix; 3165 } 3166 3167 public List<String> getQualifiers() { 3168 return myQualifiers; 3169 } 3170 3171 public LeafNodeDefinition withPathPrefix(String theResourceType, String theName) { 3172 return new LeafNodeDefinition( 3173 myParamDefinition, myOrValues, theResourceType, myLeafParamName, theName, myQualifiers); 3174 } 3175 3176 @Override 3177 public boolean equals(Object o) { 3178 if (this == o) return true; 3179 if (o == null || getClass() != o.getClass()) return false; 3180 LeafNodeDefinition that = (LeafNodeDefinition) o; 3181 return Objects.equals(myParamDefinition, that.myParamDefinition) 3182 && Objects.equals(myOrValues, that.myOrValues) 3183 && Objects.equals(myLeafTarget, that.myLeafTarget) 3184 && Objects.equals(myLeafParamName, that.myLeafParamName) 3185 && Objects.equals(myLeafPathPrefix, that.myLeafPathPrefix) 3186 && Objects.equals(myQualifiers, that.myQualifiers); 3187 } 3188 3189 @Override 3190 public int hashCode() { 3191 return Objects.hash( 3192 myParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers); 3193 } 3194 3195 /** 3196 * Return a copy of this object with the given {@link RuntimeSearchParam} 3197 * but all other values unchanged. 3198 */ 3199 public LeafNodeDefinition withParam(RuntimeSearchParam theParamDefinition) { 3200 return new LeafNodeDefinition( 3201 theParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers); 3202 } 3203 } 3204 3205 public static class SearchForIdsParams { 3206 DbColumn[] mySourceJoinColumn; 3207 String myResourceName; 3208 String myParamName; 3209 List<List<IQueryParameterType>> myAndOrParams; 3210 RequestDetails myRequest; 3211 RequestPartitionId myRequestPartitionId; 3212 3213 public static SearchForIdsParams with() { 3214 return new SearchForIdsParams(); 3215 } 3216 3217 public SearchForIdsParams setSourceJoinColumn(DbColumn[] theSourceJoinColumn) { 3218 mySourceJoinColumn = theSourceJoinColumn; 3219 return this; 3220 } 3221 3222 public String getResourceName() { 3223 return myResourceName; 3224 } 3225 3226 public SearchForIdsParams setResourceName(String theResourceName) { 3227 myResourceName = theResourceName; 3228 return this; 3229 } 3230 3231 public String getParamName() { 3232 return myParamName; 3233 } 3234 3235 public SearchForIdsParams setParamName(String theParamName) { 3236 myParamName = theParamName; 3237 return this; 3238 } 3239 3240 public SearchForIdsParams setAndOrParams(List<List<IQueryParameterType>> theAndOrParams) { 3241 myAndOrParams = theAndOrParams; 3242 return this; 3243 } 3244 3245 public RequestDetails getRequest() { 3246 return myRequest; 3247 } 3248 3249 public SearchForIdsParams setRequest(RequestDetails theRequest) { 3250 myRequest = theRequest; 3251 return this; 3252 } 3253 3254 public RequestPartitionId getRequestPartitionId() { 3255 return myRequestPartitionId; 3256 } 3257 3258 public SearchForIdsParams setRequestPartitionId(RequestPartitionId theRequestPartitionId) { 3259 myRequestPartitionId = theRequestPartitionId; 3260 return this; 3261 } 3262 } 3263}