
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()); 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<JpaParamUtil.ComponentAndCorrespondingParam> componentParams = 589 JpaParamUtil.resolveCompositeComponents(mySearchParamRegistry, theParamDef); 590 RuntimeSearchParam left = componentParams.get(0).getComponentParameter(); 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).getComponentParameter(); 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())); 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(myFhirContext, PARAM_HAS, qualifier, next.getValueAsQueryToken()); 1162 orValues.add(nextHasParam); 1163 } 1164 1165 } else if (paramName.equals(PARAM_ID)) { 1166 1167 for (IQueryParameterType next : nextOrList) { 1168 orValues.add(new TokenParam(next.getValueAsQueryToken())); 1169 } 1170 1171 } else { 1172 1173 // Ensure that the name of the search param 1174 // (e.g. the `code` in Patient?_has:Observation:subject:code=sys|val) 1175 // exists on the target resource type. 1176 RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getRuntimeSearchParam( 1177 targetResourceType, paramName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 1178 1179 // Ensure that the name of the back-referenced search param on the target (e.g. the `subject` in 1180 // Patient?_has:Observation:subject:code=sys|val) 1181 // exists on the target resource, or in the top-level Resource resource. 1182 mySearchParamRegistry.getRuntimeSearchParam( 1183 targetResourceType, paramReference, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 1184 1185 IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams( 1186 mySearchParamRegistry, myFhirContext, owningParameterDef, paramName, parameters); 1187 1188 for (IQueryParameterOr<?> next : parsedParam.getValuesAsQueryTokens()) { 1189 orValues.addAll(next.getValuesAsQueryTokens()); 1190 } 1191 } 1192 1193 // Handle internal chain inside the has. 1194 if (parameterName.contains(".")) { 1195 // Previously, for some unknown reason, we were calling getChainedPart() twice. This broke the _has 1196 // then chain, then _has use case by effectively cutting off the second part of the chain and 1197 // missing one iteration of the recursive call to build the query. 1198 // So, for example, for 1199 // Practitioner?_has:ExplanationOfBenefit:care-team:coverage.payor._has:List:item:_id=list1 1200 // instead of passing " payor._has:List:item:_id=list1" to the next recursion, the second call to 1201 // getChainedPart() was wrongly removing "payor." and passing down "_has:List:item:_id=list1" instead. 1202 // This resulted in running incorrect SQL with nonsensical join that resulted in 0 results. 1203 // However, after running the pipeline, I've concluded there's no use case at all for the 1204 // double call to "getChainedPart()", which is why there's no conditional logic at all to make a double 1205 // call to getChainedPart(). 1206 final String chainedPart = getChainedPart(parameterName); 1207 1208 orValues.stream() 1209 .filter(qp -> qp instanceof ReferenceParam) 1210 .map(qp -> (ReferenceParam) qp) 1211 .forEach(rp -> rp.setChain(chainedPart)); 1212 1213 parameterName = parameterName.substring(0, parameterName.indexOf('.')); 1214 } 1215 1216 int colonIndex = parameterName.indexOf(':'); 1217 if (colonIndex != -1) { 1218 parameterName = parameterName.substring(0, colonIndex); 1219 } 1220 1221 ResourceLinkPredicateBuilder resourceLinkTableJoin = 1222 mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn); 1223 1224 List<String> paths = resourceLinkTableJoin.createResourceLinkPaths( 1225 targetResourceType, paramReference, new ArrayList<>()); 1226 if (CollectionUtils.isEmpty(paths)) { 1227 throw new InvalidRequestException(Msg.code(2305) + "Reference field does not exist: " + paramReference); 1228 } 1229 1230 Condition typePredicate = BinaryCondition.equalTo( 1231 resourceLinkTableJoin.getColumnTargetResourceType(), 1232 mySqlBuilder.generatePlaceholder(theResourceType)); 1233 Condition pathPredicate = toEqualToOrInPredicate( 1234 resourceLinkTableJoin.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths)); 1235 1236 Condition linkedPredicate = 1237 searchForIdsWithAndOr(with().setSourceJoinColumn(resourceLinkTableJoin.getJoinColumnsForSource()) 1238 .setResourceName(targetResourceType) 1239 .setParamName(parameterName) 1240 .setAndOrParams(Collections.singletonList(orValues)) 1241 .setRequest(theRequest) 1242 .setRequestPartitionId(theRequestPartitionId)); 1243 1244 if (myPartitionSettings.isDatabasePartitionMode()) { 1245 andPredicates.add(toAndPredicate(pathPredicate, typePredicate, linkedPredicate)); 1246 } else { 1247 Condition partitionPredicate = resourceLinkTableJoin.createPartitionIdPredicate(theRequestPartitionId); 1248 andPredicates.add(toAndPredicate(partitionPredicate, pathPredicate, typePredicate, linkedPredicate)); 1249 } 1250 } 1251 1252 return toAndPredicate(andPredicates); 1253 } 1254 1255 public Condition createPredicateNumber( 1256 @Nullable DbColumn[] theSourceJoinColumn, 1257 String theResourceName, 1258 String theSpnamePrefix, 1259 RuntimeSearchParam theSearchParam, 1260 List<? extends IQueryParameterType> theList, 1261 SearchFilterParser.CompareOperation theOperation, 1262 RequestPartitionId theRequestPartitionId) { 1263 return createPredicateNumber( 1264 theSourceJoinColumn, 1265 theResourceName, 1266 theSpnamePrefix, 1267 theSearchParam, 1268 theList, 1269 theOperation, 1270 theRequestPartitionId, 1271 mySqlBuilder); 1272 } 1273 1274 public Condition createPredicateNumber( 1275 @Nullable DbColumn[] theSourceJoinColumn, 1276 String theResourceName, 1277 String theSpnamePrefix, 1278 RuntimeSearchParam theSearchParam, 1279 List<? extends IQueryParameterType> theList, 1280 SearchFilterParser.CompareOperation theOperation, 1281 RequestPartitionId theRequestPartitionId, 1282 SearchQueryBuilder theSqlBuilder) { 1283 1284 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 1285 1286 Boolean isMissing = theList.get(0).getMissing(); 1287 if (isMissing != null) { 1288 return createMissingParameterQuery(new MissingParameterQueryParams( 1289 theSqlBuilder, 1290 theSearchParam.getParamType(), 1291 theList, 1292 paramName, 1293 theResourceName, 1294 theSourceJoinColumn, 1295 theRequestPartitionId)); 1296 } else { 1297 NumberPredicateBuilder join = createOrReusePredicateBuilder( 1298 PredicateBuilderTypeEnum.NUMBER, 1299 theSourceJoinColumn, 1300 paramName, 1301 () -> theSqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn)) 1302 .getResult(); 1303 1304 List<Condition> codePredicates = new ArrayList<>(); 1305 for (IQueryParameterType nextOr : theList) { 1306 1307 if (nextOr instanceof NumberParam param) { 1308 1309 BigDecimal value = param.getValue(); 1310 if (value == null) { 1311 continue; 1312 } 1313 1314 SearchFilterParser.CompareOperation operation = theOperation; 1315 if (operation == null) { 1316 operation = toOperation(param.getPrefix()); 1317 } 1318 1319 Condition predicate = join.createPredicateNumeric( 1320 theResourceName, paramName, operation, value, theRequestPartitionId, nextOr); 1321 codePredicates.add(predicate); 1322 1323 } else { 1324 throw new IllegalArgumentException(Msg.code(1211) + "Invalid token type: " + nextOr.getClass()); 1325 } 1326 } 1327 1328 return join.combineWithRequestPartitionIdPredicate( 1329 theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); 1330 } 1331 } 1332 1333 public Condition createPredicateQuantity( 1334 @Nullable DbColumn[] theSourceJoinColumn, 1335 String theResourceName, 1336 String theSpnamePrefix, 1337 RuntimeSearchParam theSearchParam, 1338 List<? extends IQueryParameterType> theList, 1339 SearchFilterParser.CompareOperation theOperation, 1340 RequestPartitionId theRequestPartitionId) { 1341 return createPredicateQuantity( 1342 theSourceJoinColumn, 1343 theResourceName, 1344 theSpnamePrefix, 1345 theSearchParam, 1346 theList, 1347 theOperation, 1348 theRequestPartitionId, 1349 mySqlBuilder); 1350 } 1351 1352 public Condition createPredicateQuantity( 1353 @Nullable DbColumn[] theSourceJoinColumn, 1354 String theResourceName, 1355 String theSpnamePrefix, 1356 RuntimeSearchParam theSearchParam, 1357 List<? extends IQueryParameterType> theList, 1358 SearchFilterParser.CompareOperation theOperation, 1359 RequestPartitionId theRequestPartitionId, 1360 SearchQueryBuilder theSqlBuilder) { 1361 1362 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 1363 1364 Boolean isMissing = theList.get(0).getMissing(); 1365 if (isMissing != null) { 1366 return createMissingParameterQuery(new MissingParameterQueryParams( 1367 theSqlBuilder, 1368 theSearchParam.getParamType(), 1369 theList, 1370 paramName, 1371 theResourceName, 1372 theSourceJoinColumn, 1373 theRequestPartitionId)); 1374 } else { 1375 List<QuantityParam> quantityParams = 1376 theList.stream().map(QuantityParam::toQuantityParam).collect(Collectors.toList()); 1377 1378 BaseQuantityPredicateBuilder join = null; 1379 boolean normalizedSearchEnabled = myStorageSettings 1380 .getNormalizedQuantitySearchLevel() 1381 .equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED); 1382 if (normalizedSearchEnabled) { 1383 List<QuantityParam> normalizedQuantityParams = quantityParams.stream() 1384 .map(UcumServiceUtil::toCanonicalQuantityOrNull) 1385 .filter(Objects::nonNull) 1386 .collect(Collectors.toList()); 1387 1388 if (normalizedQuantityParams.size() == quantityParams.size()) { 1389 join = createOrReusePredicateBuilder( 1390 PredicateBuilderTypeEnum.QUANTITY, 1391 theSourceJoinColumn, 1392 paramName, 1393 () -> theSqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)) 1394 .getResult(); 1395 quantityParams = normalizedQuantityParams; 1396 } 1397 } 1398 1399 if (join == null) { 1400 join = createOrReusePredicateBuilder( 1401 PredicateBuilderTypeEnum.QUANTITY, 1402 theSourceJoinColumn, 1403 paramName, 1404 () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)) 1405 .getResult(); 1406 } 1407 1408 List<Condition> codePredicates = new ArrayList<>(); 1409 for (QuantityParam nextOr : quantityParams) { 1410 Condition singleCode = join.createPredicateQuantity( 1411 nextOr, theResourceName, paramName, null, join, theOperation, theRequestPartitionId); 1412 codePredicates.add(singleCode); 1413 } 1414 1415 return join.combineWithRequestPartitionIdPredicate( 1416 theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); 1417 } 1418 } 1419 1420 @Nullable 1421 public Condition createPredicateReference( 1422 @Nullable DbColumn[] theSourceJoinColumn, 1423 String theResourceName, 1424 String theParamName, 1425 List<String> theQualifiers, 1426 List<? extends IQueryParameterType> theList, 1427 SearchFilterParser.CompareOperation theOperation, 1428 RequestPartitionId theRequestPartitionId) { 1429 return createPredicateReference( 1430 theSourceJoinColumn, 1431 theResourceName, 1432 theParamName, 1433 theQualifiers, 1434 theList, 1435 theOperation, 1436 theRequestPartitionId, 1437 mySqlBuilder); 1438 } 1439 1440 @Nullable 1441 public Condition createPredicateReference( 1442 @Nullable DbColumn[] theSourceJoinColumn, 1443 String theResourceName, 1444 String theParamName, 1445 List<String> theQualifiers, 1446 List<? extends IQueryParameterType> theList, 1447 SearchFilterParser.CompareOperation theOperation, 1448 RequestPartitionId theRequestPartitionId, 1449 SearchQueryBuilder theSqlBuilder) { 1450 1451 if ((theOperation != null) 1452 && (theOperation != SearchFilterParser.CompareOperation.eq) 1453 && (theOperation != SearchFilterParser.CompareOperation.ne)) { 1454 throw new InvalidRequestException( 1455 Msg.code(1212) 1456 + "Invalid operator specified for reference predicate. Supported operators for reference predicate are \"eq\" and \"ne\"."); 1457 } 1458 1459 Boolean isMissing = theList.get(0).getMissing(); 1460 if (isMissing != null) { 1461 return createMissingParameterQuery(new MissingParameterQueryParams( 1462 theSqlBuilder, 1463 RestSearchParameterTypeEnum.REFERENCE, 1464 theList, 1465 theParamName, 1466 theResourceName, 1467 theSourceJoinColumn, 1468 theRequestPartitionId)); 1469 } else { 1470 ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder( 1471 PredicateBuilderTypeEnum.REFERENCE, 1472 theSourceJoinColumn, 1473 theParamName, 1474 () -> theSqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)) 1475 .getResult(); 1476 return predicateBuilder.createPredicate( 1477 myRequestDetails, 1478 theResourceName, 1479 theParamName, 1480 theQualifiers, 1481 theList, 1482 theOperation, 1483 theRequestPartitionId); 1484 } 1485 } 1486 1487 public void addGrouping() { 1488 if (!myGroupingAdded) { 1489 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 1490 1491 /* 1492 * Postgres and Oracle don't like it if we are doing a SELECT DISTINCT 1493 * with multiple selected columns but no GROUP BY clause. 1494 */ 1495 if (mySqlBuilder.isSelectPartitionId()) { 1496 mySqlBuilder 1497 .getSelect() 1498 .addGroupings( 1499 firstPredicateBuilder.getPartitionIdColumn(), 1500 firstPredicateBuilder.getResourceIdColumn()); 1501 } else { 1502 mySqlBuilder.getSelect().addGroupings(firstPredicateBuilder.getJoinColumns()); 1503 } 1504 myGroupingAdded = true; 1505 } 1506 } 1507 1508 public void addOrdering() { 1509 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 1510 mySqlBuilder.getSelect().addOrderings(firstPredicateBuilder.getJoinColumns()); 1511 } 1512 1513 public Condition createPredicateReferenceForEmbeddedChainedSearchResource( 1514 @Nullable DbColumn[] theSourceJoinColumn, 1515 String theResourceName, 1516 RuntimeSearchParam theSearchParam, 1517 List<? extends IQueryParameterType> theList, 1518 SearchFilterParser.CompareOperation theOperation, 1519 RequestPartitionId theRequestPartitionId, 1520 EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) { 1521 1522 boolean wantChainedAndNormal = 1523 theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN; 1524 1525 // 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 1526 // builders across different subselects 1527 EnumSet<PredicateBuilderTypeEnum> cachedReusePredicateBuilderTypes = 1528 EnumSet.copyOf(myReusePredicateBuilderTypes); 1529 if (wantChainedAndNormal) { 1530 myReusePredicateBuilderTypes.clear(); 1531 } 1532 1533 ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor(); 1534 chainExtractor.deriveChains(theResourceName, theSearchParam, theList); 1535 Map<List<ChainElement>, Set<LeafNodeDefinition>> chains = chainExtractor.getChains(); 1536 1537 Map<List<String>, Set<LeafNodeDefinition>> referenceLinks = Maps.newHashMap(); 1538 for (List<ChainElement> nextChain : chains.keySet()) { 1539 Set<LeafNodeDefinition> leafNodes = chains.get(nextChain); 1540 1541 collateChainedSearchOptions(referenceLinks, nextChain, leafNodes, theEmbeddedChainedSearchModeEnum); 1542 } 1543 1544 UnionQuery union = null; 1545 if (wantChainedAndNormal) { 1546 union = new UnionQuery(SetOperationQuery.Type.UNION_ALL); 1547 } 1548 1549 List<Condition> predicates = new ArrayList<>(); 1550 for (List<String> nextReferenceLink : referenceLinks.keySet()) { 1551 for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) { 1552 SearchQueryBuilder builder; 1553 if (wantChainedAndNormal) { 1554 builder = mySqlBuilder.newChildSqlBuilder(mySqlBuilder.isIncludePartitionIdInJoins()); 1555 } else { 1556 builder = mySqlBuilder; 1557 } 1558 1559 DbColumn[] previousJoinColumn = null; 1560 1561 // Create a reference link predicates to the subselect for every link but the last one 1562 for (String nextLink : nextReferenceLink) { 1563 // We don't want to call createPredicateReference() here, because the whole point is to avoid the 1564 // recursion. 1565 // TODO: Are we missing any important business logic from that method? All tests are passing. 1566 ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = 1567 builder.addReferencePredicateBuilder(this, previousJoinColumn); 1568 builder.addPredicate( 1569 resourceLinkPredicateBuilder.createPredicateSourcePaths(Lists.newArrayList(nextLink))); 1570 previousJoinColumn = resourceLinkPredicateBuilder.getJoinColumnsForTarget(); 1571 } 1572 1573 Condition containedCondition = createIndexPredicate( 1574 previousJoinColumn, 1575 leafNodeDefinition.getLeafTarget(), 1576 leafNodeDefinition.getLeafPathPrefix(), 1577 leafNodeDefinition.getLeafParamName(), 1578 leafNodeDefinition.getParamDefinition(), 1579 leafNodeDefinition.getOrValues(), 1580 theOperation, 1581 leafNodeDefinition.getQualifiers(), 1582 theRequestPartitionId, 1583 builder); 1584 1585 if (wantChainedAndNormal) { 1586 builder.addPredicate(containedCondition); 1587 union.addQueries(builder.getSelect()); 1588 } else { 1589 predicates.add(containedCondition); 1590 } 1591 } 1592 } 1593 1594 Condition retVal; 1595 if (wantChainedAndNormal) { 1596 1597 if (theSourceJoinColumn == null) { 1598 BaseJoiningPredicateBuilder root = mySqlBuilder.getOrCreateFirstPredicateBuilder(false, null); 1599 DbColumn[] joinColumns = root.getJoinColumns(); 1600 Object joinColumnObject; 1601 if (joinColumns.length == 1) { 1602 joinColumnObject = joinColumns[0]; 1603 } else { 1604 joinColumnObject = ColumnTupleObject.from(joinColumns); 1605 } 1606 retVal = new InCondition(joinColumnObject, union); 1607 } else { 1608 // -- for the resource link, need join with target_resource_id 1609 retVal = new InCondition(theSourceJoinColumn, union); 1610 } 1611 1612 } else { 1613 1614 retVal = toOrPredicate(predicates); 1615 } 1616 1617 // restore the state of this collection to turn caching back on before we exit 1618 myReusePredicateBuilderTypes.addAll(cachedReusePredicateBuilderTypes); 1619 return retVal; 1620 } 1621 1622 private void collateChainedSearchOptions( 1623 Map<List<String>, Set<LeafNodeDefinition>> referenceLinks, 1624 List<ChainElement> nextChain, 1625 Set<LeafNodeDefinition> leafNodes, 1626 EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) { 1627 // Manually collapse the chain using all possible variants of contained resource patterns. 1628 // This is a bit excruciating to extend beyond three references. Do we want to find a way to automate this 1629 // someday? 1630 // Note: the first element in each chain is assumed to be discrete. This may need to change when we add proper 1631 // support for `_contained` 1632 if (nextChain.size() == 1) { 1633 // discrete -> discrete 1634 if (theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN) { 1635 // If !theWantChainedAndNormal that means we're only processing refchains 1636 // so the discrete -> contained case is the only one that applies 1637 updateMapOfReferenceLinks( 1638 referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), leafNodes); 1639 } 1640 1641 // discrete -> contained 1642 RuntimeSearchParam firstParamDefinition = 1643 leafNodes.iterator().next().getParamDefinition(); 1644 updateMapOfReferenceLinks( 1645 referenceLinks, 1646 Lists.newArrayList(), 1647 leafNodes.stream() 1648 .map(t -> t.withPathPrefix( 1649 nextChain.get(0).getResourceType(), 1650 nextChain.get(0).getSearchParameterName())) 1651 // When we're handling discrete->contained the differences between search 1652 // parameters don't matter. E.g. if we're processing "subject.name=foo" 1653 // the name could be Patient:name or Group:name but it doesn't actually 1654 // matter that these are different since in this case both of these end 1655 // up being an identical search in the string table for "subject.name". 1656 .map(t -> t.withParam(firstParamDefinition)) 1657 .collect(Collectors.toSet())); 1658 } else if (nextChain.size() == 2) { 1659 // discrete -> discrete -> discrete 1660 updateMapOfReferenceLinks( 1661 referenceLinks, 1662 Lists.newArrayList( 1663 nextChain.get(0).getPath(), nextChain.get(1).getPath()), 1664 leafNodes); 1665 // discrete -> discrete -> contained 1666 updateMapOfReferenceLinks( 1667 referenceLinks, 1668 Lists.newArrayList(nextChain.get(0).getPath()), 1669 leafNodes.stream() 1670 .map(t -> t.withPathPrefix( 1671 nextChain.get(1).getResourceType(), 1672 nextChain.get(1).getSearchParameterName())) 1673 .collect(Collectors.toSet())); 1674 // discrete -> contained -> discrete 1675 updateMapOfReferenceLinks( 1676 referenceLinks, 1677 Lists.newArrayList(mergePaths( 1678 nextChain.get(0).getPath(), nextChain.get(1).getPath())), 1679 leafNodes); 1680 if (myStorageSettings.isIndexOnContainedResourcesRecursively()) { 1681 // discrete -> contained -> contained 1682 updateMapOfReferenceLinks( 1683 referenceLinks, 1684 Lists.newArrayList(), 1685 leafNodes.stream() 1686 .map(t -> t.withPathPrefix( 1687 nextChain.get(0).getResourceType(), 1688 nextChain.get(0).getSearchParameterName() + "." 1689 + nextChain.get(1).getSearchParameterName())) 1690 .collect(Collectors.toSet())); 1691 } 1692 } else if (nextChain.size() == 3) { 1693 // discrete -> discrete -> discrete -> discrete 1694 updateMapOfReferenceLinks( 1695 referenceLinks, 1696 Lists.newArrayList( 1697 nextChain.get(0).getPath(), 1698 nextChain.get(1).getPath(), 1699 nextChain.get(2).getPath()), 1700 leafNodes); 1701 // discrete -> discrete -> discrete -> contained 1702 updateMapOfReferenceLinks( 1703 referenceLinks, 1704 Lists.newArrayList( 1705 nextChain.get(0).getPath(), nextChain.get(1).getPath()), 1706 leafNodes.stream() 1707 .map(t -> t.withPathPrefix( 1708 nextChain.get(2).getResourceType(), 1709 nextChain.get(2).getSearchParameterName())) 1710 .collect(Collectors.toSet())); 1711 // discrete -> discrete -> contained -> discrete 1712 updateMapOfReferenceLinks( 1713 referenceLinks, 1714 Lists.newArrayList( 1715 nextChain.get(0).getPath(), 1716 mergePaths( 1717 nextChain.get(1).getPath(), nextChain.get(2).getPath())), 1718 leafNodes); 1719 // discrete -> contained -> discrete -> discrete 1720 updateMapOfReferenceLinks( 1721 referenceLinks, 1722 Lists.newArrayList( 1723 mergePaths( 1724 nextChain.get(0).getPath(), nextChain.get(1).getPath()), 1725 nextChain.get(2).getPath()), 1726 leafNodes); 1727 // discrete -> contained -> discrete -> contained 1728 updateMapOfReferenceLinks( 1729 referenceLinks, 1730 Lists.newArrayList(mergePaths( 1731 nextChain.get(0).getPath(), nextChain.get(1).getPath())), 1732 leafNodes.stream() 1733 .map(t -> t.withPathPrefix( 1734 nextChain.get(2).getResourceType(), 1735 nextChain.get(2).getSearchParameterName())) 1736 .collect(Collectors.toSet())); 1737 if (myStorageSettings.isIndexOnContainedResourcesRecursively()) { 1738 // discrete -> contained -> contained -> discrete 1739 updateMapOfReferenceLinks( 1740 referenceLinks, 1741 Lists.newArrayList(mergePaths( 1742 nextChain.get(0).getPath(), 1743 nextChain.get(1).getPath(), 1744 nextChain.get(2).getPath())), 1745 leafNodes); 1746 // discrete -> discrete -> contained -> contained 1747 updateMapOfReferenceLinks( 1748 referenceLinks, 1749 Lists.newArrayList(nextChain.get(0).getPath()), 1750 leafNodes.stream() 1751 .map(t -> t.withPathPrefix( 1752 nextChain.get(1).getResourceType(), 1753 nextChain.get(1).getSearchParameterName() + "." 1754 + nextChain.get(2).getSearchParameterName())) 1755 .collect(Collectors.toSet())); 1756 // discrete -> contained -> contained -> contained 1757 updateMapOfReferenceLinks( 1758 referenceLinks, 1759 Lists.newArrayList(), 1760 leafNodes.stream() 1761 .map(t -> t.withPathPrefix( 1762 nextChain.get(0).getResourceType(), 1763 nextChain.get(0).getSearchParameterName() + "." 1764 + nextChain.get(1).getSearchParameterName() + "." 1765 + nextChain.get(2).getSearchParameterName())) 1766 .collect(Collectors.toSet())); 1767 } 1768 } else { 1769 // TODO: the chain is too long, it isn't practical to hard-code all the possible patterns. If anyone ever 1770 // needs this, we should revisit the approach 1771 throw new InvalidRequestException(Msg.code(2011) 1772 + "The search chain is too long. Only chains of up to three references are supported."); 1773 } 1774 } 1775 1776 private void updateMapOfReferenceLinks( 1777 Map<List<String>, Set<LeafNodeDefinition>> theReferenceLinksMap, 1778 ArrayList<String> thePath, 1779 Set<LeafNodeDefinition> theLeafNodesToAdd) { 1780 Set<LeafNodeDefinition> leafNodes = theReferenceLinksMap.computeIfAbsent(thePath, k -> Sets.newHashSet()); 1781 leafNodes.addAll(theLeafNodesToAdd); 1782 } 1783 1784 private String mergePaths(String... paths) { 1785 String result = ""; 1786 for (String nextPath : paths) { 1787 int separatorIndex = nextPath.indexOf('.'); 1788 if (StringUtils.isEmpty(result)) { 1789 result = nextPath; 1790 } else { 1791 result = result + nextPath.substring(separatorIndex); 1792 } 1793 } 1794 return result; 1795 } 1796 1797 private Condition createIndexPredicate( 1798 DbColumn[] theSourceJoinColumn, 1799 String theResourceName, 1800 String theSpnamePrefix, 1801 String theParamName, 1802 RuntimeSearchParam theParamDefinition, 1803 ArrayList<IQueryParameterType> theOrValues, 1804 SearchFilterParser.CompareOperation theOperation, 1805 List<String> theQualifiers, 1806 RequestPartitionId theRequestPartitionId, 1807 SearchQueryBuilder theSqlBuilder) { 1808 1809 return switch (theParamDefinition.getParamType()) { 1810 case DATE -> createPredicateDate( 1811 theSourceJoinColumn, 1812 theResourceName, 1813 theSpnamePrefix, 1814 theParamDefinition, 1815 theOrValues, 1816 theOperation, 1817 theRequestPartitionId, 1818 theSqlBuilder); 1819 case NUMBER -> createPredicateNumber( 1820 theSourceJoinColumn, 1821 theResourceName, 1822 theSpnamePrefix, 1823 theParamDefinition, 1824 theOrValues, 1825 theOperation, 1826 theRequestPartitionId, 1827 theSqlBuilder); 1828 case QUANTITY -> createPredicateQuantity( 1829 theSourceJoinColumn, 1830 theResourceName, 1831 theSpnamePrefix, 1832 theParamDefinition, 1833 theOrValues, 1834 theOperation, 1835 theRequestPartitionId, 1836 theSqlBuilder); 1837 case STRING -> createPredicateString( 1838 theSourceJoinColumn, 1839 theResourceName, 1840 theSpnamePrefix, 1841 theParamDefinition, 1842 theOrValues, 1843 theOperation, 1844 theRequestPartitionId, 1845 theSqlBuilder); 1846 case TOKEN -> createPredicateToken( 1847 theSourceJoinColumn, 1848 theResourceName, 1849 theSpnamePrefix, 1850 theParamDefinition, 1851 theOrValues, 1852 theOperation, 1853 theRequestPartitionId, 1854 theSqlBuilder); 1855 case COMPOSITE -> createPredicateComposite( 1856 theSourceJoinColumn, 1857 theResourceName, 1858 theSpnamePrefix, 1859 theParamDefinition, 1860 theOrValues, 1861 theRequestPartitionId, 1862 theSqlBuilder); 1863 case URI -> createPredicateUri( 1864 theSourceJoinColumn, 1865 theResourceName, 1866 theSpnamePrefix, 1867 theParamDefinition, 1868 theOrValues, 1869 theOperation, 1870 theRequestPartitionId, 1871 theSqlBuilder); 1872 case REFERENCE -> createPredicateReference( 1873 theSourceJoinColumn, 1874 theResourceName, 1875 isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName, 1876 theQualifiers, 1877 theOrValues, 1878 theOperation, 1879 theRequestPartitionId, 1880 theSqlBuilder); 1881 default -> throw new InvalidRequestException( 1882 Msg.code(1215) + "The search type:" + theParamDefinition.getParamType() + " is not supported."); 1883 }; 1884 } 1885 1886 @Nullable 1887 public Condition createPredicateResourceId(SearchForIdsParams theSearchForIdsParams) { 1888 ResourceIdPredicateBuilder builder = mySqlBuilder.newResourceIdBuilder(); 1889 return builder.createPredicateResourceId(theSearchForIdsParams); 1890 } 1891 1892 private Condition createPredicateSourceForAndList( 1893 @Nullable DbColumn[] theSourceJoinColumn, List<List<IQueryParameterType>> theAndOrParams) { 1894 mySqlBuilder.getOrCreateFirstPredicateBuilder(); 1895 1896 List<Condition> andPredicates = new ArrayList<>(theAndOrParams.size()); 1897 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1898 andPredicates.add(createPredicateSource(theSourceJoinColumn, nextAnd)); 1899 } 1900 return toAndPredicate(andPredicates); 1901 } 1902 1903 private Condition createPredicateSource( 1904 @Nullable DbColumn[] theSourceJoinColumn, List<? extends IQueryParameterType> theList) { 1905 if (myStorageSettings.getStoreMetaSourceInformation() 1906 == JpaStorageSettings.StoreMetaSourceInformationEnum.NONE) { 1907 String msg = myFhirContext.getLocalizer().getMessage(QueryStack.class, "sourceParamDisabled"); 1908 throw new InvalidRequestException(Msg.code(1216) + msg); 1909 } 1910 1911 List<Condition> orPredicates = new ArrayList<>(); 1912 1913 // :missing=true modifier processing requires "LEFT JOIN" with HFJ_RESOURCE table to return correct results 1914 // if both sourceUri and requestId are not populated for the resource 1915 Optional<? extends IQueryParameterType> isMissingSourceOptional = theList.stream() 1916 .filter(nextParameter -> nextParameter.getMissing() != null && nextParameter.getMissing()) 1917 .findFirst(); 1918 1919 if (isMissingSourceOptional.isPresent()) { 1920 ISourcePredicateBuilder join = 1921 getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.LEFT_OUTER); 1922 orPredicates.add(join.createPredicateMissingSourceUri()); 1923 return toOrPredicate(orPredicates); 1924 } 1925 // for all other cases we use "INNER JOIN" to match search parameters 1926 ISourcePredicateBuilder join = getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.INNER); 1927 1928 for (IQueryParameterType nextParameter : theList) { 1929 SourceParam sourceParameter = new SourceParam(nextParameter.getValueAsQueryToken()); 1930 String sourceUri = sourceParameter.getSourceUri(); 1931 String requestId = sourceParameter.getRequestId(); 1932 if (isNotBlank(sourceUri) && isNotBlank(requestId)) { 1933 orPredicates.add(toAndPredicate( 1934 join.createPredicateSourceUri(sourceUri), join.createPredicateRequestId(requestId))); 1935 } else if (isNotBlank(sourceUri)) { 1936 orPredicates.add( 1937 join.createPredicateSourceUriWithModifiers(nextParameter, myStorageSettings, sourceUri)); 1938 } else if (isNotBlank(requestId)) { 1939 orPredicates.add(join.createPredicateRequestId(requestId)); 1940 } 1941 } 1942 1943 return toOrPredicate(orPredicates); 1944 } 1945 1946 private ISourcePredicateBuilder getSourcePredicateBuilder( 1947 @Nullable DbColumn[] theSourceJoinColumn, SelectQuery.JoinType theJoinType) { 1948 if (myStorageSettings.isAccessMetaSourceInformationFromProvenanceTable()) { 1949 return createOrReusePredicateBuilder( 1950 PredicateBuilderTypeEnum.SOURCE, 1951 theSourceJoinColumn, 1952 Constants.PARAM_SOURCE, 1953 () -> mySqlBuilder.addResourceHistoryProvenancePredicateBuilder( 1954 theSourceJoinColumn, theJoinType)) 1955 .getResult(); 1956 } 1957 return createOrReusePredicateBuilder( 1958 PredicateBuilderTypeEnum.SOURCE, 1959 theSourceJoinColumn, 1960 Constants.PARAM_SOURCE, 1961 () -> mySqlBuilder.addResourceHistoryPredicateBuilder(theSourceJoinColumn, theJoinType)) 1962 .getResult(); 1963 } 1964 1965 public Condition createPredicateString( 1966 @Nullable DbColumn[] theSourceJoinColumn, 1967 String theResourceName, 1968 String theSpnamePrefix, 1969 RuntimeSearchParam theSearchParam, 1970 List<? extends IQueryParameterType> theList, 1971 SearchFilterParser.CompareOperation theOperation, 1972 RequestPartitionId theRequestPartitionId) { 1973 return createPredicateString( 1974 theSourceJoinColumn, 1975 theResourceName, 1976 theSpnamePrefix, 1977 theSearchParam, 1978 theList, 1979 theOperation, 1980 theRequestPartitionId, 1981 mySqlBuilder); 1982 } 1983 1984 public Condition createPredicateString( 1985 @Nullable DbColumn[] theSourceJoinColumn, 1986 String theResourceName, 1987 String theSpnamePrefix, 1988 RuntimeSearchParam theSearchParam, 1989 List<? extends IQueryParameterType> theList, 1990 SearchFilterParser.CompareOperation theOperation, 1991 RequestPartitionId theRequestPartitionId, 1992 SearchQueryBuilder theSqlBuilder) { 1993 Boolean isMissing = theList.get(0).getMissing(); 1994 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 1995 1996 if (isMissing != null) { 1997 return createMissingParameterQuery(new MissingParameterQueryParams( 1998 theSqlBuilder, 1999 theSearchParam.getParamType(), 2000 theList, 2001 paramName, 2002 theResourceName, 2003 theSourceJoinColumn, 2004 theRequestPartitionId)); 2005 } 2006 2007 StringPredicateBuilder join = createOrReusePredicateBuilder( 2008 PredicateBuilderTypeEnum.STRING, 2009 theSourceJoinColumn, 2010 paramName, 2011 () -> theSqlBuilder.addStringPredicateBuilder(theSourceJoinColumn)) 2012 .getResult(); 2013 2014 List<Condition> codePredicates = new ArrayList<>(); 2015 for (IQueryParameterType nextOr : theList) { 2016 Condition singleCode = join.createPredicateString( 2017 nextOr, theResourceName, theSpnamePrefix, theSearchParam, join, theOperation); 2018 codePredicates.add(singleCode); 2019 } 2020 2021 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, toOrPredicate(codePredicates)); 2022 } 2023 2024 public Condition createPredicateTag( 2025 @Nullable DbColumn[] theSourceJoinColumn, 2026 List<List<IQueryParameterType>> theList, 2027 String theParamName, 2028 RequestPartitionId theRequestPartitionId) { 2029 TagTypeEnum tagType; 2030 if (Constants.PARAM_TAG.equals(theParamName)) { 2031 tagType = TagTypeEnum.TAG; 2032 } else if (Constants.PARAM_PROFILE.equals(theParamName)) { 2033 tagType = TagTypeEnum.PROFILE; 2034 } else if (Constants.PARAM_SECURITY.equals(theParamName)) { 2035 tagType = TagTypeEnum.SECURITY_LABEL; 2036 } else { 2037 throw new IllegalArgumentException(Msg.code(1217) + "Param name: " + theParamName); // shouldn't happen 2038 } 2039 2040 List<Condition> andPredicates = new ArrayList<>(); 2041 for (List<? extends IQueryParameterType> nextAndParams : theList) { 2042 if (!checkHaveTags(nextAndParams, theParamName)) { 2043 continue; 2044 } 2045 2046 List<Triple<String, String, String>> tokens = Lists.newArrayList(); 2047 boolean paramInverted = populateTokens(tokens, nextAndParams); 2048 if (tokens.isEmpty()) { 2049 continue; 2050 } 2051 2052 Condition tagPredicate; 2053 BaseJoiningPredicateBuilder join; 2054 if (paramInverted) { 2055 2056 boolean selectPartitionId = myPartitionSettings.isDatabasePartitionMode(); 2057 SearchQueryBuilder sqlBuilder = mySqlBuilder.newChildSqlBuilder(selectPartitionId); 2058 TagPredicateBuilder tagSelector = sqlBuilder.addTagPredicateBuilder(null); 2059 sqlBuilder.addPredicate( 2060 tagSelector.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId)); 2061 SelectQuery sql = sqlBuilder.getSelect(); 2062 2063 join = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 2064 Expression subSelect = new Subquery(sql); 2065 2066 Object left; 2067 if (selectPartitionId) { 2068 left = new ColumnTupleObject(join.getJoinColumns()); 2069 } else { 2070 left = join.getResourceIdColumn(); 2071 } 2072 tagPredicate = new InCondition(left, subSelect).setNegate(true); 2073 2074 } else { 2075 // Tag table can't be a query root because it will include deleted resources, and can't select by 2076 // resource type 2077 mySqlBuilder.getOrCreateFirstPredicateBuilder(); 2078 2079 TagPredicateBuilder tagJoin = createOrReusePredicateBuilder( 2080 PredicateBuilderTypeEnum.TAG, 2081 theSourceJoinColumn, 2082 theParamName, 2083 () -> mySqlBuilder.addTagPredicateBuilder(theSourceJoinColumn)) 2084 .getResult(); 2085 tagPredicate = tagJoin.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId); 2086 join = tagJoin; 2087 } 2088 2089 andPredicates.add(join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, tagPredicate)); 2090 } 2091 2092 return toAndPredicate(andPredicates); 2093 } 2094 2095 private boolean populateTokens( 2096 List<Triple<String, String, String>> theTokens, List<? extends IQueryParameterType> theAndParams) { 2097 boolean paramInverted = false; 2098 2099 for (IQueryParameterType nextOrParam : theAndParams) { 2100 String code; 2101 String system; 2102 if (nextOrParam instanceof TokenParam) { 2103 TokenParam nextParam = (TokenParam) nextOrParam; 2104 code = nextParam.getValue(); 2105 system = nextParam.getSystem(); 2106 if (nextParam.getModifier() == TokenParamModifier.NOT) { 2107 paramInverted = true; 2108 } 2109 } else if (nextOrParam instanceof ReferenceParam) { 2110 ReferenceParam nextParam = (ReferenceParam) nextOrParam; 2111 code = nextParam.getValue(); 2112 system = null; 2113 } else { 2114 UriParam nextParam = (UriParam) nextOrParam; 2115 code = nextParam.getValue(); 2116 system = null; 2117 } 2118 2119 if (isNotBlank(code)) { 2120 theTokens.add(Triple.of(system, nextOrParam.getQueryParameterQualifier(), code)); 2121 } 2122 } 2123 return paramInverted; 2124 } 2125 2126 private boolean checkHaveTags(List<? extends IQueryParameterType> theParams, String theParamName) { 2127 for (IQueryParameterType nextParamUncasted : theParams) { 2128 if (nextParamUncasted instanceof TokenParam) { 2129 TokenParam nextParam = (TokenParam) nextParamUncasted; 2130 if (isNotBlank(nextParam.getValue())) { 2131 return true; 2132 } 2133 if (isNotBlank(nextParam.getSystem())) { 2134 throw new TokenParamFormatInvalidRequestException( 2135 Msg.code(1218), theParamName, nextParam.getValueAsQueryToken()); 2136 } 2137 } 2138 2139 if (nextParamUncasted instanceof ReferenceParam 2140 && isNotBlank(((ReferenceParam) nextParamUncasted).getValue())) { 2141 return true; 2142 } else if (nextParamUncasted instanceof UriParam && isNotBlank(((UriParam) nextParamUncasted).getValue())) { 2143 return true; 2144 } 2145 } 2146 2147 return false; 2148 } 2149 2150 public Condition createPredicateToken( 2151 @Nullable DbColumn[] theSourceJoinColumn, 2152 String theResourceName, 2153 String theSpnamePrefix, 2154 RuntimeSearchParam theSearchParam, 2155 List<? extends IQueryParameterType> theList, 2156 SearchFilterParser.CompareOperation theOperation, 2157 RequestPartitionId theRequestPartitionId) { 2158 return createPredicateToken( 2159 theSourceJoinColumn, 2160 theResourceName, 2161 theSpnamePrefix, 2162 theSearchParam, 2163 theList, 2164 theOperation, 2165 theRequestPartitionId, 2166 mySqlBuilder); 2167 } 2168 2169 public Condition createPredicateToken( 2170 @Nullable DbColumn[] theSourceJoinColumn, 2171 String theResourceName, 2172 String theSpnamePrefix, 2173 RuntimeSearchParam theSearchParam, 2174 List<? extends IQueryParameterType> theList, 2175 SearchFilterParser.CompareOperation theOperation, 2176 RequestPartitionId theRequestPartitionId, 2177 SearchQueryBuilder theSqlBuilder) { 2178 2179 List<IQueryParameterType> tokens = new ArrayList<>(); 2180 2181 boolean paramInverted = false; 2182 TokenParamModifier modifier; 2183 2184 for (IQueryParameterType nextOr : theList) { 2185 if (nextOr instanceof TokenParam) { 2186 if (!((TokenParam) nextOr).isEmpty()) { 2187 TokenParam id = (TokenParam) nextOr; 2188 if (id.isText()) { 2189 2190 // Check whether the :text modifier is actually enabled here 2191 boolean tokenTextIndexingEnabled = 2192 BaseSearchParamExtractor.tokenTextIndexingEnabledForSearchParam( 2193 myStorageSettings, theSearchParam); 2194 if (!tokenTextIndexingEnabled) { 2195 String msg; 2196 if (myStorageSettings.isSuppressStringIndexingInTokens()) { 2197 msg = myFhirContext 2198 .getLocalizer() 2199 .getMessage(QueryStack.class, "textModifierDisabledForServer"); 2200 } else { 2201 msg = myFhirContext 2202 .getLocalizer() 2203 .getMessage(QueryStack.class, "textModifierDisabledForSearchParam"); 2204 } 2205 throw new MethodNotAllowedException(Msg.code(1219) + msg); 2206 } 2207 return createPredicateString( 2208 theSourceJoinColumn, 2209 theResourceName, 2210 theSpnamePrefix, 2211 theSearchParam, 2212 theList, 2213 null, 2214 theRequestPartitionId, 2215 theSqlBuilder); 2216 } 2217 2218 modifier = id.getModifier(); 2219 // for :not modifier, create a token and remove the :not modifier 2220 if (modifier == TokenParamModifier.NOT) { 2221 tokens.add(new TokenParam(((TokenParam) nextOr).getSystem(), ((TokenParam) nextOr).getValue())); 2222 paramInverted = true; 2223 } else { 2224 tokens.add(nextOr); 2225 } 2226 } 2227 } else { 2228 tokens.add(nextOr); 2229 } 2230 } 2231 2232 if (tokens.isEmpty()) { 2233 return null; 2234 } 2235 2236 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 2237 Condition predicate; 2238 BaseJoiningPredicateBuilder join; 2239 2240 if (paramInverted) { 2241 boolean selectPartitionId = myPartitionSettings.isDatabasePartitionMode(); 2242 SearchQueryBuilder sqlBuilder = theSqlBuilder.newChildSqlBuilder(selectPartitionId); 2243 TokenPredicateBuilder tokenSelector = sqlBuilder.addTokenPredicateBuilder(null); 2244 sqlBuilder.addPredicate(tokenSelector.createPredicateToken( 2245 tokens, theResourceName, theSpnamePrefix, theSearchParam, theRequestPartitionId)); 2246 SelectQuery sql = sqlBuilder.getSelect(); 2247 Expression subSelect = new Subquery(sql); 2248 2249 join = theSqlBuilder.getOrCreateFirstPredicateBuilder(); 2250 2251 DbColumn[] leftColumns; 2252 if (theSourceJoinColumn == null) { 2253 leftColumns = join.getJoinColumns(); 2254 } else { 2255 leftColumns = theSourceJoinColumn; 2256 } 2257 2258 Object left = new ColumnTupleObject(leftColumns); 2259 predicate = new InCondition(left, subSelect).setNegate(true); 2260 2261 } else { 2262 Boolean isMissing = theList.get(0).getMissing(); 2263 if (isMissing != null) { 2264 return createMissingParameterQuery(new MissingParameterQueryParams( 2265 theSqlBuilder, 2266 theSearchParam.getParamType(), 2267 theList, 2268 paramName, 2269 theResourceName, 2270 theSourceJoinColumn, 2271 theRequestPartitionId)); 2272 } 2273 2274 TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder( 2275 PredicateBuilderTypeEnum.TOKEN, 2276 theSourceJoinColumn, 2277 paramName, 2278 () -> theSqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)) 2279 .getResult(); 2280 2281 predicate = tokenJoin.createPredicateToken( 2282 tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId); 2283 join = tokenJoin; 2284 } 2285 2286 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); 2287 } 2288 2289 public Condition createPredicateUri( 2290 @Nullable DbColumn[] theSourceJoinColumn, 2291 String theResourceName, 2292 String theSpnamePrefix, 2293 RuntimeSearchParam theSearchParam, 2294 List<? extends IQueryParameterType> theList, 2295 SearchFilterParser.CompareOperation theOperation, 2296 RequestPartitionId theRequestPartitionId) { 2297 return createPredicateUri( 2298 theSourceJoinColumn, 2299 theResourceName, 2300 theSpnamePrefix, 2301 theSearchParam, 2302 theList, 2303 theOperation, 2304 theRequestPartitionId, 2305 mySqlBuilder); 2306 } 2307 2308 public Condition createPredicateUri( 2309 @Nullable DbColumn[] theSourceJoinColumn, 2310 String theResourceName, 2311 String theSpnamePrefix, 2312 RuntimeSearchParam theSearchParam, 2313 List<? extends IQueryParameterType> theList, 2314 SearchFilterParser.CompareOperation theOperation, 2315 RequestPartitionId theRequestPartitionId, 2316 SearchQueryBuilder theSqlBuilder) { 2317 2318 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 2319 2320 Boolean isMissing = theList.get(0).getMissing(); 2321 if (isMissing != null) { 2322 return createMissingParameterQuery(new MissingParameterQueryParams( 2323 theSqlBuilder, 2324 theSearchParam.getParamType(), 2325 theList, 2326 paramName, 2327 theResourceName, 2328 theSourceJoinColumn, 2329 theRequestPartitionId)); 2330 } else { 2331 UriPredicateBuilder join = theSqlBuilder.addUriPredicateBuilder(theSourceJoinColumn); 2332 2333 Condition predicate = join.addPredicate(theList, paramName, theOperation, myRequestDetails); 2334 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); 2335 } 2336 } 2337 2338 public QueryStack newChildQueryFactoryWithFullBuilderReuse() { 2339 return new QueryStack( 2340 myRequestDetails, 2341 mySearchParameters, 2342 myStorageSettings, 2343 myFhirContext, 2344 mySqlBuilder, 2345 mySearchParamRegistry, 2346 myPartitionSettings, 2347 EnumSet.allOf(PredicateBuilderTypeEnum.class)); 2348 } 2349 2350 @Nullable 2351 public Condition searchForIdsWithAndOr(SearchForIdsParams theSearchForIdsParams) { 2352 2353 if (theSearchForIdsParams.myAndOrParams.isEmpty()) { 2354 return null; 2355 } 2356 2357 switch (theSearchForIdsParams.myParamName) { 2358 case IAnyResource.SP_RES_ID: 2359 return createPredicateResourceId(theSearchForIdsParams); 2360 2361 case Constants.PARAM_PID: 2362 return createPredicateResourcePID( 2363 theSearchForIdsParams.mySourceJoinColumn, theSearchForIdsParams.myAndOrParams); 2364 2365 case PARAM_HAS: 2366 return createPredicateHas( 2367 theSearchForIdsParams.mySourceJoinColumn, 2368 theSearchForIdsParams.myResourceName, 2369 theSearchForIdsParams.myAndOrParams, 2370 theSearchForIdsParams.myRequest, 2371 theSearchForIdsParams.myRequestPartitionId); 2372 2373 case Constants.PARAM_TAG: 2374 case Constants.PARAM_PROFILE: 2375 case Constants.PARAM_SECURITY: 2376 if (myStorageSettings.getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.INLINE) { 2377 return createPredicateSearchParameter( 2378 theSearchForIdsParams.mySourceJoinColumn, 2379 theSearchForIdsParams.myResourceName, 2380 theSearchForIdsParams.myParamName, 2381 theSearchForIdsParams.myAndOrParams, 2382 theSearchForIdsParams.myRequestPartitionId); 2383 } else { 2384 return createPredicateTag( 2385 theSearchForIdsParams.mySourceJoinColumn, 2386 theSearchForIdsParams.myAndOrParams, 2387 theSearchForIdsParams.myParamName, 2388 theSearchForIdsParams.myRequestPartitionId); 2389 } 2390 2391 case Constants.PARAM_SOURCE: 2392 return createPredicateSourceForAndList( 2393 theSearchForIdsParams.mySourceJoinColumn, theSearchForIdsParams.myAndOrParams); 2394 2395 case Constants.PARAM_LASTUPDATED: 2396 // this case statement handles a _lastUpdated query as part of a reverse search 2397 // only (/Patient?_has:Encounter:patient:_lastUpdated=ge2023-10-24). 2398 // performing a _lastUpdated query on a resource (/Patient?_lastUpdated=eq2023-10-24) 2399 // is handled in {@link SearchBuilder#createChunkedQuery}. 2400 return createReverseSearchPredicateLastUpdated( 2401 theSearchForIdsParams.myAndOrParams, theSearchForIdsParams.mySourceJoinColumn); 2402 2403 default: 2404 return createPredicateSearchParameter( 2405 theSearchForIdsParams.mySourceJoinColumn, 2406 theSearchForIdsParams.myResourceName, 2407 theSearchForIdsParams.myParamName, 2408 theSearchForIdsParams.myAndOrParams, 2409 theSearchForIdsParams.myRequestPartitionId); 2410 } 2411 } 2412 2413 /** 2414 * Raw match on RES_ID 2415 */ 2416 private Condition createPredicateResourcePID( 2417 DbColumn[] theSourceJoinColumn, List<List<IQueryParameterType>> theAndOrParams) { 2418 DbColumn pidColumn = getResourceIdColumn(theSourceJoinColumn); 2419 2420 if (pidColumn == null) { 2421 BaseJoiningPredicateBuilder predicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 2422 pidColumn = predicateBuilder.getResourceIdColumn(); 2423 } 2424 2425 // we don't support any modifiers for now 2426 Set<Long> pids = theAndOrParams.stream() 2427 .map(orList -> orList.stream() 2428 .map(v -> v.getValueAsQueryToken()) 2429 .map(Long::valueOf) 2430 .collect(Collectors.toSet())) 2431 .reduce(Sets::intersection) 2432 .orElse(Set.of()); 2433 2434 if (pids.isEmpty()) { 2435 mySqlBuilder.setMatchNothing(); 2436 return null; 2437 } 2438 2439 return toEqualToOrInPredicate(pidColumn, mySqlBuilder.generatePlaceholders(pids)); 2440 } 2441 2442 private Condition createReverseSearchPredicateLastUpdated( 2443 List<List<IQueryParameterType>> theAndOrParams, DbColumn[] theSourceColumn) { 2444 2445 ResourceTablePredicateBuilder resourceTableJoin = 2446 mySqlBuilder.addResourceTablePredicateBuilder(theSourceColumn); 2447 2448 List<Condition> andPredicates = new ArrayList<>(theAndOrParams.size()); 2449 2450 for (List<IQueryParameterType> aList : theAndOrParams) { 2451 if (!aList.isEmpty()) { 2452 DateParam dateParam = (DateParam) aList.get(0); 2453 DateRangeParam dateRangeParam = new DateRangeParam(dateParam); 2454 Condition aCondition = mySqlBuilder.addPredicateLastUpdated(dateRangeParam, resourceTableJoin); 2455 andPredicates.add(aCondition); 2456 } 2457 } 2458 2459 return toAndPredicate(andPredicates); 2460 } 2461 2462 @Nullable 2463 private Condition createPredicateSearchParameter( 2464 @Nullable DbColumn[] theSourceJoinColumn, 2465 String theResourceName, 2466 String theParamName, 2467 List<List<IQueryParameterType>> theAndOrParams, 2468 RequestPartitionId theRequestPartitionId) { 2469 List<Condition> andPredicates = new ArrayList<>(); 2470 RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam( 2471 theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 2472 if (nextParamDef != null) { 2473 2474 if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.isIncludePartitionInSearchHashes()) { 2475 if (theRequestPartitionId.isAllPartitions()) { 2476 throw new PreconditionFailedException( 2477 Msg.code(1220) + "This server is not configured to support search against all partitions"); 2478 } 2479 } 2480 2481 switch (nextParamDef.getParamType()) { 2482 case DATE: 2483 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2484 // FT: 2021-01-18 use operation 'gt', 'ge', 'le' or 'lt' 2485 // to create the predicateDate instead of generic one with operation = null 2486 SearchFilterParser.CompareOperation operation = null; 2487 if (!nextAnd.isEmpty()) { 2488 DateParam param = (DateParam) nextAnd.get(0); 2489 operation = toOperation(param.getPrefix()); 2490 } 2491 andPredicates.add(createPredicateDate( 2492 theSourceJoinColumn, 2493 theResourceName, 2494 null, 2495 nextParamDef, 2496 nextAnd, 2497 operation, 2498 theRequestPartitionId)); 2499 } 2500 break; 2501 case QUANTITY: 2502 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2503 SearchFilterParser.CompareOperation operation = null; 2504 if (!nextAnd.isEmpty()) { 2505 QuantityParam param = (QuantityParam) nextAnd.get(0); 2506 operation = toOperation(param.getPrefix()); 2507 } 2508 andPredicates.add(createPredicateQuantity( 2509 theSourceJoinColumn, 2510 theResourceName, 2511 null, 2512 nextParamDef, 2513 nextAnd, 2514 operation, 2515 theRequestPartitionId)); 2516 } 2517 break; 2518 case REFERENCE: 2519 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2520 2521 // omit this param since the or-list is empty. There is no value generating sql for this. 2522 if (ParameterUtil.areAllParametersEmpty(nextAnd)) { 2523 continue; 2524 } 2525 2526 // Handle Search Parameters where the name is a full chain 2527 // (e.g. SearchParameter with name=composition.patient.identifier) 2528 if (handleFullyChainedParameter( 2529 theSourceJoinColumn, 2530 theResourceName, 2531 theParamName, 2532 theRequestPartitionId, 2533 andPredicates, 2534 nextAnd)) { 2535 continue; 2536 } 2537 2538 EmbeddedChainedSearchModeEnum embeddedChainedSearchModeEnum = 2539 isEligibleForEmbeddedChainedResourceSearch(theResourceName, theParamName, nextAnd); 2540 if (embeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY) { 2541 andPredicates.add(createPredicateReference( 2542 theSourceJoinColumn, 2543 theResourceName, 2544 theParamName, 2545 new ArrayList<>(), 2546 nextAnd, 2547 null, 2548 theRequestPartitionId)); 2549 } else { 2550 andPredicates.add(createPredicateReferenceForEmbeddedChainedSearchResource( 2551 theSourceJoinColumn, 2552 theResourceName, 2553 nextParamDef, 2554 nextAnd, 2555 null, 2556 theRequestPartitionId, 2557 embeddedChainedSearchModeEnum)); 2558 } 2559 } 2560 break; 2561 case STRING: 2562 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2563 andPredicates.add(createPredicateString( 2564 theSourceJoinColumn, 2565 theResourceName, 2566 null, 2567 nextParamDef, 2568 nextAnd, 2569 SearchFilterParser.CompareOperation.sw, 2570 theRequestPartitionId)); 2571 } 2572 break; 2573 case TOKEN: 2574 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2575 if (LOCATION_POSITION.equals(nextParamDef.getPath())) { 2576 andPredicates.add(createPredicateCoords( 2577 theSourceJoinColumn, 2578 theResourceName, 2579 null, 2580 nextParamDef, 2581 nextAnd, 2582 theRequestPartitionId, 2583 mySqlBuilder)); 2584 } else { 2585 andPredicates.add(createPredicateToken( 2586 theSourceJoinColumn, 2587 theResourceName, 2588 null, 2589 nextParamDef, 2590 nextAnd, 2591 null, 2592 theRequestPartitionId)); 2593 } 2594 } 2595 break; 2596 case NUMBER: 2597 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2598 andPredicates.add(createPredicateNumber( 2599 theSourceJoinColumn, 2600 theResourceName, 2601 null, 2602 nextParamDef, 2603 nextAnd, 2604 null, 2605 theRequestPartitionId)); 2606 } 2607 break; 2608 case COMPOSITE: 2609 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2610 andPredicates.add(createPredicateComposite( 2611 theSourceJoinColumn, 2612 theResourceName, 2613 null, 2614 nextParamDef, 2615 nextAnd, 2616 theRequestPartitionId)); 2617 } 2618 break; 2619 case URI: 2620 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2621 andPredicates.add(createPredicateUri( 2622 theSourceJoinColumn, 2623 theResourceName, 2624 null, 2625 nextParamDef, 2626 nextAnd, 2627 SearchFilterParser.CompareOperation.eq, 2628 theRequestPartitionId)); 2629 } 2630 break; 2631 case HAS: 2632 case SPECIAL: 2633 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2634 if (LOCATION_POSITION.equals(nextParamDef.getPath())) { 2635 andPredicates.add(createPredicateCoords( 2636 theSourceJoinColumn, 2637 theResourceName, 2638 null, 2639 nextParamDef, 2640 nextAnd, 2641 theRequestPartitionId, 2642 mySqlBuilder)); 2643 } 2644 } 2645 break; 2646 } 2647 } else { 2648 // These are handled later 2649 if (!Constants.PARAM_CONTENT.equals(theParamName) && !Constants.PARAM_TEXT.equals(theParamName)) { 2650 if (Constants.PARAM_FILTER.equals(theParamName)) { 2651 2652 // Parse the predicates enumerated in the _filter separated by AND or OR... 2653 if (theAndOrParams.get(0).get(0) instanceof StringParam) { 2654 String filterString = 2655 ((StringParam) theAndOrParams.get(0).get(0)).getValue(); 2656 SearchFilterParser.BaseFilter filter; 2657 try { 2658 filter = SearchFilterParser.parse(filterString); 2659 } catch (SearchFilterParser.FilterSyntaxException theE) { 2660 throw new InvalidRequestException( 2661 Msg.code(1221) + "Error parsing _filter syntax: " + theE.getMessage()); 2662 } 2663 if (filter != null) { 2664 2665 if (!myStorageSettings.isFilterParameterEnabled()) { 2666 throw new InvalidRequestException(Msg.code(1222) + Constants.PARAM_FILTER 2667 + " parameter is disabled on this server"); 2668 } 2669 2670 Condition predicate = 2671 createPredicateFilter(this, filter, theResourceName, theRequestPartitionId); 2672 if (predicate != null) { 2673 mySqlBuilder.addPredicate(predicate); 2674 } 2675 } 2676 } 2677 2678 } else { 2679 RuntimeSearchParam notEnabledForSearchParam = mySearchParamRegistry.getActiveSearchParam( 2680 theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.ALL); 2681 if (notEnabledForSearchParam == null) { 2682 String msg = myFhirContext 2683 .getLocalizer() 2684 .getMessageSanitized( 2685 BaseStorageDao.class, 2686 "invalidSearchParameter", 2687 theParamName, 2688 theResourceName, 2689 mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta( 2690 theResourceName, 2691 ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)); 2692 throw new InvalidRequestException(Msg.code(1223) + msg); 2693 } else { 2694 String msg = myFhirContext 2695 .getLocalizer() 2696 .getMessageSanitized( 2697 BaseStorageDao.class, 2698 "invalidSearchParameterNotEnabledForSearch", 2699 theParamName, 2700 theResourceName, 2701 mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta( 2702 theResourceName, 2703 ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)); 2704 throw new InvalidRequestException(Msg.code(2540) + msg); 2705 } 2706 } 2707 } 2708 } 2709 2710 return toAndPredicate(andPredicates); 2711 } 2712 2713 /** 2714 * This method handles the case of Search Parameters where the name/code 2715 * in the SP is a full chain expression. Normally to handle an expression 2716 * like <code>Observation?subject.name=foo</code> are handled by a SP 2717 * with a type of REFERENCE where the name is "subject". That is not 2718 * handled here. On the other hand, if the SP has a name value containing 2719 * the full chain (e.g. "subject.name") we handle that here. 2720 * 2721 * @return Returns {@literal true} if the search parameter was handled 2722 * by this method 2723 */ 2724 private boolean handleFullyChainedParameter( 2725 @Nullable DbColumn[] theSourceJoinColumn, 2726 String theResourceName, 2727 String theParamName, 2728 RequestPartitionId theRequestPartitionId, 2729 List<Condition> andPredicates, 2730 List<? extends IQueryParameterType> nextAnd) { 2731 if (!nextAnd.isEmpty() && nextAnd.get(0) instanceof ReferenceParam) { 2732 ReferenceParam param = (ReferenceParam) nextAnd.get(0); 2733 if (isNotBlank(param.getChain())) { 2734 String fullName = theParamName + "." + param.getChain(); 2735 RuntimeSearchParam fullChainParam = mySearchParamRegistry.getActiveSearchParam( 2736 theResourceName, fullName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 2737 if (fullChainParam != null) { 2738 List<IQueryParameterType> swappedParamTypes = nextAnd.stream() 2739 .map(t -> newParameterInstance(fullChainParam, null, t.getValueAsQueryToken())) 2740 .collect(Collectors.toList()); 2741 List<List<IQueryParameterType>> params = List.of(swappedParamTypes); 2742 Condition predicate = createPredicateSearchParameter( 2743 theSourceJoinColumn, theResourceName, fullName, params, theRequestPartitionId); 2744 andPredicates.add(predicate); 2745 return true; 2746 } 2747 } 2748 } 2749 return false; 2750 } 2751 2752 /** 2753 * When searching using a chained search expression (e.g. "Patient?organization.name=foo") 2754 * we have a few options: 2755 * <ul> 2756 * <li> 2757 * A. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceLink} for 2758 * paramName="organization" with a join on {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString} 2759 * with paramName="name", that's {@link EmbeddedChainedSearchModeEnum#REF_JOIN_ONLY} 2760 * which is the standard searching case. Let's guess that 99.9% of all searches work 2761 * this way. 2762 * </ul> 2763 * <li> 2764 * B. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString} 2765 * with paramName="organization.name", that's {@link EmbeddedChainedSearchModeEnum#UPLIFTED_ONLY}. 2766 * We only do this if there is an uplifted refchain declared on the "organization" 2767 * search parameter for the "name" search parameter, and contained indexing is disabled. 2768 * This kind of index can come from indexing normal references where the search parameter 2769 * has an uplifted refchain declared, and it can also come from indexing contained resources. 2770 * For both of these cases, the actual index in the database is identical. But the important 2771 * difference is that when you're searching for contained resources you also want to 2772 * search for normal references. When you're searching for explicit refchains, no normal 2773 * indexes matter because they'd be a duplicate of the uplifted refchain. 2774 * </li> 2775 * <li> 2776 * C. We can also do both and return a union of the two, using 2777 * {@link EmbeddedChainedSearchModeEnum#UPLIFTED_AND_REF_JOIN}. We do that if contained 2778 * resource indexing is enabled since we have to assume there may be indexes 2779 * on "organization" for both contained and non-contained Organization. 2780 * resources. 2781 * </li> 2782 */ 2783 private EmbeddedChainedSearchModeEnum isEligibleForEmbeddedChainedResourceSearch( 2784 String theResourceType, String theParameterName, List<? extends IQueryParameterType> theParameter) { 2785 boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources(); 2786 boolean indexOnUpliftedRefchains = myStorageSettings.isIndexOnUpliftedRefchains(); 2787 2788 if (!indexOnContainedResources && !indexOnUpliftedRefchains) { 2789 return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY; 2790 } 2791 2792 boolean haveUpliftCandidates = theParameter.stream() 2793 .filter(t -> t instanceof ReferenceParam) 2794 .map(t -> ((ReferenceParam) t).getChain()) 2795 .filter(StringUtils::isNotBlank) 2796 // Chains on _has can't be indexed for contained searches - At least not yet. It's not clear to me if we 2797 // ever want to support this, it would be really hard to do. 2798 .filter(t -> !t.startsWith(PARAM_HAS + ":")) 2799 .anyMatch(t -> { 2800 if (indexOnContainedResources) { 2801 return true; 2802 } 2803 RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam( 2804 theResourceType, 2805 theParameterName, 2806 ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 2807 return param != null && param.hasUpliftRefchain(t); 2808 }); 2809 2810 if (haveUpliftCandidates) { 2811 if (indexOnContainedResources) { 2812 return EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN; 2813 } 2814 ourLog.debug("Search on {}.{} uses an uplifted refchain.", theResourceType, theParameterName); 2815 return EmbeddedChainedSearchModeEnum.UPLIFTED_ONLY; 2816 } else { 2817 return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY; 2818 } 2819 } 2820 2821 public void addPredicateCompositeUnique(List<String> theIndexStrings, RequestPartitionId theRequestPartitionId) { 2822 ComboUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboUniquePredicateBuilder(); 2823 Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexStrings); 2824 mySqlBuilder.addPredicate(predicate); 2825 } 2826 2827 public void addPredicateCompositeNonUnique(List<String> theIndexStrings, RequestPartitionId theRequestPartitionId) { 2828 ComboNonUniqueSearchParameterPredicateBuilder predicateBuilder = 2829 mySqlBuilder.addComboNonUniquePredicateBuilder(); 2830 Condition predicate = predicateBuilder.createPredicateHashComplete(theRequestPartitionId, theIndexStrings); 2831 mySqlBuilder.addPredicate(predicate); 2832 } 2833 2834 // expand out the pids 2835 public void addPredicateEverythingOperation( 2836 String theResourceName, List<String> theTypeSourceResourceNames, JpaPid... theTargetPids) { 2837 ResourceLinkPredicateBuilder table = mySqlBuilder.addReferencePredicateBuilder(this, null); 2838 Condition predicate = 2839 table.createEverythingPredicate(theResourceName, theTypeSourceResourceNames, theTargetPids); 2840 mySqlBuilder.addPredicate(predicate); 2841 mySqlBuilder.getSelect().setIsDistinct(true); 2842 addGrouping(); 2843 } 2844 2845 public IQueryParameterType newParameterInstance( 2846 RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) { 2847 IQueryParameterType qp = newParameterInstance(theParam); 2848 2849 qp.setValueAsQueryToken(myFhirContext, theParam.getName(), theQualifier, theValueAsQueryToken); 2850 return qp; 2851 } 2852 2853 private IQueryParameterType newParameterInstance(RuntimeSearchParam theParam) { 2854 2855 IQueryParameterType qp; 2856 switch (theParam.getParamType()) { 2857 case DATE: 2858 qp = new DateParam(); 2859 break; 2860 case NUMBER: 2861 qp = new NumberParam(); 2862 break; 2863 case QUANTITY: 2864 qp = new QuantityParam(); 2865 break; 2866 case STRING: 2867 qp = new StringParam(); 2868 break; 2869 case TOKEN: 2870 qp = new TokenParam(); 2871 break; 2872 case COMPOSITE: 2873 List<JpaParamUtil.ComponentAndCorrespondingParam> compositeOf = 2874 JpaParamUtil.resolveCompositeComponents(mySearchParamRegistry, theParam); 2875 if (compositeOf.size() != 2) { 2876 throw new InternalErrorException(Msg.code(1224) + "Parameter " + theParam.getName() + " has " 2877 + compositeOf.size() + " composite parts. Don't know how handlt this."); 2878 } 2879 IQueryParameterType leftParam = 2880 newParameterInstance(compositeOf.get(0).getComponentParameter()); 2881 IQueryParameterType rightParam = 2882 newParameterInstance(compositeOf.get(1).getComponentParameter()); 2883 qp = new CompositeParam<>(leftParam, rightParam); 2884 break; 2885 case URI: 2886 qp = new UriParam(); 2887 break; 2888 case REFERENCE: 2889 qp = new ReferenceParam(); 2890 break; 2891 case SPECIAL: 2892 qp = new SpecialParam(); 2893 break; 2894 case HAS: 2895 default: 2896 throw new InvalidRequestException( 2897 Msg.code(1225) + "The search type: " + theParam.getParamType() + " is not supported."); 2898 } 2899 return qp; 2900 } 2901 2902 /** 2903 * @see #isEligibleForEmbeddedChainedResourceSearch(String, String, List) for an explanation of the values in this enum 2904 */ 2905 public enum EmbeddedChainedSearchModeEnum { 2906 UPLIFTED_ONLY(true), 2907 UPLIFTED_AND_REF_JOIN(true), 2908 REF_JOIN_ONLY(false); 2909 2910 private final boolean mySupportsUplifted; 2911 2912 EmbeddedChainedSearchModeEnum(boolean theSupportsUplifted) { 2913 mySupportsUplifted = theSupportsUplifted; 2914 } 2915 2916 public boolean supportsUplifted() { 2917 return mySupportsUplifted; 2918 } 2919 } 2920 2921 private static final class ChainElement { 2922 private final String myResourceType; 2923 private final String mySearchParameterName; 2924 private final String myPath; 2925 2926 public ChainElement(String theResourceType, String theSearchParameterName, String thePath) { 2927 this.myResourceType = theResourceType; 2928 this.mySearchParameterName = theSearchParameterName; 2929 this.myPath = thePath; 2930 } 2931 2932 public String getResourceType() { 2933 return myResourceType; 2934 } 2935 2936 public String getPath() { 2937 return myPath; 2938 } 2939 2940 public String getSearchParameterName() { 2941 return mySearchParameterName; 2942 } 2943 2944 @Override 2945 public boolean equals(Object o) { 2946 if (this == o) return true; 2947 if (o == null || getClass() != o.getClass()) return false; 2948 ChainElement that = (ChainElement) o; 2949 return myResourceType.equals(that.myResourceType) 2950 && mySearchParameterName.equals(that.mySearchParameterName) 2951 && myPath.equals(that.myPath); 2952 } 2953 2954 @Override 2955 public int hashCode() { 2956 return Objects.hash(myResourceType, mySearchParameterName, myPath); 2957 } 2958 } 2959 2960 private class ReferenceChainExtractor { 2961 private final Map<List<ChainElement>, Set<LeafNodeDefinition>> myChains = Maps.newHashMap(); 2962 2963 public Map<List<ChainElement>, Set<LeafNodeDefinition>> getChains() { 2964 return myChains; 2965 } 2966 2967 private boolean isReferenceParamValid(ReferenceParam theReferenceParam) { 2968 return split(theReferenceParam.getChain(), '.').length <= 3; 2969 } 2970 2971 private List<String> extractPaths(String theResourceType, RuntimeSearchParam theSearchParam) { 2972 List<String> pathsForType = theSearchParam.getPathsSplit().stream() 2973 .map(String::trim) 2974 .filter(t -> (t.startsWith(theResourceType) || t.startsWith("(" + theResourceType))) 2975 .collect(Collectors.toList()); 2976 if (pathsForType.isEmpty()) { 2977 ourLog.warn( 2978 "Search parameter {} does not have a path for resource type {}.", 2979 theSearchParam.getName(), 2980 theResourceType); 2981 } 2982 2983 return pathsForType; 2984 } 2985 2986 public void deriveChains( 2987 String theResourceType, 2988 RuntimeSearchParam theSearchParam, 2989 List<? extends IQueryParameterType> theList) { 2990 List<String> paths = extractPaths(theResourceType, theSearchParam); 2991 for (String path : paths) { 2992 List<ChainElement> searchParams = Lists.newArrayList(); 2993 searchParams.add(new ChainElement(theResourceType, theSearchParam.getName(), path)); 2994 for (IQueryParameterType nextOr : theList) { 2995 String targetValue = nextOr.getValueAsQueryToken(); 2996 if (nextOr instanceof ReferenceParam) { 2997 ReferenceParam referenceParam = (ReferenceParam) nextOr; 2998 if (!isReferenceParamValid(referenceParam)) { 2999 throw new InvalidRequestException(Msg.code(2007) + "The search chain " 3000 + theSearchParam.getName() + "." + referenceParam.getChain() 3001 + " is too long. Only chains up to three references are supported."); 3002 } 3003 3004 String targetChain = referenceParam.getChain(); 3005 List<String> qualifiers = Lists.newArrayList(referenceParam.getResourceType()); 3006 3007 processNextLinkInChain( 3008 searchParams, 3009 theSearchParam, 3010 targetChain, 3011 targetValue, 3012 qualifiers, 3013 referenceParam.getResourceType()); 3014 } 3015 } 3016 } 3017 } 3018 3019 private void processNextLinkInChain( 3020 List<ChainElement> theSearchParams, 3021 RuntimeSearchParam thePreviousSearchParam, 3022 String theChain, 3023 String theTargetValue, 3024 List<String> theQualifiers, 3025 String theResourceType) { 3026 3027 String nextParamName = theChain; 3028 String nextChain = null; 3029 String nextQualifier = null; 3030 int linkIndex = theChain.indexOf('.'); 3031 if (linkIndex != -1) { 3032 nextParamName = theChain.substring(0, linkIndex); 3033 nextChain = theChain.substring(linkIndex + 1); 3034 } 3035 3036 int qualifierIndex = nextParamName.indexOf(':'); 3037 if (qualifierIndex != -1) { 3038 if (StringUtils.isEmpty(nextChain)) { 3039 nextQualifier = nextParamName.substring(qualifierIndex); 3040 nextParamName = nextParamName.substring(0, qualifierIndex); 3041 } else { 3042 nextParamName = nextParamName.substring(0, qualifierIndex); 3043 nextQualifier = nextParamName.substring(qualifierIndex); 3044 } 3045 } 3046 3047 List<String> qualifiersBranch = Lists.newArrayList(); 3048 qualifiersBranch.addAll(theQualifiers); 3049 qualifiersBranch.add(nextQualifier); 3050 3051 boolean searchParamFound = false; 3052 for (String nextTarget : thePreviousSearchParam.getTargets()) { 3053 RuntimeSearchParam nextSearchParam = null; 3054 if (isBlank(theResourceType) || theResourceType.equals(nextTarget)) { 3055 nextSearchParam = mySearchParamRegistry.getActiveSearchParam( 3056 nextTarget, nextParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 3057 } 3058 if (nextSearchParam != null) { 3059 searchParamFound = true; 3060 // If we find a search param on this resource type for this parameter name, keep iterating 3061 // Otherwise, abandon this branch and carry on to the next one 3062 if (StringUtils.isEmpty(nextChain)) { 3063 // We've reached the end of the chain 3064 ArrayList<IQueryParameterType> orValues = Lists.newArrayList(); 3065 3066 if (RestSearchParameterTypeEnum.REFERENCE.equals(nextSearchParam.getParamType())) { 3067 orValues.add(new ReferenceParam(nextQualifier, "", theTargetValue)); 3068 } else { 3069 IQueryParameterType qp = newParameterInstance(nextSearchParam); 3070 qp.setValueAsQueryToken( 3071 myFhirContext, nextSearchParam.getName(), nextQualifier, theTargetValue); 3072 orValues.add(qp); 3073 } 3074 3075 Set<LeafNodeDefinition> leafNodes = 3076 myChains.computeIfAbsent(theSearchParams, k -> Sets.newHashSet()); 3077 leafNodes.add(new LeafNodeDefinition( 3078 nextSearchParam, orValues, nextTarget, nextParamName, "", qualifiersBranch)); 3079 } else { 3080 List<String> nextPaths = extractPaths(nextTarget, nextSearchParam); 3081 for (String nextPath : nextPaths) { 3082 List<ChainElement> searchParamBranch = Lists.newArrayList(); 3083 searchParamBranch.addAll(theSearchParams); 3084 3085 searchParamBranch.add(new ChainElement(nextTarget, nextSearchParam.getName(), nextPath)); 3086 processNextLinkInChain( 3087 searchParamBranch, 3088 nextSearchParam, 3089 nextChain, 3090 theTargetValue, 3091 qualifiersBranch, 3092 nextQualifier); 3093 } 3094 } 3095 } 3096 } 3097 if (!searchParamFound) { 3098 throw new InvalidRequestException(Msg.code(1214) 3099 + myFhirContext 3100 .getLocalizer() 3101 .getMessage( 3102 BaseStorageDao.class, 3103 "invalidParameterChain", 3104 thePreviousSearchParam.getName() + '.' + theChain)); 3105 } 3106 } 3107 } 3108 3109 private static class LeafNodeDefinition { 3110 private final RuntimeSearchParam myParamDefinition; 3111 private final ArrayList<IQueryParameterType> myOrValues; 3112 private final String myLeafTarget; 3113 private final String myLeafParamName; 3114 private final String myLeafPathPrefix; 3115 private final List<String> myQualifiers; 3116 3117 public LeafNodeDefinition( 3118 RuntimeSearchParam theParamDefinition, 3119 ArrayList<IQueryParameterType> theOrValues, 3120 String theLeafTarget, 3121 String theLeafParamName, 3122 String theLeafPathPrefix, 3123 List<String> theQualifiers) { 3124 myParamDefinition = theParamDefinition; 3125 myOrValues = theOrValues; 3126 myLeafTarget = theLeafTarget; 3127 myLeafParamName = theLeafParamName; 3128 myLeafPathPrefix = theLeafPathPrefix; 3129 myQualifiers = theQualifiers; 3130 } 3131 3132 public RuntimeSearchParam getParamDefinition() { 3133 return myParamDefinition; 3134 } 3135 3136 public ArrayList<IQueryParameterType> getOrValues() { 3137 return myOrValues; 3138 } 3139 3140 public String getLeafTarget() { 3141 return myLeafTarget; 3142 } 3143 3144 public String getLeafParamName() { 3145 return myLeafParamName; 3146 } 3147 3148 public String getLeafPathPrefix() { 3149 return myLeafPathPrefix; 3150 } 3151 3152 public List<String> getQualifiers() { 3153 return myQualifiers; 3154 } 3155 3156 public LeafNodeDefinition withPathPrefix(String theResourceType, String theName) { 3157 return new LeafNodeDefinition( 3158 myParamDefinition, myOrValues, theResourceType, myLeafParamName, theName, myQualifiers); 3159 } 3160 3161 @Override 3162 public boolean equals(Object o) { 3163 if (this == o) return true; 3164 if (o == null || getClass() != o.getClass()) return false; 3165 LeafNodeDefinition that = (LeafNodeDefinition) o; 3166 return Objects.equals(myParamDefinition, that.myParamDefinition) 3167 && Objects.equals(myOrValues, that.myOrValues) 3168 && Objects.equals(myLeafTarget, that.myLeafTarget) 3169 && Objects.equals(myLeafParamName, that.myLeafParamName) 3170 && Objects.equals(myLeafPathPrefix, that.myLeafPathPrefix) 3171 && Objects.equals(myQualifiers, that.myQualifiers); 3172 } 3173 3174 @Override 3175 public int hashCode() { 3176 return Objects.hash( 3177 myParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers); 3178 } 3179 3180 /** 3181 * Return a copy of this object with the given {@link RuntimeSearchParam} 3182 * but all other values unchanged. 3183 */ 3184 public LeafNodeDefinition withParam(RuntimeSearchParam theParamDefinition) { 3185 return new LeafNodeDefinition( 3186 theParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers); 3187 } 3188 } 3189 3190 public static class SearchForIdsParams { 3191 DbColumn[] mySourceJoinColumn; 3192 String myResourceName; 3193 String myParamName; 3194 List<List<IQueryParameterType>> myAndOrParams; 3195 RequestDetails myRequest; 3196 RequestPartitionId myRequestPartitionId; 3197 SearchIncludeDeletedEnum myIncludeDeleted; 3198 SearchFilterParser.CompareOperation myOperation; 3199 3200 public static SearchForIdsParams with() { 3201 return new SearchForIdsParams(); 3202 } 3203 3204 public SearchForIdsParams setSourceJoinColumn(DbColumn[] theSourceJoinColumn) { 3205 mySourceJoinColumn = theSourceJoinColumn; 3206 return this; 3207 } 3208 3209 public DbColumn[] getSourceJoinColumn() { 3210 return mySourceJoinColumn; 3211 } 3212 3213 public String getResourceName() { 3214 return myResourceName; 3215 } 3216 3217 public SearchForIdsParams setResourceName(String theResourceName) { 3218 myResourceName = theResourceName; 3219 return this; 3220 } 3221 3222 public String getParamName() { 3223 return myParamName; 3224 } 3225 3226 public SearchForIdsParams setParamName(String theParamName) { 3227 myParamName = theParamName; 3228 return this; 3229 } 3230 3231 public SearchForIdsParams setAndOrParams(List<List<IQueryParameterType>> theAndOrParams) { 3232 myAndOrParams = theAndOrParams; 3233 return this; 3234 } 3235 3236 public List<List<IQueryParameterType>> getAndOrParams() { 3237 return myAndOrParams; 3238 } 3239 3240 public RequestDetails getRequest() { 3241 return myRequest; 3242 } 3243 3244 public SearchForIdsParams setRequest(RequestDetails theRequest) { 3245 myRequest = theRequest; 3246 return this; 3247 } 3248 3249 public RequestPartitionId getRequestPartitionId() { 3250 return myRequestPartitionId; 3251 } 3252 3253 public SearchForIdsParams setRequestPartitionId(RequestPartitionId theRequestPartitionId) { 3254 myRequestPartitionId = theRequestPartitionId; 3255 return this; 3256 } 3257 3258 @Nonnull 3259 public SearchIncludeDeletedEnum getIncludeDeleted() { 3260 if (myIncludeDeleted == null) { 3261 return SearchIncludeDeletedEnum.NEVER; 3262 } 3263 return myIncludeDeleted; 3264 } 3265 3266 public SearchForIdsParams setIncludeDeleted(SearchIncludeDeletedEnum myIncludeDeleted) { 3267 this.myIncludeDeleted = myIncludeDeleted; 3268 return this; 3269 } 3270 3271 public SearchFilterParser.CompareOperation getOperation() { 3272 return myOperation; 3273 } 3274 3275 public SearchForIdsParams setOperation(SearchFilterParser.CompareOperation myOperation) { 3276 this.myOperation = myOperation; 3277 return this; 3278 } 3279 } 3280}