001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 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.NumberPredicateBuilder; 036import ca.uhn.fhir.jpa.search.builder.predicate.QuantityNormalizedPredicateBuilder; 037import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder; 038import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder; 039import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder; 040import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder; 041import ca.uhn.fhir.jpa.search.builder.predicate.SearchParamPresentPredicateBuilder; 042import ca.uhn.fhir.jpa.search.builder.predicate.SourcePredicateBuilder; 043import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder; 044import ca.uhn.fhir.jpa.search.builder.predicate.TagPredicateBuilder; 045import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder; 046import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder; 047import ca.uhn.fhir.rest.param.DateParam; 048import ca.uhn.fhir.rest.param.DateRangeParam; 049import ca.uhn.fhir.rest.param.ParamPrefixEnum; 050import com.healthmarketscience.sqlbuilder.BinaryCondition; 051import com.healthmarketscience.sqlbuilder.ComboCondition; 052import com.healthmarketscience.sqlbuilder.ComboExpression; 053import com.healthmarketscience.sqlbuilder.Condition; 054import com.healthmarketscience.sqlbuilder.FunctionCall; 055import com.healthmarketscience.sqlbuilder.InCondition; 056import com.healthmarketscience.sqlbuilder.OrderObject; 057import com.healthmarketscience.sqlbuilder.SelectQuery; 058import com.healthmarketscience.sqlbuilder.dbspec.Join; 059import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; 060import com.healthmarketscience.sqlbuilder.dbspec.basic.DbJoin; 061import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSchema; 062import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSpec; 063import com.healthmarketscience.sqlbuilder.dbspec.basic.DbTable; 064import jakarta.annotation.Nonnull; 065import jakarta.annotation.Nullable; 066import org.hibernate.dialect.Dialect; 067import org.hibernate.dialect.SQLServerDialect; 068import org.hibernate.dialect.pagination.AbstractLimitHandler; 069import org.hibernate.query.internal.QueryOptionsImpl; 070import org.hibernate.query.spi.Limit; 071import org.hibernate.query.spi.QueryOptions; 072import org.slf4j.Logger; 073import org.slf4j.LoggerFactory; 074 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 final boolean mySelectPartitionId; 106 private boolean myMatchNothing; 107 private ResourceTablePredicateBuilder myResourceTableRoot; 108 private boolean myHaveAtLeastOnePredicate; 109 private BaseJoiningPredicateBuilder myFirstPredicateBuilder; 110 private boolean dialectIsMsSql; 111 private boolean dialectIsMySql; 112 private boolean myNeedResourceTableRoot; 113 private int myNextNearnessColumnId = 0; 114 private DbColumn mySelectedResourceIdColumn; 115 private DbColumn mySelectedPartitionIdColumn; 116 117 /** 118 * Constructor 119 */ 120 public SearchQueryBuilder( 121 FhirContext theFhirContext, 122 StorageSettings theStorageSettings, 123 PartitionSettings thePartitionSettings, 124 RequestPartitionId theRequestPartitionId, 125 String theResourceType, 126 SqlObjectFactory theSqlBuilderFactory, 127 HibernatePropertiesProvider theDialectProvider, 128 boolean theCountQuery) { 129 this( 130 theFhirContext, 131 theStorageSettings, 132 thePartitionSettings, 133 theRequestPartitionId, 134 theResourceType, 135 theSqlBuilderFactory, 136 UUID.randomUUID() + "-", 137 theDialectProvider.getDialect(), 138 theCountQuery, 139 new ArrayList<>(), 140 thePartitionSettings.isPartitioningEnabled()); 141 } 142 143 /** 144 * Constructor for child SQL Builders 145 */ 146 private SearchQueryBuilder( 147 FhirContext theFhirContext, 148 StorageSettings theStorageSettings, 149 PartitionSettings thePartitionSettings, 150 RequestPartitionId theRequestPartitionId, 151 String theResourceType, 152 SqlObjectFactory theSqlBuilderFactory, 153 String theBindVariableSubstitutionBase, 154 Dialect theDialect, 155 boolean theCountQuery, 156 ArrayList<Object> theBindVariableValues, 157 boolean theSelectPartitionId) { 158 myFhirContext = theFhirContext; 159 myStorageSettings = theStorageSettings; 160 myPartitionSettings = thePartitionSettings; 161 myRequestPartitionId = theRequestPartitionId; 162 myResourceType = theResourceType; 163 mySqlBuilderFactory = theSqlBuilderFactory; 164 myCountQuery = theCountQuery; 165 myDialect = theDialect; 166 if (myDialect instanceof org.hibernate.dialect.MySQLDialect) { 167 dialectIsMySql = true; 168 } 169 if (myDialect instanceof org.hibernate.dialect.SQLServerDialect) { 170 dialectIsMsSql = true; 171 } 172 173 mySpec = new DbSpec(); 174 mySchema = mySpec.addDefaultSchema(); 175 mySelect = new SelectQuery(); 176 177 myBindVariableSubstitutionBase = theBindVariableSubstitutionBase; 178 myBindVariableValues = theBindVariableValues; 179 mySelectPartitionId = theSelectPartitionId; 180 } 181 182 public FhirContext getFhirContext() { 183 return myFhirContext; 184 } 185 186 /** 187 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a Composite Unique search parameter 188 */ 189 public ComboUniqueSearchParameterPredicateBuilder addComboUniquePredicateBuilder() { 190 ComboUniqueSearchParameterPredicateBuilder retVal = 191 mySqlBuilderFactory.newComboUniqueSearchParameterPredicateBuilder(this); 192 addTable(retVal, null); 193 return retVal; 194 } 195 196 /** 197 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a Composite Unique search parameter 198 */ 199 public ComboNonUniqueSearchParameterPredicateBuilder addComboNonUniquePredicateBuilder() { 200 ComboNonUniqueSearchParameterPredicateBuilder retVal = 201 mySqlBuilderFactory.newComboNonUniqueSearchParameterPredicateBuilder(this); 202 addTable(retVal, null); 203 return retVal; 204 } 205 206 /** 207 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a COORDS search parameter 208 */ 209 public CoordsPredicateBuilder addCoordsPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 210 CoordsPredicateBuilder retVal = mySqlBuilderFactory.coordsPredicateBuilder(this); 211 addTable(retVal, theSourceJoinColumn); 212 return retVal; 213 } 214 215 /** 216 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a DATE search parameter 217 */ 218 public DatePredicateBuilder addDatePredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 219 DatePredicateBuilder retVal = mySqlBuilderFactory.dateIndexTable(this); 220 addTable(retVal, theSourceJoinColumn); 221 return retVal; 222 } 223 224 /** 225 * Create a predicate builder for selecting on a DATE search parameter 226 */ 227 public DatePredicateBuilder createDatePredicateBuilder() { 228 return mySqlBuilderFactory.dateIndexTable(this); 229 } 230 231 /** 232 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a NUMBER search parameter 233 */ 234 public NumberPredicateBuilder addNumberPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 235 NumberPredicateBuilder retVal = createNumberPredicateBuilder(); 236 addTable(retVal, theSourceJoinColumn); 237 return retVal; 238 } 239 240 /** 241 * Create a predicate builder for selecting on a NUMBER search parameter 242 */ 243 public NumberPredicateBuilder createNumberPredicateBuilder() { 244 return mySqlBuilderFactory.numberIndexTable(this); 245 } 246 247 /** 248 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on the Resource table 249 */ 250 public ResourceTablePredicateBuilder addResourceTablePredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 251 ResourceTablePredicateBuilder retVal = mySqlBuilderFactory.resourceTable(this); 252 addTable(retVal, theSourceJoinColumn); 253 return retVal; 254 } 255 256 /** 257 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a QUANTITY search parameter 258 */ 259 public QuantityPredicateBuilder addQuantityPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 260 QuantityPredicateBuilder retVal = createQuantityPredicateBuilder(); 261 addTable(retVal, theSourceJoinColumn); 262 263 return retVal; 264 } 265 266 /** 267 * Create a predicate builder for selecting on a QUANTITY search parameter 268 */ 269 public QuantityPredicateBuilder createQuantityPredicateBuilder() { 270 return mySqlBuilderFactory.quantityIndexTable(this); 271 } 272 273 public QuantityNormalizedPredicateBuilder addQuantityNormalizedPredicateBuilder( 274 @Nullable DbColumn[] theSourceJoinColumn) { 275 276 QuantityNormalizedPredicateBuilder retVal = mySqlBuilderFactory.quantityNormalizedIndexTable(this); 277 addTable(retVal, theSourceJoinColumn); 278 279 return retVal; 280 } 281 282 /** 283 * 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 284 */ 285 public SourcePredicateBuilder addSourcePredicateBuilder( 286 @Nullable DbColumn[] theSourceJoinColumn, SelectQuery.JoinType theJoinType) { 287 SourcePredicateBuilder retVal = mySqlBuilderFactory.newSourcePredicateBuilder(this); 288 addTable(retVal, theSourceJoinColumn, theJoinType); 289 return retVal; 290 } 291 292 /** 293 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a REFERENCE search parameter 294 */ 295 public ResourceLinkPredicateBuilder addReferencePredicateBuilder( 296 QueryStack theQueryStack, @Nullable DbColumn[] theSourceJoinColumn) { 297 ResourceLinkPredicateBuilder retVal = createReferencePredicateBuilder(theQueryStack); 298 addTable(retVal, theSourceJoinColumn); 299 return retVal; 300 } 301 302 /** 303 * Create a predicate builder for selecting on a REFERENCE search parameter 304 */ 305 public ResourceLinkPredicateBuilder createReferencePredicateBuilder(QueryStack theQueryStack) { 306 return mySqlBuilderFactory.referenceIndexTable(theQueryStack, this, false); 307 } 308 309 /** 310 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a resource link where the 311 * source and target are reversed. This is used for _has queries. 312 */ 313 public ResourceLinkPredicateBuilder addReferencePredicateBuilderReversed( 314 QueryStack theQueryStack, DbColumn[] theSourceJoinColumn) { 315 ResourceLinkPredicateBuilder retVal = mySqlBuilderFactory.referenceIndexTable(theQueryStack, this, true); 316 addTable(retVal, theSourceJoinColumn); 317 return retVal; 318 } 319 320 /** 321 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a STRING search parameter 322 */ 323 public StringPredicateBuilder addStringPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 324 StringPredicateBuilder retVal = createStringPredicateBuilder(); 325 addTable(retVal, theSourceJoinColumn); 326 return retVal; 327 } 328 329 /** 330 * Create a predicate builder for selecting on a STRING search parameter 331 */ 332 public StringPredicateBuilder createStringPredicateBuilder() { 333 return mySqlBuilderFactory.stringIndexTable(this); 334 } 335 336 /** 337 * 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 338 */ 339 public TagPredicateBuilder addTagPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 340 TagPredicateBuilder retVal = mySqlBuilderFactory.newTagPredicateBuilder(this); 341 addTable(retVal, theSourceJoinColumn); 342 return retVal; 343 } 344 345 /** 346 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a TOKEN search parameter 347 */ 348 public TokenPredicateBuilder addTokenPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 349 TokenPredicateBuilder retVal = createTokenPredicateBuilder(); 350 addTable(retVal, theSourceJoinColumn); 351 return retVal; 352 } 353 354 /** 355 * Create a predicate builder for selecting on a TOKEN search parameter 356 */ 357 public TokenPredicateBuilder createTokenPredicateBuilder() { 358 return mySqlBuilderFactory.tokenIndexTable(this); 359 } 360 361 public void addCustomJoin( 362 SelectQuery.JoinType theJoinType, DbTable theFromTable, DbTable theToTable, Condition theCondition) { 363 mySelect.addCustomJoin(theJoinType, theFromTable, theToTable, theCondition); 364 } 365 366 public ComboCondition createOnCondition(DbColumn[] theSourceColumn, DbColumn[] theTargetColumn) { 367 ComboCondition onCondition = ComboCondition.and(); 368 for (int i = 0; i < theSourceColumn.length; i += 1) { 369 onCondition.addCondition(BinaryCondition.equalTo(theSourceColumn[i], theTargetColumn[i])); 370 } 371 return onCondition; 372 } 373 374 /** 375 * 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 376 */ 377 public SearchParamPresentPredicateBuilder addSearchParamPresentPredicateBuilder( 378 @Nullable DbColumn[] theSourceJoinColumn) { 379 SearchParamPresentPredicateBuilder retVal = mySqlBuilderFactory.searchParamPresentPredicateBuilder(this); 380 addTable(retVal, theSourceJoinColumn); 381 return retVal; 382 } 383 384 /** 385 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a URI search parameter 386 */ 387 public UriPredicateBuilder addUriPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 388 UriPredicateBuilder retVal = createUriPredicateBuilder(); 389 addTable(retVal, theSourceJoinColumn); 390 return retVal; 391 } 392 393 /** 394 * Create a predicate builder for selecting on a URI search parameter 395 */ 396 public UriPredicateBuilder createUriPredicateBuilder() { 397 return mySqlBuilderFactory.uriIndexTable(this); 398 } 399 400 public SqlObjectFactory getSqlBuilderFactory() { 401 return mySqlBuilderFactory; 402 } 403 404 public ResourceIdPredicateBuilder newResourceIdBuilder() { 405 return mySqlBuilderFactory.resourceId(this); 406 } 407 408 /** 409 * Add and return a predicate builder (or a root query if no root query exists yet) for an arbitrary table 410 */ 411 private void addTable(BaseJoiningPredicateBuilder thePredicateBuilder, @Nullable DbColumn[] theSourceJoinColumn) { 412 addTable(thePredicateBuilder, theSourceJoinColumn, SelectQuery.JoinType.INNER); 413 } 414 415 private void addTable( 416 BaseJoiningPredicateBuilder thePredicateBuilder, 417 @Nullable DbColumn[] theSourceJoinColumns, 418 SelectQuery.JoinType theJoinType) { 419 if (theSourceJoinColumns != null) { 420 DbTable fromTable = theSourceJoinColumns[0].getTable(); 421 DbTable toTable = thePredicateBuilder.getTable(); 422 DbColumn[] toColumn = toJoinColumns(thePredicateBuilder); 423 addJoin(fromTable, toTable, theSourceJoinColumns, toColumn, theJoinType); 424 } else { 425 if (myFirstPredicateBuilder == null) { 426 427 BaseJoiningPredicateBuilder root; 428 if (!myNeedResourceTableRoot) { 429 root = thePredicateBuilder; 430 } else { 431 if (thePredicateBuilder instanceof ResourceTablePredicateBuilder) { 432 root = thePredicateBuilder; 433 } else { 434 root = mySqlBuilderFactory.resourceTable(this); 435 } 436 } 437 438 if (myCountQuery) { 439 mySelect.addCustomColumns( 440 FunctionCall.count().setIsDistinct(true).addColumnParams(root.getResourceIdColumn())); 441 } else { 442 if (mySelectPartitionId) { 443 mySelectedResourceIdColumn = root.getResourceIdColumn(); 444 mySelectedPartitionIdColumn = root.getPartitionIdColumn(); 445 mySelect.addColumns(mySelectedPartitionIdColumn, mySelectedResourceIdColumn); 446 } else { 447 mySelectedResourceIdColumn = root.getResourceIdColumn(); 448 mySelect.addColumns(mySelectedResourceIdColumn); 449 } 450 } 451 mySelect.addFromTable(root.getTable()); 452 myFirstPredicateBuilder = root; 453 454 if (!myNeedResourceTableRoot || (thePredicateBuilder instanceof ResourceTablePredicateBuilder)) { 455 return; 456 } 457 } 458 459 DbTable fromTable = myFirstPredicateBuilder.getTable(); 460 DbTable toTable = thePredicateBuilder.getTable(); 461 DbColumn[] fromColumn = toJoinColumns(myFirstPredicateBuilder); 462 DbColumn[] toColumn = toJoinColumns(thePredicateBuilder); 463 addJoin(fromTable, toTable, fromColumn, toColumn, theJoinType); 464 } 465 } 466 467 @Nonnull 468 public DbColumn[] toJoinColumns(BaseJoiningPredicateBuilder theBuilder) { 469 DbColumn partitionIdColumn = theBuilder.getPartitionIdColumn(); 470 DbColumn resourceIdColumn = theBuilder.getResourceIdColumn(); 471 return toJoinColumns(partitionIdColumn, resourceIdColumn); 472 } 473 474 /** 475 * Remove or keep partition_id columns depending on settings. 476 */ 477 @Nonnull 478 public DbColumn[] toJoinColumns(DbColumn partitionIdColumn, DbColumn resourceIdColumn) { 479 if (isIncludePartitionIdInJoins()) { 480 return new DbColumn[] {partitionIdColumn, resourceIdColumn}; 481 } else { 482 return new DbColumn[] {resourceIdColumn}; 483 } 484 } 485 486 public boolean isIncludePartitionIdInJoins() { 487 return mySelectPartitionId && myPartitionSettings.isPartitionIdsInPrimaryKeys(); 488 } 489 490 public void addJoin(DbTable theFromTable, DbTable theToTable, DbColumn[] theFromColumn, DbColumn[] theToColumn) { 491 addJoin(theFromTable, theToTable, theFromColumn, theToColumn, SelectQuery.JoinType.INNER); 492 } 493 494 public void addJoin( 495 DbTable theFromTable, 496 DbTable theToTable, 497 DbColumn[] theFromColumn, 498 DbColumn[] theToColumn, 499 SelectQuery.JoinType theJoinType) { 500 assert theFromColumn.length == theToColumn.length; 501 Join join = new DbJoin(mySpec, theFromTable, theToTable, theFromColumn, theToColumn); 502 mySelect.addJoins(theJoinType, join); 503 } 504 505 public boolean isSelectPartitionId() { 506 return mySelectPartitionId; 507 } 508 509 /** 510 * Generate and return the SQL generated by this builder 511 */ 512 public GeneratedSql generate(@Nullable Integer theOffset, @Nullable Integer theMaxResultsToFetch) { 513 514 getOrCreateFirstPredicateBuilder(); 515 516 mySelect.validate(); 517 String sql = mySelect.toString(); 518 519 List<Object> bindVariables = new ArrayList<>(); 520 while (true) { 521 522 int idx = sql.indexOf(myBindVariableSubstitutionBase); 523 if (idx == -1) { 524 break; 525 } 526 527 int endIdx = sql.indexOf("'", idx + myBindVariableSubstitutionBase.length()); 528 String substitutionIndexString = sql.substring(idx + myBindVariableSubstitutionBase.length(), endIdx); 529 int substitutionIndex = Integer.parseInt(substitutionIndexString); 530 bindVariables.add(myBindVariableValues.get(substitutionIndex)); 531 532 sql = sql.substring(0, idx - 1) + "?" + sql.substring(endIdx + 1); 533 } 534 535 Integer maxResultsToFetch = theMaxResultsToFetch; 536 Integer offset = theOffset; 537 if (offset != null && offset == 0) { 538 offset = null; 539 } 540 if (maxResultsToFetch != null || offset != null) { 541 542 maxResultsToFetch = defaultIfNull(maxResultsToFetch, 10000); 543 String selectedResourceIdColumn = mySelectedResourceIdColumn.getColumnNameSQL(); 544 545 sql = applyLimitToSql(myDialect, offset, maxResultsToFetch, sql, selectedResourceIdColumn, bindVariables); 546 } 547 548 return new GeneratedSql(myMatchNothing, sql, bindVariables); 549 } 550 551 /** 552 * This method applies the theDialect limiter (select first NNN offset MMM etc etc..) to 553 * a SQL string. It enhances the built-in Hibernate dialect version with some additional 554 * enhancements. 555 */ 556 public static String applyLimitToSql( 557 Dialect theDialect, 558 Integer theOffset, 559 Integer theMaxResultsToFetch, 560 String theInputSql, 561 @Nullable String theSelectedColumnOrNull, 562 List<Object> theBindVariables) { 563 AbstractLimitHandler limitHandler = (AbstractLimitHandler) theDialect.getLimitHandler(); 564 Limit selection = new Limit(); 565 selection.setFirstRow(theOffset); 566 selection.setMaxRows(theMaxResultsToFetch); 567 QueryOptions queryOptions = new QueryOptionsImpl(); 568 theInputSql = limitHandler.processSql(theInputSql, selection, queryOptions); 569 570 int startOfQueryParameterIndex = 0; 571 572 boolean isSqlServer = (theDialect instanceof SQLServerDialect); 573 if (isSqlServer) { 574 575 /* 576 * SQL server requires an ORDER BY clause to be present in the SQL if there is 577 * an OFFSET/FETCH FIRST clause, so if there isn't already an ORDER BY clause, 578 * the theDialect will automatically add an order by with a pseudo-column name. This 579 * happens in SQLServer2012LimitHandler. 580 * 581 * But, SQL Server also pukes if you include an ORDER BY on a column that you 582 * aren't also SELECTing, if the select statement contains a UNION, INTERSECT or EXCEPT operator. 583 * Who knows why SQL Server is so picky.. but anyhow, this causes an issue, so we manually replace 584 * the pseudo-column with an actual selected column. 585 */ 586 if (theInputSql.contains("order by @@version")) { 587 if (theSelectedColumnOrNull != null) { 588 theInputSql = theInputSql.replace("order by @@version", "order by " + theSelectedColumnOrNull); 589 } else { 590 // not certain if this case can happen, but ordering by the ordinal first column should always 591 // be syntactically valid and seems like a better option than ordering by a static value 592 // regardless 593 theInputSql = theInputSql.replace("order by @@version", "order by 1"); 594 } 595 } 596 597 // The SQLServerDialect has a bunch of one-off processing to deal with rules on when 598 // a limit can be used, so we can't rely on the flags that the limithandler exposes since 599 // the exact structure of the query depends on the parameters 600 if (theInputSql.contains("top(?)")) { 601 theBindVariables.add(0, theMaxResultsToFetch); 602 } 603 if (theInputSql.contains("offset 0 rows fetch first ? rows only")) { 604 theBindVariables.add(theMaxResultsToFetch); 605 } 606 if (theInputSql.contains("offset ? rows fetch next ? rows only")) { 607 theBindVariables.add(theOffset); 608 theBindVariables.add(theMaxResultsToFetch); 609 } 610 if (theOffset != null && theInputSql.contains("rownumber_")) { 611 theBindVariables.add(theOffset + 1); 612 theBindVariables.add(theOffset + theMaxResultsToFetch + 1); 613 } 614 615 } else if (limitHandler.supportsVariableLimit()) { 616 617 boolean bindLimitParametersFirst = limitHandler.bindLimitParametersFirst(); 618 if (limitHandler.useMaxForLimit() && theOffset != null) { 619 theMaxResultsToFetch = theMaxResultsToFetch + theOffset; 620 } 621 622 if (limitHandler.bindLimitParametersInReverseOrder()) { 623 startOfQueryParameterIndex = bindCountParameter( 624 theBindVariables, 625 theMaxResultsToFetch, 626 limitHandler, 627 startOfQueryParameterIndex, 628 bindLimitParametersFirst); 629 bindOffsetParameter( 630 theBindVariables, 631 theOffset, 632 limitHandler, 633 startOfQueryParameterIndex, 634 bindLimitParametersFirst); 635 } else { 636 startOfQueryParameterIndex = bindOffsetParameter( 637 theBindVariables, 638 theOffset, 639 limitHandler, 640 startOfQueryParameterIndex, 641 bindLimitParametersFirst); 642 bindCountParameter( 643 theBindVariables, 644 theMaxResultsToFetch, 645 limitHandler, 646 startOfQueryParameterIndex, 647 bindLimitParametersFirst); 648 } 649 } 650 return theInputSql; 651 } 652 653 private static int bindCountParameter( 654 List<Object> bindVariables, 655 Integer maxResultsToFetch, 656 AbstractLimitHandler limitHandler, 657 int startOfQueryParameterIndex, 658 boolean bindLimitParametersFirst) { 659 if (limitHandler.supportsLimit()) { 660 if (bindLimitParametersFirst) { 661 bindVariables.add(startOfQueryParameterIndex++, maxResultsToFetch); 662 } else { 663 bindVariables.add(maxResultsToFetch); 664 } 665 } 666 return startOfQueryParameterIndex; 667 } 668 669 public static int bindOffsetParameter( 670 List<Object> theBindVariables, 671 @Nullable Integer theOffset, 672 AbstractLimitHandler theLimitHandler, 673 int theStartOfQueryParameterIndex, 674 boolean theBindLimitParametersFirst) { 675 if (theLimitHandler.supportsLimitOffset() && theOffset != null) { 676 if (theBindLimitParametersFirst) { 677 theBindVariables.add(theStartOfQueryParameterIndex++, theOffset); 678 } else { 679 theBindVariables.add(theOffset); 680 } 681 } 682 return theStartOfQueryParameterIndex; 683 } 684 685 /** 686 * 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. 687 */ 688 public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder() { 689 return getOrCreateFirstPredicateBuilder(true); 690 } 691 692 /** 693 * 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. 694 */ 695 public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder( 696 boolean theIncludeResourceTypeAndNonDeletedFlag) { 697 if (myFirstPredicateBuilder == null) { 698 getOrCreateResourceTablePredicateBuilder(theIncludeResourceTypeAndNonDeletedFlag); 699 } 700 return myFirstPredicateBuilder; 701 } 702 703 public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder() { 704 return getOrCreateResourceTablePredicateBuilder(true); 705 } 706 707 public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder( 708 boolean theIncludeResourceTypeAndNonDeletedFlag) { 709 if (myResourceTableRoot == null) { 710 ResourceTablePredicateBuilder resourceTable = mySqlBuilderFactory.resourceTable(this); 711 addTable(resourceTable, null); 712 if (theIncludeResourceTypeAndNonDeletedFlag) { 713 Condition typeAndDeletionPredicate = resourceTable.createResourceTypeAndNonDeletedPredicates(); 714 addPredicate(typeAndDeletionPredicate); 715 } 716 myResourceTableRoot = resourceTable; 717 } 718 return myResourceTableRoot; 719 } 720 721 /** 722 * The SQL Builder library has one annoying limitation, which is that it does not use/understand bind variables 723 * for its generated SQL. So we work around this by replacing our contents with a string in the SQL consisting 724 * of <code>[random UUID]-[value index]</code> and then 725 */ 726 public String generatePlaceholder(Object theValue) { 727 String placeholder = myBindVariableSubstitutionBase + myBindVariableValues.size(); 728 myBindVariableValues.add(theValue); 729 return placeholder; 730 } 731 732 public List<String> generatePlaceholders(Collection<?> theValues) { 733 return theValues.stream().map(this::generatePlaceholder).collect(Collectors.toList()); 734 } 735 736 public int countBindVariables() { 737 return myBindVariableValues.size(); 738 } 739 740 public void setMatchNothing() { 741 myMatchNothing = true; 742 } 743 744 public DbTable addTable(String theTableName) { 745 return mySchema.addTable(theTableName); 746 } 747 748 public PartitionSettings getPartitionSettings() { 749 return myPartitionSettings; 750 } 751 752 public RequestPartitionId getRequestPartitionId() { 753 return myRequestPartitionId; 754 } 755 756 public String getResourceType() { 757 return myResourceType; 758 } 759 760 public StorageSettings getStorageSettings() { 761 return myStorageSettings; 762 } 763 764 public void addPredicate(@Nonnull Condition theCondition) { 765 assert theCondition != null; 766 mySelect.addCondition(theCondition); 767 myHaveAtLeastOnePredicate = true; 768 } 769 770 public ComboCondition addPredicateLastUpdated(DateRangeParam theDateRange) { 771 ResourceTablePredicateBuilder resourceTableRoot = getOrCreateResourceTablePredicateBuilder(false); 772 return addPredicateLastUpdated(theDateRange, resourceTableRoot); 773 } 774 775 public ComboCondition addPredicateLastUpdated( 776 DateRangeParam theDateRange, ResourceTablePredicateBuilder theResourceTablePredicateBuilder) { 777 List<Condition> conditions = new ArrayList<>(2); 778 BinaryCondition condition; 779 780 if (isNotEqualsComparator(theDateRange)) { 781 condition = createConditionForValueWithComparator( 782 LESSTHAN, 783 theResourceTablePredicateBuilder.getLastUpdatedColumn(), 784 theDateRange.getLowerBoundAsInstant()); 785 conditions.add(condition); 786 condition = createConditionForValueWithComparator( 787 GREATERTHAN, 788 theResourceTablePredicateBuilder.getLastUpdatedColumn(), 789 theDateRange.getUpperBoundAsInstant()); 790 conditions.add(condition); 791 return ComboCondition.or(conditions.toArray(new Condition[0])); 792 } 793 794 if (theDateRange.getLowerBoundAsInstant() != null) { 795 condition = createConditionForValueWithComparator( 796 GREATERTHAN_OR_EQUALS, 797 theResourceTablePredicateBuilder.getLastUpdatedColumn(), 798 theDateRange.getLowerBoundAsInstant()); 799 conditions.add(condition); 800 } 801 802 if (theDateRange.getUpperBoundAsInstant() != null) { 803 condition = createConditionForValueWithComparator( 804 LESSTHAN_OR_EQUALS, 805 theResourceTablePredicateBuilder.getLastUpdatedColumn(), 806 theDateRange.getUpperBoundAsInstant()); 807 conditions.add(condition); 808 } 809 810 return ComboCondition.and(conditions.toArray(new Condition[0])); 811 } 812 813 private boolean isNotEqualsComparator(DateRangeParam theDateRange) { 814 if (theDateRange != null) { 815 DateParam lb = theDateRange.getLowerBound(); 816 DateParam ub = theDateRange.getUpperBound(); 817 818 return lb != null 819 && ub != null 820 && lb.getPrefix().equals(NOT_EQUAL) 821 && ub.getPrefix().equals(NOT_EQUAL); 822 } 823 return false; 824 } 825 826 public void addResourceIdsPredicate(List<Long> thePidList) { 827 DbColumn resourceIdColumn = getOrCreateFirstPredicateBuilder().getResourceIdColumn(); 828 InCondition predicate = new InCondition(resourceIdColumn, generatePlaceholders(thePidList)); 829 addPredicate(predicate); 830 } 831 832 public void excludeResourceIdsPredicate(Set<JpaPid> theExistingPidSetToExclude) { 833 834 // Do nothing if it's empty 835 if (theExistingPidSetToExclude == null || theExistingPidSetToExclude.isEmpty()) return; 836 837 List<Long> excludePids = JpaPid.toLongList(theExistingPidSetToExclude); 838 839 ourLog.trace("excludePids = {}", excludePids); 840 841 DbColumn resourceIdColumn = getOrCreateFirstPredicateBuilder().getResourceIdColumn(); 842 InCondition predicate = new InCondition(resourceIdColumn, generatePlaceholders(excludePids)); 843 predicate.setNegate(true); 844 addPredicate(predicate); 845 } 846 847 public BinaryCondition createConditionForValueWithComparator( 848 ParamPrefixEnum theComparator, DbColumn theColumn, Object theValue) { 849 switch (theComparator) { 850 case LESSTHAN: 851 return BinaryCondition.lessThan(theColumn, generatePlaceholder(theValue)); 852 case LESSTHAN_OR_EQUALS: 853 return BinaryCondition.lessThanOrEq(theColumn, generatePlaceholder(theValue)); 854 case GREATERTHAN: 855 return BinaryCondition.greaterThan(theColumn, generatePlaceholder(theValue)); 856 case GREATERTHAN_OR_EQUALS: 857 return BinaryCondition.greaterThanOrEq(theColumn, generatePlaceholder(theValue)); 858 case NOT_EQUAL: 859 return BinaryCondition.notEqualTo(theColumn, generatePlaceholder(theValue)); 860 case EQUAL: 861 // NB: fhir searches are always range searches; 862 // which is why we do not use "EQUAL" 863 case STARTS_AFTER: 864 case APPROXIMATE: 865 case ENDS_BEFORE: 866 default: 867 throw new IllegalArgumentException(Msg.code(1263)); 868 } 869 } 870 871 public SearchQueryBuilder newChildSqlBuilder(boolean theSelectPartitionId) { 872 return new SearchQueryBuilder( 873 myFhirContext, 874 myStorageSettings, 875 myPartitionSettings, 876 myRequestPartitionId, 877 myResourceType, 878 mySqlBuilderFactory, 879 myBindVariableSubstitutionBase, 880 myDialect, 881 false, 882 myBindVariableValues, 883 theSelectPartitionId); 884 } 885 886 public SelectQuery getSelect() { 887 return mySelect; 888 } 889 890 public boolean haveAtLeastOnePredicate() { 891 return myHaveAtLeastOnePredicate; 892 } 893 894 public void addSortCoordsNear( 895 CoordsPredicateBuilder theCoordsBuilder, 896 double theLatitudeValue, 897 double theLongitudeValue, 898 boolean theAscending) { 899 FunctionCall absLatitude = new FunctionCall("ABS"); 900 String latitudePlaceholder = generatePlaceholder(theLatitudeValue); 901 ComboExpression absLatitudeMiddle = new ComboExpression( 902 ComboExpression.Op.SUBTRACT, theCoordsBuilder.getColumnLatitude(), latitudePlaceholder); 903 absLatitude = absLatitude.addCustomParams(absLatitudeMiddle); 904 905 FunctionCall absLongitude = new FunctionCall("ABS"); 906 String longitudePlaceholder = generatePlaceholder(theLongitudeValue); 907 ComboExpression absLongitudeMiddle = new ComboExpression( 908 ComboExpression.Op.SUBTRACT, theCoordsBuilder.getColumnLongitude(), longitudePlaceholder); 909 absLongitude = absLongitude.addCustomParams(absLongitudeMiddle); 910 911 ComboExpression sum = new ComboExpression(ComboExpression.Op.ADD, absLatitude, absLongitude); 912 String ordering; 913 if (theAscending) { 914 ordering = ""; 915 } else { 916 ordering = " DESC"; 917 } 918 919 String columnName = "MHD" + (myNextNearnessColumnId++); 920 mySelect.addAliasedColumn(sum, columnName); 921 mySelect.addCustomOrderings(columnName + ordering); 922 } 923 924 public void addSortString(DbColumn theColumnValueNormalized, boolean theAscending) { 925 addSortString(theColumnValueNormalized, theAscending, false); 926 } 927 928 public void addSortString(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) { 929 OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST; 930 addSortString(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate); 931 } 932 933 public void addSortNumeric(DbColumn theColumnValueNormalized, boolean theAscending) { 934 addSortNumeric(theColumnValueNormalized, theAscending, false); 935 } 936 937 public void addSortNumeric(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) { 938 OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST; 939 addSortNumeric(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate); 940 } 941 942 public void addSortDate(DbColumn theColumnValueNormalized, boolean theAscending) { 943 addSortDate(theColumnValueNormalized, theAscending, false); 944 } 945 946 public void addSortDate(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) { 947 OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST; 948 addSortDate(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate); 949 } 950 951 public void addSortString( 952 DbColumn theTheColumnValueNormalized, 953 boolean theTheAscending, 954 OrderObject.NullOrder theNullOrder, 955 boolean theUseAggregate) { 956 if ((dialectIsMySql || dialectIsMsSql)) { 957 // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax. 958 String direction = theTheAscending ? " ASC" : " DESC"; 959 String sortColumnName = 960 theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName(); 961 final StringBuilder sortColumnNameBuilder = new StringBuilder(); 962 // The following block has been commented out for performance. 963 // Uncomment if NullOrder is needed for MariaDB, MySQL or MSSQL 964 /* 965 // Null values are always treated as less than non-null values. 966 if ((theTheAscending && theNullOrder == OrderObject.NullOrder.LAST) 967 || (!theTheAscending && theNullOrder == OrderObject.NullOrder.FIRST)) { 968 // In this case, precede the "order by" column with a case statement that returns 969 // 1 for null and 0 non-null so that nulls will be sorted as greater than non-nulls. 970 sortColumnNameBuilder.append( "CASE WHEN " ).append( sortColumnName ).append( " IS NULL THEN 1 ELSE 0 END" ).append(direction).append(", "); 971 } 972 */ 973 sortColumnName = formatColumnNameForAggregate(theTheAscending, theUseAggregate, sortColumnName); 974 sortColumnNameBuilder.append(sortColumnName).append(direction); 975 mySelect.addCustomOrderings(sortColumnNameBuilder.toString()); 976 } else { 977 addSort(theTheColumnValueNormalized, theTheAscending, theNullOrder, theUseAggregate); 978 } 979 } 980 981 private static String formatColumnNameForAggregate( 982 boolean theTheAscending, boolean theUseAggregate, String sortColumnName) { 983 if (theUseAggregate) { 984 String aggregateFunction; 985 if (theTheAscending) { 986 aggregateFunction = "MIN"; 987 } else { 988 aggregateFunction = "MAX"; 989 } 990 sortColumnName = aggregateFunction + "(" + sortColumnName + ")"; 991 } 992 return sortColumnName; 993 } 994 995 public void addSortNumeric( 996 DbColumn theTheColumnValueNormalized, 997 boolean theAscending, 998 OrderObject.NullOrder theNullOrder, 999 boolean theUseAggregate) { 1000 if ((dialectIsMySql || dialectIsMsSql)) { 1001 // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax. 1002 // Null values are always treated as less than non-null values. 1003 // As such special handling is required here. 1004 String direction; 1005 String sortColumnName = 1006 theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName(); 1007 if ((theAscending && theNullOrder == OrderObject.NullOrder.LAST) 1008 || (!theAscending && theNullOrder == OrderObject.NullOrder.FIRST)) { 1009 // Negating the numeric column value and reversing the sort order will ensure that the rows appear 1010 // in the correct order with nulls appearing first or last as needed. 1011 direction = theAscending ? " DESC" : " ASC"; 1012 sortColumnName = "-" + sortColumnName; 1013 } else { 1014 direction = theAscending ? " ASC" : " DESC"; 1015 } 1016 sortColumnName = formatColumnNameForAggregate(theAscending, theUseAggregate, sortColumnName); 1017 mySelect.addCustomOrderings(sortColumnName + direction); 1018 } else { 1019 addSort(theTheColumnValueNormalized, theAscending, theNullOrder, theUseAggregate); 1020 } 1021 } 1022 1023 public void addSortDate( 1024 DbColumn theTheColumnValueNormalized, 1025 boolean theTheAscending, 1026 OrderObject.NullOrder theNullOrder, 1027 boolean theUseAggregate) { 1028 if ((dialectIsMySql || dialectIsMsSql)) { 1029 // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax. 1030 String direction = theTheAscending ? " ASC" : " DESC"; 1031 String sortColumnName = 1032 theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName(); 1033 final StringBuilder sortColumnNameBuilder = new StringBuilder(); 1034 // The following block has been commented out for performance. 1035 // Uncomment if NullOrder is needed for MariaDB, MySQL or MSSQL 1036 /* 1037 // Null values are always treated as less than non-null values. 1038 if ((theTheAscending && theNullOrder == OrderObject.NullOrder.LAST) 1039 || (!theTheAscending && theNullOrder == OrderObject.NullOrder.FIRST)) { 1040 // In this case, precede the "order by" column with a case statement that returns 1041 // 1 for null and 0 non-null so that nulls will be sorted as greater than non-nulls. 1042 sortColumnNameBuilder.append( "CASE WHEN " ).append( sortColumnName ).append( " IS NULL THEN 1 ELSE 0 END" ).append(direction).append(", "); 1043 } 1044 */ 1045 sortColumnName = formatColumnNameForAggregate(theTheAscending, theUseAggregate, sortColumnName); 1046 sortColumnNameBuilder.append(sortColumnName).append(direction); 1047 mySelect.addCustomOrderings(sortColumnNameBuilder.toString()); 1048 } else { 1049 addSort(theTheColumnValueNormalized, theTheAscending, theNullOrder, theUseAggregate); 1050 } 1051 } 1052 1053 private void addSort( 1054 DbColumn theTheColumnValueNormalized, 1055 boolean theTheAscending, 1056 OrderObject.NullOrder theNullOrder, 1057 boolean theUseAggregate) { 1058 OrderObject.Dir direction = theTheAscending ? OrderObject.Dir.ASCENDING : OrderObject.Dir.DESCENDING; 1059 Object columnToOrder = theTheColumnValueNormalized; 1060 if (theUseAggregate) { 1061 if (theTheAscending) { 1062 columnToOrder = FunctionCall.min().addColumnParams(theTheColumnValueNormalized); 1063 } else { 1064 columnToOrder = FunctionCall.max().addColumnParams(theTheColumnValueNormalized); 1065 } 1066 } 1067 OrderObject orderObject = new OrderObject(direction, columnToOrder); 1068 orderObject.setNullOrder(theNullOrder); 1069 mySelect.addCustomOrderings(orderObject); 1070 } 1071 1072 /** 1073 * If set to true (default is false), force the generated SQL to start 1074 * with the {@link ca.uhn.fhir.jpa.model.entity.ResourceTable HFJ_RESOURCE} 1075 * table at the root of the query. 1076 * <p> 1077 * This seems to perform better if there are multiple joins on the 1078 * resource ID table. 1079 */ 1080 public void setNeedResourceTableRoot(boolean theNeedResourceTableRoot) { 1081 myNeedResourceTableRoot = theNeedResourceTableRoot; 1082 } 1083}