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