001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.pyranid; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import javax.annotation.concurrent.ThreadSafe; 024import javax.sql.DataSource; 025import java.sql.Connection; 026import java.sql.DatabaseMetaData; 027import java.sql.PreparedStatement; 028import java.sql.ResultSet; 029import java.sql.SQLException; 030import java.sql.SQLFeatureNotSupportedException; 031import java.sql.Types; 032import java.time.Duration; 033import java.time.ZoneId; 034import java.util.ArrayDeque; 035import java.util.ArrayList; 036import java.util.Arrays; 037import java.util.Collection; 038import java.util.Deque; 039import java.util.HashSet; 040import java.util.LinkedHashMap; 041import java.util.List; 042import java.util.Locale; 043import java.util.Map; 044import java.util.Optional; 045import java.util.OptionalDouble; 046import java.util.OptionalInt; 047import java.util.OptionalLong; 048import java.util.Queue; 049import java.util.Set; 050import java.util.Spliterator; 051import java.util.Spliterators; 052import java.util.concurrent.atomic.AtomicReference; 053import java.util.concurrent.atomic.AtomicLong; 054import java.util.concurrent.locks.ReentrantLock; 055import java.util.function.Consumer; 056import java.util.function.Function; 057import java.util.logging.Level; 058import java.util.logging.Logger; 059import java.util.regex.Pattern; 060import java.util.stream.Collectors; 061import java.util.stream.Stream; 062import java.util.stream.StreamSupport; 063 064import static java.lang.String.format; 065import static java.lang.System.nanoTime; 066import static java.util.Objects.requireNonNull; 067 068/** 069 * Main class for performing database access operations. 070 * 071 * @author <a href="https://www.revetkn.com">Mark Allen</a> 072 * @since 1.0.0 073 */ 074@ThreadSafe 075public final class Database { 076 @NonNull 077 private static final ThreadLocal<Deque<Transaction>> TRANSACTION_STACK_HOLDER; 078 private static final int DEFAULT_PARSED_SQL_CACHE_CAPACITY = 1024; 079 private static final int MAX_DIAGNOSTIC_MESSAGE_LENGTH = 1024; 080 private static final int MAX_DIAGNOSTIC_PARAMETER_LENGTH = 256; 081 private static final int MAX_DIAGNOSTIC_PARAMETERS_LENGTH = 2048; 082 private static final int MAX_DIAGNOSTIC_SQL_LENGTH = 2048; 083 @NonNull 084 private static final String TRUNCATED_SUFFIX = "... (truncated)"; 085 @NonNull 086 private static final Pattern DIAGNOSTIC_WHITESPACE_PATTERN = Pattern.compile("\\s+"); 087 088 static { 089 TRANSACTION_STACK_HOLDER = new ThreadLocal<>(); 090 } 091 092 @NonNull 093 private final DataSource dataSource; 094 @NonNull 095 private final AtomicReference<@Nullable DatabaseType> databaseType; 096 @NonNull 097 private final AtomicReference<@Nullable DatabaseDialect> databaseDialect; 098 @NonNull 099 private final ThreadLocal<Connection> databaseTypeDetectionConnectionHolder; 100 @NonNull 101 private final ZoneId timeZone; 102 @NonNull 103 private final AmbiguousTimestampBindingStrategy ambiguousTimestampBindingStrategy; 104 @NonNull 105 private final InstanceProvider instanceProvider; 106 @NonNull 107 private final PreparedStatementBinder preparedStatementBinder; 108 @NonNull 109 private final ResultSetMapper resultSetMapper; 110 @NonNull 111 private final StatementLogger statementLogger; 112 @NonNull 113 private final ParameterRedactor parameterRedactor; 114 @NonNull 115 private final MetricsCollectorDispatcher metricsCollectorDispatcher; 116 @Nullable 117 private final Duration queryTimeout; 118 @Nullable 119 private final Integer fetchSize; 120 @Nullable 121 private final Integer maxRows; 122 @Nullable 123 private final Map<String, ParsedSql> parsedSqlCache; 124 @NonNull 125 private final AtomicLong defaultIdGenerator; 126 @NonNull 127 private final Logger logger; 128 129 @NonNull 130 private volatile DatabaseOperationSupportStatus executeLargeBatchSupported; 131 @NonNull 132 private volatile DatabaseOperationSupportStatus executeLargeUpdateSupported; 133 134 protected Database(@NonNull Builder builder) { 135 requireNonNull(builder); 136 137 this.dataSource = requireNonNull(builder.dataSource); 138 this.databaseType = new AtomicReference<>(builder.databaseType); 139 this.databaseDialect = new AtomicReference<>(builder.databaseType == null ? null : builder.databaseType.dialect()); 140 this.databaseTypeDetectionConnectionHolder = new ThreadLocal<>(); 141 this.timeZone = builder.timeZone == null ? ZoneId.systemDefault() : builder.timeZone; 142 this.ambiguousTimestampBindingStrategy = builder.ambiguousTimestampBindingStrategy == null 143 ? AmbiguousTimestampBindingStrategy.TIMESTAMP_WITH_TIME_ZONE 144 : builder.ambiguousTimestampBindingStrategy; 145 this.instanceProvider = builder.instanceProvider == null ? new InstanceProvider() {} : builder.instanceProvider; 146 this.preparedStatementBinder = builder.preparedStatementBinder == null ? PreparedStatementBinder.withDefaultConfiguration() : builder.preparedStatementBinder; 147 this.resultSetMapper = builder.resultSetMapper == null ? ResultSetMapper.withDefaultConfiguration() : builder.resultSetMapper; 148 this.statementLogger = builder.statementLogger == null ? (statementLog) -> {} : builder.statementLogger; 149 this.parameterRedactor = builder.parameterRedactor == null ? ParameterRedactor.none() : builder.parameterRedactor; 150 this.metricsCollectorDispatcher = new MetricsCollectorDispatcher(builder.metricsCollector); 151 this.queryTimeout = validateQueryTimeout(builder.queryTimeout); 152 this.fetchSize = validateNonNegativeStatementSetting("fetchSize", builder.fetchSize); 153 this.maxRows = validateNonNegativeStatementSetting("maxRows", builder.maxRows); 154 if (builder.parsedSqlCacheCapacity != null && builder.parsedSqlCacheCapacity < 0) 155 throw new IllegalArgumentException("parsedSqlCacheCapacity must be >= 0"); 156 157 int parsedSqlCacheCapacity = builder.parsedSqlCacheCapacity == null 158 ? DEFAULT_PARSED_SQL_CACHE_CAPACITY 159 : builder.parsedSqlCacheCapacity; 160 this.parsedSqlCache = parsedSqlCacheCapacity == 0 ? null : new ConcurrentLruMap<>(parsedSqlCacheCapacity); 161 this.defaultIdGenerator = new AtomicLong(); 162 this.logger = Logger.getLogger(getClass().getName()); 163 this.executeLargeBatchSupported = DatabaseOperationSupportStatus.UNKNOWN; 164 this.executeLargeUpdateSupported = DatabaseOperationSupportStatus.UNKNOWN; 165 } 166 167 /** 168 * Provides a {@link Database} builder for the given {@link DataSource}. 169 * 170 * @param dataSource data source used to create the {@link Database} builder 171 * @return a {@link Database} builder 172 */ 173 @NonNull 174 public static Builder withDataSource(@NonNull DataSource dataSource) { 175 requireNonNull(dataSource); 176 return new Builder(dataSource); 177 } 178 179 /** 180 * Gets a reference to the current transaction, if any. 181 * 182 * @return the current transaction 183 */ 184 @NonNull 185 public Optional<Transaction> currentTransaction() { 186 @Nullable Deque<Transaction> transactionStack = TRANSACTION_STACK_HOLDER.get(); 187 Transaction transaction = transactionStack == null || transactionStack.isEmpty() ? null : transactionStack.peek(); 188 189 if (transaction == null || !isTransactionOwnedByThisDatabase(transaction)) 190 return Optional.empty(); 191 192 return Optional.of(transaction); 193 } 194 195 @NonNull 196 private Optional<Transaction> currentTransactionForDatabaseOperation() { 197 @Nullable Deque<Transaction> transactionStack = TRANSACTION_STACK_HOLDER.get(); 198 Transaction transaction = transactionStack == null || transactionStack.isEmpty() ? null : transactionStack.peek(); 199 200 if (transaction == null) 201 return Optional.empty(); 202 203 if (!isTransactionOwnedByThisDatabase(transaction)) 204 throw wrongDatabaseTransactionException(transaction); 205 206 return Optional.of(transaction); 207 } 208 209 @NonNull 210 private Deque<Transaction> transactionStackForPush() { 211 Deque<Transaction> transactionStack = TRANSACTION_STACK_HOLDER.get(); 212 213 if (transactionStack == null) { 214 transactionStack = new ArrayDeque<>(); 215 TRANSACTION_STACK_HOLDER.set(transactionStack); 216 } 217 218 return transactionStack; 219 } 220 221 private boolean isTransactionOwnedByThisDatabase(@NonNull Transaction transaction) { 222 requireNonNull(transaction); 223 return transaction.isOwnedBy(getDataSource()); 224 } 225 226 @NonNull 227 private DatabaseException wrongDatabaseTransactionException(@NonNull Transaction transaction) { 228 requireNonNull(transaction); 229 return new DatabaseException(format("Transaction %s belongs to a different %s than this %s. " 230 + "Use the %s instance that created the transaction, or explicitly participate only with a transaction " 231 + "created from the same %s.", 232 transaction.id(), DataSource.class.getSimpleName(), Database.class.getSimpleName(), 233 Database.class.getSimpleName(), DataSource.class.getSimpleName())); 234 } 235 236 /** 237 * Performs an operation transactionally. 238 * <p> 239 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 240 * <p> 241 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 242 * not automatically join an outer transaction. Use {@link #participate(Transaction, TransactionalOperation)} to join an 243 * existing transaction explicitly. A transaction is scoped to the {@link DataSource} instance that created it; a 244 * {@link Database} using a different {@link DataSource} fails fast instead of joining it. 245 * 246 * @param transactionalOperation the operation to perform transactionally 247 */ 248 public void transaction(@NonNull TransactionalOperation transactionalOperation) { 249 requireNonNull(transactionalOperation); 250 251 transaction(() -> { 252 transactionalOperation.perform(); 253 return Optional.empty(); 254 }); 255 } 256 257 /** 258 * Performs an operation transactionally with the given options. 259 * <p> 260 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 261 * <p> 262 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 263 * not automatically join an outer transaction. Use {@link #participate(Transaction, TransactionalOperation)} to join an 264 * existing transaction explicitly. A transaction is scoped to the {@link DataSource} instance that created it; a 265 * {@link Database} using a different {@link DataSource} fails fast instead of joining it. 266 * 267 * @param transactionOptions options to apply to the transaction 268 * @param transactionalOperation the operation to perform transactionally 269 * @since 4.2.0 270 */ 271 public void transaction(@NonNull TransactionOptions transactionOptions, 272 @NonNull TransactionalOperation transactionalOperation) { 273 requireNonNull(transactionOptions); 274 requireNonNull(transactionalOperation); 275 276 transaction(transactionOptions, () -> { 277 transactionalOperation.perform(); 278 return Optional.empty(); 279 }); 280 } 281 282 /** 283 * Performs an operation transactionally and optionally returns a value. 284 * <p> 285 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 286 * <p> 287 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 288 * not automatically join an outer transaction. Use {@link #participate(Transaction, ReturningTransactionalOperation)} to 289 * join an existing transaction explicitly. A transaction is scoped to the {@link DataSource} instance that created it; a 290 * {@link Database} using a different {@link DataSource} fails fast instead of joining it. 291 * 292 * @param transactionalOperation the operation to perform transactionally 293 * @param <T> the type to be returned 294 * @return the result of the transactional operation 295 */ 296 @NonNull 297 public <T> Optional<T> transaction(@NonNull ReturningTransactionalOperation<T> transactionalOperation) { 298 requireNonNull(transactionalOperation); 299 return transaction(TransactionOptions.DEFAULT, transactionalOperation); 300 } 301 302 /** 303 * Performs an operation transactionally with the given options, optionally returning a value. 304 * <p> 305 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 306 * <p> 307 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 308 * not automatically join an outer transaction. Use {@link #participate(Transaction, ReturningTransactionalOperation)} to 309 * join an existing transaction explicitly. A transaction is scoped to the {@link DataSource} instance that created it; a 310 * {@link Database} using a different {@link DataSource} fails fast instead of joining it. 311 * 312 * @param transactionOptions options to apply to the transaction 313 * @param transactionalOperation the operation to perform transactionally 314 * @param <T> the type to be returned 315 * @return the result of the transactional operation 316 * @since 4.2.0 317 */ 318 @NonNull 319 public <T> Optional<T> transaction(@NonNull TransactionOptions transactionOptions, 320 @NonNull ReturningTransactionalOperation<T> transactionalOperation) { 321 requireNonNull(transactionOptions); 322 requireNonNull(transactionalOperation); 323 324 Transaction transaction = new Transaction(dataSource, transactionOptions, getMetricsCollectorDispatcher(), peekDatabaseType()); 325 Deque<Transaction> transactionStack = transactionStackForPush(); 326 transactionStack.push(transaction); 327 boolean committed = false; 328 boolean commitFailed = false; 329 boolean rollbackFailed = false; 330 boolean rollbackAttempted = false; 331 Throwable thrown = null; 332 long transactionStartTime = nanoTime(); 333 getMetricsCollectorDispatcher().didEnterTransactionClosure(transaction, transactionOptions.getIsolation(), transaction.getDatabaseType()); 334 335 try { 336 Optional<T> returnValue = transactionalOperation.perform(); 337 338 // Safeguard in case user code accidentally returns null instead of Optional.empty() 339 if (returnValue == null) 340 returnValue = Optional.empty(); 341 342 transaction.getConnectionLock().lock(); 343 344 try { 345 if (transaction.isRollbackOnly()) { 346 rollbackAttempted = true; 347 transaction.rollback(); 348 } else { 349 try { 350 transaction.commit(); 351 } catch (RuntimeException | Error e) { 352 commitFailed = true; 353 throw e; 354 } 355 committed = true; 356 } 357 358 transaction.markCompleted(); 359 } finally { 360 transaction.getConnectionLock().unlock(); 361 } 362 363 return returnValue; 364 } catch (RuntimeException e) { 365 thrown = e; 366 if (rollbackAttempted) { 367 rollbackFailed = true; 368 markTransactionCompleted(transaction); 369 } else { 370 rollbackFailed = rollbackTransactionAfterFailure(transaction, e); 371 } 372 373 restoreInterruptIfNeeded(e); 374 throw e; 375 } catch (Error e) { 376 thrown = e; 377 if (rollbackAttempted) { 378 rollbackFailed = true; 379 markTransactionCompleted(transaction); 380 } else { 381 rollbackFailed = rollbackTransactionAfterFailure(transaction, e); 382 } 383 384 restoreInterruptIfNeeded(e); 385 throw e; 386 } catch (Throwable t) { 387 RuntimeException wrapped = new RuntimeException(t); 388 thrown = wrapped; 389 if (rollbackAttempted) { 390 rollbackFailed = true; 391 markTransactionCompleted(transaction); 392 } else { 393 rollbackFailed = rollbackTransactionAfterFailure(transaction, wrapped); 394 } 395 396 restoreInterruptIfNeeded(t); 397 throw wrapped; 398 } finally { 399 transactionStack.pop(); 400 401 // Ensure txn stack is fully cleaned up 402 if (transactionStack.isEmpty()) 403 TRANSACTION_STACK_HOLDER.remove(); 404 405 Throwable cleanupFailure = null; 406 boolean hadPhysicalTransaction = false; 407 408 try { 409 transaction.getConnectionLock().lock(); 410 411 try { 412 hadPhysicalTransaction = transaction.hasConnection(); 413 414 if (!transaction.isCompleted()) 415 transaction.markCompleted(); 416 417 cleanupFailure = cleanupCompletedTransactionConnection(transaction, cleanupFailure); 418 } finally { 419 transaction.getConnectionLock().unlock(); 420 } 421 } finally { 422 // Execute any user-supplied post-execution hooks 423 for (Consumer<TransactionResult> postTransactionOperation : transaction.getPostTransactionOperations()) { 424 long postTransactionStartTime = nanoTime(); 425 Throwable postTransactionThrowable = null; 426 TransactionResult transactionResult = transactionResult(committed, commitFailed); 427 try { 428 postTransactionOperation.accept(transactionResult); 429 } catch (Throwable cleanupException) { 430 postTransactionThrowable = cleanupException; 431 PostTransactionOperationException postTransactionOperationException = 432 new PostTransactionOperationException(transactionResult, cleanupException); 433 434 if (cleanupFailure == null) 435 cleanupFailure = postTransactionOperationException; 436 else 437 cleanupFailure.addSuppressed(postTransactionOperationException); 438 } finally { 439 getMetricsCollectorDispatcher().didRunPostTransactionOperation(transaction, transactionResult, transaction.getDatabaseType(), 440 Duration.ofNanos(nanoTime() - postTransactionStartTime), postTransactionThrowable); 441 } 442 } 443 } 444 445 Throwable exitThrown = thrown == null ? cleanupFailure : thrown; 446 getMetricsCollectorDispatcher().didExitTransactionClosure(transaction, 447 transactionClosureOutcome(committed, commitFailed, hadPhysicalTransaction, rollbackFailed), 448 transaction.getDatabaseType(), Duration.ofNanos(nanoTime() - transactionStartTime), exitThrown); 449 450 if (cleanupFailure != null) { 451 if (thrown != null) { 452 thrown.addSuppressed(cleanupFailure); 453 } else if (cleanupFailure instanceof RuntimeException) { 454 throw (RuntimeException) cleanupFailure; 455 } else if (cleanupFailure instanceof Error) { 456 throw (Error) cleanupFailure; 457 } else { 458 throw new RuntimeException(cleanupFailure); 459 } 460 } 461 } 462 } 463 464 /** 465 * Performs an operation transactionally, retrying according to the given retry policy. 466 * <p> 467 * The entire transaction closure may run more than once. Keep non-idempotent external side effects outside the closure 468 * unless they are safe to repeat. 469 * <p> 470 * Unlike {@link #transaction(TransactionalOperation)} and related transaction methods, retrying methods return 471 * {@link TransactionRetryResult} so callers can inspect failures that were recovered before success. 472 * <p> 473 * This method fails fast if called inside an active transaction for this {@code Database}. Retrying a nested unit cannot 474 * restart the outer transaction safely. 475 * 476 * @param retryPolicy retry policy to apply 477 * @param transactionalOperation the operation to perform transactionally 478 * @return retry result containing any failures retried before success 479 * @since 4.4.0 480 */ 481 @NonNull 482 public TransactionRetryResult<Void> transactionWithRetry(@NonNull RetryPolicy retryPolicy, 483 @NonNull TransactionalOperation transactionalOperation) { 484 requireNonNull(retryPolicy); 485 requireNonNull(transactionalOperation); 486 487 return transactionWithRetry(retryPolicy, () -> { 488 transactionalOperation.perform(); 489 return Optional.<Void>empty(); 490 }); 491 } 492 493 /** 494 * Performs an operation transactionally with the given options, retrying according to the given retry policy. 495 * <p> 496 * The entire transaction closure may run more than once. Keep non-idempotent external side effects outside the closure 497 * unless they are safe to repeat. 498 * <p> 499 * Unlike {@link #transaction(TransactionOptions, TransactionalOperation)} and related transaction methods, retrying 500 * methods return {@link TransactionRetryResult} so callers can inspect failures that were recovered before success. 501 * <p> 502 * This method fails fast if called inside an active transaction for this {@code Database}. Retrying a nested unit cannot 503 * restart the outer transaction safely. 504 * 505 * @param retryPolicy retry policy to apply 506 * @param transactionOptions options to apply to each transaction attempt 507 * @param transactionalOperation the operation to perform transactionally 508 * @return retry result containing any failures retried before success 509 * @since 4.4.0 510 */ 511 @NonNull 512 public TransactionRetryResult<Void> transactionWithRetry(@NonNull RetryPolicy retryPolicy, 513 @NonNull TransactionOptions transactionOptions, 514 @NonNull TransactionalOperation transactionalOperation) { 515 requireNonNull(retryPolicy); 516 requireNonNull(transactionOptions); 517 requireNonNull(transactionalOperation); 518 519 return transactionWithRetry(retryPolicy, transactionOptions, () -> { 520 transactionalOperation.perform(); 521 return Optional.<Void>empty(); 522 }); 523 } 524 525 /** 526 * Performs an operation transactionally and optionally returns a value, retrying according to the given retry policy. 527 * <p> 528 * The entire transaction closure may run more than once. Keep non-idempotent external side effects outside the closure 529 * unless they are safe to repeat. 530 * <p> 531 * Unlike {@link #transaction(ReturningTransactionalOperation)} and related transaction methods, retrying methods return 532 * {@link TransactionRetryResult} so callers can inspect failures that were recovered before success. 533 * <p> 534 * This method fails fast if called inside an active transaction for this {@code Database}. Retrying a nested unit cannot 535 * restart the outer transaction safely. 536 * 537 * @param retryPolicy retry policy to apply 538 * @param transactionalOperation the operation to perform transactionally 539 * @param <T> the type to be returned 540 * @return retry result containing the successful transaction value and any failures retried before success 541 * @since 4.4.0 542 */ 543 @NonNull 544 public <T> TransactionRetryResult<T> transactionWithRetry(@NonNull RetryPolicy retryPolicy, 545 @NonNull ReturningTransactionalOperation<T> transactionalOperation) { 546 requireNonNull(retryPolicy); 547 requireNonNull(transactionalOperation); 548 549 return transactionWithRetry(retryPolicy, TransactionOptions.DEFAULT, transactionalOperation); 550 } 551 552 /** 553 * Performs an operation transactionally with the given options and optionally returns a value, retrying according to the 554 * given retry policy. 555 * <p> 556 * The entire transaction closure may run more than once. Keep non-idempotent external side effects outside the closure 557 * unless they are safe to repeat. 558 * <p> 559 * Unlike {@link #transaction(TransactionOptions, ReturningTransactionalOperation)} and related transaction methods, 560 * retrying methods return {@link TransactionRetryResult} so callers can inspect failures that were recovered before 561 * success. 562 * <p> 563 * This method fails fast if called inside an active transaction for this {@code Database}. Retrying a nested unit cannot 564 * restart the outer transaction safely. 565 * 566 * @param retryPolicy retry policy to apply 567 * @param transactionOptions options to apply to each transaction attempt 568 * @param transactionalOperation the operation to perform transactionally 569 * @param <T> the type to be returned 570 * @return retry result containing the successful transaction value and any failures retried before success 571 * @since 4.4.0 572 */ 573 @NonNull 574 public <T> TransactionRetryResult<T> transactionWithRetry(@NonNull RetryPolicy retryPolicy, 575 @NonNull TransactionOptions transactionOptions, 576 @NonNull ReturningTransactionalOperation<T> transactionalOperation) { 577 requireNonNull(retryPolicy); 578 requireNonNull(transactionOptions); 579 requireNonNull(transactionalOperation); 580 581 if (currentTransaction().isPresent()) 582 throw new IllegalStateException("transactionWithRetry must not be called within an existing transaction"); 583 584 List<DatabaseException> priorFailures = new ArrayList<>(); 585 586 for (int attempt = 1; attempt <= retryPolicy.getMaxAttempts(); ++attempt) { 587 try { 588 return TransactionRetryResult.of(transaction(transactionOptions, transactionalOperation), priorFailures); 589 } catch (DatabaseException e) { 590 boolean finalAttempt = attempt == retryPolicy.getMaxAttempts(); 591 592 if (finalAttempt) { 593 suppressPriorFailures(e, priorFailures); 594 throw e; 595 } 596 597 Boolean retryable; 598 599 try { 600 retryable = retryPolicy.getCondition().shouldRetry(e); 601 } catch (RuntimeException | Error conditionFailure) { 602 suppressRetryFailures(conditionFailure, e, priorFailures); 603 throw conditionFailure; 604 } 605 606 if (retryable == null) { 607 NullPointerException nullConditionFailure = 608 new NullPointerException("RetryPolicy.Condition returned null"); 609 suppressRetryFailures(nullConditionFailure, e, priorFailures); 610 throw nullConditionFailure; 611 } 612 613 if (!retryable) { 614 suppressPriorFailures(e, priorFailures); 615 throw e; 616 } 617 618 priorFailures.add(e); 619 620 Duration delay; 621 622 try { 623 delay = retryPolicy.getBackoff().delayAfterFailedAttempt(attempt, e); 624 } catch (RuntimeException | Error backoffFailure) { 625 suppressPriorFailures(backoffFailure, priorFailures); 626 throw backoffFailure; 627 } 628 629 if (delay == null) { 630 NullPointerException nullBackoffFailure = new NullPointerException("RetryPolicy.Backoff returned null"); 631 suppressPriorFailures(nullBackoffFailure, priorFailures); 632 throw nullBackoffFailure; 633 } 634 635 if (delay.isNegative()) { 636 IllegalArgumentException negativeBackoffFailure = 637 new IllegalArgumentException("RetryPolicy.Backoff returned a negative delay"); 638 suppressPriorFailures(negativeBackoffFailure, priorFailures); 639 throw negativeBackoffFailure; 640 } 641 642 try { 643 sleepBackoff(delay); 644 } catch (InterruptedException interruptedException) { 645 Thread.currentThread().interrupt(); 646 suppressPriorFailures(e, priorFailures); 647 e.addSuppressed(interruptedException); 648 throw e; 649 } 650 } 651 } 652 653 throw new AssertionError("unreachable"); 654 } 655 656 private void sleepBackoff(@NonNull Duration delay) throws InterruptedException { 657 requireNonNull(delay); 658 659 if (delay.isZero()) 660 return; 661 662 long millis; 663 int nanos; 664 665 try { 666 millis = delay.toMillis(); 667 Duration remainder = delay.minusMillis(millis); 668 nanos = (int) Math.min(999_999L, Math.max(0L, remainder.toNanos())); 669 } catch (ArithmeticException e) { 670 millis = Long.MAX_VALUE; 671 nanos = 999_999; 672 } 673 674 Thread.sleep(millis, nanos); 675 } 676 677 private void suppressRetryFailures(@NonNull Throwable failure, 678 @NonNull DatabaseException currentFailure, 679 @NonNull List<@NonNull DatabaseException> priorFailures) { 680 requireNonNull(failure); 681 requireNonNull(currentFailure); 682 requireNonNull(priorFailures); 683 684 suppressPriorFailures(failure, priorFailures); 685 suppressIfDifferent(failure, currentFailure); 686 } 687 688 private void suppressPriorFailures(@NonNull Throwable failure, 689 @NonNull List<@NonNull DatabaseException> priorFailures) { 690 requireNonNull(failure); 691 requireNonNull(priorFailures); 692 693 for (DatabaseException priorFailure : priorFailures) 694 suppressIfDifferent(failure, priorFailure); 695 } 696 697 private void suppressIfDifferent(@NonNull Throwable failure, 698 @NonNull Throwable suppressed) { 699 requireNonNull(failure); 700 requireNonNull(suppressed); 701 702 if (failure != suppressed) 703 failure.addSuppressed(suppressed); 704 } 705 706 private boolean rollbackTransactionAfterFailure(@NonNull Transaction transaction, 707 @NonNull Throwable primary) { 708 requireNonNull(transaction); 709 requireNonNull(primary); 710 711 boolean rollbackFailed = false; 712 transaction.getConnectionLock().lock(); 713 714 try { 715 try { 716 transaction.rollback(); 717 } catch (Throwable rollbackException) { 718 rollbackFailed = true; 719 primary.addSuppressed(rollbackException); 720 } finally { 721 transaction.markCompleted(); 722 } 723 } finally { 724 transaction.getConnectionLock().unlock(); 725 } 726 727 return rollbackFailed; 728 } 729 730 private void markTransactionCompleted(@NonNull Transaction transaction) { 731 requireNonNull(transaction); 732 transaction.getConnectionLock().lock(); 733 734 try { 735 transaction.markCompleted(); 736 } finally { 737 transaction.getConnectionLock().unlock(); 738 } 739 } 740 741 @Nullable 742 private Throwable cleanupCompletedTransactionConnection(@NonNull Transaction transaction, 743 @Nullable Throwable cleanupFailure) { 744 requireNonNull(transaction); 745 746 try { 747 transaction.restoreTransactionIsolationIfNeeded(); 748 } catch (Throwable cleanupException) { 749 cleanupFailure = appendSuppressed(cleanupFailure, cleanupException); 750 } 751 752 try { 753 transaction.restoreReadOnlyIfNeeded(); 754 } catch (Throwable cleanupException) { 755 cleanupFailure = appendSuppressed(cleanupFailure, cleanupException); 756 } 757 758 if (transaction.getInitialAutoCommit().isPresent() && transaction.getInitialAutoCommit().get()) { 759 try { 760 // Autocommit was true initially, so restoring to true now that transaction has completed 761 transaction.setAutoCommit(true); 762 } catch (Throwable cleanupException) { 763 cleanupFailure = appendSuppressed(cleanupFailure, cleanupException); 764 } 765 } 766 767 Connection connection = transaction.getExistingConnection().orElse(null); 768 769 if (connection != null) { 770 Duration heldDuration = transaction.getConnectionAcquiredAtNanos() 771 .map(acquiredAtNanos -> Duration.ofNanos(nanoTime() - acquiredAtNanos)) 772 .orElse(Duration.ZERO); 773 774 try { 775 closeConnection(connection); 776 getMetricsCollectorDispatcher().didReleaseTransactionConnection(transaction, transaction.getDatabaseType(), heldDuration); 777 transaction.clearConnection(); 778 } catch (Throwable cleanupException) { 779 getMetricsCollectorDispatcher().didFailToReleaseTransactionConnection(transaction, transaction.getDatabaseType(), heldDuration, cleanupException); 780 cleanupFailure = appendSuppressed(cleanupFailure, cleanupException); 781 } 782 } 783 784 return cleanupFailure; 785 } 786 787 @NonNull 788 private static Throwable appendSuppressed(@Nullable Throwable existing, 789 @NonNull Throwable additional) { 790 requireNonNull(additional); 791 792 if (existing == null) 793 return additional; 794 795 existing.addSuppressed(additional); 796 return existing; 797 } 798 799 protected void closeConnection(@NonNull Connection connection) { 800 requireNonNull(connection); 801 802 try { 803 connection.close(); 804 } catch (SQLException e) { 805 throw new DatabaseException("Unable to close database connection", e); 806 } 807 } 808 809 @NonNull 810 private static DatabaseException databaseExceptionWithStatementContext(@NonNull StatementContext<?> statementContext, 811 @NonNull Throwable cause) { 812 requireNonNull(statementContext); 813 requireNonNull(cause); 814 815 String message = cause.getMessage(); 816 817 if (message == null || message.trim().length() == 0) 818 message = "Database operation failed"; 819 820 return databaseExceptionWithStatementContext(statementContext, message, cause); 821 } 822 823 @NonNull 824 private static DatabaseException databaseExceptionWithStatementContext(@NonNull StatementContext<?> statementContext, 825 @NonNull String message, 826 @NonNull Throwable cause) { 827 requireNonNull(statementContext); 828 requireNonNull(message); 829 requireNonNull(cause); 830 831 StatementDiagnostic statementDiagnostic = statementDiagnostic(statementContext); 832 DatabaseException databaseException = new DatabaseException(format("%s [%s]", 833 boundedDiagnosticMessage(message), statementDiagnostic.diagnostic()), cause, 834 databaseDialectForException(statementContext, cause)); 835 836 if (statementDiagnostic.parameterRenderingFailure() != null) 837 databaseException.addSuppressed(statementDiagnostic.parameterRenderingFailure()); 838 839 return databaseException; 840 } 841 842 @NonNull 843 private static DatabaseDialect databaseDialectForException(@NonNull StatementContext<?> statementContext, 844 @NonNull Throwable cause) { 845 requireNonNull(statementContext); 846 requireNonNull(cause); 847 848 try { 849 return statementContext.getDatabaseDialect(); 850 } catch (Throwable ignored) { 851 return DatabaseDialect.forExceptionCause(cause); 852 } 853 } 854 855 @NonNull 856 private DatabaseException databaseExceptionWithRawConnectionContext(@Nullable Connection connection, 857 @NonNull Exception cause) { 858 requireNonNull(cause); 859 860 return new DatabaseException(cause.getMessage(), cause, databaseDialectForRawConnectionException(connection, cause)); 861 } 862 863 @NonNull 864 private DatabaseDialect databaseDialectForRawConnectionException(@Nullable Connection connection, 865 @NonNull Throwable cause) { 866 requireNonNull(cause); 867 868 if (connection != null) { 869 try { 870 return getDatabaseDialect(connection); 871 } catch (Throwable ignored) { 872 // Fall through to cause-based classification below. 873 } 874 } 875 876 return DatabaseDialect.forExceptionCause(cause); 877 } 878 879 @NonNull 880 private static StatementDiagnostic statementDiagnostic(@NonNull StatementContext<?> statementContext) { 881 requireNonNull(statementContext); 882 883 Statement statement = statementContext.getStatement(); 884 String parameters; 885 Throwable parameterRenderingFailure = null; 886 887 try { 888 parameters = boundedDiagnosticParameters(statementContext.getRedactedParameters()); 889 } catch (Throwable t) { 890 parameters = "<unavailable>"; 891 parameterRenderingFailure = t; 892 } 893 894 String diagnostic = format("statementId=%s, sql=%s, parameters=%s", 895 statement.getId(), boundedSql(statement.getSql()), parameters); 896 897 if (parameterRenderingFailure != null) 898 diagnostic = format("%s, parameterRenderingFailure=%s", diagnostic, 899 boundedDiagnosticParameter(parameterRenderingFailureDiagnostic(parameterRenderingFailure))); 900 901 return new StatementDiagnostic(diagnostic, parameterRenderingFailure); 902 } 903 904 private record StatementDiagnostic(@NonNull String diagnostic, 905 @Nullable Throwable parameterRenderingFailure) {} 906 907 @NonNull 908 private static String parameterRenderingFailureDiagnostic(@NonNull Throwable parameterRenderingFailure) { 909 requireNonNull(parameterRenderingFailure); 910 911 String message; 912 913 try { 914 message = parameterRenderingFailure.getMessage(); 915 } catch (Throwable ignored) { 916 message = null; 917 } 918 919 if (message == null || message.trim().length() == 0) 920 return parameterRenderingFailure.getClass().getName(); 921 922 return format("%s: %s", parameterRenderingFailure.getClass().getName(), message); 923 } 924 925 @NonNull 926 private static TransactionResult transactionResult(@NonNull Boolean committed, 927 @NonNull Boolean commitFailed) { 928 requireNonNull(committed); 929 requireNonNull(commitFailed); 930 931 if (committed) 932 return TransactionResult.COMMITTED; 933 934 return commitFailed ? TransactionResult.IN_DOUBT : TransactionResult.ROLLED_BACK; 935 } 936 937 private static MetricsCollector.TransactionClosureOutcome transactionClosureOutcome(@NonNull Boolean committed, 938 @NonNull Boolean commitFailed, 939 @NonNull Boolean hadPhysicalTransaction, 940 @NonNull Boolean rollbackFailed) { 941 requireNonNull(committed); 942 requireNonNull(commitFailed); 943 requireNonNull(hadPhysicalTransaction); 944 requireNonNull(rollbackFailed); 945 946 if (!hadPhysicalTransaction) 947 return MetricsCollector.TransactionClosureOutcome.NO_PHYSICAL_TX; 948 949 if (committed) 950 return MetricsCollector.TransactionClosureOutcome.COMMITTED; 951 952 if (commitFailed) 953 return MetricsCollector.TransactionClosureOutcome.FAILED; 954 955 return rollbackFailed 956 ? MetricsCollector.TransactionClosureOutcome.FAILED 957 : MetricsCollector.TransactionClosureOutcome.ROLLED_BACK; 958 } 959 960 @Nullable 961 static Long sumBatchUpdateCounts(@NonNull List<Long> updateCounts) { 962 requireNonNull(updateCounts); 963 964 long total = 0L; 965 966 for (Long updateCount : updateCounts) { 967 if (updateCount == null || updateCount < 0L) 968 return null; 969 970 try { 971 total = Math.addExact(total, updateCount); 972 } catch (ArithmeticException e) { 973 return null; 974 } 975 } 976 977 return total; 978 } 979 980 @NonNull 981 private static String boundedDiagnosticMessage(@NonNull String message) { 982 requireNonNull(message); 983 984 String compactMessage = DIAGNOSTIC_WHITESPACE_PATTERN.matcher(message).replaceAll(" ").trim(); 985 986 if (compactMessage.length() <= MAX_DIAGNOSTIC_MESSAGE_LENGTH) 987 return compactMessage; 988 989 int prefixLength = Math.max(0, MAX_DIAGNOSTIC_MESSAGE_LENGTH - TRUNCATED_SUFFIX.length()); 990 return compactMessage.substring(0, prefixLength) + TRUNCATED_SUFFIX; 991 } 992 993 @NonNull 994 private static String boundedSql(@NonNull String sql) { 995 requireNonNull(sql); 996 997 String compactSql = DIAGNOSTIC_WHITESPACE_PATTERN.matcher(sql).replaceAll(" ").trim(); 998 999 if (compactSql.length() <= MAX_DIAGNOSTIC_SQL_LENGTH) 1000 return compactSql; 1001 1002 int prefixLength = Math.max(0, MAX_DIAGNOSTIC_SQL_LENGTH - TRUNCATED_SUFFIX.length()); 1003 return compactSql.substring(0, prefixLength) + TRUNCATED_SUFFIX; 1004 } 1005 1006 @NonNull 1007 private static String boundedDiagnosticParameters(@NonNull List<@Nullable Object> parameters) { 1008 requireNonNull(parameters); 1009 1010 StringBuilder parametersBuilder = new StringBuilder("["); 1011 1012 for (int i = 0; i < parameters.size(); ++i) { 1013 String separator = i == 0 ? "" : ", "; 1014 String renderedParameter = boundedDiagnosticParameter(parameters.get(i)); 1015 int requiredLength = parametersBuilder.length() + separator.length() + renderedParameter.length() + 1; 1016 1017 if (requiredLength > MAX_DIAGNOSTIC_PARAMETERS_LENGTH) { 1018 int availableLength = MAX_DIAGNOSTIC_PARAMETERS_LENGTH 1019 - parametersBuilder.length() 1020 - separator.length() 1021 - TRUNCATED_SUFFIX.length() 1022 - 1; 1023 1024 parametersBuilder.append(separator); 1025 1026 if (availableLength > 0) 1027 parametersBuilder.append(renderedParameter, 0, Math.min(availableLength, renderedParameter.length())); 1028 1029 parametersBuilder.append(TRUNCATED_SUFFIX).append(']'); 1030 if (parametersBuilder.length() > MAX_DIAGNOSTIC_PARAMETERS_LENGTH) { 1031 parametersBuilder.setLength(MAX_DIAGNOSTIC_PARAMETERS_LENGTH - 1); 1032 parametersBuilder.append(']'); 1033 } 1034 return parametersBuilder.toString(); 1035 } 1036 1037 parametersBuilder.append(separator).append(renderedParameter); 1038 } 1039 1040 parametersBuilder.append(']'); 1041 return parametersBuilder.toString(); 1042 } 1043 1044 @NonNull 1045 private static String boundedDiagnosticParameter(@Nullable Object parameter) { 1046 String renderedParameter = String.valueOf(parameter); 1047 String compactParameter = DIAGNOSTIC_WHITESPACE_PATTERN.matcher(renderedParameter).replaceAll(" ").trim(); 1048 1049 if (compactParameter.length() <= MAX_DIAGNOSTIC_PARAMETER_LENGTH) 1050 return compactParameter; 1051 1052 int prefixLength = Math.max(0, MAX_DIAGNOSTIC_PARAMETER_LENGTH - TRUNCATED_SUFFIX.length()); 1053 return compactParameter.substring(0, prefixLength) + TRUNCATED_SUFFIX; 1054 } 1055 1056 /** 1057 * Performs an operation in the context of a pre-existing transaction. 1058 * <p> 1059 * No commit or rollback on the transaction will occur when {@code transactionalOperation} completes. 1060 * <p> 1061 * However, if an exception bubbles out of {@code transactionalOperation}, the transaction will be marked as rollback-only. 1062 * <p> 1063 * The transaction must have been created by this {@link Database}, or by another {@link Database} using the same 1064 * {@link DataSource} instance. 1065 * <p> 1066 * If this thread is interrupted while waiting for another participant to release the transaction connection, Pyranid 1067 * restores the interrupt flag and throws {@link DatabaseException}. 1068 * 1069 * @param transaction the transaction in which to participate 1070 * @param transactionalOperation the operation that should participate in the transaction 1071 */ 1072 public void participate(@NonNull Transaction transaction, 1073 @NonNull TransactionalOperation transactionalOperation) { 1074 requireNonNull(transaction); 1075 requireNonNull(transactionalOperation); 1076 1077 participate(transaction, () -> { 1078 transactionalOperation.perform(); 1079 return Optional.empty(); 1080 }); 1081 } 1082 1083 /** 1084 * Performs an operation in the context of a pre-existing transaction, optionally returning a value. 1085 * <p> 1086 * No commit or rollback on the transaction will occur when {@code transactionalOperation} completes. 1087 * <p> 1088 * However, if an exception bubbles out of {@code transactionalOperation}, the transaction will be marked as rollback-only. 1089 * <p> 1090 * The transaction must have been created by this {@link Database}, or by another {@link Database} using the same 1091 * {@link DataSource} instance. 1092 * <p> 1093 * If this thread is interrupted while waiting for another participant to release the transaction connection, Pyranid 1094 * restores the interrupt flag and throws {@link DatabaseException}. 1095 * 1096 * @param transaction the transaction in which to participate 1097 * @param transactionalOperation the operation that should participate in the transaction 1098 * @param <T> the type to be returned 1099 * @return the result of the transactional operation 1100 */ 1101 @NonNull 1102 public <T> Optional<T> participate(@NonNull Transaction transaction, 1103 @NonNull ReturningTransactionalOperation<T> transactionalOperation) { 1104 requireNonNull(transaction); 1105 requireNonNull(transactionalOperation); 1106 1107 if (!isTransactionOwnedByThisDatabase(transaction)) 1108 throw wrongDatabaseTransactionException(transaction); 1109 1110 if (transaction.isCompleted()) 1111 throw new IllegalStateException(format("Transaction %s has already completed and cannot participate", transaction.id())); 1112 1113 Deque<Transaction> transactionStack = transactionStackForPush(); 1114 transactionStack.push(transaction); 1115 1116 try { 1117 Optional<T> returnValue = transactionalOperation.perform(); 1118 return returnValue == null ? Optional.empty() : returnValue; 1119 } catch (RuntimeException e) { 1120 setRollbackOnlyAfterParticipationFailure(transaction, e); 1121 restoreInterruptIfNeeded(e); 1122 throw e; 1123 } catch (Error e) { 1124 setRollbackOnlyAfterParticipationFailure(transaction, e); 1125 restoreInterruptIfNeeded(e); 1126 throw e; 1127 } catch (Throwable t) { 1128 RuntimeException wrapped = new RuntimeException(t); 1129 setRollbackOnlyAfterParticipationFailure(transaction, wrapped); 1130 restoreInterruptIfNeeded(t); 1131 throw wrapped; 1132 } finally { 1133 try { 1134 transactionStack.pop(); 1135 } finally { 1136 if (transactionStack.isEmpty()) 1137 TRANSACTION_STACK_HOLDER.remove(); 1138 } 1139 } 1140 } 1141 1142 private void setRollbackOnlyAfterParticipationFailure(@NonNull Transaction transaction, 1143 @NonNull Throwable primary) { 1144 requireNonNull(transaction); 1145 requireNonNull(primary); 1146 1147 try { 1148 transaction.setRollbackOnly(true); 1149 } catch (IllegalStateException e) { 1150 primary.addSuppressed(e); 1151 } 1152 } 1153 1154 /** 1155 * Creates a fluent builder for executing SQL. 1156 * <p> 1157 * Named parameters use the {@code :paramName} syntax and are bound via {@link Query#bind(String, Object)}. 1158 * Positional parameters via {@code ?} are not supported. 1159 * Pyranid ignores parameter-looking text inside SQL string literals, quoted identifiers, comments, PostgreSQL 1160 * dollar-quoted strings, and SQL Server-style bracket-quoted identifiers. PostgreSQL JSONB/hstore {@code ?}, 1161 * {@code ?|}, and {@code ?&} operators are supported; when running against {@link DatabaseType#POSTGRESQL}, Pyranid 1162 * emits pgjdbc's escaped {@code ??} form automatically. Unterminated quotes and comments fail fast. 1163 * <p> 1164 * Example: 1165 * <pre>{@code 1166 * Optional<Employee> employee = database.query("SELECT * FROM employee WHERE id = :id") 1167 * .bind("id", 42) 1168 * .fetchObject(Employee.class); 1169 * }</pre> 1170 * 1171 * @param sql SQL containing {@code :paramName} placeholders 1172 * @return a fluent builder for binding parameters and executing 1173 * @since 4.0.0 1174 */ 1175 @NonNull 1176 public Query query(@NonNull String sql) { 1177 requireNonNull(sql); 1178 return new DefaultQuery(this, sql); 1179 } 1180 1181 /** 1182 * Performs a portable connectivity check using JDBC {@link Connection#isValid(int)}. 1183 * <p> 1184 * This method borrows a fresh connection from this database's {@link DataSource}, calls 1185 * {@link Connection#isValid(int)}, and closes the connection before returning. It does <strong>not</strong> 1186 * participate in an active Pyranid transaction, if one exists. 1187 * <p> 1188 * JDBC accepts timeout values in whole seconds. Positive sub-second durations are rounded up to one second; 1189 * {@link Duration#ZERO} passes a timeout of {@code 0} to the driver. 1190 * 1191 * @param timeout maximum time to wait for driver validation 1192 * @throws IllegalArgumentException if {@code timeout} is negative or too large for JDBC's integer-second timeout 1193 * @throws DatabaseException if connection acquisition fails, validation throws, or the driver reports the 1194 * connection is not valid 1195 * @since 4.2.0 1196 */ 1197 public void performHealthCheck(@NonNull Duration timeout) { 1198 requireNonNull(timeout); 1199 int timeoutSeconds = healthCheckTimeoutSeconds(timeout); 1200 1201 performRawConnectionOperation(connection -> { 1202 boolean valid; 1203 1204 try { 1205 valid = connection.isValid(timeoutSeconds); 1206 } catch (SQLException e) { 1207 throw new DatabaseException("Unable to perform database health check", e); 1208 } 1209 1210 if (!valid) 1211 throw new DatabaseException("Database health check failed: connection is not valid"); 1212 1213 return Optional.empty(); 1214 }, false); 1215 } 1216 1217 private static void restoreInterruptIfNeeded(@NonNull Throwable throwable) { 1218 requireNonNull(throwable); 1219 1220 Throwable current = throwable; 1221 1222 while (current != null) { 1223 if (current instanceof InterruptedException) { 1224 Thread.currentThread().interrupt(); 1225 return; 1226 } 1227 1228 current = current.getCause(); 1229 } 1230 } 1231 1232 private static void lockInterruptibly(@NonNull ReentrantLock lock, 1233 @NonNull String operation) { 1234 requireNonNull(lock); 1235 requireNonNull(operation); 1236 1237 try { 1238 lock.lockInterruptibly(); 1239 } catch (InterruptedException e) { 1240 Thread.currentThread().interrupt(); 1241 throw new DatabaseException(format("Interrupted while waiting to %s", operation), e); 1242 } 1243 } 1244 1245 @Nullable 1246 private static Object unwrapOptionalValue(@Nullable Object value) { 1247 if (value == null) 1248 return null; 1249 1250 if (value instanceof Optional<?> optional) 1251 return optional.orElse(null); 1252 if (value instanceof OptionalInt optionalInt) 1253 return optionalInt.isPresent() ? optionalInt.getAsInt() : null; 1254 if (value instanceof OptionalLong optionalLong) 1255 return optionalLong.isPresent() ? optionalLong.getAsLong() : null; 1256 if (value instanceof OptionalDouble optionalDouble) 1257 return optionalDouble.isPresent() ? optionalDouble.getAsDouble() : null; 1258 1259 return value; 1260 } 1261 1262 @Nullable 1263 private static Throwable closeStatementContextResources(@NonNull StatementContext<?> statementContext, 1264 @Nullable Throwable cleanupFailure) { 1265 requireNonNull(statementContext); 1266 1267 Queue<AutoCloseable> cleanupOperations = statementContext.getCleanupOperations(); 1268 AutoCloseable cleanupOperation; 1269 1270 while ((cleanupOperation = cleanupOperations.poll()) != null) { 1271 try { 1272 cleanupOperation.close(); 1273 } catch (Throwable cleanupException) { 1274 if (cleanupFailure == null) 1275 cleanupFailure = cleanupException; 1276 else 1277 cleanupFailure.addSuppressed(cleanupException); 1278 } 1279 } 1280 1281 return cleanupFailure; 1282 } 1283 1284 private static boolean isUnsupportedSqlFeature(@NonNull SQLException e) { 1285 requireNonNull(e); 1286 1287 String sqlState = e.getSQLState(); 1288 if (sqlState != null) { 1289 if (sqlState.startsWith("0A") || "HYC00".equals(sqlState)) 1290 return true; 1291 } 1292 1293 Throwable cause = e.getCause(); 1294 if (cause instanceof SQLFeatureNotSupportedException 1295 || cause instanceof UnsupportedOperationException 1296 || cause instanceof AbstractMethodError) 1297 return true; 1298 1299 String message = e.getMessage(); 1300 if (message == null) 1301 return false; 1302 1303 String lower = message.toLowerCase(Locale.ROOT); 1304 return lower.contains("not supported") 1305 || lower.contains("unsupported") 1306 || lower.contains("not implemented") 1307 || lower.contains("feature not supported"); 1308 } 1309 1310 @Nullable 1311 private static Duration validateQueryTimeout(@Nullable Duration queryTimeout) { 1312 if (queryTimeout != null) { 1313 if (queryTimeout.isNegative()) 1314 throw new IllegalArgumentException("queryTimeout must be >= 0"); 1315 1316 queryTimeoutSeconds(queryTimeout); 1317 } 1318 1319 return queryTimeout; 1320 } 1321 1322 @Nullable 1323 private static Integer validateNonNegativeStatementSetting(@NonNull String name, 1324 @Nullable Integer value) { 1325 requireNonNull(name); 1326 1327 if (value != null && value < 0) 1328 throw new IllegalArgumentException(format("%s must be >= 0", name)); 1329 1330 return value; 1331 } 1332 1333 @Nullable 1334 private static Integer validatePositiveQuerySetting(@NonNull String name, 1335 @Nullable Integer value) { 1336 requireNonNull(name); 1337 1338 if (value != null && value <= 0) 1339 throw new IllegalArgumentException(format("%s must be > 0", name)); 1340 1341 return value; 1342 } 1343 1344 private static int queryTimeoutSeconds(@NonNull Duration queryTimeout) { 1345 requireNonNull(queryTimeout); 1346 1347 long seconds = queryTimeout.getSeconds(); 1348 1349 if (queryTimeout.getNano() > 0) { 1350 if (seconds == Long.MAX_VALUE) 1351 throw new IllegalArgumentException(format("queryTimeout must be <= %s seconds", Integer.MAX_VALUE)); 1352 1353 ++seconds; 1354 } 1355 1356 if (seconds > Integer.MAX_VALUE) 1357 throw new IllegalArgumentException(format("queryTimeout must be <= %s seconds", Integer.MAX_VALUE)); 1358 1359 return (int) seconds; 1360 } 1361 1362 private static int healthCheckTimeoutSeconds(@NonNull Duration timeout) { 1363 requireNonNull(timeout); 1364 1365 if (timeout.isNegative()) 1366 throw new IllegalArgumentException("timeout must be >= 0"); 1367 1368 long seconds = timeout.getSeconds(); 1369 1370 if (timeout.getNano() > 0) { 1371 if (seconds == Long.MAX_VALUE) 1372 throw new IllegalArgumentException(format("timeout must be <= %s seconds", Integer.MAX_VALUE)); 1373 1374 ++seconds; 1375 } 1376 1377 if (seconds > Integer.MAX_VALUE) 1378 throw new IllegalArgumentException(format("timeout must be <= %s seconds", Integer.MAX_VALUE)); 1379 1380 return (int) seconds; 1381 } 1382 1383 @NonNull 1384 private ParsedSql getParsedSql(@NonNull String sql) { 1385 requireNonNull(sql); 1386 1387 if (this.parsedSqlCache == null) 1388 return parseNamedParameterSql(sql); 1389 1390 return this.parsedSqlCache.computeIfAbsent(sql, Database::parseNamedParameterSql); 1391 } 1392 1393 /** 1394 * Default internal implementation of {@link Query}. 1395 * <p> 1396 * This class is intended for use by a single thread. 1397 */ 1398 @NotThreadSafe 1399 private static final class DefaultQuery implements Query { 1400 @NonNull 1401 private final Database database; 1402 @NonNull 1403 private final String originalSql; 1404 @NonNull 1405 private final ParsedSql parsedSql; 1406 @NonNull 1407 private final List<String> sqlFragments; 1408 @NonNull 1409 private final List<String> parameterNames; 1410 @NonNull 1411 private final Set<String> distinctParameterNames; 1412 @NonNull 1413 private final Map<String, Object> bindings; 1414 @Nullable 1415 private PreparedStatementCustomizer preparedStatementCustomizer; 1416 @Nullable 1417 private Duration queryTimeout; 1418 @Nullable 1419 private Integer fetchSize; 1420 @Nullable 1421 private Integer maxRows; 1422 @Nullable 1423 private Integer batchChunkSize; 1424 @Nullable 1425 private Object id; 1426 1427 private DefaultQuery(@NonNull Database database, 1428 @NonNull String sql) { 1429 requireNonNull(database); 1430 requireNonNull(sql); 1431 1432 this.database = database; 1433 this.originalSql = sql; 1434 1435 ParsedSql parsedSql = database.getParsedSql(sql); 1436 this.parsedSql = parsedSql; 1437 this.sqlFragments = parsedSql.sqlFragments; 1438 this.parameterNames = parsedSql.parameterNames; 1439 this.distinctParameterNames = parsedSql.distinctParameterNames; 1440 1441 this.bindings = new LinkedHashMap<>(Math.max(8, this.distinctParameterNames.size())); 1442 this.preparedStatementCustomizer = null; 1443 this.queryTimeout = null; 1444 this.fetchSize = null; 1445 this.maxRows = null; 1446 this.batchChunkSize = null; 1447 } 1448 1449 @NonNull 1450 @Override 1451 public Query bind(@NonNull String name, 1452 @Nullable Object value) { 1453 requireNonNull(name); 1454 1455 if (!this.distinctParameterNames.contains(name)) 1456 throw new IllegalArgumentException(format("Unknown named parameter '%s' for SQL: %s", name, this.originalSql)); 1457 1458 this.bindings.put(name, value); 1459 return this; 1460 } 1461 1462 @NonNull 1463 @Override 1464 public Query bindAll(@NonNull Map<@NonNull String, @Nullable Object> parameters) { 1465 requireNonNull(parameters); 1466 1467 for (Map.Entry<@NonNull String, @Nullable Object> entry : parameters.entrySet()) 1468 bind(entry.getKey(), entry.getValue()); 1469 1470 return this; 1471 } 1472 1473 @NonNull 1474 @Override 1475 public Query id(@Nullable Object id) { 1476 this.id = id; 1477 return this; 1478 } 1479 1480 @NonNull 1481 @Override 1482 public Query queryTimeout(@Nullable Duration queryTimeout) { 1483 this.queryTimeout = validateQueryTimeout(queryTimeout); 1484 return this; 1485 } 1486 1487 @NonNull 1488 @Override 1489 public Query fetchSize(@Nullable Integer fetchSize) { 1490 this.fetchSize = validateNonNegativeStatementSetting("fetchSize", fetchSize); 1491 return this; 1492 } 1493 1494 @NonNull 1495 @Override 1496 public Query maxRows(@Nullable Integer maxRows) { 1497 this.maxRows = validateNonNegativeStatementSetting("maxRows", maxRows); 1498 return this; 1499 } 1500 1501 @NonNull 1502 @Override 1503 public Query batchChunkSize(@Nullable Integer batchChunkSize) { 1504 this.batchChunkSize = validatePositiveQuerySetting("batchChunkSize", batchChunkSize); 1505 return this; 1506 } 1507 1508 @NonNull 1509 @Override 1510 public Query customize(@NonNull PreparedStatementCustomizer preparedStatementCustomizer) { 1511 requireNonNull(preparedStatementCustomizer); 1512 this.preparedStatementCustomizer = preparedStatementCustomizer; 1513 1514 return this; 1515 } 1516 1517 @NonNull 1518 @Override 1519 public <T> Optional<T> fetchObject(@NonNull Class<T> resultType) { 1520 validateBatchChunkSizeNotSet(); 1521 requireNonNull(resultType); 1522 PreparedQuery preparedQuery = prepare(this.bindings); 1523 return this.database.queryForObject(preparedQuery.statement, resultType, effectivePreparedStatementCustomizer(), preparedQuery.parameters); 1524 } 1525 1526 @NonNull 1527 @Override 1528 public <T> List<@Nullable T> fetchList(@NonNull Class<T> resultType) { 1529 validateBatchChunkSizeNotSet(); 1530 requireNonNull(resultType); 1531 PreparedQuery preparedQuery = prepare(this.bindings); 1532 return this.database.queryForList(preparedQuery.statement, resultType, effectivePreparedStatementCustomizer(), preparedQuery.parameters); 1533 } 1534 1535 @Nullable 1536 @Override 1537 public <T, R> R fetchStream(@NonNull Class<T> resultType, 1538 @NonNull Function<Stream<@Nullable T>, R> streamFunction) { 1539 validateBatchChunkSizeNotSet(); 1540 requireNonNull(resultType); 1541 requireNonNull(streamFunction); 1542 PreparedQuery preparedQuery = prepare(this.bindings); 1543 return this.database.queryForStream(preparedQuery.statement, resultType, effectivePreparedStatementCustomizer(), 1544 this.fetchSize != null, streamFunction, preparedQuery.parameters); 1545 } 1546 1547 1548 @NonNull 1549 @Override 1550 public Long execute() { 1551 validateBatchChunkSizeNotSet(); 1552 PreparedQuery preparedQuery = prepare(this.bindings); 1553 return this.database.execute(preparedQuery.statement, effectivePreparedStatementCustomizer(), preparedQuery.parameters); 1554 } 1555 1556 @NonNull 1557 @Override 1558 public <T> Optional<T> executeReturningGeneratedKey(@NonNull Class<T> resultType) { 1559 validateBatchChunkSizeNotSet(); 1560 requireNonNull(resultType); 1561 PreparedQuery preparedQuery = prepare(this.bindings); 1562 return this.database.executeReturningGeneratedKey(preparedQuery.statement, resultType, 1563 effectivePreparedStatementCustomizer(), new String[0], preparedQuery.parameters); 1564 } 1565 1566 @NonNull 1567 @Override 1568 public <T> Optional<T> executeReturningGeneratedKey(@NonNull Class<T> resultType, 1569 @NonNull String @NonNull ... keyColumnNames) { 1570 validateBatchChunkSizeNotSet(); 1571 requireNonNull(resultType); 1572 PreparedQuery preparedQuery = prepare(this.bindings); 1573 return this.database.executeReturningGeneratedKey(preparedQuery.statement, resultType, 1574 effectivePreparedStatementCustomizer(), keyColumnNames, preparedQuery.parameters); 1575 } 1576 1577 @NonNull 1578 @Override 1579 public <T> List<@Nullable T> executeReturningGeneratedKeys(@NonNull Class<T> resultType) { 1580 validateBatchChunkSizeNotSet(); 1581 requireNonNull(resultType); 1582 PreparedQuery preparedQuery = prepare(this.bindings); 1583 return this.database.executeReturningGeneratedKeys(preparedQuery.statement, resultType, 1584 effectivePreparedStatementCustomizer(), new String[0], preparedQuery.parameters); 1585 } 1586 1587 @NonNull 1588 @Override 1589 public <T> List<@Nullable T> executeReturningGeneratedKeys(@NonNull Class<T> resultType, 1590 @NonNull String @NonNull ... keyColumnNames) { 1591 validateBatchChunkSizeNotSet(); 1592 requireNonNull(resultType); 1593 PreparedQuery preparedQuery = prepare(this.bindings); 1594 return this.database.executeReturningGeneratedKeys(preparedQuery.statement, resultType, 1595 effectivePreparedStatementCustomizer(), keyColumnNames, preparedQuery.parameters); 1596 } 1597 1598 @NonNull 1599 @Override 1600 public List<Long> executeBatch(@NonNull List<@NonNull Map<@NonNull String, @Nullable Object>> parameterGroups) { 1601 requireNonNull(parameterGroups); 1602 if (parameterGroups.isEmpty()) 1603 return List.of(); 1604 1605 List<List<Object>> parametersAsList = new ArrayList<>(parameterGroups.size()); 1606 Object statementId = this.id == null ? this.database.generateId() : this.id; 1607 Statement statement = null; 1608 String expandedSql = null; 1609 1610 for (Map<@NonNull String, @Nullable Object> parameterGroup : parameterGroups) { 1611 requireNonNull(parameterGroup); 1612 1613 for (String parameterName : parameterGroup.keySet()) 1614 if (!this.distinctParameterNames.contains(parameterName)) 1615 throw new IllegalArgumentException(format("Unknown named parameter '%s' for SQL: %s", parameterName, this.originalSql)); 1616 1617 Map<String, Object> mergedBindings; 1618 if (this.bindings.isEmpty()) { 1619 mergedBindings = parameterGroup; 1620 } else if (parameterGroup.isEmpty()) { 1621 mergedBindings = this.bindings; 1622 } else { 1623 Map<String, Object> combinedBindings = new LinkedHashMap<>(this.bindings); 1624 combinedBindings.putAll(parameterGroup); 1625 mergedBindings = combinedBindings; 1626 } 1627 1628 PreparedQuery preparedQuery = prepare(mergedBindings, statementId); 1629 1630 if (expandedSql == null) { 1631 expandedSql = preparedQuery.statement.getSql(); 1632 statement = preparedQuery.statement; 1633 } else if (!expandedSql.equals(preparedQuery.statement.getSql())) { 1634 throw new IllegalArgumentException(format( 1635 "Inconsistent SQL after expanding parameters for batch execution; ensure collection sizes are consistent. SQL: %s", 1636 this.originalSql)); 1637 } 1638 1639 parametersAsList.add(Arrays.asList(preparedQuery.parameters)); 1640 } 1641 1642 if (statement == null) 1643 statement = Statement.of(statementId, buildPlaceholderSql()); 1644 1645 return this.database.executeBatch(statement, parametersAsList, effectivePreparedStatementCustomizer(), this.batchChunkSize); 1646 } 1647 1648 @NonNull 1649 @Override 1650 public <T> Optional<T> executeForObject(@NonNull Class<T> resultType) { 1651 validateBatchChunkSizeNotSet(); 1652 requireNonNull(resultType); 1653 PreparedQuery preparedQuery = prepare(this.bindings); 1654 return this.database.executeForObject(preparedQuery.statement, resultType, effectivePreparedStatementCustomizer(), preparedQuery.parameters); 1655 } 1656 1657 @NonNull 1658 @Override 1659 public <T> List<@Nullable T> executeForList(@NonNull Class<T> resultType) { 1660 validateBatchChunkSizeNotSet(); 1661 requireNonNull(resultType); 1662 PreparedQuery preparedQuery = prepare(this.bindings); 1663 return this.database.executeForList(preparedQuery.statement, resultType, effectivePreparedStatementCustomizer(), preparedQuery.parameters); 1664 } 1665 1666 private void validateBatchChunkSizeNotSet() { 1667 if (this.batchChunkSize != null) 1668 throw new IllegalStateException("batchChunkSize applies only to executeBatch(...)"); 1669 } 1670 1671 @Nullable 1672 private PreparedStatementCustomizer effectivePreparedStatementCustomizer() { 1673 if (!this.database.hasDefaultPreparedStatementSettings() 1674 && !hasQueryPreparedStatementSettings() 1675 && this.preparedStatementCustomizer == null) 1676 return null; 1677 1678 return (statementContext, preparedStatement) -> { 1679 this.database.applyDefaultPreparedStatementSettings(preparedStatement); 1680 applyPreparedStatementSettings(preparedStatement, this.queryTimeout, this.fetchSize, this.maxRows); 1681 1682 if (this.preparedStatementCustomizer != null) 1683 this.preparedStatementCustomizer.customize(statementContext, preparedStatement); 1684 }; 1685 } 1686 1687 private boolean hasQueryPreparedStatementSettings() { 1688 return this.queryTimeout != null || this.fetchSize != null || this.maxRows != null; 1689 } 1690 1691 @NonNull 1692 private PreparedQuery prepare(@NonNull Map<String, Object> bindings) { 1693 Object statementId = this.id == null ? this.database.generateId() : this.id; 1694 return prepare(bindings, statementId); 1695 } 1696 1697 @NonNull 1698 private PreparedQuery prepare(@NonNull Map<String, Object> bindings, 1699 @NonNull Object statementId) { 1700 requireNonNull(bindings); 1701 requireNonNull(statementId); 1702 1703 List<String> sqlFragments = sqlFragmentsForDatabaseType(); 1704 1705 if (this.parameterNames.isEmpty()) 1706 return new PreparedQuery(Statement.of(statementId, sqlFragments.get(0)), new Object[0]); 1707 1708 StringBuilder sql = new StringBuilder(this.originalSql.length() + this.parameterNames.size() * 2); 1709 List<String> missingParameterNames = null; 1710 List<Object> parameters = new ArrayList<>(this.parameterNames.size()); 1711 1712 for (int i = 0; i < this.parameterNames.size(); ++i) { 1713 String parameterName = this.parameterNames.get(i); 1714 sql.append(sqlFragments.get(i)); 1715 1716 if (!bindings.containsKey(parameterName)) { 1717 if (missingParameterNames == null) 1718 missingParameterNames = new ArrayList<>(); 1719 1720 missingParameterNames.add(parameterName); 1721 sql.append('?'); 1722 continue; 1723 } 1724 1725 SecureParameterSupport.SecureParameterUnwrapResult secureParameterUnwrapResult = 1726 SecureParameterSupport.unwrapSecureAndOptionalParameterWithMetadata(bindings.get(parameterName)); 1727 SecureParameter secureParameter = secureParameterUnwrapResult.secureParameter(); 1728 Object value = secureParameterUnwrapResult.value(); 1729 1730 if (value instanceof InListParameter inListParameter) { 1731 Object[] elements = inListParameter.getElements(); 1732 1733 if (elements.length == 0) 1734 throw new IllegalArgumentException(format("IN-list parameter '%s' for SQL: %s is empty", parameterName, this.originalSql)); 1735 1736 appendPlaceholders(sql, elements.length); 1737 1738 for (int j = 0; j < elements.length; ++j) { 1739 Object element = unwrapOptionalValue(elements[j]); 1740 1741 if (element == null) 1742 throw new IllegalArgumentException(format( 1743 "IN-list parameter '%s' for SQL: %s contains null element at index %d. " 1744 + "SQL IN does not match NULL values; use an explicit IS NULL predicate instead.", 1745 parameterName, this.originalSql, j)); 1746 1747 parameters.add(secureParameter == null ? element : Parameters.secure(element, SecureParameterSupport.maskOf(secureParameter))); 1748 } 1749 } else if (value instanceof Collection<?>) { 1750 throw new IllegalArgumentException(format( 1751 "Collection parameter '%s' for SQL: %s must be wrapped with %s.inList(...) or %s.listOf/%s.setOf(...)", 1752 parameterName, this.originalSql, 1753 Parameters.class.getSimpleName(), 1754 Parameters.class.getSimpleName(), Parameters.class.getSimpleName())); 1755 } else if (value != null && value.getClass().isArray() && !(value instanceof byte[])) { 1756 throw new IllegalArgumentException(format( 1757 "Array parameter '%s' for SQL: %s must be wrapped with %s.inList(...), %s.sqlArrayOf(...), or %s.arrayOf(Class, ...)", 1758 parameterName, this.originalSql, 1759 Parameters.class.getSimpleName(), Parameters.class.getSimpleName(), Parameters.class.getSimpleName())); 1760 } else { 1761 sql.append('?'); 1762 parameters.add(secureParameter == null ? value : secureParameter); 1763 } 1764 } 1765 1766 sql.append(sqlFragments.get(sqlFragments.size() - 1)); 1767 1768 if (missingParameterNames != null) 1769 throw new IllegalArgumentException(format("Missing required named parameters %s for SQL: %s", missingParameterNames, this.originalSql)); 1770 1771 return new PreparedQuery(Statement.of(statementId, sql.toString()), parameters.toArray()); 1772 } 1773 1774 @NonNull 1775 private String buildPlaceholderSql() { 1776 List<String> sqlFragments = sqlFragmentsForDatabaseType(); 1777 1778 if (this.parameterNames.isEmpty()) 1779 return sqlFragments.get(0); 1780 1781 StringBuilder sql = new StringBuilder(this.originalSql.length() + this.parameterNames.size() * 2); 1782 1783 for (int i = 0; i < this.parameterNames.size(); ++i) 1784 sql.append(sqlFragments.get(i)).append('?'); 1785 1786 sql.append(sqlFragments.get(sqlFragments.size() - 1)); 1787 return sql.toString(); 1788 } 1789 1790 @NonNull 1791 private List<String> sqlFragmentsForDatabaseType() { 1792 if (!this.parsedSql.hasQuestionMarkOperators) 1793 return this.sqlFragments; 1794 1795 return this.database.getDatabaseDialect().sqlFragmentsForOperators( 1796 this.parsedSql.hasQuestionMarkOperators, 1797 this.sqlFragments, 1798 this.parsedSql.questionMarkOperatorFragmentIndexes); 1799 } 1800 1801 private void appendPlaceholders(@NonNull StringBuilder sql, 1802 int count) { 1803 requireNonNull(sql); 1804 1805 for (int i = 0; i < count; ++i) { 1806 if (i > 0) 1807 sql.append(", "); 1808 sql.append('?'); 1809 } 1810 } 1811 1812 private static final class PreparedQuery { 1813 @NonNull 1814 private final Statement statement; 1815 @NonNull 1816 private final Object @NonNull [] parameters; 1817 1818 private PreparedQuery(@NonNull Statement statement, 1819 Object @NonNull [] parameters) { 1820 this.statement = requireNonNull(statement); 1821 this.parameters = requireNonNull(parameters); 1822 } 1823 } 1824 1825 } 1826 1827 static final class ParsedSql { 1828 @NonNull 1829 private final List<String> sqlFragments; 1830 @NonNull 1831 private final List<@NonNull List<@NonNull Integer>> questionMarkOperatorFragmentIndexes; 1832 private final boolean hasQuestionMarkOperators; 1833 @NonNull 1834 private final List<String> parameterNames; 1835 @NonNull 1836 private final Set<String> distinctParameterNames; 1837 1838 private ParsedSql(@NonNull List<String> sqlFragments, 1839 @NonNull List<@NonNull List<@NonNull Integer>> questionMarkOperatorFragmentIndexes, 1840 @NonNull List<String> parameterNames, 1841 @NonNull Set<String> distinctParameterNames) { 1842 requireNonNull(sqlFragments); 1843 requireNonNull(questionMarkOperatorFragmentIndexes); 1844 requireNonNull(parameterNames); 1845 requireNonNull(distinctParameterNames); 1846 1847 this.sqlFragments = sqlFragments; 1848 this.questionMarkOperatorFragmentIndexes = questionMarkOperatorFragmentIndexes.stream() 1849 .map(List::copyOf) 1850 .toList(); 1851 this.hasQuestionMarkOperators = questionMarkOperatorFragmentIndexes.stream().anyMatch(indexes -> !indexes.isEmpty()); 1852 this.parameterNames = parameterNames; 1853 this.distinctParameterNames = distinctParameterNames; 1854 } 1855 } 1856 1857 @NonNull 1858 static ParsedSql parseNamedParameterSql(@NonNull String sql) { 1859 requireNonNull(sql); 1860 1861 List<String> sqlFragments = new ArrayList<>(); 1862 StringBuilder sqlFragment = new StringBuilder(sql.length()); 1863 List<List<Integer>> questionMarkOperatorFragmentIndexes = new ArrayList<>(); 1864 List<Integer> currentQuestionMarkOperatorIndexes = new ArrayList<>(); 1865 List<String> parameterNames = new ArrayList<>(); 1866 Set<String> distinctParameterNames = new HashSet<>(); 1867 1868 boolean inSingleQuote = false; 1869 boolean inSingleQuoteEscapesBackslash = false; 1870 int singleQuoteStartIndex = -1; 1871 boolean inDoubleQuote = false; 1872 int doubleQuoteStartIndex = -1; 1873 boolean inBacktickQuote = false; 1874 int backtickQuoteStartIndex = -1; 1875 boolean inBracketQuote = false; 1876 int bracketQuoteStartIndex = -1; 1877 boolean inLineComment = false; 1878 int blockCommentDepth = 0; 1879 int blockCommentStartIndex = -1; 1880 String dollarQuoteDelimiter = null; 1881 int dollarQuoteStartIndex = -1; 1882 int previousMeaningfulIndex = -1; 1883 1884 for (int i = 0; i < sql.length(); ) { 1885 if (dollarQuoteDelimiter != null) { 1886 if (sql.startsWith(dollarQuoteDelimiter, i)) { 1887 sqlFragment.append(dollarQuoteDelimiter); 1888 previousMeaningfulIndex = i + dollarQuoteDelimiter.length() - 1; 1889 i += dollarQuoteDelimiter.length(); 1890 dollarQuoteDelimiter = null; 1891 dollarQuoteStartIndex = -1; 1892 } else { 1893 sqlFragment.append(sql.charAt(i)); 1894 ++i; 1895 } 1896 1897 continue; 1898 } 1899 1900 char c = sql.charAt(i); 1901 1902 if (inLineComment) { 1903 sqlFragment.append(c); 1904 ++i; 1905 1906 if (c == '\n' || c == '\r') 1907 inLineComment = false; 1908 1909 continue; 1910 } 1911 1912 if (blockCommentDepth > 0) { 1913 if (c == '/' && i + 1 < sql.length() && sql.charAt(i + 1) == '*') { 1914 sqlFragment.append("/*"); 1915 i += 2; 1916 ++blockCommentDepth; 1917 } else if (c == '*' && i + 1 < sql.length() && sql.charAt(i + 1) == '/') { 1918 sqlFragment.append("*/"); 1919 i += 2; 1920 --blockCommentDepth; 1921 if (blockCommentDepth == 0) 1922 blockCommentStartIndex = -1; 1923 } else { 1924 sqlFragment.append(c); 1925 ++i; 1926 } 1927 1928 continue; 1929 } 1930 1931 if (inSingleQuote) { 1932 sqlFragment.append(c); 1933 1934 if (inSingleQuoteEscapesBackslash && c == '\\' && i + 1 < sql.length()) { 1935 sqlFragment.append(sql.charAt(i + 1)); 1936 i += 2; 1937 continue; 1938 } 1939 1940 if (c == '\'') { 1941 // Escaped quote: '' 1942 if (i + 1 < sql.length() && sql.charAt(i + 1) == '\'') { 1943 sqlFragment.append('\''); 1944 i += 2; 1945 continue; 1946 } 1947 1948 inSingleQuote = false; 1949 inSingleQuoteEscapesBackslash = false; 1950 singleQuoteStartIndex = -1; 1951 previousMeaningfulIndex = i; 1952 } 1953 1954 ++i; 1955 continue; 1956 } 1957 1958 if (inDoubleQuote) { 1959 sqlFragment.append(c); 1960 1961 if (c == '"') { 1962 // Escaped quote: "" 1963 if (i + 1 < sql.length() && sql.charAt(i + 1) == '"') { 1964 sqlFragment.append('"'); 1965 i += 2; 1966 continue; 1967 } 1968 1969 inDoubleQuote = false; 1970 doubleQuoteStartIndex = -1; 1971 previousMeaningfulIndex = i; 1972 } 1973 1974 ++i; 1975 continue; 1976 } 1977 1978 if (inBacktickQuote) { 1979 sqlFragment.append(c); 1980 1981 if (c == '`') { 1982 inBacktickQuote = false; 1983 backtickQuoteStartIndex = -1; 1984 previousMeaningfulIndex = i; 1985 } 1986 1987 ++i; 1988 continue; 1989 } 1990 1991 if (inBracketQuote) { 1992 sqlFragment.append(c); 1993 1994 if (c == ']' && i + 1 < sql.length() && sql.charAt(i + 1) == ']') { 1995 sqlFragment.append(']'); 1996 i += 2; 1997 continue; 1998 } 1999 2000 if (c == ']') { 2001 inBracketQuote = false; 2002 bracketQuoteStartIndex = -1; 2003 previousMeaningfulIndex = i; 2004 } 2005 2006 ++i; 2007 continue; 2008 } 2009 2010 // Not inside string/comment 2011 if (c == '-' && i + 1 < sql.length() && sql.charAt(i + 1) == '-') { 2012 sqlFragment.append("--"); 2013 i += 2; 2014 inLineComment = true; 2015 continue; 2016 } 2017 2018 if (c == '/' && i + 1 < sql.length() && sql.charAt(i + 1) == '*') { 2019 sqlFragment.append("/*"); 2020 i += 2; 2021 blockCommentDepth = 1; 2022 blockCommentStartIndex = i - 2; 2023 continue; 2024 } 2025 2026 if ((c == 'U' || c == 'u') && !isIdentifierContinuation(sql, i) 2027 && i + 2 < sql.length() && sql.charAt(i + 1) == '&' && sql.charAt(i + 2) == '\'') { 2028 inSingleQuote = true; 2029 inSingleQuoteEscapesBackslash = true; 2030 singleQuoteStartIndex = i; 2031 sqlFragment.append(c).append("&'"); 2032 i += 3; 2033 continue; 2034 } 2035 2036 if ((c == 'E' || c == 'e') && !isIdentifierContinuation(sql, i) 2037 && i + 1 < sql.length() && sql.charAt(i + 1) == '\'') { 2038 inSingleQuote = true; 2039 inSingleQuoteEscapesBackslash = true; 2040 singleQuoteStartIndex = i; 2041 sqlFragment.append(c).append('\''); 2042 i += 2; 2043 continue; 2044 } 2045 2046 if (c == '\'') { 2047 inSingleQuote = true; 2048 inSingleQuoteEscapesBackslash = false; 2049 singleQuoteStartIndex = i; 2050 sqlFragment.append(c); 2051 ++i; 2052 continue; 2053 } 2054 2055 if (c == '"') { 2056 inDoubleQuote = true; 2057 doubleQuoteStartIndex = i; 2058 sqlFragment.append(c); 2059 ++i; 2060 continue; 2061 } 2062 2063 if (c == '`') { 2064 inBacktickQuote = true; 2065 backtickQuoteStartIndex = i; 2066 sqlFragment.append(c); 2067 ++i; 2068 continue; 2069 } 2070 2071 if (c == '[' && !isBracketSubscriptStart(sql, i)) { 2072 inBracketQuote = true; 2073 bracketQuoteStartIndex = i; 2074 sqlFragment.append(c); 2075 ++i; 2076 continue; 2077 } 2078 2079 if (c == '$' && !isIdentifierContinuation(sql, i)) { 2080 String delimiter = parseDollarQuoteDelimiter(sql, i); 2081 2082 if (delimiter != null) { 2083 sqlFragment.append(delimiter); 2084 i += delimiter.length(); 2085 dollarQuoteDelimiter = delimiter; 2086 dollarQuoteStartIndex = i - delimiter.length(); 2087 continue; 2088 } 2089 } 2090 2091 if (c == '?') { 2092 if (isAllowedQuestionMarkOperator(sql, i, previousMeaningfulIndex)) { 2093 currentQuestionMarkOperatorIndexes.add(sqlFragment.length()); 2094 sqlFragment.append(c); 2095 ++i; 2096 continue; 2097 } 2098 2099 throw new IllegalArgumentException(format("Positional parameters ('?') are not supported. Use named parameters (e.g. ':id') and %s#bind. SQL: %s", 2100 Query.class.getSimpleName(), sql)); 2101 } 2102 2103 if (c == ':' && i + 1 < sql.length() && sql.charAt(i + 1) == ':') { 2104 // Postgres type-cast operator (::), do not treat second ':' as a parameter prefix. 2105 sqlFragment.append("::"); 2106 i += 2; 2107 continue; 2108 } 2109 2110 if (c == ':' && i + 1 < sql.length() && Character.isJavaIdentifierStart(sql.charAt(i + 1))) { 2111 int nameStartIndex = i + 1; 2112 int nameEndIndex = nameStartIndex + 1; 2113 2114 while (nameEndIndex < sql.length() && Character.isJavaIdentifierPart(sql.charAt(nameEndIndex))) 2115 ++nameEndIndex; 2116 2117 String parameterName = sql.substring(nameStartIndex, nameEndIndex); 2118 parameterNames.add(parameterName); 2119 distinctParameterNames.add(parameterName); 2120 sqlFragments.add(sqlFragment.toString()); 2121 questionMarkOperatorFragmentIndexes.add(List.copyOf(currentQuestionMarkOperatorIndexes)); 2122 currentQuestionMarkOperatorIndexes.clear(); 2123 sqlFragment.setLength(0); 2124 i = nameEndIndex; 2125 previousMeaningfulIndex = nameEndIndex - 1; 2126 continue; 2127 } 2128 2129 sqlFragment.append(c); 2130 if (!Character.isWhitespace(c)) 2131 previousMeaningfulIndex = i; 2132 ++i; 2133 } 2134 2135 validateParserTerminalState(sql, inSingleQuote, singleQuoteStartIndex, inDoubleQuote, doubleQuoteStartIndex, 2136 inBacktickQuote, backtickQuoteStartIndex, inBracketQuote, bracketQuoteStartIndex, 2137 blockCommentDepth, blockCommentStartIndex, dollarQuoteDelimiter, dollarQuoteStartIndex); 2138 2139 sqlFragments.add(sqlFragment.toString()); 2140 questionMarkOperatorFragmentIndexes.add(List.copyOf(currentQuestionMarkOperatorIndexes)); 2141 2142 return new ParsedSql(List.copyOf(sqlFragments), List.copyOf(questionMarkOperatorFragmentIndexes), 2143 List.copyOf(parameterNames), Set.copyOf(distinctParameterNames)); 2144 } 2145 2146 @Nullable 2147 private static String parseDollarQuoteDelimiter(@NonNull String sql, 2148 int startIndex) { 2149 requireNonNull(sql); 2150 2151 if (startIndex < 0 || startIndex >= sql.length()) 2152 return null; 2153 2154 if (sql.charAt(startIndex) != '$') 2155 return null; 2156 2157 int i = startIndex + 1; 2158 2159 if (i >= sql.length()) 2160 return null; 2161 2162 char firstTagCharacter = sql.charAt(i); 2163 2164 if (firstTagCharacter == '$') 2165 return "$$"; 2166 2167 if (!isDollarQuoteTagStart(firstTagCharacter)) 2168 return null; 2169 2170 ++i; 2171 2172 while (i < sql.length()) { 2173 char c = sql.charAt(i); 2174 2175 if (c == '$') 2176 return sql.substring(startIndex, i + 1); 2177 2178 if (!isDollarQuoteTagPart(c)) 2179 return null; 2180 2181 ++i; 2182 } 2183 2184 return null; 2185 } 2186 2187 private static boolean isDollarQuoteTagStart(char character) { 2188 return Character.isLetter(character) || character == '_'; 2189 } 2190 2191 private static boolean isDollarQuoteTagPart(char character) { 2192 return Character.isLetterOrDigit(character) || character == '_'; 2193 } 2194 2195 private static boolean isIdentifierContinuation(@NonNull String sql, 2196 int startIndex) { 2197 requireNonNull(sql); 2198 2199 if (startIndex <= 0) 2200 return false; 2201 2202 return Character.isJavaIdentifierPart(sql.charAt(startIndex - 1)); 2203 } 2204 2205 private static boolean isBracketSubscriptStart(@NonNull String sql, 2206 int startIndex) { 2207 requireNonNull(sql); 2208 2209 if (startIndex <= 0) 2210 return false; 2211 2212 char previousChar = sql.charAt(startIndex - 1); 2213 return Character.isJavaIdentifierPart(previousChar) 2214 || previousChar == ')' 2215 || previousChar == ']' 2216 || previousChar == '"'; 2217 } 2218 2219 private static void validateParserTerminalState(@NonNull String sql, 2220 boolean inSingleQuote, 2221 int singleQuoteStartIndex, 2222 boolean inDoubleQuote, 2223 int doubleQuoteStartIndex, 2224 boolean inBacktickQuote, 2225 int backtickQuoteStartIndex, 2226 boolean inBracketQuote, 2227 int bracketQuoteStartIndex, 2228 int blockCommentDepth, 2229 int blockCommentStartIndex, 2230 @Nullable String dollarQuoteDelimiter, 2231 int dollarQuoteStartIndex) { 2232 requireNonNull(sql); 2233 2234 if (inSingleQuote) 2235 throw unterminatedSqlConstructException("single-quoted string", singleQuoteStartIndex, sql); 2236 if (inDoubleQuote) 2237 throw unterminatedSqlConstructException("double-quoted identifier", doubleQuoteStartIndex, sql); 2238 if (inBacktickQuote) 2239 throw unterminatedSqlConstructException("backtick-quoted identifier", backtickQuoteStartIndex, sql); 2240 if (inBracketQuote) 2241 throw unterminatedSqlConstructException("bracket-quoted identifier", bracketQuoteStartIndex, sql); 2242 if (blockCommentDepth > 0) 2243 throw unterminatedSqlConstructException("block comment", blockCommentStartIndex, sql); 2244 if (dollarQuoteDelimiter != null) 2245 throw unterminatedSqlConstructException(format("dollar-quoted string %s", dollarQuoteDelimiter), dollarQuoteStartIndex, sql); 2246 } 2247 2248 @NonNull 2249 private static IllegalArgumentException unterminatedSqlConstructException(@NonNull String construct, 2250 int startIndex, 2251 @NonNull String sql) { 2252 requireNonNull(construct); 2253 requireNonNull(sql); 2254 return new IllegalArgumentException(format("Unterminated %s starting at index %s. SQL: %s", construct, startIndex, sql)); 2255 } 2256 2257 @NonNull 2258 private static final Set<@NonNull String> QUESTION_MARK_PREFIX_KEYWORDS = Set.of( 2259 "SELECT", "WHERE", "AND", "OR", "ON", "HAVING", "WHEN", "THEN", "ELSE", "IN", 2260 "VALUES", "SET", "RETURNING", "USING", "LIKE", "BETWEEN", "IS", "NOT", "NULL", 2261 "JOIN", "FROM" 2262 ); 2263 2264 @NonNull 2265 private static final Set<@NonNull String> QUESTION_MARK_SUFFIX_KEYWORDS = Set.of( 2266 "FROM", "WHERE", "AND", "OR", "GROUP", "ORDER", "HAVING", "LIMIT", "OFFSET", 2267 "UNION", "EXCEPT", "INTERSECT", "RETURNING", "JOIN", "ON" 2268 ); 2269 2270 private static boolean isAllowedQuestionMarkOperator(@NonNull String sql, 2271 int questionMarkIndex, 2272 int previousMeaningfulIndex) { 2273 requireNonNull(sql); 2274 2275 int previousIndex = previousMeaningfulIndex; 2276 int nextIndex = nextNonWhitespaceIndex(sql, questionMarkIndex + 1); 2277 2278 if (previousIndex < 0 || nextIndex < 0) 2279 return false; 2280 2281 char previousChar = sql.charAt(previousIndex); 2282 char nextChar = sql.charAt(nextIndex); 2283 2284 if (isOperatorBeforeQuestionMark(previousChar)) 2285 return false; 2286 2287 if (isTerminatorAfterQuestionMark(nextChar)) 2288 return false; 2289 2290 if (questionMarkIndex + 1 < sql.length()) { 2291 char immediateNextChar = sql.charAt(questionMarkIndex + 1); 2292 if (immediateNextChar == '|' || immediateNextChar == '&') { 2293 if (questionMarkIndex + 2 < sql.length() && sql.charAt(questionMarkIndex + 2) == immediateNextChar) 2294 return false; 2295 2296 String previousKeyword = keywordBefore(sql, previousIndex); 2297 if (previousKeyword != null && QUESTION_MARK_PREFIX_KEYWORDS.contains(previousKeyword) 2298 && !isNamedParameterKeywordBefore(sql, previousIndex, previousKeyword)) 2299 return false; 2300 2301 String nextKeyword = keywordAfter(sql, nextIndex); 2302 if (nextKeyword != null && QUESTION_MARK_SUFFIX_KEYWORDS.contains(nextKeyword)) 2303 return false; 2304 2305 return true; 2306 } 2307 } 2308 2309 String previousKeyword = keywordBefore(sql, previousIndex); 2310 if (previousKeyword != null && QUESTION_MARK_PREFIX_KEYWORDS.contains(previousKeyword)) 2311 return false; 2312 2313 String nextKeyword = keywordAfter(sql, nextIndex); 2314 if (nextKeyword != null && QUESTION_MARK_SUFFIX_KEYWORDS.contains(nextKeyword)) 2315 return false; 2316 2317 return true; 2318 } 2319 2320 private static boolean isOperatorBeforeQuestionMark(char c) { 2321 return switch (c) { 2322 case '=', '<', '>', '!', '+', '-', '*', '/', '%', ',', '(' -> true; 2323 default -> false; 2324 }; 2325 } 2326 2327 private static boolean isTerminatorAfterQuestionMark(char c) { 2328 return switch (c) { 2329 case ')', ',', ';' -> true; 2330 default -> false; 2331 }; 2332 } 2333 2334 private static int nextNonWhitespaceIndex(@NonNull String sql, 2335 int startIndex) { 2336 for (int i = startIndex; i < sql.length(); i++) 2337 if (!Character.isWhitespace(sql.charAt(i))) 2338 return i; 2339 return -1; 2340 } 2341 2342 @Nullable 2343 private static String keywordBefore(@NonNull String sql, 2344 int index) { 2345 char c = sql.charAt(index); 2346 if (!Character.isJavaIdentifierPart(c)) 2347 return null; 2348 2349 int endIndex = index + 1; 2350 int startIndex = index; 2351 while (startIndex >= 0 && Character.isJavaIdentifierPart(sql.charAt(startIndex))) 2352 --startIndex; 2353 2354 return sql.substring(startIndex + 1, endIndex).toUpperCase(Locale.ROOT); 2355 } 2356 2357 private static boolean isNamedParameterKeywordBefore(@NonNull String sql, 2358 int keywordEndIndex, 2359 @NonNull String keyword) { 2360 requireNonNull(sql); 2361 requireNonNull(keyword); 2362 2363 int startIndex = keywordEndIndex - keyword.length() + 1; 2364 return startIndex > 0 && sql.charAt(startIndex - 1) == ':'; 2365 } 2366 2367 @Nullable 2368 private static String keywordAfter(@NonNull String sql, 2369 int index) { 2370 char c = sql.charAt(index); 2371 if (!Character.isJavaIdentifierPart(c)) 2372 return null; 2373 2374 int endIndex = index + 1; 2375 while (endIndex < sql.length() && Character.isJavaIdentifierPart(sql.charAt(endIndex))) 2376 ++endIndex; 2377 2378 return sql.substring(index, endIndex).toUpperCase(Locale.ROOT); 2379 } 2380 2381 /** 2382 * Performs a SQL query that is expected to return 0 or 1 result rows. 2383 * 2384 * @param sql the SQL query to execute 2385 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 2386 * @param parameters {@link PreparedStatement} parameters, if any 2387 * @param <T> the type to be returned 2388 * @return a single result (or no result) 2389 * @throws DatabaseException if > 1 row is returned 2390 */ 2391 @NonNull 2392 private <T> Optional<T> queryForObject(@NonNull String sql, 2393 @NonNull Class<T> resultSetRowType, 2394 Object @Nullable ... parameters) { 2395 requireNonNull(sql); 2396 requireNonNull(resultSetRowType); 2397 2398 return queryForObject(Statement.of(generateId(), sql), resultSetRowType, parameters); 2399 } 2400 2401 /** 2402 * Performs a SQL query that is expected to return 0 or 1 result rows. 2403 * 2404 * @param statement the SQL statement to execute 2405 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 2406 * @param parameters {@link PreparedStatement} parameters, if any 2407 * @param <T> the type to be returned 2408 * @return a single result (or no result) 2409 * @throws DatabaseException if > 1 row is returned 2410 */ 2411 private <T> Optional<T> queryForObject(@NonNull Statement statement, 2412 @NonNull Class<T> resultSetRowType, 2413 Object @Nullable ... parameters) { 2414 requireNonNull(statement); 2415 requireNonNull(resultSetRowType); 2416 2417 return queryForObject(statement, resultSetRowType, null, parameters); 2418 } 2419 2420 private <T> Optional<T> queryForObject(@NonNull Statement statement, 2421 @NonNull Class<T> resultSetRowType, 2422 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 2423 Object @Nullable ... parameters) { 2424 requireNonNull(statement); 2425 requireNonNull(resultSetRowType); 2426 2427 ResultHolder<Optional<T>> resultHolder = new ResultHolder<>(); 2428 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 2429 .resultSetRowType(resultSetRowType) 2430 .parameters(parameters) 2431 .build(); 2432 2433 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 2434 2435 performDatabaseOperation(statementContext, parametersAsList, preparedStatementCustomizer, (PreparedStatement preparedStatement) -> { 2436 long startTime = nanoTime(); 2437 2438 try (ResultSet resultSet = preparedStatement.executeQuery()) { 2439 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 2440 startTime = nanoTime(); 2441 2442 Optional<T> result = Optional.empty(); 2443 long rowsReturned = 0L; 2444 2445 if (resultSet.next()) { 2446 rowsReturned = 1L; 2447 try { 2448 T value = getResultSetMapper().map(statementContext, resultSet, statementContext.getResultSetRowType().get(), getInstanceProvider()).orElse(null); 2449 result = Optional.ofNullable(value); 2450 } catch (SQLException e) { 2451 throw databaseExceptionWithStatementContext(statementContext, 2452 format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), statementContext.getResultSetRowType().get()), e); 2453 } 2454 2455 if (resultSet.next()) 2456 throw databaseExceptionWithStatementContext(statementContext, 2457 "Expected 1 row in resultset but got more than 1 instead", 2458 new IllegalStateException("Expected 1 row in resultset but got more than 1 instead")); 2459 } 2460 2461 resultHolder.value = result; 2462 Duration resultSetMappingDuration = Duration.ofNanos(nanoTime() - startTime); 2463 StatementResult statementResult = getMetricsCollectorDispatcher().isEnabled() 2464 ? StatementResult.ofRowsReturned(rowsReturned) 2465 : StatementResult.empty(); 2466 return new DatabaseOperationResult(executionDuration, resultSetMappingDuration, statementResult); 2467 } 2468 }); 2469 2470 return resultHolder.value; 2471 } 2472 2473 /** 2474 * Performs a SQL query that is expected to return any number of result rows. 2475 * 2476 * @param sql the SQL query to execute 2477 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 2478 * @param parameters {@link PreparedStatement} parameters, if any 2479 * @param <T> the type to be returned 2480 * @return a list of results 2481 */ 2482 @NonNull 2483 private <T> List<@Nullable T> queryForList(@NonNull String sql, 2484 @NonNull Class<T> resultSetRowType, 2485 Object @Nullable ... parameters) { 2486 requireNonNull(sql); 2487 requireNonNull(resultSetRowType); 2488 2489 return queryForList(Statement.of(generateId(), sql), resultSetRowType, parameters); 2490 } 2491 2492 /** 2493 * Performs a SQL query that is expected to return any number of result rows. 2494 * 2495 * @param statement the SQL statement to execute 2496 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 2497 * @param parameters {@link PreparedStatement} parameters, if any 2498 * @param <T> the type to be returned 2499 * @return a list of results 2500 */ 2501 @NonNull 2502 private <T> List<@Nullable T> queryForList(@NonNull Statement statement, 2503 @NonNull Class<T> resultSetRowType, 2504 Object @Nullable ... parameters) { 2505 requireNonNull(statement); 2506 requireNonNull(resultSetRowType); 2507 2508 return queryForList(statement, resultSetRowType, null, parameters); 2509 } 2510 2511 private <T> List<@Nullable T> queryForList(@NonNull Statement statement, 2512 @NonNull Class<T> resultSetRowType, 2513 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 2514 Object @Nullable ... parameters) { 2515 requireNonNull(statement); 2516 requireNonNull(resultSetRowType); 2517 2518 List<T> list = new ArrayList<>(); 2519 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 2520 .resultSetRowType(resultSetRowType) 2521 .parameters(parameters) 2522 .build(); 2523 2524 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 2525 2526 performDatabaseOperation(statementContext, parametersAsList, preparedStatementCustomizer, (PreparedStatement preparedStatement) -> { 2527 long startTime = nanoTime(); 2528 2529 try (ResultSet resultSet = preparedStatement.executeQuery()) { 2530 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 2531 startTime = nanoTime(); 2532 2533 while (resultSet.next()) { 2534 try { 2535 T listElement = getResultSetMapper().map(statementContext, resultSet, statementContext.getResultSetRowType().get(), getInstanceProvider()).orElse(null); 2536 list.add(listElement); 2537 } catch (SQLException e) { 2538 throw databaseExceptionWithStatementContext(statementContext, 2539 format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), statementContext.getResultSetRowType().get()), e); 2540 } 2541 } 2542 2543 Duration resultSetMappingDuration = Duration.ofNanos(nanoTime() - startTime); 2544 StatementResult statementResult = getMetricsCollectorDispatcher().isEnabled() 2545 ? StatementResult.ofRowsReturned((long) list.size()) 2546 : StatementResult.empty(); 2547 return new DatabaseOperationResult(executionDuration, resultSetMappingDuration, statementResult); 2548 } 2549 }); 2550 2551 return list; 2552 } 2553 2554 @Nullable 2555 private <T, R> R queryForStream(@NonNull Statement statement, 2556 @NonNull Class<T> resultSetRowType, 2557 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 2558 boolean queryFetchSizeConfigured, 2559 @NonNull Function<Stream<@Nullable T>, R> streamFunction, 2560 Object @Nullable ... parameters) { 2561 requireNonNull(statement); 2562 requireNonNull(resultSetRowType); 2563 requireNonNull(streamFunction); 2564 2565 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 2566 .resultSetRowType(resultSetRowType) 2567 .parameters(parameters) 2568 .build(); 2569 2570 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 2571 StreamingResultSet<T> iterator = new StreamingResultSet<>(this, statementContext, parametersAsList, 2572 preparedStatementCustomizer, queryFetchSizeConfigured); 2573 2574 try { 2575 try (Stream<@Nullable T> stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false) 2576 .onClose(iterator::close)) { 2577 try { 2578 return streamFunction.apply(stream); 2579 } catch (Throwable throwable) { 2580 iterator.callbackFailed(throwable); 2581 if (throwable instanceof RuntimeException runtimeException) 2582 throw runtimeException; 2583 if (throwable instanceof Error error) 2584 throw error; 2585 throw new RuntimeException(throwable); 2586 } 2587 } 2588 } finally { 2589 iterator.emitTerminalMetrics(); 2590 } 2591 } 2592 2593 /** 2594 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}; 2595 * or a SQL statement that returns nothing, such as a DDL statement. 2596 * 2597 * @param sql the SQL to execute 2598 * @param parameters {@link PreparedStatement} parameters, if any 2599 * @return the number of rows affected by the SQL statement 2600 */ 2601 @NonNull 2602 private Long execute(@NonNull String sql, 2603 Object @Nullable ... parameters) { 2604 requireNonNull(sql); 2605 return execute(Statement.of(generateId(), sql), parameters); 2606 } 2607 2608 /** 2609 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}; 2610 * or a SQL statement that returns nothing, such as a DDL statement. 2611 * 2612 * @param statement the SQL statement to execute 2613 * @param parameters {@link PreparedStatement} parameters, if any 2614 * @return the number of rows affected by the SQL statement 2615 */ 2616 @NonNull 2617 private Long execute(@NonNull Statement statement, 2618 Object @Nullable ... parameters) { 2619 requireNonNull(statement); 2620 2621 return execute(statement, null, parameters); 2622 } 2623 2624 private Long execute(@NonNull Statement statement, 2625 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 2626 Object @Nullable ... parameters) { 2627 requireNonNull(statement); 2628 2629 ResultHolder<Long> resultHolder = new ResultHolder<>(); 2630 StatementContext<Void> statementContext = StatementContext.with(statement, this) 2631 .parameters(parameters) 2632 .build(); 2633 2634 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 2635 2636 performDatabaseOperation(statementContext, parametersAsList, preparedStatementCustomizer, (PreparedStatement preparedStatement) -> { 2637 long startTime = nanoTime(); 2638 resultHolder.value = executeUpdate(preparedStatement); 2639 2640 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 2641 StatementResult statementResult = getMetricsCollectorDispatcher().isEnabled() 2642 ? StatementResult.ofRowsAffected(resultHolder.value) 2643 : StatementResult.empty(); 2644 return new DatabaseOperationResult(executionDuration, null, statementResult); 2645 }); 2646 2647 return resultHolder.value; 2648 } 2649 2650 @NonNull 2651 private Long executeUpdate(@NonNull PreparedStatement preparedStatement) throws SQLException { 2652 requireNonNull(preparedStatement); 2653 2654 DatabaseOperationSupportStatus executeLargeUpdateSupported = getExecuteLargeUpdateSupported(); 2655 2656 // Use the appropriate "large" value if we know it. 2657 // If we don't know it, detect it and store it. 2658 if (executeLargeUpdateSupported == DatabaseOperationSupportStatus.YES) 2659 return preparedStatement.executeLargeUpdate(); 2660 2661 if (executeLargeUpdateSupported == DatabaseOperationSupportStatus.NO) 2662 return (long) preparedStatement.executeUpdate(); 2663 2664 // If the driver doesn't support executeLargeUpdate, then UnsupportedOperationException is thrown. 2665 try { 2666 Long result = preparedStatement.executeLargeUpdate(); 2667 setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.YES); 2668 return result; 2669 } catch (SQLFeatureNotSupportedException | UnsupportedOperationException | AbstractMethodError e) { 2670 setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.NO); 2671 return (long) preparedStatement.executeUpdate(); 2672 } catch (SQLException e) { 2673 if (isUnsupportedSqlFeature(e)) { 2674 setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.NO); 2675 return (long) preparedStatement.executeUpdate(); 2676 } 2677 2678 throw e; 2679 } 2680 } 2681 2682 @NonNull 2683 private <T> Optional<T> executeReturningGeneratedKey(@NonNull Statement statement, 2684 @NonNull Class<T> resultSetRowType, 2685 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 2686 @Nullable String @Nullable [] keyColumnNames, 2687 Object @Nullable ... parameters) { 2688 requireNonNull(statement); 2689 requireNonNull(resultSetRowType); 2690 2691 ResultHolder<Optional<T>> resultHolder = new ResultHolder<>(); 2692 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 2693 .resultSetRowType(resultSetRowType) 2694 .parameters(parameters) 2695 .build(); 2696 2697 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 2698 String[] requestedKeyColumnNames = copyGeneratedKeyColumnNames(keyColumnNames); 2699 2700 performDatabaseOperation(statementContext, parametersAsList, preparedStatementCustomizer, (PreparedStatement preparedStatement) -> { 2701 long startTime = nanoTime(); 2702 Long rowsAffected = executeUpdate(preparedStatement); 2703 2704 try (ResultSet resultSet = preparedStatement.getGeneratedKeys()) { 2705 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 2706 startTime = nanoTime(); 2707 2708 Optional<T> result = Optional.empty(); 2709 long rowsReturned = 0L; 2710 2711 if (resultSet.next()) { 2712 rowsReturned = 1L; 2713 try { 2714 T value = getResultSetMapper().map(statementContext, resultSet, statementContext.getResultSetRowType().get(), getInstanceProvider()).orElse(null); 2715 result = Optional.ofNullable(value); 2716 } catch (SQLException e) { 2717 throw databaseExceptionWithStatementContext(statementContext, 2718 format("Unable to map JDBC generated-key row to %s", statementContext.getResultSetRowType().get()), e); 2719 } 2720 2721 if (resultSet.next()) 2722 throw databaseExceptionWithStatementContext(statementContext, 2723 "Expected 1 generated-key row but got more than 1 instead", 2724 new IllegalStateException("Expected 1 generated-key row but got more than 1 instead")); 2725 } 2726 2727 resultHolder.value = result; 2728 Duration resultSetMappingDuration = Duration.ofNanos(nanoTime() - startTime); 2729 StatementResult statementResult = getMetricsCollectorDispatcher().isEnabled() 2730 ? new StatementResult(rowsReturned, rowsAffected) 2731 : StatementResult.empty(); 2732 return new DatabaseOperationResult(executionDuration, resultSetMappingDuration, statementResult); 2733 } 2734 }, generatedKeysPreparedStatementFactory(requestedKeyColumnNames)); 2735 2736 return resultHolder.value; 2737 } 2738 2739 @NonNull 2740 private <T> List<@Nullable T> executeReturningGeneratedKeys(@NonNull Statement statement, 2741 @NonNull Class<T> resultSetRowType, 2742 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 2743 @Nullable String @Nullable [] keyColumnNames, 2744 Object @Nullable ... parameters) { 2745 requireNonNull(statement); 2746 requireNonNull(resultSetRowType); 2747 2748 List<T> list = new ArrayList<>(); 2749 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 2750 .resultSetRowType(resultSetRowType) 2751 .parameters(parameters) 2752 .build(); 2753 2754 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 2755 String[] requestedKeyColumnNames = copyGeneratedKeyColumnNames(keyColumnNames); 2756 2757 performDatabaseOperation(statementContext, parametersAsList, preparedStatementCustomizer, (PreparedStatement preparedStatement) -> { 2758 long startTime = nanoTime(); 2759 Long rowsAffected = executeUpdate(preparedStatement); 2760 2761 try (ResultSet resultSet = preparedStatement.getGeneratedKeys()) { 2762 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 2763 startTime = nanoTime(); 2764 2765 while (resultSet.next()) { 2766 try { 2767 T listElement = getResultSetMapper().map(statementContext, resultSet, statementContext.getResultSetRowType().get(), getInstanceProvider()).orElse(null); 2768 list.add(listElement); 2769 } catch (SQLException e) { 2770 throw databaseExceptionWithStatementContext(statementContext, 2771 format("Unable to map JDBC generated-key row to %s", statementContext.getResultSetRowType().get()), e); 2772 } 2773 } 2774 2775 Duration resultSetMappingDuration = Duration.ofNanos(nanoTime() - startTime); 2776 StatementResult statementResult = getMetricsCollectorDispatcher().isEnabled() 2777 ? new StatementResult((long) list.size(), rowsAffected) 2778 : StatementResult.empty(); 2779 return new DatabaseOperationResult(executionDuration, resultSetMappingDuration, statementResult); 2780 } 2781 }, generatedKeysPreparedStatementFactory(requestedKeyColumnNames)); 2782 2783 return list; 2784 } 2785 2786 @NonNull 2787 private String[] copyGeneratedKeyColumnNames(@Nullable String @Nullable [] keyColumnNames) { 2788 if (keyColumnNames == null || keyColumnNames.length == 0) 2789 return new String[0]; 2790 2791 String[] copy = Arrays.copyOf(keyColumnNames, keyColumnNames.length); 2792 2793 for (String keyColumnName : copy) 2794 requireNonNull(keyColumnName); 2795 2796 return copy; 2797 } 2798 2799 @NonNull 2800 private PreparedStatementFactory generatedKeysPreparedStatementFactory(@NonNull String @NonNull [] keyColumnNames) { 2801 requireNonNull(keyColumnNames); 2802 2803 return (connection, statementContext) -> getDatabaseDialect(connection) 2804 .prepareGeneratedKeysStatement(connection, statementContext, keyColumnNames); 2805 } 2806 2807 /** 2808 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 2809 * which returns 0 or 1 rows with database-native syntax such as PostgreSQL/SQLite/MariaDB {@code RETURNING} 2810 * or SQL Server {@code OUTPUT}. 2811 * 2812 * @param sql the SQL query to execute 2813 * @param resultSetRowType the type to which the {@link ResultSet} row should be marshaled 2814 * @param parameters {@link PreparedStatement} parameters, if any 2815 * @param <T> the type to be returned 2816 * @return a single result (or no result) 2817 * @throws DatabaseException if > 1 row is returned 2818 */ 2819 @NonNull 2820 private <T> Optional<T> executeForObject(@NonNull String sql, 2821 @NonNull Class<T> resultSetRowType, 2822 Object @Nullable ... parameters) { 2823 requireNonNull(sql); 2824 requireNonNull(resultSetRowType); 2825 2826 return executeForObject(Statement.of(generateId(), sql), resultSetRowType, parameters); 2827 } 2828 2829 /** 2830 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 2831 * which returns 0 or 1 rows with database-native syntax such as PostgreSQL/SQLite/MariaDB {@code RETURNING} 2832 * or SQL Server {@code OUTPUT}. 2833 * 2834 * @param statement the SQL statement to execute 2835 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 2836 * @param parameters {@link PreparedStatement} parameters, if any 2837 * @param <T> the type to be returned 2838 * @return a single result (or no result) 2839 * @throws DatabaseException if > 1 row is returned 2840 */ 2841 private <T> Optional<T> executeForObject(@NonNull Statement statement, 2842 @NonNull Class<T> resultSetRowType, 2843 Object @Nullable ... parameters) { 2844 requireNonNull(statement); 2845 requireNonNull(resultSetRowType); 2846 2847 return executeForObject(statement, resultSetRowType, null, parameters); 2848 } 2849 2850 private <T> Optional<T> executeForObject(@NonNull Statement statement, 2851 @NonNull Class<T> resultSetRowType, 2852 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 2853 Object @Nullable ... parameters) { 2854 requireNonNull(statement); 2855 requireNonNull(resultSetRowType); 2856 2857 // Ultimately we just delegate to queryForObject. 2858 // Having `executeForList` is to allow for users to explicitly express intent 2859 // and make static analysis of code easier (e.g. maybe you'd like to hook all of your "execute" statements for 2860 // logging, or delegation to a writable master as opposed to a read replica) 2861 return queryForObject(statement, resultSetRowType, preparedStatementCustomizer, parameters); 2862 } 2863 2864 /** 2865 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 2866 * which returns any number of rows with database-native syntax such as PostgreSQL/SQLite/MariaDB {@code RETURNING} 2867 * or SQL Server {@code OUTPUT}. 2868 * 2869 * @param sql the SQL to execute 2870 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 2871 * @param parameters {@link PreparedStatement} parameters, if any 2872 * @param <T> the type to be returned 2873 * @return a list of results 2874 */ 2875 @NonNull 2876 private <T> List<@Nullable T> executeForList(@NonNull String sql, 2877 @NonNull Class<T> resultSetRowType, 2878 Object @Nullable ... parameters) { 2879 requireNonNull(sql); 2880 requireNonNull(resultSetRowType); 2881 2882 return executeForList(Statement.of(generateId(), sql), resultSetRowType, parameters); 2883 } 2884 2885 /** 2886 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 2887 * which returns any number of rows with database-native syntax such as PostgreSQL/SQLite/MariaDB {@code RETURNING} 2888 * or SQL Server {@code OUTPUT}. 2889 * 2890 * @param statement the SQL statement to execute 2891 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 2892 * @param parameters {@link PreparedStatement} parameters, if any 2893 * @param <T> the type to be returned 2894 * @return a list of results 2895 */ 2896 @NonNull 2897 private <T> List<@Nullable T> executeForList(@NonNull Statement statement, 2898 @NonNull Class<T> resultSetRowType, 2899 Object @Nullable ... parameters) { 2900 requireNonNull(statement); 2901 requireNonNull(resultSetRowType); 2902 2903 return executeForList(statement, resultSetRowType, null, parameters); 2904 } 2905 2906 private <T> List<@Nullable T> executeForList(@NonNull Statement statement, 2907 @NonNull Class<T> resultSetRowType, 2908 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 2909 Object @Nullable ... parameters) { 2910 requireNonNull(statement); 2911 requireNonNull(resultSetRowType); 2912 2913 // Ultimately we just delegate to queryForList. 2914 // Having `executeForList` is to allow for users to explicitly express intent 2915 // and make static analysis of code easier (e.g. maybe you'd like to hook all of your "execute" statements for 2916 // logging, or delegation to a writable master as opposed to a read replica) 2917 return queryForList(statement, resultSetRowType, preparedStatementCustomizer, parameters); 2918 } 2919 2920 /** 2921 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE} 2922 * in "batch" over a set of parameter groups. 2923 * <p> 2924 * Useful for bulk-inserting or updating large amounts of data. 2925 * 2926 * @param sql the SQL to execute 2927 * @param parameterGroups Groups of {@link PreparedStatement} parameters 2928 * @return the number of rows affected by the SQL statement per-group 2929 */ 2930 @NonNull 2931 private List<Long> executeBatch(@NonNull String sql, 2932 @NonNull List<List<Object>> parameterGroups) { 2933 requireNonNull(sql); 2934 requireNonNull(parameterGroups); 2935 2936 return executeBatch(Statement.of(generateId(), sql), parameterGroups); 2937 } 2938 2939 /** 2940 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE} 2941 * in "batch" over a set of parameter groups. 2942 * <p> 2943 * Useful for bulk-inserting or updating large amounts of data. 2944 * 2945 * @param statement the SQL statement to execute 2946 * @param parameterGroups Groups of {@link PreparedStatement} parameters 2947 * @return the number of rows affected by the SQL statement per-group 2948 */ 2949 @NonNull 2950 private List<Long> executeBatch(@NonNull Statement statement, 2951 @NonNull List<List<Object>> parameterGroups) { 2952 requireNonNull(statement); 2953 requireNonNull(parameterGroups); 2954 2955 return executeBatch(statement, parameterGroups, null, null); 2956 } 2957 2958 private List<Long> executeBatch(@NonNull Statement statement, 2959 @NonNull List<List<Object>> parameterGroups, 2960 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 2961 @Nullable Integer batchChunkSize) { 2962 requireNonNull(statement); 2963 requireNonNull(parameterGroups); 2964 if (parameterGroups.isEmpty()) 2965 return List.of(); 2966 2967 Integer expectedParameterCount = null; 2968 2969 for (int i = 0; i < parameterGroups.size(); i++) { 2970 List<Object> parameterGroup = parameterGroups.get(i); 2971 2972 if (parameterGroup == null) 2973 throw new IllegalArgumentException(format("Parameter group at index %s is null", i)); 2974 2975 int parameterCount = parameterGroup.size(); 2976 if (expectedParameterCount == null) { 2977 expectedParameterCount = parameterCount; 2978 } else if (parameterCount != expectedParameterCount) { 2979 throw new IllegalArgumentException(format( 2980 "Inconsistent parameter group size at index %s: expected %s but found %s", 2981 i, expectedParameterCount, parameterCount)); 2982 } 2983 } 2984 2985 ResultHolder<List<Long>> resultHolder = new ResultHolder<>(); 2986 StatementContext<List<Long>> statementContext = StatementContext.with(statement, this) 2987 .parameters((List) parameterGroups) 2988 .resultSetRowType(List.class) 2989 .batchParameterGroups(true) 2990 .build(); 2991 2992 if (batchChunkSize == null || batchChunkSize >= parameterGroups.size()) { 2993 performDatabaseOperation(statementContext, (preparedStatement) -> { 2994 applyPreparedStatementCustomizer(statementContext, preparedStatement, preparedStatementCustomizer); 2995 2996 for (List<Object> parameterGroup : parameterGroups) { 2997 if (parameterGroup.size() > 0) 2998 performPreparedStatementBinding(statementContext, preparedStatement, parameterGroup); 2999 3000 preparedStatement.addBatch(); 3001 } 3002 }, (PreparedStatement preparedStatement) -> { 3003 long startTime = nanoTime(); 3004 List<Long> result = executePreparedStatementBatch(preparedStatement); 3005 3006 resultHolder.value = result; 3007 return batchDatabaseOperationResult(startTime, result); 3008 }, parameterGroups.size()); 3009 } else { 3010 int effectiveBatchChunkSize = batchChunkSize; 3011 3012 performDatabaseOperation(statementContext, (preparedStatement) -> { 3013 applyPreparedStatementCustomizer(statementContext, preparedStatement, preparedStatementCustomizer); 3014 }, (PreparedStatement preparedStatement) -> { 3015 long startTime = nanoTime(); 3016 int currentBatchSize = 0; 3017 List<Long> result = new ArrayList<>(parameterGroups.size()); 3018 3019 for (List<Object> parameterGroup : parameterGroups) { 3020 if (parameterGroup.size() > 0) 3021 performPreparedStatementBinding(statementContext, preparedStatement, parameterGroup); 3022 3023 preparedStatement.addBatch(); 3024 ++currentBatchSize; 3025 3026 if (currentBatchSize == effectiveBatchChunkSize) { 3027 result.addAll(executePreparedStatementBatch(preparedStatement)); 3028 preparedStatement.clearBatch(); 3029 currentBatchSize = 0; 3030 } 3031 } 3032 3033 if (currentBatchSize > 0) { 3034 result.addAll(executePreparedStatementBatch(preparedStatement)); 3035 preparedStatement.clearBatch(); 3036 } 3037 3038 resultHolder.value = result; 3039 return batchDatabaseOperationResult(startTime, result); 3040 }, parameterGroups.size()); 3041 } 3042 3043 return resultHolder.value; 3044 } 3045 3046 @NonNull 3047 private DatabaseOperationResult batchDatabaseOperationResult(long startTime, 3048 @NonNull List<Long> result) { 3049 requireNonNull(result); 3050 3051 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 3052 StatementResult statementResult = StatementResult.empty(); 3053 if (getMetricsCollectorDispatcher().isEnabled()) { 3054 Long rowsAffected = sumBatchUpdateCounts(result); 3055 statementResult = rowsAffected == null ? StatementResult.empty() : StatementResult.ofRowsAffected(rowsAffected); 3056 } 3057 3058 return new DatabaseOperationResult(executionDuration, null, statementResult); 3059 } 3060 3061 @NonNull 3062 private List<Long> executePreparedStatementBatch(@NonNull PreparedStatement preparedStatement) throws SQLException { 3063 requireNonNull(preparedStatement); 3064 3065 DatabaseOperationSupportStatus executeLargeBatchSupported = getExecuteLargeBatchSupported(); 3066 3067 // Use the appropriate "large" value if we know it. 3068 // If we don't know it, detect it and store it. 3069 if (executeLargeBatchSupported == DatabaseOperationSupportStatus.YES) { 3070 long[] resultArray = preparedStatement.executeLargeBatch(); 3071 return Arrays.stream(resultArray).boxed().collect(Collectors.toList()); 3072 } 3073 if (executeLargeBatchSupported == DatabaseOperationSupportStatus.NO) { 3074 int[] resultArray = preparedStatement.executeBatch(); 3075 return Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList()); 3076 } 3077 3078 // If the driver doesn't support executeLargeBatch, then UnsupportedOperationException is thrown. 3079 try { 3080 long[] resultArray = preparedStatement.executeLargeBatch(); 3081 setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.YES); 3082 return Arrays.stream(resultArray).boxed().collect(Collectors.toList()); 3083 } catch (SQLFeatureNotSupportedException | UnsupportedOperationException | AbstractMethodError e) { 3084 setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.NO); 3085 int[] resultArray = preparedStatement.executeBatch(); 3086 return Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList()); 3087 } catch (SQLException e) { 3088 if (!isUnsupportedSqlFeature(e)) 3089 throw e; 3090 3091 setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.NO); 3092 int[] resultArray = preparedStatement.executeBatch(); 3093 return Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList()); 3094 } 3095 } 3096 3097 /** 3098 * Exposes a temporary handle to JDBC {@link DatabaseMetaData}, which provides comprehensive vendor-specific information about this database as a whole. 3099 * <p> 3100 * This method acquires {@link DatabaseMetaData} on its own newly-borrowed connection, which it manages internally. 3101 * <p> 3102 * It does <strong>not</strong> participate in the active transaction, if one exists. 3103 * <p> 3104 * The connection is closed as soon as {@link DatabaseMetaDataReader#read(DatabaseMetaData)} completes. 3105 * <p> 3106 * See <a href="https://docs.oracle.com/en/java/javase/26/docs/api/java.sql/java/sql/DatabaseMetaData.html">{@code DatabaseMetaData} Javadoc</a> for details. 3107 */ 3108 public void readDatabaseMetaData(@NonNull DatabaseMetaDataReader databaseMetaDataReader) { 3109 requireNonNull(databaseMetaDataReader); 3110 3111 performRawConnectionOperation((connection -> { 3112 databaseMetaDataReader.read(connection.getMetaData()); 3113 return Optional.empty(); 3114 }), false); 3115 } 3116 3117 /** 3118 * Performs raw JDBC work with a Pyranid-managed {@link Connection}. 3119 * <p> 3120 * If called inside a Pyranid transaction, this operation uses the transaction's connection and participates in that 3121 * transaction. Otherwise, Pyranid borrows a connection for the duration of the callback and closes it afterwards. 3122 * <p> 3123 * The {@link Connection} passed to {@code rawConnectionOperation} is a guarded handle. Normal JDBC operations are 3124 * delegated to the underlying driver connection, but lifecycle, transaction-management, and connection-wide state 3125 * methods such as 3126 * {@link Connection#close()}, {@link Connection#commit()}, {@link Connection#rollback()}, 3127 * {@link Connection#setAutoCommit(boolean)}, {@link Connection#setCatalog(String)}, {@link Connection#setSchema(String)}, and 3128 * {@link Connection#setNetworkTimeout(java.util.concurrent.Executor, int)} throw {@link IllegalStateException}. Use 3129 * Pyranid transaction APIs instead. 3130 * JDBC objects created from this handle are also guarded: {@link java.sql.Statement#getConnection()} and 3131 * {@link java.sql.DatabaseMetaData#getConnection()} return the Pyranid-managed handle, and 3132 * {@link ResultSet#getStatement()} returns a guarded statement. Guarded statements, resultsets, and metadata refuse 3133 * driver-specific {@code unwrap(...)} calls that could expose the driver's underlying connection. 3134 * <p> 3135 * The connection handle is valid only for the duration of the callback. Do not close it, retain it, or use it after this 3136 * method returns. 3137 * 3138 * @param rawConnectionOperation the raw JDBC operation to perform 3139 * @param <T> the type to be returned 3140 * @return the operation result 3141 * @throws DatabaseException if connection acquisition, callback execution, or cleanup fails 3142 * @since 4.2.0 3143 */ 3144 @NonNull 3145 public <T> Optional<T> useRawConnection(@NonNull RawConnectionOperation<T> rawConnectionOperation) { 3146 requireNonNull(rawConnectionOperation); 3147 3148 return performRawConnectionOperation(connection -> { 3149 PyranidRawConnection rawConnection = new PyranidRawConnection(connection); 3150 3151 try { 3152 Optional<T> result = rawConnectionOperation.perform(rawConnection); 3153 return result == null ? Optional.empty() : result; 3154 } finally { 3155 rawConnection.release(); 3156 } 3157 }, true); 3158 } 3159 3160 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 3161 @NonNull List<Object> parameters, 3162 @NonNull DatabaseOperation databaseOperation) { 3163 requireNonNull(statementContext); 3164 requireNonNull(parameters); 3165 requireNonNull(databaseOperation); 3166 3167 performDatabaseOperation(statementContext, parameters, null, databaseOperation); 3168 } 3169 3170 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 3171 @NonNull List<Object> parameters, 3172 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 3173 @NonNull DatabaseOperation databaseOperation) { 3174 performDatabaseOperation(statementContext, parameters, preparedStatementCustomizer, databaseOperation, 3175 (connection, context) -> connection.prepareStatement(context.getStatement().getSql())); 3176 } 3177 3178 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 3179 @NonNull List<Object> parameters, 3180 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 3181 @NonNull DatabaseOperation databaseOperation, 3182 @NonNull PreparedStatementFactory preparedStatementFactory) { 3183 requireNonNull(statementContext); 3184 requireNonNull(parameters); 3185 requireNonNull(databaseOperation); 3186 requireNonNull(preparedStatementFactory); 3187 3188 performDatabaseOperation(statementContext, (preparedStatement) -> { 3189 applyPreparedStatementCustomizer(statementContext, preparedStatement, preparedStatementCustomizer); 3190 if (parameters.size() > 0) 3191 performPreparedStatementBinding(statementContext, preparedStatement, parameters); 3192 }, databaseOperation, null, preparedStatementFactory); 3193 } 3194 3195 protected <T> void performPreparedStatementBinding(@NonNull StatementContext<T> statementContext, 3196 @NonNull PreparedStatement preparedStatement, 3197 @NonNull List<Object> parameters) { 3198 requireNonNull(statementContext); 3199 requireNonNull(preparedStatement); 3200 requireNonNull(parameters); 3201 3202 try { 3203 DefaultPreparedStatementBinder.ParameterSqlTypeResolver parameterSqlTypeResolver = 3204 new DefaultPreparedStatementBinder.ParameterSqlTypeResolver(preparedStatement); 3205 PreparedStatementBinder preparedStatementBinder = getPreparedStatementBinder(); 3206 3207 for (int i = 0; i < parameters.size(); ++i) { 3208 Object parameter = SecureParameterSupport.unwrapSecureAndOptionalParameter(parameters.get(i)); 3209 Integer parameterIndex = i + 1; 3210 3211 if (parameter != null) { 3212 if (preparedStatementBinder instanceof DefaultPreparedStatementBinder defaultPreparedStatementBinder) { 3213 defaultPreparedStatementBinder.bindParameter(statementContext, preparedStatement, parameterIndex, parameter, 3214 parameterSqlTypeResolver); 3215 } else { 3216 preparedStatementBinder.bindParameter(statementContext, preparedStatement, parameterIndex, parameter); 3217 } 3218 } else { 3219 Integer sqlType = parameterSqlTypeResolver.determineParameterSqlType(parameterIndex) 3220 .map(DefaultPreparedStatementBinder.ParameterSqlType::getSqlType) 3221 .orElse(Types.NULL); 3222 try { 3223 preparedStatement.setNull(parameterIndex, sqlType); 3224 } catch (SQLException | AbstractMethodError e) { 3225 if (sqlType == Types.NULL) 3226 throw e; 3227 3228 preparedStatement.setNull(parameterIndex, Types.NULL); 3229 } 3230 } 3231 } 3232 } catch (Exception e) { 3233 throw databaseExceptionWithStatementContext(statementContext, e); 3234 } 3235 } 3236 3237 protected void applyPreparedStatementCustomizer(@NonNull StatementContext<?> statementContext, 3238 @NonNull PreparedStatement preparedStatement, 3239 @Nullable PreparedStatementCustomizer preparedStatementCustomizer) throws SQLException { 3240 requireNonNull(statementContext); 3241 requireNonNull(preparedStatement); 3242 3243 if (preparedStatementCustomizer == null) 3244 return; 3245 3246 preparedStatementCustomizer.customize(statementContext, preparedStatement); 3247 } 3248 3249 private boolean hasDefaultPreparedStatementSettings() { 3250 return this.queryTimeout != null || this.fetchSize != null || this.maxRows != null; 3251 } 3252 3253 private void applyDefaultPreparedStatementSettings(@NonNull PreparedStatement preparedStatement) throws SQLException { 3254 requireNonNull(preparedStatement); 3255 applyPreparedStatementSettings(preparedStatement, this.queryTimeout, this.fetchSize, this.maxRows); 3256 } 3257 3258 private static void applyPreparedStatementSettings(@NonNull PreparedStatement preparedStatement, 3259 @Nullable Duration queryTimeout, 3260 @Nullable Integer fetchSize, 3261 @Nullable Integer maxRows) throws SQLException { 3262 requireNonNull(preparedStatement); 3263 3264 if (queryTimeout != null) 3265 preparedStatement.setQueryTimeout(queryTimeoutSeconds(queryTimeout)); 3266 3267 if (fetchSize != null) 3268 preparedStatement.setFetchSize(fetchSize); 3269 3270 if (maxRows != null) 3271 preparedStatement.setMaxRows(maxRows); 3272 } 3273 3274 @FunctionalInterface 3275 protected interface InternalRawConnectionOperation<R> { 3276 @NonNull 3277 Optional<R> perform(@NonNull Connection connection) throws Exception; 3278 } 3279 3280 /** 3281 * Gets the database type for this database. 3282 * <p> 3283 * If {@link Builder#databaseType(DatabaseType)} was not configured and the database type has not already been detected, 3284 * this method may acquire a connection and inspect {@link DatabaseMetaData}. Configure an explicit database type to avoid 3285 * runtime detection. 3286 * 3287 * @return the database type 3288 * @throws DatabaseException if automatic database type detection fails 3289 * @since 3.0.0 3290 */ 3291 @NonNull 3292 public DatabaseType getDatabaseType() { 3293 return getDatabaseType(this.databaseTypeDetectionConnectionHolder.get()); 3294 } 3295 3296 @NonNull 3297 DatabaseType peekDatabaseType() { 3298 DatabaseType cachedDatabaseType = this.databaseType.get(); 3299 return cachedDatabaseType == null ? DatabaseType.GENERIC : cachedDatabaseType; 3300 } 3301 3302 @NonNull 3303 DatabaseDialect getDatabaseDialect() { 3304 return getDatabaseDialect(this.databaseTypeDetectionConnectionHolder.get()); 3305 } 3306 3307 @NonNull 3308 DatabaseDialect getDatabaseDialect(@Nullable Connection connection) { 3309 DatabaseDialect cachedDatabaseDialect = this.databaseDialect.get(); 3310 3311 if (cachedDatabaseDialect != null) 3312 return cachedDatabaseDialect; 3313 3314 DatabaseDialect detectedDatabaseDialect = getDatabaseType(connection).dialect(); 3315 3316 if (this.databaseDialect.compareAndSet(null, detectedDatabaseDialect)) 3317 return detectedDatabaseDialect; 3318 3319 return requireNonNull(this.databaseDialect.get()); 3320 } 3321 3322 private void warmDatabaseTypeCacheForMetricsIfNeeded(@NonNull StatementContext<?> statementContext) { 3323 requireNonNull(statementContext); 3324 3325 if (!getMetricsCollectorDispatcher().isEnabled()) 3326 return; 3327 3328 // Trigger lazy database-type detection while the statement's JDBC connection is active so later 3329 // transaction-scope metrics can report an accurate db.system.name without opening a second metadata connection. 3330 try { 3331 statementContext.getDatabaseType(); 3332 } catch (Throwable t) { 3333 this.logger.log(Level.FINE, "Unable to warm database type cache for metrics", t); 3334 } 3335 } 3336 3337 private void dispatchWithDatabaseTypeDetectionConnection(@NonNull Connection connection, 3338 @NonNull Runnable operation) { 3339 requireNonNull(connection); 3340 requireNonNull(operation); 3341 3342 Connection previousDatabaseTypeDetectionConnection = this.databaseTypeDetectionConnectionHolder.get(); 3343 this.databaseTypeDetectionConnectionHolder.set(connection); 3344 3345 try { 3346 operation.run(); 3347 } finally { 3348 if (previousDatabaseTypeDetectionConnection == null) 3349 this.databaseTypeDetectionConnectionHolder.remove(); 3350 else 3351 this.databaseTypeDetectionConnectionHolder.set(previousDatabaseTypeDetectionConnection); 3352 } 3353 } 3354 3355 @NonNull 3356 private DatabaseType getDatabaseType(@Nullable Connection connection) { 3357 DatabaseType cachedDatabaseType = this.databaseType.get(); 3358 3359 if (cachedDatabaseType != null) 3360 return cachedDatabaseType; 3361 3362 DatabaseType detectedDatabaseType; 3363 3364 try { 3365 detectedDatabaseType = connection == null 3366 ? DatabaseType.fromDataSource(getDataSource()) 3367 : DatabaseType.fromConnection(connection); 3368 } catch (DatabaseException e) { 3369 throw new DatabaseException(format( 3370 "Unable to determine database type automatically. Configure %s.%s(%s) explicitly to avoid runtime detection.", 3371 Builder.class.getSimpleName(), "databaseType", DatabaseType.class.getSimpleName()), e); 3372 } 3373 3374 if (this.databaseType.compareAndSet(null, detectedDatabaseType)) 3375 return detectedDatabaseType; 3376 3377 return this.databaseType.get(); 3378 } 3379 3380 /** 3381 * @since 3.0.0 3382 */ 3383 @NonNull 3384 public ZoneId getTimeZone() { 3385 return this.timeZone; 3386 } 3387 3388 /** 3389 * How should Pyranid bind {@link java.time.Instant} and {@link java.time.OffsetDateTime} parameters when JDBC 3390 * parameter metadata cannot identify whether the target is {@code TIMESTAMP} or {@code TIMESTAMP WITH TIME ZONE}? 3391 * 3392 * @return behavior to use when timestamp target metadata is unavailable or non-identifying 3393 * @since 4.2.0 3394 */ 3395 @NonNull 3396 public AmbiguousTimestampBindingStrategy getAmbiguousTimestampBindingStrategy() { 3397 return this.ambiguousTimestampBindingStrategy; 3398 } 3399 3400 /** 3401 * Gets the configured redactor used for non-secure parameters in diagnostics. 3402 * 3403 * @return the configured parameter redactor 3404 * @since 4.4.0 3405 */ 3406 @NonNull 3407 public ParameterRedactor getParameterRedactor() { 3408 return this.parameterRedactor; 3409 } 3410 3411 /** 3412 * Useful for single-shot "utility" calls that operate outside of normal query operations, e.g. pulling DB metadata. 3413 * <p> 3414 * Example: {@link #readDatabaseMetaData(DatabaseMetaDataReader)}. 3415 */ 3416 @NonNull 3417 protected <R> Optional<R> performRawConnectionOperation(@NonNull InternalRawConnectionOperation<R> rawConnectionOperation, 3418 @NonNull Boolean shouldParticipateInExistingTransactionIfPossible) { 3419 requireNonNull(rawConnectionOperation); 3420 requireNonNull(shouldParticipateInExistingTransactionIfPossible); 3421 3422 if (shouldParticipateInExistingTransactionIfPossible) { 3423 Optional<Transaction> transaction = currentTransactionForDatabaseOperation(); 3424 ReentrantLock connectionLock = transaction.isPresent() ? transaction.get().getConnectionLock() : null; 3425 // Try to participate in txn if it's available 3426 Connection connection = null; 3427 Throwable thrown = null; 3428 boolean connectionLockAcquired = false; 3429 3430 try { 3431 if (connectionLock != null) { 3432 lockInterruptibly(connectionLock, "use the transaction connection for a raw connection operation"); 3433 connectionLockAcquired = true; 3434 } 3435 3436 connection = transaction.isPresent() ? transaction.get().getConnection() : acquireConnection(); 3437 return rawConnectionOperation.perform(connection); 3438 } catch (DatabaseException e) { 3439 thrown = e; 3440 throw e; 3441 } catch (Exception e) { 3442 DatabaseException wrapped = databaseExceptionWithRawConnectionContext(connection, e); 3443 thrown = wrapped; 3444 throw wrapped; 3445 } finally { 3446 Throwable cleanupFailure = null; 3447 3448 try { 3449 // If this was a single-shot operation (not in a transaction), close the connection 3450 if (connection != null && !transaction.isPresent()) { 3451 try { 3452 closeConnection(connection); 3453 } catch (Throwable cleanupException) { 3454 cleanupFailure = cleanupException; 3455 } 3456 } 3457 } finally { 3458 if (connectionLockAcquired) 3459 connectionLock.unlock(); 3460 3461 if (cleanupFailure != null) { 3462 if (thrown != null) { 3463 thrown.addSuppressed(cleanupFailure); 3464 } else if (cleanupFailure instanceof RuntimeException) { 3465 throw (RuntimeException) cleanupFailure; 3466 } else if (cleanupFailure instanceof Error) { 3467 throw (Error) cleanupFailure; 3468 } else { 3469 throw new RuntimeException(cleanupFailure); 3470 } 3471 } 3472 } 3473 } 3474 } else { 3475 boolean acquiredConnection = false; 3476 Connection connection = null; 3477 Throwable thrown = null; 3478 3479 // Always get a fresh connection no matter what and close it afterwards 3480 try { 3481 connection = getDataSource().getConnection(); 3482 acquiredConnection = true; 3483 return rawConnectionOperation.perform(connection); 3484 } catch (DatabaseException e) { 3485 thrown = e; 3486 throw e; 3487 } catch (Exception e) { 3488 DatabaseException wrapped = acquiredConnection 3489 ? databaseExceptionWithRawConnectionContext(connection, e) 3490 : new DatabaseException("Unable to acquire database connection", e); 3491 thrown = wrapped; 3492 throw wrapped; 3493 } finally { 3494 if (connection != null) { 3495 try { 3496 closeConnection(connection); 3497 } catch (Throwable cleanupException) { 3498 if (thrown != null) { 3499 thrown.addSuppressed(cleanupException); 3500 } else if (cleanupException instanceof RuntimeException) { 3501 throw (RuntimeException) cleanupException; 3502 } else if (cleanupException instanceof Error) { 3503 throw (Error) cleanupException; 3504 } else { 3505 throw new RuntimeException(cleanupException); 3506 } 3507 } 3508 } 3509 } 3510 } 3511 } 3512 3513 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 3514 @NonNull PreparedStatementBindingOperation preparedStatementBindingOperation, 3515 @NonNull DatabaseOperation databaseOperation) { 3516 performDatabaseOperation(statementContext, preparedStatementBindingOperation, databaseOperation, null); 3517 } 3518 3519 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 3520 @NonNull PreparedStatementBindingOperation preparedStatementBindingOperation, 3521 @NonNull DatabaseOperation databaseOperation, 3522 @Nullable Integer batchSize) { 3523 performDatabaseOperation(statementContext, preparedStatementBindingOperation, databaseOperation, batchSize, 3524 (connection, context) -> connection.prepareStatement(context.getStatement().getSql())); 3525 } 3526 3527 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 3528 @NonNull PreparedStatementBindingOperation preparedStatementBindingOperation, 3529 @NonNull DatabaseOperation databaseOperation, 3530 @Nullable Integer batchSize, 3531 @NonNull PreparedStatementFactory preparedStatementFactory) { 3532 requireNonNull(statementContext); 3533 requireNonNull(preparedStatementBindingOperation); 3534 requireNonNull(databaseOperation); 3535 requireNonNull(preparedStatementFactory); 3536 3537 long startTime = nanoTime(); 3538 Duration connectionAcquisitionDuration = null; 3539 Duration preparationDuration = null; 3540 Duration executionDuration = null; 3541 Duration resultSetMappingDuration = null; 3542 StatementResult statementResult = StatementResult.empty(); 3543 Exception exception = null; 3544 Throwable thrown = null; 3545 Connection connection = null; 3546 long connectionHeldStartTime = 0L; 3547 Optional<Transaction> transaction = currentTransactionForDatabaseOperation(); 3548 ReentrantLock connectionLock = transaction.isPresent() ? transaction.get().getConnectionLock() : null; 3549 boolean connectionLockAcquired = false; 3550 3551 try { 3552 if (connectionLock != null) { 3553 lockInterruptibly(connectionLock, "execute a statement on the transaction connection"); 3554 connectionLockAcquired = true; 3555 } 3556 3557 boolean alreadyHasConnection = transaction.isPresent() && transaction.get().hasConnection(); 3558 if (transaction.isPresent()) { 3559 connection = transaction.get().getConnection(); 3560 } else { 3561 getMetricsCollectorDispatcher().willAcquireStatementConnection(statementContext); 3562 try { 3563 connection = getDataSource().getConnection(); 3564 } catch (SQLException e) { 3565 DatabaseException wrapped = new DatabaseException("Unable to acquire database connection", e); 3566 connectionAcquisitionDuration = Duration.ofNanos(nanoTime() - startTime); 3567 getMetricsCollectorDispatcher().didFailToAcquireStatementConnection(statementContext, peekDatabaseType(), 3568 connectionAcquisitionDuration, wrapped); 3569 throw wrapped; 3570 } catch (RuntimeException e) { 3571 connectionAcquisitionDuration = Duration.ofNanos(nanoTime() - startTime); 3572 getMetricsCollectorDispatcher().didFailToAcquireStatementConnection(statementContext, peekDatabaseType(), 3573 connectionAcquisitionDuration, e); 3574 throw e; 3575 } 3576 } 3577 3578 connectionAcquisitionDuration = alreadyHasConnection ? null : Duration.ofNanos(nanoTime() - startTime); 3579 if (!transaction.isPresent()) { 3580 connectionHeldStartTime = nanoTime(); 3581 Duration acquiredDuration = connectionAcquisitionDuration; 3582 MetricsCollectorDispatcher metricsCollectorDispatcher = getMetricsCollectorDispatcher(); 3583 if (metricsCollectorDispatcher.isEnabled()) 3584 dispatchWithDatabaseTypeDetectionConnection(connection, () -> 3585 metricsCollectorDispatcher.didAcquireStatementConnection(statementContext, acquiredDuration)); 3586 } 3587 startTime = nanoTime(); 3588 3589 try (PreparedStatement preparedStatement = preparedStatementFactory.prepare(connection, statementContext)) { 3590 Connection previousDatabaseTypeDetectionConnection = this.databaseTypeDetectionConnectionHolder.get(); 3591 this.databaseTypeDetectionConnectionHolder.set(connection); 3592 3593 try { 3594 preparedStatementBindingOperation.perform(preparedStatement); 3595 preparationDuration = Duration.ofNanos(nanoTime() - startTime); 3596 3597 getMetricsCollectorDispatcher().willExecuteStatement(statementContext); 3598 DatabaseOperationResult databaseOperationResult = databaseOperation.perform(preparedStatement); 3599 executionDuration = databaseOperationResult.getExecutionDuration().orElse(null); 3600 resultSetMappingDuration = databaseOperationResult.getResultSetMappingDuration().orElse(null); 3601 statementResult = databaseOperationResult.getStatementResult(); 3602 warmDatabaseTypeCacheForMetricsIfNeeded(statementContext); 3603 } finally { 3604 if (previousDatabaseTypeDetectionConnection == null) 3605 this.databaseTypeDetectionConnectionHolder.remove(); 3606 else 3607 this.databaseTypeDetectionConnectionHolder.set(previousDatabaseTypeDetectionConnection); 3608 } 3609 } 3610 } catch (DatabaseException e) { 3611 exception = e; 3612 thrown = e; 3613 throw e; 3614 } catch (Error e) { 3615 exception = databaseExceptionWithStatementContext(statementContext, e); 3616 thrown = e; 3617 throw e; 3618 } catch (Exception e) { 3619 DatabaseException wrapped = databaseExceptionWithStatementContext(statementContext, e); 3620 exception = e; 3621 thrown = wrapped; 3622 throw wrapped; 3623 } finally { 3624 Throwable cleanupFailure = null; 3625 3626 try { 3627 cleanupFailure = closeStatementContextResources(statementContext, cleanupFailure); 3628 3629 // If this was a single-shot operation (not in a transaction), close the connection 3630 if (connection != null && !transaction.isPresent()) { 3631 Duration heldDuration = Duration.ofNanos(nanoTime() - connectionHeldStartTime); 3632 try { 3633 closeConnection(connection); 3634 getMetricsCollectorDispatcher().didReleaseStatementConnection(statementContext, heldDuration); 3635 } catch (Throwable cleanupException) { 3636 getMetricsCollectorDispatcher().didFailToReleaseStatementConnection(statementContext, heldDuration, cleanupException); 3637 if (cleanupFailure == null) 3638 cleanupFailure = cleanupException; 3639 else 3640 cleanupFailure.addSuppressed(cleanupException); 3641 } 3642 } 3643 } finally { 3644 if (connectionLockAcquired) 3645 connectionLock.unlock(); 3646 3647 StatementLog statementLog = 3648 StatementLog.withStatementContext(statementContext) 3649 .connectionAcquisitionDuration(connectionAcquisitionDuration) 3650 .preparationDuration(preparationDuration) 3651 .executionDuration(executionDuration) 3652 .resultSetMappingDuration(resultSetMappingDuration) 3653 .batchSize(batchSize) 3654 .exception(exception) 3655 .build(); 3656 3657 if (thrown == null && exception == null) { 3658 getMetricsCollectorDispatcher().didExecuteStatement(statementContext, statementLog, statementResult); 3659 } else { 3660 Throwable statementThrowable = thrown == null ? exception : thrown; 3661 getMetricsCollectorDispatcher().didFailToExecuteStatement(statementContext, statementLog, peekDatabaseType(), 3662 requireNonNull(statementThrowable)); 3663 } 3664 3665 try { 3666 getStatementLogger().log(statementLog); 3667 } catch (Throwable cleanupException) { 3668 if (cleanupFailure == null) 3669 cleanupFailure = cleanupException; 3670 else 3671 cleanupFailure.addSuppressed(cleanupException); 3672 } 3673 } 3674 3675 if (cleanupFailure != null) { 3676 if (thrown != null) { 3677 thrown.addSuppressed(cleanupFailure); 3678 } else if (cleanupFailure instanceof RuntimeException) { 3679 throw (RuntimeException) cleanupFailure; 3680 } else if (cleanupFailure instanceof Error) { 3681 throw (Error) cleanupFailure; 3682 } else { 3683 throw new RuntimeException(cleanupFailure); 3684 } 3685 } 3686 } 3687 } 3688 3689 @NonNull 3690 protected Connection acquireConnection() { 3691 Optional<Transaction> transaction = currentTransactionForDatabaseOperation(); 3692 3693 if (transaction.isPresent()) 3694 return transaction.get().getConnection(); 3695 3696 try { 3697 return getDataSource().getConnection(); 3698 } catch (SQLException e) { 3699 throw new DatabaseException("Unable to acquire database connection", e); 3700 } 3701 } 3702 3703 @NonNull 3704 protected DataSource getDataSource() { 3705 return this.dataSource; 3706 } 3707 3708 @NonNull 3709 protected InstanceProvider getInstanceProvider() { 3710 return this.instanceProvider; 3711 } 3712 3713 @NonNull 3714 protected PreparedStatementBinder getPreparedStatementBinder() { 3715 return this.preparedStatementBinder; 3716 } 3717 3718 @NonNull 3719 protected ResultSetMapper getResultSetMapper() { 3720 return this.resultSetMapper; 3721 } 3722 3723 @NonNull 3724 protected StatementLogger getStatementLogger() { 3725 return this.statementLogger; 3726 } 3727 3728 @NonNull 3729 public MetricsCollector getMetricsCollector() { 3730 return getMetricsCollectorDispatcher().getMetricsCollector(); 3731 } 3732 3733 @NonNull 3734 MetricsCollectorDispatcher getMetricsCollectorDispatcher() { 3735 return this.metricsCollectorDispatcher; 3736 } 3737 3738 @NonNull 3739 protected DatabaseOperationSupportStatus getExecuteLargeBatchSupported() { 3740 return this.executeLargeBatchSupported; 3741 } 3742 3743 protected void setExecuteLargeBatchSupported(@NonNull DatabaseOperationSupportStatus executeLargeBatchSupported) { 3744 requireNonNull(executeLargeBatchSupported); 3745 this.executeLargeBatchSupported = executeLargeBatchSupported; 3746 } 3747 3748 @NonNull 3749 protected DatabaseOperationSupportStatus getExecuteLargeUpdateSupported() { 3750 return this.executeLargeUpdateSupported; 3751 } 3752 3753 protected void setExecuteLargeUpdateSupported(@NonNull DatabaseOperationSupportStatus executeLargeUpdateSupported) { 3754 requireNonNull(executeLargeUpdateSupported); 3755 this.executeLargeUpdateSupported = executeLargeUpdateSupported; 3756 } 3757 3758 @NonNull 3759 protected Object generateId() { 3760 // "Unique" keys 3761 return this.defaultIdGenerator.incrementAndGet(); 3762 } 3763 3764 @FunctionalInterface 3765 protected interface DatabaseOperation { 3766 @NonNull 3767 DatabaseOperationResult perform(@NonNull PreparedStatement preparedStatement) throws Exception; 3768 } 3769 3770 @FunctionalInterface 3771 protected interface PreparedStatementFactory { 3772 @NonNull 3773 PreparedStatement prepare(@NonNull Connection connection, 3774 @NonNull StatementContext<?> statementContext) throws SQLException; 3775 } 3776 3777 @FunctionalInterface 3778 protected interface PreparedStatementBindingOperation { 3779 void perform(@NonNull PreparedStatement preparedStatement) throws Exception; 3780 } 3781 3782 @NotThreadSafe 3783 private static final class StreamingResultSet<T> implements java.util.Iterator<T>, AutoCloseable { 3784 private final Database database; 3785 private final StatementContext<T> statementContext; 3786 private final List<Object> parameters; 3787 @Nullable 3788 private final PreparedStatementCustomizer preparedStatementCustomizer; 3789 @NonNull 3790 private final Optional<Transaction> transaction; 3791 @Nullable 3792 private final ReentrantLock connectionLock; 3793 @Nullable 3794 private Connection connection; 3795 @Nullable 3796 private PreparedStatement preparedStatement; 3797 @Nullable 3798 private ResultSet resultSet; 3799 private boolean closed; 3800 private boolean hasNextEvaluated; 3801 private boolean hasNext; 3802 @Nullable 3803 private Duration connectionAcquisitionDuration; 3804 @Nullable 3805 private Duration preparationDuration; 3806 @Nullable 3807 private Duration executionDuration; 3808 private long resultSetMappingNanos; 3809 @Nullable 3810 private Exception exception; 3811 @Nullable 3812 private Throwable thrown; 3813 private long rowsConsumed; 3814 private long openStartTime; 3815 private boolean exhausted; 3816 private boolean openFailed; 3817 private boolean terminalMetricsEmitted; 3818 @Nullable 3819 private Throwable callbackThrowable; 3820 @Nullable 3821 private Throwable iterationThrowable; 3822 @Nullable 3823 private Throwable cleanupFailure; 3824 private long connectionHeldStartTime; 3825 @NonNull 3826 private DatabaseDialect databaseStreamDialect = GenericDialect.INSTANCE; 3827 @NonNull 3828 private DatabaseStreamState databaseStreamState = DatabaseStreamState.none(); 3829 private boolean connectionLockAcquired; 3830 private final boolean queryFetchSizeConfigured; 3831 3832 private StreamingResultSet(@NonNull Database database, 3833 @NonNull StatementContext<T> statementContext, 3834 @NonNull List<Object> parameters, 3835 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 3836 boolean queryFetchSizeConfigured) { 3837 this.database = requireNonNull(database); 3838 this.statementContext = requireNonNull(statementContext); 3839 this.parameters = requireNonNull(parameters); 3840 this.preparedStatementCustomizer = preparedStatementCustomizer; 3841 this.transaction = database.currentTransactionForDatabaseOperation(); 3842 this.connectionLock = this.transaction.isPresent() ? this.transaction.get().getConnectionLock() : null; 3843 this.queryFetchSizeConfigured = queryFetchSizeConfigured; 3844 3845 open(); 3846 } 3847 3848 private void open() { 3849 long startTime = nanoTime(); 3850 this.openStartTime = startTime; 3851 this.database.getMetricsCollectorDispatcher().willOpenStream(this.statementContext); 3852 3853 try { 3854 if (this.connectionLock != null) { 3855 lockInterruptibly(this.connectionLock, "open a stream on the transaction connection"); 3856 this.connectionLockAcquired = true; 3857 } 3858 3859 boolean alreadyHasConnection = this.transaction.isPresent() && this.transaction.get().hasConnection(); 3860 if (this.transaction.isPresent()) { 3861 this.connection = this.transaction.get().getConnection(); 3862 } else { 3863 this.database.getMetricsCollectorDispatcher().willAcquireStatementConnection(this.statementContext); 3864 try { 3865 this.connection = this.database.getDataSource().getConnection(); 3866 } catch (SQLException e) { 3867 DatabaseException wrapped = new DatabaseException("Unable to acquire database connection", e); 3868 this.connectionAcquisitionDuration = Duration.ofNanos(nanoTime() - startTime); 3869 this.database.getMetricsCollectorDispatcher().didFailToAcquireStatementConnection(this.statementContext, 3870 this.database.peekDatabaseType(), this.connectionAcquisitionDuration, wrapped); 3871 throw wrapped; 3872 } catch (RuntimeException e) { 3873 this.connectionAcquisitionDuration = Duration.ofNanos(nanoTime() - startTime); 3874 this.database.getMetricsCollectorDispatcher().didFailToAcquireStatementConnection(this.statementContext, 3875 this.database.peekDatabaseType(), this.connectionAcquisitionDuration, e); 3876 throw e; 3877 } 3878 } 3879 this.connectionAcquisitionDuration = alreadyHasConnection ? null : Duration.ofNanos(nanoTime() - startTime); 3880 if (this.transaction.isEmpty()) { 3881 this.connectionHeldStartTime = nanoTime(); 3882 MetricsCollectorDispatcher metricsCollectorDispatcher = this.database.getMetricsCollectorDispatcher(); 3883 if (metricsCollectorDispatcher.isEnabled()) 3884 this.database.dispatchWithDatabaseTypeDetectionConnection(requireNonNull(this.connection), () -> 3885 metricsCollectorDispatcher.didAcquireStatementConnection(this.statementContext, this.connectionAcquisitionDuration)); 3886 } 3887 startTime = nanoTime(); 3888 3889 this.databaseStreamDialect = databaseDialectForStreamingConnection(); 3890 this.databaseStreamState = this.databaseStreamDialect.configureStreamingConnection(requireNonNull(this.connection), this.transaction.isPresent()); 3891 3892 this.preparedStatement = this.databaseStreamDialect.prepareStreamingStatement(requireNonNull(this.connection), this.statementContext); 3893 Connection previousDatabaseTypeDetectionConnection = this.database.databaseTypeDetectionConnectionHolder.get(); 3894 this.database.databaseTypeDetectionConnectionHolder.set(this.connection); 3895 3896 try { 3897 this.database.applyPreparedStatementCustomizer(this.statementContext, this.preparedStatement, this.preparedStatementCustomizer); 3898 this.databaseStreamDialect.configureStreamingPreparedStatement(this.preparedStatement, this.databaseStreamState, 3899 this.transaction.isPresent(), this.queryFetchSizeConfigured); 3900 if (this.parameters.size() > 0) 3901 this.database.performPreparedStatementBinding(this.statementContext, this.preparedStatement, this.parameters); 3902 this.preparationDuration = Duration.ofNanos(nanoTime() - startTime); 3903 3904 startTime = nanoTime(); 3905 this.resultSet = this.preparedStatement.executeQuery(); 3906 this.executionDuration = Duration.ofNanos(nanoTime() - startTime); 3907 this.database.warmDatabaseTypeCacheForMetricsIfNeeded(this.statementContext); 3908 this.database.getMetricsCollectorDispatcher().didOpenStream(this.statementContext, Duration.ofNanos(nanoTime() - this.openStartTime)); 3909 } finally { 3910 if (previousDatabaseTypeDetectionConnection == null) 3911 this.database.databaseTypeDetectionConnectionHolder.remove(); 3912 else 3913 this.database.databaseTypeDetectionConnectionHolder.set(previousDatabaseTypeDetectionConnection); 3914 } 3915 } catch (DatabaseException e) { 3916 this.exception = e; 3917 this.thrown = e; 3918 this.openFailed = true; 3919 this.database.getMetricsCollectorDispatcher().didFailToOpenStream(this.statementContext, this.database.peekDatabaseType(), 3920 Duration.ofNanos(nanoTime() - this.openStartTime), e); 3921 close(); 3922 throw e; 3923 } catch (Exception e) { 3924 DatabaseException wrapped = databaseExceptionWithStatementContext(this.statementContext, e); 3925 this.exception = e; 3926 this.thrown = wrapped; 3927 this.openFailed = true; 3928 this.database.getMetricsCollectorDispatcher().didFailToOpenStream(this.statementContext, this.database.peekDatabaseType(), 3929 Duration.ofNanos(nanoTime() - this.openStartTime), wrapped); 3930 close(); 3931 throw wrapped; 3932 } catch (Error e) { 3933 this.exception = databaseExceptionWithStatementContext(this.statementContext, e); 3934 this.thrown = e; 3935 this.openFailed = true; 3936 this.database.getMetricsCollectorDispatcher().didFailToOpenStream(this.statementContext, this.database.peekDatabaseType(), 3937 Duration.ofNanos(nanoTime() - this.openStartTime), e); 3938 close(); 3939 throw e; 3940 } 3941 } 3942 3943 @NonNull 3944 private DatabaseDialect databaseDialectForStreamingConnection() { 3945 try { 3946 return this.database.getDatabaseDialect(requireNonNull(this.connection)); 3947 } catch (DatabaseException e) { 3948 return this.database.peekDatabaseType().dialect(); 3949 } 3950 } 3951 3952 @Override 3953 public boolean hasNext() { 3954 if (this.closed) 3955 return false; 3956 3957 if (!this.hasNextEvaluated) { 3958 try { 3959 this.hasNext = this.resultSet != null && this.resultSet.next(); 3960 this.hasNextEvaluated = true; 3961 if (!this.hasNext) { 3962 this.exhausted = true; 3963 close(); 3964 } 3965 } catch (SQLException e) { 3966 DatabaseException wrapped = databaseExceptionWithStatementContext(this.statementContext, e); 3967 this.exception = e; 3968 this.thrown = wrapped; 3969 this.iterationThrowable = wrapped; 3970 close(); 3971 throw wrapped; 3972 } catch (RuntimeException e) { 3973 this.exception = e; 3974 this.thrown = e; 3975 this.iterationThrowable = e; 3976 close(); 3977 throw e; 3978 } catch (Error e) { 3979 this.exception = databaseExceptionWithStatementContext(this.statementContext, e); 3980 this.thrown = e; 3981 this.iterationThrowable = e; 3982 close(); 3983 throw e; 3984 } 3985 } 3986 3987 return this.hasNext; 3988 } 3989 3990 @Override 3991 public T next() { 3992 if (!hasNext()) 3993 throw new java.util.NoSuchElementException(); 3994 3995 this.hasNextEvaluated = false; 3996 long startTime = nanoTime(); 3997 3998 try { 3999 Connection previousDatabaseTypeDetectionConnection = this.database.databaseTypeDetectionConnectionHolder.get(); 4000 this.database.databaseTypeDetectionConnectionHolder.set(requireNonNull(this.connection)); 4001 T value; 4002 4003 try { 4004 value = this.database.getResultSetMapper() 4005 .map(this.statementContext, requireNonNull(this.resultSet), this.statementContext.getResultSetRowType().get(), this.database.getInstanceProvider()) 4006 .orElse(null); 4007 } finally { 4008 if (previousDatabaseTypeDetectionConnection == null) 4009 this.database.databaseTypeDetectionConnectionHolder.remove(); 4010 else 4011 this.database.databaseTypeDetectionConnectionHolder.set(previousDatabaseTypeDetectionConnection); 4012 } 4013 4014 this.resultSetMappingNanos += nanoTime() - startTime; 4015 this.rowsConsumed++; 4016 return value; 4017 } catch (SQLException e) { 4018 DatabaseException wrapped = databaseExceptionWithStatementContext(this.statementContext, 4019 format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), this.statementContext.getResultSetRowType().get()), e); 4020 this.exception = e; 4021 this.thrown = wrapped; 4022 this.iterationThrowable = wrapped; 4023 close(); 4024 throw wrapped; 4025 } catch (DatabaseException e) { 4026 this.exception = e; 4027 this.thrown = e; 4028 this.iterationThrowable = e; 4029 close(); 4030 throw e; 4031 } catch (RuntimeException e) { 4032 this.exception = e; 4033 this.thrown = e; 4034 this.iterationThrowable = e; 4035 close(); 4036 throw e; 4037 } catch (Error e) { 4038 this.exception = databaseExceptionWithStatementContext(this.statementContext, 4039 format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), this.statementContext.getResultSetRowType().get()), e); 4040 this.thrown = e; 4041 this.iterationThrowable = e; 4042 close(); 4043 throw e; 4044 } 4045 } 4046 4047 private void callbackFailed(@NonNull Throwable throwable) { 4048 requireNonNull(throwable); 4049 this.callbackThrowable = throwable; 4050 } 4051 4052 @Override 4053 public void close() { 4054 if (this.closed) 4055 return; 4056 4057 if (this.connectionLockAcquired && this.connectionLock != null && !this.connectionLock.isHeldByCurrentThread()) 4058 throw new DatabaseException("Transactional streams must be closed by the thread that opened them"); 4059 4060 this.closed = true; 4061 Throwable cleanupFailure = null; 4062 4063 try { 4064 cleanupFailure = closeStatementContextResources(this.statementContext, cleanupFailure); 4065 4066 if (this.resultSet != null) { 4067 try { 4068 this.resultSet.close(); 4069 } catch (Throwable cleanupException) { 4070 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 4071 } 4072 } 4073 4074 if (this.preparedStatement != null) { 4075 try { 4076 this.preparedStatement.close(); 4077 } catch (Throwable cleanupException) { 4078 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 4079 } 4080 } 4081 4082 cleanupFailure = completeDialectStreamingConnectionIfNeeded(cleanupFailure); 4083 4084 if (this.connection != null && this.transaction.isEmpty()) { 4085 Duration heldDuration = this.connectionHeldStartTime == 0L 4086 ? Duration.ZERO 4087 : Duration.ofNanos(nanoTime() - this.connectionHeldStartTime); 4088 try { 4089 this.database.closeConnection(this.connection); 4090 this.database.getMetricsCollectorDispatcher().didReleaseStatementConnection(this.statementContext, heldDuration); 4091 } catch (Throwable cleanupException) { 4092 this.database.getMetricsCollectorDispatcher().didFailToReleaseStatementConnection(this.statementContext, heldDuration, cleanupException); 4093 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 4094 } 4095 } 4096 } finally { 4097 if (this.connectionLockAcquired) { 4098 this.connectionLock.unlock(); 4099 this.connectionLockAcquired = false; 4100 } 4101 4102 Duration mappingDuration = this.resultSetMappingNanos == 0L ? null : Duration.ofNanos(this.resultSetMappingNanos); 4103 4104 StatementLog statementLog = 4105 StatementLog.withStatementContext(this.statementContext) 4106 .connectionAcquisitionDuration(this.connectionAcquisitionDuration) 4107 .preparationDuration(this.preparationDuration) 4108 .executionDuration(this.executionDuration) 4109 .resultSetMappingDuration(mappingDuration) 4110 .exception(this.exception) 4111 .build(); 4112 4113 if (this.thrown == null && this.exception == null) { 4114 this.database.getMetricsCollectorDispatcher().didExecuteStatement(this.statementContext, statementLog, StatementResult.empty()); 4115 } else { 4116 Throwable statementThrowable = this.thrown == null ? this.exception : this.thrown; 4117 this.database.getMetricsCollectorDispatcher().didFailToExecuteStatement(this.statementContext, statementLog, 4118 this.database.peekDatabaseType(), requireNonNull(statementThrowable)); 4119 } 4120 4121 try { 4122 this.database.getStatementLogger().log(statementLog); 4123 } catch (Throwable cleanupException) { 4124 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 4125 } 4126 } 4127 4128 this.cleanupFailure = cleanupFailure; 4129 4130 if (cleanupFailure != null) { 4131 if (this.thrown != null) { 4132 this.thrown.addSuppressed(cleanupFailure); 4133 } else if (cleanupFailure instanceof RuntimeException) { 4134 throw (RuntimeException) cleanupFailure; 4135 } else if (cleanupFailure instanceof Error) { 4136 throw (Error) cleanupFailure; 4137 } else { 4138 throw new RuntimeException(cleanupFailure); 4139 } 4140 } 4141 } 4142 4143 @Nullable 4144 private Throwable completeDialectStreamingConnectionIfNeeded(@Nullable Throwable cleanupFailure) { 4145 if (this.connection == null) 4146 return cleanupFailure; 4147 4148 boolean streamSucceeded = this.thrown == null && this.exception == null && this.callbackThrowable == null && !this.openFailed; 4149 cleanupFailure = this.databaseStreamDialect.completeStreamingConnection(requireNonNull(this.connection), 4150 this.databaseStreamState, streamSucceeded, cleanupFailure); 4151 4152 this.databaseStreamDialect = GenericDialect.INSTANCE; 4153 this.databaseStreamState = DatabaseStreamState.none(); 4154 return cleanupFailure; 4155 } 4156 4157 private void emitTerminalMetrics() { 4158 if (this.terminalMetricsEmitted) 4159 return; 4160 4161 this.terminalMetricsEmitted = true; 4162 4163 if (this.openFailed) 4164 return; 4165 4166 MetricsCollector.StreamTerminalOutcome outcome; 4167 Throwable throwable; 4168 4169 if (this.iterationThrowable != null) { 4170 outcome = MetricsCollector.StreamTerminalOutcome.ITERATION_FAILURE; 4171 throwable = this.iterationThrowable; 4172 } else if (this.callbackThrowable != null) { 4173 outcome = MetricsCollector.StreamTerminalOutcome.CALLBACK_FAILURE; 4174 throwable = this.callbackThrowable; 4175 } else if (this.exhausted) { 4176 outcome = MetricsCollector.StreamTerminalOutcome.COMPLETED_NORMALLY; 4177 throwable = this.cleanupFailure; 4178 } else { 4179 outcome = MetricsCollector.StreamTerminalOutcome.EARLY_CLOSE; 4180 throwable = this.cleanupFailure; 4181 } 4182 4183 this.database.getMetricsCollectorDispatcher().didCloseStream(this.statementContext, outcome, this.rowsConsumed, 4184 Duration.ofNanos(nanoTime() - this.openStartTime), throwable); 4185 } 4186 4187 @NonNull 4188 private static Throwable addSuppressed(@NonNull Throwable existing, 4189 @NonNull Throwable additional) { 4190 existing.addSuppressed(additional); 4191 return existing; 4192 } 4193 } 4194 4195 /** 4196 * Builder used to construct instances of {@link Database}. 4197 * <p> 4198 * This class is intended for use by a single thread. 4199 * 4200 * @author <a href="https://www.revetkn.com">Mark Allen</a> 4201 * @since 1.0.0 4202 */ 4203 @NotThreadSafe 4204 public static class Builder { 4205 @NonNull 4206 private final DataSource dataSource; 4207 @Nullable 4208 private DatabaseType databaseType; 4209 @Nullable 4210 private ZoneId timeZone; 4211 @Nullable 4212 private AmbiguousTimestampBindingStrategy ambiguousTimestampBindingStrategy; 4213 @Nullable 4214 private InstanceProvider instanceProvider; 4215 @Nullable 4216 private PreparedStatementBinder preparedStatementBinder; 4217 @Nullable 4218 private ResultSetMapper resultSetMapper; 4219 @Nullable 4220 private StatementLogger statementLogger; 4221 @Nullable 4222 private ParameterRedactor parameterRedactor; 4223 @Nullable 4224 private MetricsCollector metricsCollector; 4225 @Nullable 4226 private Duration queryTimeout; 4227 @Nullable 4228 private Integer fetchSize; 4229 @Nullable 4230 private Integer maxRows; 4231 @Nullable 4232 private Integer parsedSqlCacheCapacity; 4233 4234 private Builder(@NonNull DataSource dataSource) { 4235 this.dataSource = requireNonNull(dataSource); 4236 this.databaseType = null; 4237 this.metricsCollector = null; 4238 this.queryTimeout = null; 4239 this.fetchSize = null; 4240 this.maxRows = null; 4241 this.parsedSqlCacheCapacity = null; 4242 } 4243 4244 /** 4245 * Overrides automatic database type detection. 4246 * <p> 4247 * If {@code null}, the database type is detected lazily when database-type-specific behavior is first needed. 4248 * Supplying a non-null value avoids automatic detection and its metadata lookup entirely. 4249 * 4250 * @param databaseType the database type to use (null to enable auto-detection) 4251 * @return this {@code Builder}, for chaining 4252 * @since 4.0.0 4253 */ 4254 @NonNull 4255 public Builder databaseType(@Nullable DatabaseType databaseType) { 4256 this.databaseType = databaseType; 4257 return this; 4258 } 4259 4260 /** 4261 * Configures the database time zone Pyranid should use when converting zone-less temporal values. 4262 * <p> 4263 * This value is used when mapping {@code TIMESTAMP} values to instant-based Java types, and when binding 4264 * {@link java.time.Instant} or {@link java.time.OffsetDateTime} parameters to known {@code TIMESTAMP} 4265 * targets. It also applies to ambiguous timestamp bindings if 4266 * {@link #ambiguousTimestampBindingStrategy(AmbiguousTimestampBindingStrategy)} is configured with 4267 * {@link AmbiguousTimestampBindingStrategy#TIMESTAMP_WITHOUT_TIME_ZONE}. 4268 * <p> 4269 * If {@code null}, Pyranid uses {@link ZoneId#systemDefault()}. 4270 * 4271 * @param timeZone database time zone to use, or {@code null} for the JVM default zone 4272 * @return this {@code Builder}, for chaining 4273 * @since 3.0.0 4274 */ 4275 @NonNull 4276 public Builder timeZone(@Nullable ZoneId timeZone) { 4277 this.timeZone = timeZone; 4278 return this; 4279 } 4280 4281 /** 4282 * Configures how Pyranid binds {@link java.time.Instant} and {@link java.time.OffsetDateTime} parameters 4283 * when JDBC parameter metadata cannot identify whether the target is {@code TIMESTAMP} or 4284 * {@code TIMESTAMP WITH TIME ZONE}. 4285 * <p> 4286 * The default, {@link AmbiguousTimestampBindingStrategy#TIMESTAMP_WITH_TIME_ZONE}, is appropriate for 4287 * timestamp-with-time-zone targets. Use 4288 * {@link AmbiguousTimestampBindingStrategy#TIMESTAMP_WITHOUT_TIME_ZONE} for drivers or proxies that 4289 * cannot provide identifying parameter metadata when your target columns are zone-less {@code TIMESTAMP} 4290 * values that should be interpreted in {@link #timeZone(ZoneId)}. 4291 * 4292 * @param ambiguousTimestampBindingStrategy strategy to use, or {@code null} for the default 4293 * @return this {@code Builder}, for chaining 4294 * @since 4.2.0 4295 */ 4296 @NonNull 4297 public Builder ambiguousTimestampBindingStrategy(@Nullable AmbiguousTimestampBindingStrategy ambiguousTimestampBindingStrategy) { 4298 this.ambiguousTimestampBindingStrategy = ambiguousTimestampBindingStrategy; 4299 return this; 4300 } 4301 4302 @NonNull 4303 public Builder instanceProvider(@Nullable InstanceProvider instanceProvider) { 4304 this.instanceProvider = instanceProvider; 4305 return this; 4306 } 4307 4308 @NonNull 4309 public Builder preparedStatementBinder(@Nullable PreparedStatementBinder preparedStatementBinder) { 4310 this.preparedStatementBinder = preparedStatementBinder; 4311 return this; 4312 } 4313 4314 @NonNull 4315 public Builder resultSetMapper(@Nullable ResultSetMapper resultSetMapper) { 4316 this.resultSetMapper = resultSetMapper; 4317 return this; 4318 } 4319 4320 /** 4321 * Configures the statement logger for the {@link Database} being built. 4322 * <p> 4323 * {@link StatementLogger} failures are fail-fast: a logger exception can make a successful statement operation throw, 4324 * and inside a Pyranid transaction it participates in normal rollback handling. If the database statement itself failed, 4325 * logger failures are suppressed onto the primary statement failure. 4326 * 4327 * @param statementLogger statement logger to use, or {@code null} for a no-op logger 4328 * @return this {@code Builder}, for chaining 4329 */ 4330 @NonNull 4331 public Builder statementLogger(@Nullable StatementLogger statementLogger) { 4332 this.statementLogger = statementLogger; 4333 return this; 4334 } 4335 4336 /** 4337 * Configures the redactor used for non-secure parameters in diagnostics. 4338 * <p> 4339 * {@link SecureParameter} values always render via {@link SecureParameter#getMask()} and are never passed to this 4340 * redactor. Batch executions render a bounded batch summary instead of invoking the redactor for each batch value. 4341 * Specify {@code null} or omit this setter to render non-secure, non-batch values verbatim. 4342 * 4343 * @param parameterRedactor parameter redactor to use, or {@code null} for the default 4344 * @return this {@code Builder}, for chaining 4345 * @since 4.4.0 4346 */ 4347 @NonNull 4348 public Builder parameterRedactor(@Nullable ParameterRedactor parameterRedactor) { 4349 this.parameterRedactor = parameterRedactor; 4350 return this; 4351 } 4352 4353 /** 4354 * Configures the metrics collector for the {@link Database} being built. 4355 * <p> 4356 * Like all {@code Database} configuration, this value is fixed at {@link #build()} time. Specify {@code null} 4357 * or omit this setter to disable metrics collection. 4358 * 4359 * @param metricsCollector metrics collector to use, or {@code null} to disable 4360 * @return this {@code Builder}, for chaining 4361 * @since 4.2.0 4362 */ 4363 @NonNull 4364 public Builder metricsCollector(@Nullable MetricsCollector metricsCollector) { 4365 this.metricsCollector = metricsCollector; 4366 return this; 4367 } 4368 4369 /** 4370 * Configures a database-wide JDBC query timeout default. 4371 * <p> 4372 * This maps to {@link java.sql.Statement#setQueryTimeout(int)}. {@code null} leaves the timeout unset. 4373 * {@link Duration#ZERO} disables the JDBC timeout. Positive sub-second durations are rounded up to one second 4374 * because JDBC accepts whole seconds. Per-query {@link Query#queryTimeout(Duration)} settings override this value, 4375 * and {@link Query#customize(PreparedStatementCustomizer)} runs last. 4376 * 4377 * @param queryTimeout timeout to apply by default, or {@code null} to leave unset 4378 * @return this {@code Builder}, for chaining 4379 * @since 4.2.0 4380 */ 4381 @NonNull 4382 public Builder queryTimeout(@Nullable Duration queryTimeout) { 4383 this.queryTimeout = validateQueryTimeout(queryTimeout); 4384 return this; 4385 } 4386 4387 /** 4388 * Configures a database-wide JDBC fetch size default. 4389 * <p> 4390 * This maps to {@link java.sql.Statement#setFetchSize(int)}. {@code null} leaves the fetch size unset. A value 4391 * of {@code 0} uses the driver's default fetch-size behavior. Per-query {@link Query#fetchSize(Integer)} 4392 * settings override this value, and {@link Query#customize(PreparedStatementCustomizer)} runs last. 4393 * 4394 * @param fetchSize fetch size to apply by default, or {@code null} to leave unset 4395 * @return this {@code Builder}, for chaining 4396 * @since 4.2.0 4397 */ 4398 @NonNull 4399 public Builder fetchSize(@Nullable Integer fetchSize) { 4400 this.fetchSize = validateNonNegativeStatementSetting("fetchSize", fetchSize); 4401 return this; 4402 } 4403 4404 /** 4405 * Configures a database-wide JDBC maximum row count default. 4406 * <p> 4407 * This maps to {@link java.sql.Statement#setMaxRows(int)}. {@code null} leaves the maximum row count unset. A 4408 * value of {@code 0} disables the JDBC row limit. Per-query {@link Query#maxRows(Integer)} settings override 4409 * this value, and {@link Query#customize(PreparedStatementCustomizer)} runs last. 4410 * 4411 * @param maxRows maximum rows to apply by default, or {@code null} to leave unset 4412 * @return this {@code Builder}, for chaining 4413 * @since 4.2.0 4414 */ 4415 @NonNull 4416 public Builder maxRows(@Nullable Integer maxRows) { 4417 this.maxRows = validateNonNegativeStatementSetting("maxRows", maxRows); 4418 return this; 4419 } 4420 4421 /** 4422 * Configures the size of the parsed SQL cache. 4423 * <p> 4424 * A value of {@code 0} disables caching. A value of {@code null} uses the default size. 4425 * 4426 * @param parsedSqlCacheCapacity cache size (0 disables caching, null uses default) 4427 * @return this {@code Builder}, for chaining 4428 */ 4429 @NonNull 4430 public Builder parsedSqlCacheCapacity(@Nullable Integer parsedSqlCacheCapacity) { 4431 if (parsedSqlCacheCapacity != null && parsedSqlCacheCapacity < 0) 4432 throw new IllegalArgumentException("parsedSqlCacheCapacity must be >= 0"); 4433 4434 this.parsedSqlCacheCapacity = parsedSqlCacheCapacity; 4435 return this; 4436 } 4437 4438 @NonNull 4439 public Database build() { 4440 return new Database(this); 4441 } 4442 } 4443 4444 @ThreadSafe 4445 static class DatabaseOperationResult { 4446 @Nullable 4447 private final Duration executionDuration; 4448 @Nullable 4449 private final Duration resultSetMappingDuration; 4450 @NonNull 4451 private final StatementResult statementResult; 4452 4453 public DatabaseOperationResult(@Nullable Duration executionDuration, 4454 @Nullable Duration resultSetMappingDuration) { 4455 this(executionDuration, resultSetMappingDuration, StatementResult.empty()); 4456 } 4457 4458 public DatabaseOperationResult(@Nullable Duration executionDuration, 4459 @Nullable Duration resultSetMappingDuration, 4460 @NonNull StatementResult statementResult) { 4461 this.executionDuration = executionDuration; 4462 this.resultSetMappingDuration = resultSetMappingDuration; 4463 this.statementResult = requireNonNull(statementResult); 4464 } 4465 4466 @NonNull 4467 public Optional<Duration> getExecutionDuration() { 4468 return Optional.ofNullable(this.executionDuration); 4469 } 4470 4471 @NonNull 4472 public Optional<Duration> getResultSetMappingDuration() { 4473 return Optional.ofNullable(this.resultSetMappingDuration); 4474 } 4475 4476 @NonNull 4477 public StatementResult getStatementResult() { 4478 return this.statementResult; 4479 } 4480 } 4481 4482 @NotThreadSafe 4483 static class ResultHolder<T> { 4484 T value; 4485 } 4486 4487 enum DatabaseOperationSupportStatus { 4488 UNKNOWN, 4489 YES, 4490 NO 4491 } 4492 4493}