
001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2023 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.sql; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.model.RequestPartitionId; 025import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider; 026import ca.uhn.fhir.jpa.model.config.PartitionSettings; 027import ca.uhn.fhir.jpa.model.dao.JpaPid; 028import ca.uhn.fhir.jpa.model.entity.StorageSettings; 029import ca.uhn.fhir.jpa.search.builder.QueryStack; 030import ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder; 031import ca.uhn.fhir.jpa.search.builder.predicate.ComboNonUniqueSearchParameterPredicateBuilder; 032import ca.uhn.fhir.jpa.search.builder.predicate.ComboUniqueSearchParameterPredicateBuilder; 033import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder; 034import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder; 035import ca.uhn.fhir.jpa.search.builder.predicate.ForcedIdPredicateBuilder; 036import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder; 037import ca.uhn.fhir.jpa.search.builder.predicate.QuantityNormalizedPredicateBuilder; 038import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder; 039import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder; 040import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder; 041import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder; 042import ca.uhn.fhir.jpa.search.builder.predicate.SearchParamPresentPredicateBuilder; 043import ca.uhn.fhir.jpa.search.builder.predicate.SourcePredicateBuilder; 044import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder; 045import ca.uhn.fhir.jpa.search.builder.predicate.TagPredicateBuilder; 046import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder; 047import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder; 048import ca.uhn.fhir.rest.param.DateParam; 049import ca.uhn.fhir.rest.param.DateRangeParam; 050import ca.uhn.fhir.rest.param.ParamPrefixEnum; 051import com.healthmarketscience.sqlbuilder.BinaryCondition; 052import com.healthmarketscience.sqlbuilder.ComboCondition; 053import com.healthmarketscience.sqlbuilder.ComboExpression; 054import com.healthmarketscience.sqlbuilder.Condition; 055import com.healthmarketscience.sqlbuilder.FunctionCall; 056import com.healthmarketscience.sqlbuilder.InCondition; 057import com.healthmarketscience.sqlbuilder.OrderObject; 058import com.healthmarketscience.sqlbuilder.SelectQuery; 059import com.healthmarketscience.sqlbuilder.dbspec.Join; 060import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; 061import com.healthmarketscience.sqlbuilder.dbspec.basic.DbJoin; 062import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSchema; 063import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSpec; 064import com.healthmarketscience.sqlbuilder.dbspec.basic.DbTable; 065import org.apache.commons.lang3.Validate; 066import org.hibernate.dialect.Dialect; 067import org.hibernate.dialect.SQLServerDialect; 068import org.hibernate.dialect.pagination.AbstractLimitHandler; 069import org.hibernate.engine.spi.RowSelection; 070import org.slf4j.Logger; 071import org.slf4j.LoggerFactory; 072 073import javax.annotation.Nonnull; 074import javax.annotation.Nullable; 075import java.util.ArrayList; 076import java.util.Collection; 077import java.util.List; 078import java.util.Set; 079import java.util.UUID; 080import java.util.stream.Collectors; 081 082import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN; 083import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS; 084import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN; 085import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS; 086import static ca.uhn.fhir.rest.param.ParamPrefixEnum.NOT_EQUAL; 087import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 088 089public class SearchQueryBuilder { 090 091 private static final Logger ourLog = LoggerFactory.getLogger(SearchQueryBuilder.class); 092 private final String myBindVariableSubstitutionBase; 093 private final ArrayList<Object> myBindVariableValues; 094 private final DbSpec mySpec; 095 private final DbSchema mySchema; 096 private final SelectQuery mySelect; 097 private final PartitionSettings myPartitionSettings; 098 private final RequestPartitionId myRequestPartitionId; 099 private final String myResourceType; 100 private final StorageSettings myStorageSettings; 101 private final FhirContext myFhirContext; 102 private final SqlObjectFactory mySqlBuilderFactory; 103 private final boolean myCountQuery; 104 private final Dialect myDialect; 105 private boolean myMatchNothing; 106 private ResourceTablePredicateBuilder myResourceTableRoot; 107 private boolean myHaveAtLeastOnePredicate; 108 private BaseJoiningPredicateBuilder myFirstPredicateBuilder; 109 private boolean dialectIsMsSql; 110 private boolean dialectIsMySql; 111 private boolean myNeedResourceTableRoot; 112 private int myNextNearnessColumnId = 0; 113 114 /** 115 * Constructor 116 */ 117 public SearchQueryBuilder(FhirContext theFhirContext, StorageSettings theStorageSettings, PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, SqlObjectFactory theSqlBuilderFactory, HibernatePropertiesProvider theDialectProvider, boolean theCountQuery) { 118 this(theFhirContext, theStorageSettings, thePartitionSettings, theRequestPartitionId, theResourceType, theSqlBuilderFactory, UUID.randomUUID() + "-", theDialectProvider.getDialect(), theCountQuery, new ArrayList<>()); 119 } 120 121 /** 122 * Constructor for child SQL Builders 123 */ 124 private SearchQueryBuilder(FhirContext theFhirContext, StorageSettings theStorageSettings, PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, SqlObjectFactory theSqlBuilderFactory, String theBindVariableSubstitutionBase, Dialect theDialect, boolean theCountQuery, ArrayList<Object> theBindVariableValues) { 125 myFhirContext = theFhirContext; 126 myStorageSettings = theStorageSettings; 127 myPartitionSettings = thePartitionSettings; 128 myRequestPartitionId = theRequestPartitionId; 129 myResourceType = theResourceType; 130 mySqlBuilderFactory = theSqlBuilderFactory; 131 myCountQuery = theCountQuery; 132 myDialect = theDialect; 133 if (myDialect instanceof org.hibernate.dialect.MySQLDialect) { 134 dialectIsMySql = true; 135 } 136 if (myDialect instanceof org.hibernate.dialect.SQLServerDialect) { 137 dialectIsMsSql = true; 138 } 139 140 141 mySpec = new DbSpec(); 142 mySchema = mySpec.addDefaultSchema(); 143 mySelect = new SelectQuery(); 144 145 myBindVariableSubstitutionBase = theBindVariableSubstitutionBase; 146 myBindVariableValues = theBindVariableValues; 147 } 148 149 public FhirContext getFhirContext() { 150 return myFhirContext; 151 } 152 153 /** 154 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a Composite Unique search parameter 155 */ 156 public ComboUniqueSearchParameterPredicateBuilder addComboUniquePredicateBuilder() { 157 ComboUniqueSearchParameterPredicateBuilder retVal = mySqlBuilderFactory.newComboUniqueSearchParameterPredicateBuilder(this); 158 addTable(retVal, null); 159 return retVal; 160 } 161 162 /** 163 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a Composite Unique search parameter 164 */ 165 public ComboNonUniqueSearchParameterPredicateBuilder addComboNonUniquePredicateBuilder() { 166 ComboNonUniqueSearchParameterPredicateBuilder retVal = mySqlBuilderFactory.newComboNonUniqueSearchParameterPredicateBuilder(this); 167 addTable(retVal, null); 168 return retVal; 169 } 170 171 /** 172 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a COORDS search parameter 173 */ 174 public CoordsPredicateBuilder addCoordsPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 175 CoordsPredicateBuilder retVal = mySqlBuilderFactory.coordsPredicateBuilder(this); 176 addTable(retVal, theSourceJoinColumn); 177 return retVal; 178 } 179 180 /** 181 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a DATE search parameter 182 */ 183 public DatePredicateBuilder addDatePredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 184 DatePredicateBuilder retVal = mySqlBuilderFactory.dateIndexTable(this); 185 addTable(retVal, theSourceJoinColumn); 186 return retVal; 187 } 188 189 /** 190 * Create a predicate builder for selecting on a DATE search parameter 191 */ 192 public DatePredicateBuilder createDatePredicateBuilder() { 193 return mySqlBuilderFactory.dateIndexTable(this); 194 } 195 196 /** 197 * Add and return a predicate builder for selecting a forced ID. This is only intended for use with sorts so it can not 198 * be the root query. 199 */ 200 public ForcedIdPredicateBuilder addForcedIdPredicateBuilder(@Nonnull DbColumn theSourceJoinColumn) { 201 Validate.isTrue(theSourceJoinColumn != null); 202 203 ForcedIdPredicateBuilder retVal = mySqlBuilderFactory.newForcedIdPredicateBuilder(this); 204 addTableForSorting(retVal, theSourceJoinColumn); 205 return retVal; 206 } 207 208 /** 209 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a NUMBER search parameter 210 */ 211 public NumberPredicateBuilder addNumberPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 212 NumberPredicateBuilder retVal = createNumberPredicateBuilder(); 213 addTable(retVal, theSourceJoinColumn); 214 return retVal; 215 } 216 217 /** 218 * Create a predicate builder for selecting on a NUMBER search parameter 219 */ 220 public NumberPredicateBuilder createNumberPredicateBuilder() { 221 return mySqlBuilderFactory.numberIndexTable(this); 222 } 223 224 /** 225 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on the Resource table 226 */ 227 public ResourceTablePredicateBuilder addResourceTablePredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 228 ResourceTablePredicateBuilder retVal = mySqlBuilderFactory.resourceTable(this); 229 addTable(retVal, theSourceJoinColumn); 230 return retVal; 231 } 232 233 /** 234 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a QUANTITY search parameter 235 */ 236 public QuantityPredicateBuilder addQuantityPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 237 QuantityPredicateBuilder retVal = createQuantityPredicateBuilder(); 238 addTable(retVal, theSourceJoinColumn); 239 240 return retVal; 241 } 242 243 /** 244 * Create a predicate builder for selecting on a QUANTITY search parameter 245 */ 246 public QuantityPredicateBuilder createQuantityPredicateBuilder() { 247 return mySqlBuilderFactory.quantityIndexTable(this); 248 } 249 250 public QuantityNormalizedPredicateBuilder addQuantityNormalizedPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 251 252 QuantityNormalizedPredicateBuilder retVal = mySqlBuilderFactory.quantityNormalizedIndexTable(this); 253 addTable(retVal, theSourceJoinColumn); 254 255 return retVal; 256 } 257 258 /** 259 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a <code>_source</code> search parameter 260 */ 261 public SourcePredicateBuilder addSourcePredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 262 SourcePredicateBuilder retVal = mySqlBuilderFactory.newSourcePredicateBuilder(this); 263 addTable(retVal, theSourceJoinColumn); 264 return retVal; 265 } 266 267 /** 268 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a REFERENCE search parameter 269 */ 270 public ResourceLinkPredicateBuilder addReferencePredicateBuilder(QueryStack theQueryStack, @Nullable DbColumn theSourceJoinColumn) { 271 ResourceLinkPredicateBuilder retVal = createReferencePredicateBuilder(theQueryStack); 272 addTable(retVal, theSourceJoinColumn); 273 return retVal; 274 } 275 276 /** 277 * Create a predicate builder for selecting on a REFERENCE search parameter 278 */ 279 public ResourceLinkPredicateBuilder createReferencePredicateBuilder(QueryStack theQueryStack) { 280 return mySqlBuilderFactory.referenceIndexTable(theQueryStack, this, false); 281 } 282 283 /** 284 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a resource link where the 285 * source and target are reversed. This is used for _has queries. 286 */ 287 public ResourceLinkPredicateBuilder addReferencePredicateBuilderReversed(QueryStack theQueryStack, DbColumn theSourceJoinColumn) { 288 ResourceLinkPredicateBuilder retVal = mySqlBuilderFactory.referenceIndexTable(theQueryStack, this, true); 289 addTable(retVal, theSourceJoinColumn); 290 return retVal; 291 } 292 293 /** 294 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a STRING search parameter 295 */ 296 public StringPredicateBuilder addStringPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 297 StringPredicateBuilder retVal = createStringPredicateBuilder(); 298 addTable(retVal, theSourceJoinColumn); 299 return retVal; 300 } 301 302 /** 303 * Create a predicate builder for selecting on a STRING search parameter 304 */ 305 public StringPredicateBuilder createStringPredicateBuilder() { 306 return mySqlBuilderFactory.stringIndexTable(this); 307 } 308 309 /** 310 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a <code>_tag</code> search parameter 311 */ 312 public TagPredicateBuilder addTagPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 313 TagPredicateBuilder retVal = mySqlBuilderFactory.newTagPredicateBuilder(this); 314 addTable(retVal, theSourceJoinColumn); 315 return retVal; 316 } 317 318 /** 319 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a TOKEN search parameter 320 */ 321 public TokenPredicateBuilder addTokenPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 322 TokenPredicateBuilder retVal = createTokenPredicateBuilder(); 323 addTable(retVal, theSourceJoinColumn); 324 return retVal; 325 } 326 327 /** 328 * Create a predicate builder for selecting on a TOKEN search parameter 329 */ 330 public TokenPredicateBuilder createTokenPredicateBuilder() { 331 return mySqlBuilderFactory.tokenIndexTable(this); 332 } 333 334 public void addCustomJoin(SelectQuery.JoinType theJoinType, DbTable theFromTable, DbTable theToTable, Condition theCondition) { 335 mySelect.addCustomJoin(theJoinType, theFromTable, theToTable, theCondition); 336 } 337 338 public ComboCondition createOnCondition(DbColumn theSourceColumn, DbColumn theTargetColumn) { 339 ComboCondition onCondition = ComboCondition.and(); 340 onCondition.addCondition(BinaryCondition.equalTo(theSourceColumn, theTargetColumn)); 341 342 return onCondition; 343 } 344 345 /** 346 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a <code>:missing</code> search parameter 347 */ 348 public SearchParamPresentPredicateBuilder addSearchParamPresentPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 349 SearchParamPresentPredicateBuilder retVal = mySqlBuilderFactory.searchParamPresentPredicateBuilder(this); 350 addTable(retVal, theSourceJoinColumn); 351 return retVal; 352 } 353 354 /** 355 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a URI search parameter 356 */ 357 public UriPredicateBuilder addUriPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { 358 UriPredicateBuilder retVal = createUriPredicateBuilder(); 359 addTable(retVal, theSourceJoinColumn); 360 return retVal; 361 } 362 363 /** 364 * Create a predicate builder for selecting on a URI search parameter 365 */ 366 public UriPredicateBuilder createUriPredicateBuilder() { 367 return mySqlBuilderFactory.uriIndexTable(this); 368 } 369 370 public SqlObjectFactory getSqlBuilderFactory() { 371 return mySqlBuilderFactory; 372 } 373 374 public ResourceIdPredicateBuilder newResourceIdBuilder() { 375 return mySqlBuilderFactory.resourceId(this); 376 } 377 378 379 /** 380 * Add and return a predicate builder (or a root query if no root query exists yet) for an arbitrary table 381 */ 382 private void addTable(BaseJoiningPredicateBuilder thePredicateBuilder, @Nullable DbColumn theSourceJoinColumn) { 383 addTable(thePredicateBuilder, theSourceJoinColumn, SelectQuery.JoinType.INNER); 384 } 385 386 private void addTableForSorting(BaseJoiningPredicateBuilder thePredicateBuilder, @Nullable DbColumn theSourceJoinColumn) { 387 addTable(thePredicateBuilder, theSourceJoinColumn, SelectQuery.JoinType.LEFT_OUTER); 388 } 389 390 private void addTable(BaseJoiningPredicateBuilder thePredicateBuilder, @Nullable DbColumn theSourceJoinColumn, SelectQuery.JoinType theJoinType) { 391 if (theSourceJoinColumn != null) { 392 DbTable fromTable = theSourceJoinColumn.getTable(); 393 DbTable toTable = thePredicateBuilder.getTable(); 394 DbColumn toColumn = thePredicateBuilder.getResourceIdColumn(); 395 addJoin(fromTable, toTable, theSourceJoinColumn, toColumn, theJoinType); 396 } else { 397 if (myFirstPredicateBuilder == null) { 398 399 BaseJoiningPredicateBuilder root; 400 if (!myNeedResourceTableRoot) { 401 root = thePredicateBuilder; 402 } else { 403 if (thePredicateBuilder instanceof ResourceTablePredicateBuilder) { 404 root = thePredicateBuilder; 405 } else { 406 root = mySqlBuilderFactory.resourceTable(this); 407 } 408 } 409 410 if (myCountQuery) { 411 mySelect.addCustomColumns(FunctionCall.count().setIsDistinct(true).addColumnParams(root.getResourceIdColumn())); 412 } else { 413 mySelect.addColumns(root.getResourceIdColumn()); 414 } 415 mySelect.addFromTable(root.getTable()); 416 myFirstPredicateBuilder = root; 417 418 if (!myNeedResourceTableRoot || (thePredicateBuilder instanceof ResourceTablePredicateBuilder)) { 419 return; 420 } 421 } 422 423 DbTable fromTable = myFirstPredicateBuilder.getTable(); 424 DbTable toTable = thePredicateBuilder.getTable(); 425 DbColumn fromColumn = myFirstPredicateBuilder.getResourceIdColumn(); 426 DbColumn toColumn = thePredicateBuilder.getResourceIdColumn(); 427 addJoin(fromTable, toTable, fromColumn, toColumn, theJoinType); 428 } 429 } 430 431 432 public void addJoin(DbTable theFromTable, DbTable theToTable, DbColumn theFromColumn, DbColumn theToColumn, SelectQuery.JoinType theJoinType) { 433 Join join = new DbJoin(mySpec, theFromTable, theToTable, new DbColumn[]{theFromColumn}, new DbColumn[]{theToColumn}); 434 mySelect.addJoins(theJoinType, join); 435 } 436 437 public void addJoin(DbTable theFromTable, DbTable theToTable, DbColumn theFromColumn, DbColumn theToColumn) { 438 Join join = new DbJoin(mySpec, theFromTable, theToTable, new DbColumn[]{theFromColumn}, new DbColumn[]{theToColumn}); 439 mySelect.addJoins(SelectQuery.JoinType.INNER, join); 440 } 441 442 /** 443 * Generate and return the SQL generated by this builder 444 */ 445 public GeneratedSql generate(@Nullable Integer theOffset, @Nullable Integer theMaxResultsToFetch) { 446 447 getOrCreateFirstPredicateBuilder(); 448 449 mySelect.validate(); 450 String sql = mySelect.toString(); 451 452 List<Object> bindVariables = new ArrayList<>(); 453 while (true) { 454 455 int idx = sql.indexOf(myBindVariableSubstitutionBase); 456 if (idx == -1) { 457 break; 458 } 459 460 int endIdx = sql.indexOf("'", idx + myBindVariableSubstitutionBase.length()); 461 String substitutionIndexString = sql.substring(idx + myBindVariableSubstitutionBase.length(), endIdx); 462 int substitutionIndex = Integer.parseInt(substitutionIndexString); 463 bindVariables.add(myBindVariableValues.get(substitutionIndex)); 464 465 sql = sql.substring(0, idx - 1) + "?" + sql.substring(endIdx + 1); 466 } 467 468 Integer maxResultsToFetch = theMaxResultsToFetch; 469 Integer offset = theOffset; 470 if (offset != null && offset == 0) { 471 offset = null; 472 } 473 if (maxResultsToFetch != null || offset != null) { 474 475 maxResultsToFetch = defaultIfNull(maxResultsToFetch, 10000); 476 477 AbstractLimitHandler limitHandler = (AbstractLimitHandler) myDialect.getLimitHandler(); 478 RowSelection selection = new RowSelection(); 479 selection.setFirstRow(offset); 480 selection.setMaxRows(maxResultsToFetch); 481 sql = limitHandler.processSql(sql, selection); 482 483 int startOfQueryParameterIndex = 0; 484 485 boolean isSqlServer = (myDialect instanceof SQLServerDialect); 486 if (isSqlServer) { 487 488 // The SQLServerDialect has a bunch of one-off processing to deal with rules on when 489 // a limit can be used, so we can't rely on the flags that the limithandler exposes since 490 // the exact structure of the query depends on the parameters 491 if (sql.contains("top(?)")) { 492 bindVariables.add(0, maxResultsToFetch); 493 } 494 if (sql.contains("offset 0 rows fetch next ? rows only")) { 495 bindVariables.add(maxResultsToFetch); 496 } 497 if (sql.contains("offset ? rows fetch next ? rows only")) { 498 bindVariables.add(theOffset); 499 bindVariables.add(maxResultsToFetch); 500 } 501 if (offset != null && sql.contains("__row__")) { 502 bindVariables.add(theOffset + 1); 503 bindVariables.add(theOffset + maxResultsToFetch + 1); 504 } 505 506 } else if (limitHandler.supportsVariableLimit()) { 507 508 boolean bindLimitParametersFirst = limitHandler.bindLimitParametersFirst(); 509 if (limitHandler.useMaxForLimit() && offset != null) { 510 maxResultsToFetch = maxResultsToFetch + offset; 511 } 512 513 if (limitHandler.bindLimitParametersInReverseOrder()) { 514 startOfQueryParameterIndex = bindCountParameter(bindVariables, maxResultsToFetch, limitHandler, startOfQueryParameterIndex, bindLimitParametersFirst); 515 bindOffsetParameter(bindVariables, offset, limitHandler, startOfQueryParameterIndex, bindLimitParametersFirst); 516 } else { 517 startOfQueryParameterIndex = bindOffsetParameter(bindVariables, offset, limitHandler, startOfQueryParameterIndex, bindLimitParametersFirst); 518 bindCountParameter(bindVariables, maxResultsToFetch, limitHandler, startOfQueryParameterIndex, bindLimitParametersFirst); 519 } 520 } 521 } 522 523 return new GeneratedSql(myMatchNothing, sql, bindVariables); 524 } 525 526 private int bindCountParameter(List<Object> bindVariables, Integer maxResultsToFetch, AbstractLimitHandler limitHandler, int startOfQueryParameterIndex, boolean bindLimitParametersFirst) { 527 if (limitHandler.supportsLimit()) { 528 if (bindLimitParametersFirst) { 529 bindVariables.add(startOfQueryParameterIndex++, maxResultsToFetch); 530 } else { 531 bindVariables.add(maxResultsToFetch); 532 } 533 } 534 return startOfQueryParameterIndex; 535 } 536 537 public int bindOffsetParameter(List<Object> theBindVariables, @Nullable Integer theOffset, AbstractLimitHandler theLimitHandler, int theStartOfQueryParameterIndex, boolean theBindLimitParametersFirst) { 538 if (theLimitHandler.supportsLimitOffset() && theOffset != null) { 539 if (theBindLimitParametersFirst) { 540 theBindVariables.add(theStartOfQueryParameterIndex++, theOffset); 541 } else { 542 theBindVariables.add(theOffset); 543 } 544 } 545 return theStartOfQueryParameterIndex; 546 } 547 548 /** 549 * If at least one predicate builder already exists, return the last one added to the chain. If none has been selected, create a builder on HFJ_RESOURCE, add it and return it. 550 */ 551 public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder() { 552 return getOrCreateFirstPredicateBuilder(true); 553 } 554 555 /** 556 * If at least one predicate builder already exists, return the last one added to the chain. If none has been selected, create a builder on HFJ_RESOURCE, add it and return it. 557 */ 558 public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder(boolean theIncludeResourceTypeAndNonDeletedFlag) { 559 if (myFirstPredicateBuilder == null) { 560 getOrCreateResourceTablePredicateBuilder(theIncludeResourceTypeAndNonDeletedFlag); 561 } 562 return myFirstPredicateBuilder; 563 } 564 565 public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder() { 566 return getOrCreateResourceTablePredicateBuilder(true); 567 } 568 569 public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder(boolean theIncludeResourceTypeAndNonDeletedFlag) { 570 if (myResourceTableRoot == null) { 571 ResourceTablePredicateBuilder resourceTable = mySqlBuilderFactory.resourceTable(this); 572 addTable(resourceTable, null); 573 if (theIncludeResourceTypeAndNonDeletedFlag) { 574 Condition typeAndDeletionPredicate = resourceTable.createResourceTypeAndNonDeletedPredicates(); 575 addPredicate(typeAndDeletionPredicate); 576 } 577 myResourceTableRoot = resourceTable; 578 } 579 return myResourceTableRoot; 580 } 581 582 /** 583 * The SQL Builder library has one annoying limitation, which is that it does not use/understand bind variables 584 * for its generated SQL. So we work around this by replacing our contents with a string in the SQL consisting 585 * of <code>[random UUID]-[value index]</code> and then 586 */ 587 public String generatePlaceholder(Object theValue) { 588 String placeholder = myBindVariableSubstitutionBase + myBindVariableValues.size(); 589 myBindVariableValues.add(theValue); 590 return placeholder; 591 } 592 593 public List<String> generatePlaceholders(Collection<?> theValues) { 594 return theValues 595 .stream() 596 .map(this::generatePlaceholder) 597 .collect(Collectors.toList()); 598 } 599 600 public int countBindVariables() { 601 return myBindVariableValues.size(); 602 } 603 604 605 public void setMatchNothing() { 606 myMatchNothing = true; 607 } 608 609 public DbTable addTable(String theTableName) { 610 return mySchema.addTable(theTableName); 611 } 612 613 public PartitionSettings getPartitionSettings() { 614 return myPartitionSettings; 615 } 616 617 public RequestPartitionId getRequestPartitionId() { 618 return myRequestPartitionId; 619 } 620 621 public String getResourceType() { 622 return myResourceType; 623 } 624 625 public StorageSettings getStorageSettings() { 626 return myStorageSettings; 627 } 628 629 public void addPredicate(@Nonnull Condition theCondition) { 630 assert theCondition != null; 631 mySelect.addCondition(theCondition); 632 myHaveAtLeastOnePredicate = true; 633 } 634 635 public ComboCondition addPredicateLastUpdated(DateRangeParam theDateRange) { 636 ResourceTablePredicateBuilder resourceTableRoot = getOrCreateResourceTablePredicateBuilder(false); 637 List<Condition> conditions = new ArrayList<>(2); 638 BinaryCondition condition; 639 640 if (isNotEqualsComparator(theDateRange)) { 641 condition = createConditionForValueWithComparator(LESSTHAN, resourceTableRoot.getLastUpdatedColumn(), theDateRange.getLowerBoundAsInstant()); 642 conditions.add(condition); 643 condition = createConditionForValueWithComparator(GREATERTHAN, resourceTableRoot.getLastUpdatedColumn(), theDateRange.getUpperBoundAsInstant()); 644 conditions.add(condition); 645 return ComboCondition.or(conditions.toArray(new Condition[0])); 646 } 647 648 if (theDateRange.getLowerBoundAsInstant() != null) { 649 condition = createConditionForValueWithComparator(GREATERTHAN_OR_EQUALS, resourceTableRoot.getLastUpdatedColumn(), theDateRange.getLowerBoundAsInstant()); 650 conditions.add(condition); 651 } 652 653 if (theDateRange.getUpperBoundAsInstant() != null) { 654 condition = createConditionForValueWithComparator(LESSTHAN_OR_EQUALS, resourceTableRoot.getLastUpdatedColumn(), theDateRange.getUpperBoundAsInstant()); 655 conditions.add(condition); 656 } 657 658 return ComboCondition.and(conditions.toArray(new Condition[0])); 659 } 660 661 private boolean isNotEqualsComparator(DateRangeParam theDateRange) { 662 if (theDateRange != null) { 663 DateParam lb = theDateRange.getLowerBound(); 664 DateParam ub = theDateRange.getUpperBound(); 665 666 return lb != null && ub != null && lb.getPrefix().equals(NOT_EQUAL) && ub.getPrefix().equals(NOT_EQUAL); 667 } 668 return false; 669 } 670 671 672 public void addResourceIdsPredicate(List<Long> thePidList) { 673 DbColumn resourceIdColumn = getOrCreateFirstPredicateBuilder().getResourceIdColumn(); 674 InCondition predicate = new InCondition(resourceIdColumn, generatePlaceholders(thePidList)); 675 addPredicate(predicate); 676 } 677 678 public void excludeResourceIdsPredicate(Set<JpaPid> theExistingPidSetToExclude) { 679 680 // Do nothing if it's empty 681 if (theExistingPidSetToExclude == null || theExistingPidSetToExclude.isEmpty()) 682 return; 683 684 List<Long> excludePids = JpaPid.toLongList(theExistingPidSetToExclude); 685 686 ourLog.trace("excludePids = " + excludePids); 687 688 DbColumn resourceIdColumn = getOrCreateFirstPredicateBuilder().getResourceIdColumn(); 689 InCondition predicate = new InCondition(resourceIdColumn, generatePlaceholders(excludePids)); 690 predicate.setNegate(true); 691 addPredicate(predicate); 692 } 693 694 public BinaryCondition createConditionForValueWithComparator(ParamPrefixEnum theComparator, DbColumn theColumn, Object theValue) { 695 switch (theComparator) { 696 case LESSTHAN: 697 return BinaryCondition.lessThan(theColumn, generatePlaceholder(theValue)); 698 case LESSTHAN_OR_EQUALS: 699 return BinaryCondition.lessThanOrEq(theColumn, generatePlaceholder(theValue)); 700 case GREATERTHAN: 701 return BinaryCondition.greaterThan(theColumn, generatePlaceholder(theValue)); 702 case GREATERTHAN_OR_EQUALS: 703 return BinaryCondition.greaterThanOrEq(theColumn, generatePlaceholder(theValue)); 704 case NOT_EQUAL: 705 return BinaryCondition.notEqualTo(theColumn, generatePlaceholder(theValue)); 706 case STARTS_AFTER: 707 case APPROXIMATE: 708 case ENDS_BEFORE: 709 case EQUAL: 710 default: 711 throw new IllegalArgumentException(Msg.code(1263)); 712 } 713 } 714 715 public SearchQueryBuilder newChildSqlBuilder() { 716 return new SearchQueryBuilder(myFhirContext, myStorageSettings, myPartitionSettings, myRequestPartitionId, myResourceType, mySqlBuilderFactory, myBindVariableSubstitutionBase, myDialect, false, myBindVariableValues); 717 } 718 719 public SelectQuery getSelect() { 720 return mySelect; 721 } 722 723 public boolean haveAtLeastOnePredicate() { 724 return myHaveAtLeastOnePredicate; 725 } 726 727 public void addSortCoordsNear(CoordsPredicateBuilder theCoordsBuilder, double theLatitudeValue, double theLongitudeValue, boolean theAscending) { 728 FunctionCall absLatitude = new FunctionCall("ABS"); 729 String latitudePlaceholder = generatePlaceholder(theLatitudeValue); 730 ComboExpression absLatitudeMiddle = new ComboExpression(ComboExpression.Op.SUBTRACT, theCoordsBuilder.getColumnLatitude(), latitudePlaceholder); 731 absLatitude = absLatitude.addCustomParams(absLatitudeMiddle); 732 733 FunctionCall absLongitude = new FunctionCall("ABS"); 734 String longitudePlaceholder = generatePlaceholder(theLongitudeValue); 735 ComboExpression absLongitudeMiddle = new ComboExpression(ComboExpression.Op.SUBTRACT, theCoordsBuilder.getColumnLongitude(), longitudePlaceholder); 736 absLongitude = absLongitude.addCustomParams(absLongitudeMiddle); 737 738 ComboExpression sum = new ComboExpression(ComboExpression.Op.ADD, absLatitude, absLongitude); 739 String ordering; 740 if (theAscending) { 741 ordering = ""; 742 } else { 743 ordering = " DESC"; 744 } 745 746 String columnName = "MHD" + (myNextNearnessColumnId++); 747 mySelect.addAliasedColumn(sum, columnName); 748 mySelect.addCustomOrderings(columnName + ordering); 749 } 750 751 public void addSortString(DbColumn theColumnValueNormalized, boolean theAscending) { 752 addSortString(theColumnValueNormalized, theAscending, false); 753 } 754 755 public void addSortString(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) { 756 OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST; 757 addSortString(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate); 758 } 759 760 public void addSortNumeric(DbColumn theColumnValueNormalized, boolean theAscending) { 761 addSortNumeric(theColumnValueNormalized, theAscending, false); 762 } 763 764 public void addSortNumeric(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) { 765 OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST; 766 addSortNumeric(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate); 767 } 768 769 public void addSortDate(DbColumn theColumnValueNormalized, boolean theAscending) { 770 addSortDate(theColumnValueNormalized, theAscending, false); 771 } 772 773 public void addSortDate(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) { 774 OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST; 775 addSortDate(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate); 776 } 777 778 public void addSortString(DbColumn theTheColumnValueNormalized, boolean theTheAscending, OrderObject.NullOrder theNullOrder, boolean theUseAggregate) { 779 if ((dialectIsMySql || dialectIsMsSql)) { 780 // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax. 781 String direction = theTheAscending ? " ASC" : " DESC"; 782 String sortColumnName = theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName(); 783 final StringBuilder sortColumnNameBuilder = new StringBuilder(); 784 // The following block has been commented out for performance. 785 // Uncomment if NullOrder is needed for MariaDB, MySQL or MSSQL 786 /* 787 // Null values are always treated as less than non-null values. 788 if ((theTheAscending && theNullOrder == OrderObject.NullOrder.LAST) 789 || (!theTheAscending && theNullOrder == OrderObject.NullOrder.FIRST)) { 790 // In this case, precede the "order by" column with a case statement that returns 791 // 1 for null and 0 non-null so that nulls will be sorted as greater than non-nulls. 792 sortColumnNameBuilder.append( "CASE WHEN " ).append( sortColumnName ).append( " IS NULL THEN 1 ELSE 0 END" ).append(direction).append(", "); 793 } 794 */ 795 sortColumnName = formatColumnNameForAggregate(theTheAscending, theUseAggregate, sortColumnName); 796 sortColumnNameBuilder.append(sortColumnName).append(direction); 797 mySelect.addCustomOrderings(sortColumnNameBuilder.toString()); 798 } else { 799 addSort(theTheColumnValueNormalized, theTheAscending, theNullOrder, theUseAggregate); 800 } 801 } 802 803 private static String formatColumnNameForAggregate(boolean theTheAscending, boolean theUseAggregate, String sortColumnName) { 804 if (theUseAggregate) { 805 String aggregateFunction; 806 if (theTheAscending) { 807 aggregateFunction = "MIN"; 808 } else { 809 aggregateFunction = "MAX"; 810 } 811 sortColumnName = aggregateFunction + "(" + sortColumnName + ")"; 812 } 813 return sortColumnName; 814 } 815 816 public void addSortNumeric(DbColumn theTheColumnValueNormalized, boolean theAscending, OrderObject.NullOrder theNullOrder, boolean theUseAggregate) { 817 if ((dialectIsMySql || dialectIsMsSql)) { 818 // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax. 819 // Null values are always treated as less than non-null values. 820 // As such special handling is required here. 821 String direction; 822 String sortColumnName = theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName(); 823 if ((theAscending && theNullOrder == OrderObject.NullOrder.LAST) 824 || (!theAscending && theNullOrder == OrderObject.NullOrder.FIRST)) { 825 // Negating the numeric column value and reversing the sort order will ensure that the rows appear 826 // in the correct order with nulls appearing first or last as needed. 827 direction = theAscending ? " DESC" : " ASC"; 828 sortColumnName = "-" + sortColumnName; 829 } else { 830 direction = theAscending ? " ASC" : " DESC"; 831 } 832 sortColumnName = formatColumnNameForAggregate(theAscending, theUseAggregate, sortColumnName); 833 mySelect.addCustomOrderings(sortColumnName + direction); 834 } else { 835 addSort(theTheColumnValueNormalized, theAscending, theNullOrder, theUseAggregate); 836 } 837 } 838 839 public void addSortDate(DbColumn theTheColumnValueNormalized, boolean theTheAscending, OrderObject.NullOrder theNullOrder, boolean theUseAggregate) { 840 if ((dialectIsMySql || dialectIsMsSql)) { 841 // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax. 842 String direction = theTheAscending ? " ASC" : " DESC"; 843 String sortColumnName = theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName(); 844 final StringBuilder sortColumnNameBuilder = new StringBuilder(); 845 // The following block has been commented out for performance. 846 // Uncomment if NullOrder is needed for MariaDB, MySQL or MSSQL 847 /* 848 // Null values are always treated as less than non-null values. 849 if ((theTheAscending && theNullOrder == OrderObject.NullOrder.LAST) 850 || (!theTheAscending && theNullOrder == OrderObject.NullOrder.FIRST)) { 851 // In this case, precede the "order by" column with a case statement that returns 852 // 1 for null and 0 non-null so that nulls will be sorted as greater than non-nulls. 853 sortColumnNameBuilder.append( "CASE WHEN " ).append( sortColumnName ).append( " IS NULL THEN 1 ELSE 0 END" ).append(direction).append(", "); 854 } 855 */ 856 sortColumnName = formatColumnNameForAggregate(theTheAscending, theUseAggregate, sortColumnName); 857 sortColumnNameBuilder.append(sortColumnName).append(direction); 858 mySelect.addCustomOrderings(sortColumnNameBuilder.toString()); 859 } else { 860 addSort(theTheColumnValueNormalized, theTheAscending, theNullOrder, theUseAggregate); 861 } 862 } 863 864 private void addSort(DbColumn theTheColumnValueNormalized, boolean theTheAscending, OrderObject.NullOrder theNullOrder, boolean theUseAggregate) { 865 OrderObject.Dir direction = theTheAscending ? OrderObject.Dir.ASCENDING : OrderObject.Dir.DESCENDING; 866 Object columnToOrder = theTheColumnValueNormalized; 867 if (theUseAggregate) { 868 if (theTheAscending) { 869 columnToOrder = FunctionCall.min().addColumnParams(theTheColumnValueNormalized); 870 } else { 871 columnToOrder = FunctionCall.max().addColumnParams(theTheColumnValueNormalized); 872 } 873 } 874 OrderObject orderObject = new OrderObject(direction, columnToOrder); 875 orderObject.setNullOrder(theNullOrder); 876 mySelect.addCustomOrderings(orderObject); 877 } 878 879 /** 880 * If set to true (default is false), force the generated SQL to start 881 * with the {@link ca.uhn.fhir.jpa.model.entity.ResourceTable HFJ_RESOURCE} 882 * table at the root of the query. 883 * <p> 884 * This seems to perform better if there are multiple joins on the 885 * resource ID table. 886 */ 887 public void setNeedResourceTableRoot(boolean theNeedResourceTableRoot) { 888 myNeedResourceTableRoot = theNeedResourceTableRoot; 889 } 890}