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