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