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.time.Duration; 032import java.time.ZoneId; 033import java.util.Arrays; 034import java.util.List; 035import java.util.Locale; 036import java.util.Optional; 037import java.util.Queue; 038import java.util.Set; 039import java.util.concurrent.atomic.AtomicReference; 040import java.util.concurrent.atomic.AtomicLong; 041import java.util.concurrent.locks.ReentrantLock; 042import java.util.function.UnaryOperator; 043import java.util.regex.Pattern; 044 045import static java.lang.String.format; 046import static java.lang.System.nanoTime; 047import static java.util.Objects.requireNonNull; 048 049/** 050 * Main class for performing database access operations. 051 * 052 * @author <a href="https://www.revetkn.com">Mark Allen</a> 053 * @since 1.0.0 054 */ 055@ThreadSafe 056public final class Database { 057 private static final int DEFAULT_PARSED_SQL_CACHE_CAPACITY = 1024; 058 private static final int MAX_DIAGNOSTIC_MESSAGE_LENGTH = 1024; 059 private static final int MAX_DIAGNOSTIC_PARAMETER_LENGTH = 256; 060 private static final int MAX_DIAGNOSTIC_PARAMETERS_LENGTH = 2048; 061 private static final int MAX_DIAGNOSTIC_SQL_LENGTH = 2048; 062 @NonNull 063 private static final String TRUNCATED_SUFFIX = "... (truncated)"; 064 @NonNull 065 private static final Pattern DIAGNOSTIC_WHITESPACE_PATTERN = Pattern.compile("\\s+"); 066 067 @NonNull 068 private final DataSource dataSource; 069 @NonNull 070 private final DatabaseType databaseType; 071 @NonNull 072 private final DatabaseDialect databaseDialect; 073 @NonNull 074 private final ZoneId timeZone; 075 @NonNull 076 private final AmbiguousTimestampBindingStrategy ambiguousTimestampBindingStrategy; 077 @NonNull 078 private final InstanceProvider instanceProvider; 079 @NonNull 080 private final ResultSetMapper resultSetMapper; 081 @NonNull 082 private final ParameterRedactor parameterRedactor; 083 @NonNull 084 private final MetricsCollectorDispatcher metricsCollectorDispatcher; 085 @NonNull 086 private final TransactionCoordinator transactions; 087 @Nullable 088 private final Duration queryTimeout; 089 @Nullable 090 private final Integer fetchSize; 091 @Nullable 092 private final Integer maxRows; 093 @NonNull 094 private final SqlCompiler sqlCompiler; 095 @NonNull 096 private final AtomicLong defaultIdGenerator; 097 098 @NonNull 099 private final StatementExecutor statementExecutor; 100 101 private Database(@NonNull Builder builder) { 102 requireNonNull(builder); 103 104 this.dataSource = requireNonNull(builder.dataSource); 105 this.databaseType = builder.databaseType; 106 this.databaseDialect = this.databaseType.dialect(); 107 this.timeZone = builder.timeZone == null ? ZoneId.systemDefault() : builder.timeZone; 108 this.ambiguousTimestampBindingStrategy = builder.ambiguousTimestampBindingStrategy == null 109 ? AmbiguousTimestampBindingStrategy.TIMESTAMP_WITH_TIME_ZONE 110 : builder.ambiguousTimestampBindingStrategy; 111 this.instanceProvider = builder.instanceProvider == null ? new InstanceProvider() {} : builder.instanceProvider; 112 PreparedStatementBinder preparedStatementBinder = builder.preparedStatementBinder == null ? PreparedStatementBinder.withDefaultConfiguration() : builder.preparedStatementBinder; 113 this.resultSetMapper = builder.resultSetMapper == null ? ResultSetMapper.withDefaultConfiguration() : builder.resultSetMapper; 114 StatementLogger statementLogger = builder.statementLogger == null ? (statementLog) -> {} : builder.statementLogger; 115 this.parameterRedactor = builder.parameterRedactor == null ? ParameterRedactor.none() : builder.parameterRedactor; 116 this.metricsCollectorDispatcher = new MetricsCollectorDispatcher(builder.metricsCollector); 117 this.transactions = new TransactionCoordinator(this.dataSource, this.metricsCollectorDispatcher, 118 this.databaseType); 119 this.queryTimeout = validateQueryTimeout(builder.queryTimeout); 120 this.fetchSize = validateNonNegativeStatementSetting("fetchSize", builder.fetchSize); 121 this.maxRows = validateNonNegativeStatementSetting("maxRows", builder.maxRows); 122 if (builder.parsedSqlCacheCapacity != null && builder.parsedSqlCacheCapacity < 0) 123 throw new IllegalArgumentException("parsedSqlCacheCapacity must be >= 0"); 124 125 int parsedSqlCacheCapacity = builder.parsedSqlCacheCapacity == null 126 ? DEFAULT_PARSED_SQL_CACHE_CAPACITY 127 : builder.parsedSqlCacheCapacity; 128 this.sqlCompiler = new SqlCompiler(parsedSqlCacheCapacity); 129 this.defaultIdGenerator = new AtomicLong(); 130 this.statementExecutor = new StatementExecutor(this, this.instanceProvider, this.resultSetMapper, preparedStatementBinder, 131 statementLogger, this.metricsCollectorDispatcher, new StatementConnections(this.dataSource, 132 this::currentTransactionForDatabaseOperation, connection -> closeConnection(connection, this.databaseDialect), 133 this.databaseType, this.databaseDialect)); 134 } 135 136 /** 137 * Provides a {@link Database} builder for the given {@link DataSource} and database type. 138 * <p> 139 * Neither creating the builder nor calling {@link Builder#build()} acquires a connection or inspects JDBC metadata. 140 * 141 * @param dataSource data source used to create the {@link Database} builder 142 * @param databaseType database type to use; choose {@link DatabaseType#GENERIC} for a database without dedicated handling 143 * @return a {@link Database} builder 144 * @since 4.8.0 145 */ 146 @NonNull 147 public static Builder with(@NonNull DataSource dataSource, @NonNull DatabaseType databaseType) { 148 return new Builder(dataSource, databaseType); 149 } 150 151 /** 152 * Gets a reference to the current transaction, if any. 153 * 154 * @return the current transaction 155 */ 156 @NonNull 157 public Optional<Transaction> currentTransaction() { 158 return this.transactions.currentTransaction(); 159 } 160 161 @NonNull 162 Optional<Transaction> currentTransactionForDatabaseOperation() { 163 return this.transactions.currentTransactionForDatabaseOperation(); 164 } 165 166 static boolean hasAmbientTransaction() { 167 return TransactionCoordinator.hasAmbientTransaction(); 168 } 169 170 /** 171 * Performs an operation transactionally. 172 * <p> 173 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 174 * <p> 175 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 176 * not automatically join an outer transaction. Use {@link #participate(Transaction, TransactionalOperation)} to join an 177 * existing transaction explicitly. A transaction is scoped to the {@link DataSource} instance that created it; a 178 * {@link Database} using a different {@link DataSource} fails fast instead of joining it. 179 * 180 * @param transactionalOperation the operation to perform transactionally 181 */ 182 public void transaction(@NonNull TransactionalOperation transactionalOperation) { 183 requireNonNull(transactionalOperation); 184 185 transaction(() -> { 186 transactionalOperation.perform(); 187 return Optional.empty(); 188 }); 189 } 190 191 /** 192 * Performs an operation transactionally with the given options. 193 * <p> 194 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 195 * <p> 196 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 197 * not automatically join an outer transaction. Use {@link #participate(Transaction, TransactionalOperation)} to join an 198 * existing transaction explicitly. A transaction is scoped to the {@link DataSource} instance that created it; a 199 * {@link Database} using a different {@link DataSource} fails fast instead of joining it. 200 * 201 * @param transactionOptions options to apply to the transaction 202 * @param transactionalOperation the operation to perform transactionally 203 * @since 4.2.0 204 */ 205 public void transaction(@NonNull TransactionOptions transactionOptions, 206 @NonNull TransactionalOperation transactionalOperation) { 207 requireNonNull(transactionOptions); 208 requireNonNull(transactionalOperation); 209 210 transaction(transactionOptions, () -> { 211 transactionalOperation.perform(); 212 return Optional.empty(); 213 }); 214 } 215 216 /** 217 * Performs an operation transactionally and optionally returns a value. 218 * <p> 219 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 220 * <p> 221 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 222 * not automatically join an outer transaction. Use {@link #participate(Transaction, ReturningTransactionalOperation)} to 223 * join an existing transaction explicitly. A transaction is scoped to the {@link DataSource} instance that created it; a 224 * {@link Database} using a different {@link DataSource} fails fast instead of joining it. 225 * 226 * @param transactionalOperation the operation to perform transactionally 227 * @param <T> the type to be returned 228 * @return the result of the transactional operation 229 */ 230 @NonNull 231 public <T> Optional<T> transaction(@NonNull ReturningTransactionalOperation<T> transactionalOperation) { 232 requireNonNull(transactionalOperation); 233 return transaction(TransactionOptions.DEFAULT, transactionalOperation); 234 } 235 236 /** 237 * Performs an operation transactionally with the given options, optionally returning a value. 238 * <p> 239 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 240 * <p> 241 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 242 * not automatically join an outer transaction. Use {@link #participate(Transaction, ReturningTransactionalOperation)} to 243 * join an existing transaction explicitly. A transaction is scoped to the {@link DataSource} instance that created it; a 244 * {@link Database} using a different {@link DataSource} fails fast instead of joining it. 245 * 246 * @param transactionOptions options to apply to the transaction 247 * @param transactionalOperation the operation to perform transactionally 248 * @param <T> the type to be returned 249 * @return the result of the transactional operation 250 * @since 4.2.0 251 */ 252 @NonNull 253 public <T> Optional<T> transaction(@NonNull TransactionOptions transactionOptions, 254 @NonNull ReturningTransactionalOperation<T> transactionalOperation) { 255 return this.transactions.run(transactionOptions, transactionalOperation); 256 } 257 258 /** 259 * Performs an operation transactionally, retrying according to the given retry policy. 260 * <p> 261 * The entire transaction closure may run more than once. Keep non-idempotent external side effects outside the closure 262 * unless they are safe to repeat. 263 * <p> 264 * Pyranid consults the retry policy only when replay is known to be safe. This includes a failure before a physical 265 * transaction becomes available to application work, even if best-effort rollback of that begin candidate fails, because 266 * the candidate is discarded. It also includes a recognized serialization failure reported by physical commit when the 267 * follow-up rollback succeeds. Other commit failures and rollback failures after application work could execute are 268 * terminal because their outcome is indeterminate. 269 * <p> 270 * A savepoint-scope ownership conflict at completion is never replayed, with or without a successful rollback: the 271 * guard raises an {@link IllegalStateException}, not a {@link DatabaseException}, so the conflict leaves this method 272 * without reaching the retry policy. What a successful rollback changes is the attempt's outcome evidence; the conflict 273 * no longer records an indeterminate outcome, so a conflict raised and handled by an independent transaction inside the 274 * closure no longer fences a retry of a later failure. 275 * <p> 276 * Unlike {@link #transaction(TransactionalOperation)} and related transaction methods, retrying methods return 277 * {@link TransactionRetryResult} so callers can inspect failures that were recovered before success. 278 * <p> 279 * This method rejects a call when the innermost ambient transaction on the calling thread uses the same 280 * {@link DataSource} instance, including across different {@code Database} wrappers. Only that innermost transaction 281 * is checked. If a transaction on another source hides an older matching transaction, this method starts an independent 282 * retrying transaction; retries do not restart either surrounding transaction. 283 * 284 * @param retryPolicy retry policy to apply 285 * @param transactionalOperation the operation to perform transactionally 286 * @return retry result containing any failures retried before success 287 * @since 4.4.0 288 */ 289 @NonNull 290 public TransactionRetryResult<Void> transactionWithRetry(@NonNull RetryPolicy retryPolicy, 291 @NonNull TransactionalOperation transactionalOperation) { 292 requireNonNull(retryPolicy); 293 requireNonNull(transactionalOperation); 294 295 return transactionWithRetry(retryPolicy, () -> { 296 transactionalOperation.perform(); 297 return Optional.<Void>empty(); 298 }); 299 } 300 301 /** 302 * Performs an operation transactionally with the given options, retrying according to the given retry policy. 303 * <p> 304 * The entire transaction closure may run more than once. Keep non-idempotent external side effects outside the closure 305 * unless they are safe to repeat. 306 * <p> 307 * Pyranid consults the retry policy only when replay is known to be safe. This includes a failure before a physical 308 * transaction becomes available to application work, even if best-effort rollback of that begin candidate fails, because 309 * the candidate is discarded. It also includes a recognized serialization failure reported by physical commit when the 310 * follow-up rollback succeeds. Other commit failures and rollback failures after application work could execute are 311 * terminal because their outcome is indeterminate. 312 * <p> 313 * A savepoint-scope ownership conflict at completion is never replayed, with or without a successful rollback: the 314 * guard raises an {@link IllegalStateException}, not a {@link DatabaseException}, so the conflict leaves this method 315 * without reaching the retry policy. What a successful rollback changes is the attempt's outcome evidence; the conflict 316 * no longer records an indeterminate outcome, so a conflict raised and handled by an independent transaction inside the 317 * closure no longer fences a retry of a later failure. 318 * <p> 319 * Unlike {@link #transaction(TransactionOptions, TransactionalOperation)} and related transaction methods, retrying 320 * methods return {@link TransactionRetryResult} so callers can inspect failures that were recovered before success. 321 * <p> 322 * This method rejects a call when the innermost ambient transaction on the calling thread uses the same 323 * {@link DataSource} instance, including across different {@code Database} wrappers. Only that innermost transaction 324 * is checked. If a transaction on another source hides an older matching transaction, this method starts an independent 325 * retrying transaction; retries do not restart either surrounding transaction. 326 * 327 * @param retryPolicy retry policy to apply 328 * @param transactionOptions options to apply to each transaction attempt 329 * @param transactionalOperation the operation to perform transactionally 330 * @return retry result containing any failures retried before success 331 * @since 4.4.0 332 */ 333 @NonNull 334 public TransactionRetryResult<Void> transactionWithRetry(@NonNull RetryPolicy retryPolicy, 335 @NonNull TransactionOptions transactionOptions, 336 @NonNull TransactionalOperation transactionalOperation) { 337 requireNonNull(retryPolicy); 338 requireNonNull(transactionOptions); 339 requireNonNull(transactionalOperation); 340 341 return transactionWithRetry(retryPolicy, transactionOptions, () -> { 342 transactionalOperation.perform(); 343 return Optional.<Void>empty(); 344 }); 345 } 346 347 /** 348 * Performs an operation transactionally and optionally returns a value, retrying according to the given retry policy. 349 * <p> 350 * The entire transaction closure may run more than once. Keep non-idempotent external side effects outside the closure 351 * unless they are safe to repeat. 352 * <p> 353 * Pyranid consults the retry policy only when replay is known to be safe. This includes a failure before a physical 354 * transaction becomes available to application work, even if best-effort rollback of that begin candidate fails, because 355 * the candidate is discarded. It also includes a recognized serialization failure reported by physical commit when the 356 * follow-up rollback succeeds. Other commit failures and rollback failures after application work could execute are 357 * terminal because their outcome is indeterminate. 358 * <p> 359 * A savepoint-scope ownership conflict at completion is never replayed, with or without a successful rollback: the 360 * guard raises an {@link IllegalStateException}, not a {@link DatabaseException}, so the conflict leaves this method 361 * without reaching the retry policy. What a successful rollback changes is the attempt's outcome evidence; the conflict 362 * no longer records an indeterminate outcome, so a conflict raised and handled by an independent transaction inside the 363 * closure no longer fences a retry of a later failure. 364 * <p> 365 * Unlike {@link #transaction(ReturningTransactionalOperation)} and related transaction methods, retrying methods return 366 * {@link TransactionRetryResult} so callers can inspect failures that were recovered before success. 367 * <p> 368 * This method rejects a call when the innermost ambient transaction on the calling thread uses the same 369 * {@link DataSource} instance, including across different {@code Database} wrappers. Only that innermost transaction 370 * is checked. If a transaction on another source hides an older matching transaction, this method starts an independent 371 * retrying transaction; retries do not restart either surrounding transaction. 372 * 373 * @param retryPolicy retry policy to apply 374 * @param transactionalOperation the operation to perform transactionally 375 * @param <T> the type to be returned 376 * @return retry result containing the successful transaction value and any failures retried before success 377 * @since 4.4.0 378 */ 379 @NonNull 380 public <T> TransactionRetryResult<T> transactionWithRetry(@NonNull RetryPolicy retryPolicy, 381 @NonNull ReturningTransactionalOperation<T> transactionalOperation) { 382 requireNonNull(retryPolicy); 383 requireNonNull(transactionalOperation); 384 385 return transactionWithRetry(retryPolicy, TransactionOptions.DEFAULT, transactionalOperation); 386 } 387 388 /** 389 * Performs an operation transactionally with the given options and optionally returns a value, retrying according to the 390 * given retry policy. 391 * <p> 392 * The entire transaction closure may run more than once. Keep non-idempotent external side effects outside the closure 393 * unless they are safe to repeat. 394 * <p> 395 * Pyranid consults the retry policy only when replay is known to be safe. This includes a failure before a physical 396 * transaction becomes available to application work, even if best-effort rollback of that begin candidate fails, because 397 * the candidate is discarded. It also includes a recognized serialization failure reported by physical commit when the 398 * follow-up rollback succeeds. Other commit failures and rollback failures after application work could execute are 399 * terminal because their outcome is indeterminate. 400 * <p> 401 * A savepoint-scope ownership conflict at completion is never replayed, with or without a successful rollback: the 402 * guard raises an {@link IllegalStateException}, not a {@link DatabaseException}, so the conflict leaves this method 403 * without reaching the retry policy. What a successful rollback changes is the attempt's outcome evidence; the conflict 404 * no longer records an indeterminate outcome, so a conflict raised and handled by an independent transaction inside the 405 * closure no longer fences a retry of a later failure. 406 * <p> 407 * Unlike {@link #transaction(TransactionOptions, ReturningTransactionalOperation)} and related transaction methods, 408 * retrying methods return {@link TransactionRetryResult} so callers can inspect failures that were recovered before 409 * success. 410 * <p> 411 * This method rejects a call when the innermost ambient transaction on the calling thread uses the same 412 * {@link DataSource} instance, including across different {@code Database} wrappers. Only that innermost transaction 413 * is checked. If a transaction on another source hides an older matching transaction, this method starts an independent 414 * retrying transaction; retries do not restart either surrounding transaction. 415 * 416 * @param retryPolicy retry policy to apply 417 * @param transactionOptions options to apply to each transaction attempt 418 * @param transactionalOperation the operation to perform transactionally 419 * @param <T> the type to be returned 420 * @return retry result containing the successful transaction value and any failures retried before success 421 * @since 4.4.0 422 */ 423 @NonNull 424 public <T> TransactionRetryResult<T> transactionWithRetry(@NonNull RetryPolicy retryPolicy, 425 @NonNull TransactionOptions transactionOptions, 426 @NonNull ReturningTransactionalOperation<T> transactionalOperation) { 427 requireNonNull(retryPolicy); 428 requireNonNull(transactionOptions); 429 requireNonNull(transactionalOperation); 430 431 if (currentTransaction().isPresent()) 432 throw new IllegalStateException("transactionWithRetry must not be called within an existing transaction"); 433 434 return RetryExecutor.DEFAULT.execute(retryPolicy, () -> transaction(transactionOptions, transactionalOperation)); 435 } 436 437 static void closeConnection(@NonNull Connection connection, @NonNull DatabaseDialect databaseDialect) { 438 requireNonNull(connection); 439 requireNonNull(databaseDialect); 440 441 try { 442 connection.close(); 443 } catch (SQLException e) { 444 throw DatabaseException.wrapSafely("Unable to close database connection", e, databaseDialect); 445 } 446 } 447 448 @NonNull 449 static DatabaseException databaseExceptionWithStatementContext(@NonNull StatementContext<?> statementContext, 450 @NonNull Throwable cause) { 451 requireNonNull(statementContext); 452 requireNonNull(cause); 453 454 if (cause instanceof DatabaseException databaseException && databaseException.areStatementDiagnosticsApplied()) 455 return databaseException; 456 457 String message = DatabaseException.messageSafely(cause, "Database operation failed"); 458 459 if (cause instanceof DatabaseException databaseException) { 460 SecureParameterSupport.DiagnosticScrub diagnosticScrub = diagnosticScrubForStatementContext(statementContext); 461 462 if (diagnosticScrub.redactor() == null && diagnosticScrub.needleRenderingFailureDescriptions().isEmpty()) 463 return databaseException; 464 465 return databaseExceptionWithStatementContext(statementContext, message, cause, diagnosticScrub); 466 } 467 468 return databaseExceptionWithStatementContext(statementContext, message, cause); 469 } 470 471 @NonNull 472 static DatabaseException databaseExceptionWithStatementContext(@NonNull StatementContext<?> statementContext, 473 @NonNull String message, 474 @NonNull Throwable cause) { 475 requireNonNull(statementContext); 476 requireNonNull(message); 477 requireNonNull(cause); 478 479 SecureParameterSupport.DiagnosticScrub diagnosticScrub = diagnosticScrubForStatementContext(statementContext); 480 return databaseExceptionWithStatementContext(statementContext, message, cause, diagnosticScrub); 481 } 482 483 @NonNull 484 static DatabaseException databaseExceptionWithStatementContext(@NonNull StatementContext<?> statementContext, 485 @NonNull String message, 486 @NonNull Throwable cause, 487 SecureParameterSupport.@NonNull DiagnosticScrub diagnosticScrub) { 488 requireNonNull(statementContext); 489 requireNonNull(message); 490 requireNonNull(cause); 491 requireNonNull(diagnosticScrub); 492 493 UnaryOperator<String> diagnosticRedactor = diagnosticScrub.redactor(); 494 495 // Scrub the RAW message before whitespace-collapsing/truncation: bounding first would break needles 496 // containing consecutive whitespace and can cut a secret mid-way, leaving an unmatchable prefix 497 String scrubbedMessage; 498 try { 499 scrubbedMessage = diagnosticRedactor == null ? message : diagnosticRedactor.apply(message); 500 } catch (Throwable diagnosticFailure) { 501 if (cause != diagnosticFailure) cause.addSuppressed(diagnosticFailure); 502 // A failed redactor must not expose the unredacted driver text as the replacement message. 503 scrubbedMessage = "Database operation failed"; 504 } 505 506 StatementDiagnostic statementDiagnostic = statementDiagnostic(statementContext); 507 String diagnosticMessage = format("%s [%s]", 508 boundedDiagnosticMessage(scrubbedMessage), statementDiagnostic.diagnostic()); 509 DatabaseException databaseException = DatabaseException.wrapSafely(diagnosticMessage, cause, 510 statementContext.getDatabaseDialect(), diagnosticRedactor); 511 databaseException.markStatementDiagnosticsApplied(); 512 513 if (statementDiagnostic.parameterRenderingFailure() != null) 514 databaseException.addSuppressed(statementDiagnostic.parameterRenderingFailure()); 515 516 for (String failureDescription : diagnosticScrub.needleRenderingFailureDescriptions()) 517 databaseException.addSuppressed(new RuntimeException(format( 518 "Unable to render a secure parameter for diagnostic scrubbing: %s", failureDescription))); 519 520 return databaseException; 521 } 522 523 private static SecureParameterSupport.@NonNull DiagnosticScrub diagnosticScrubForStatementContext(@NonNull StatementContext<?> statementContext) { 524 requireNonNull(statementContext); 525 526 try { 527 return SecureParameterSupport.diagnosticScrubForParameters(statementContext.getParameters()); 528 } catch (Throwable ignored) { 529 // Per-needle failures are handled inside diagnosticScrubForParameters; reaching here would be 530 // catastrophic (a plain getter throwing) - fall back to no scrubbing rather than losing the exception 531 return SecureParameterSupport.DiagnosticScrub.NONE; 532 } 533 } 534 535 @NonNull 536 private DatabaseException databaseExceptionWithRawConnectionContext(@NonNull Exception cause) { 537 requireNonNull(cause); 538 return DatabaseException.wrapSafely(DatabaseException.messageSafely(cause, "Database operation failed"), 539 cause, this.databaseDialect); 540 } 541 542 @NonNull 543 private static StatementDiagnostic statementDiagnostic(@NonNull StatementContext<?> statementContext) { 544 requireNonNull(statementContext); 545 546 Statement statement = statementContext.getStatement(); 547 String parameters; 548 Throwable parameterRenderingFailure = null; 549 550 try { 551 parameters = boundedDiagnosticParameters(statementContext.getRedactedParameters()); 552 } catch (Throwable t) { 553 parameters = "<unavailable>"; 554 parameterRenderingFailure = t; 555 } 556 557 String diagnostic = format("statementId=%s, sql=%s, parameters=%s", 558 statement.getId(), boundedSql(statement.getSql()), parameters); 559 560 if (parameterRenderingFailure != null) 561 diagnostic = format("%s, parameterRenderingFailure=%s", diagnostic, 562 boundedDiagnosticParameter(parameterRenderingFailureDiagnostic(parameterRenderingFailure))); 563 564 return new StatementDiagnostic(diagnostic, parameterRenderingFailure); 565 } 566 567 private record StatementDiagnostic(@NonNull String diagnostic, 568 @Nullable Throwable parameterRenderingFailure) {} 569 570 @NonNull 571 private static String parameterRenderingFailureDiagnostic(@NonNull Throwable parameterRenderingFailure) { 572 requireNonNull(parameterRenderingFailure); 573 574 String message; 575 576 try { 577 message = parameterRenderingFailure.getMessage(); 578 } catch (Throwable ignored) { 579 message = null; 580 } 581 582 if (message == null || message.trim().length() == 0) 583 return parameterRenderingFailure.getClass().getName(); 584 585 return format("%s: %s", parameterRenderingFailure.getClass().getName(), message); 586 } 587 588 @Nullable 589 static Long sumBatchUpdateCounts(@NonNull List<Long> updateCounts) { 590 requireNonNull(updateCounts); 591 592 long total = 0L; 593 594 for (Long updateCount : updateCounts) { 595 if (updateCount == null || updateCount < 0L) 596 return null; 597 598 try { 599 total = Math.addExact(total, updateCount); 600 } catch (ArithmeticException e) { 601 return null; 602 } 603 } 604 605 return total; 606 } 607 608 @NonNull 609 private static String boundedDiagnosticMessage(@NonNull String message) { 610 requireNonNull(message); 611 612 String compactMessage = DIAGNOSTIC_WHITESPACE_PATTERN.matcher(message).replaceAll(" ").trim(); 613 614 if (compactMessage.length() <= MAX_DIAGNOSTIC_MESSAGE_LENGTH) 615 return compactMessage; 616 617 int prefixLength = Math.max(0, MAX_DIAGNOSTIC_MESSAGE_LENGTH - TRUNCATED_SUFFIX.length()); 618 return compactMessage.substring(0, prefixLength) + TRUNCATED_SUFFIX; 619 } 620 621 @NonNull 622 private static String boundedSql(@NonNull String sql) { 623 requireNonNull(sql); 624 625 String compactSql = DIAGNOSTIC_WHITESPACE_PATTERN.matcher(sql).replaceAll(" ").trim(); 626 627 if (compactSql.length() <= MAX_DIAGNOSTIC_SQL_LENGTH) 628 return compactSql; 629 630 int prefixLength = Math.max(0, MAX_DIAGNOSTIC_SQL_LENGTH - TRUNCATED_SUFFIX.length()); 631 return compactSql.substring(0, prefixLength) + TRUNCATED_SUFFIX; 632 } 633 634 @NonNull 635 private static String boundedDiagnosticParameters(@NonNull List<@Nullable Object> parameters) { 636 requireNonNull(parameters); 637 638 StringBuilder parametersBuilder = new StringBuilder("["); 639 640 for (int i = 0; i < parameters.size(); ++i) { 641 String separator = i == 0 ? "" : ", "; 642 String renderedParameter = boundedDiagnosticParameter(parameters.get(i)); 643 int requiredLength = parametersBuilder.length() + separator.length() + renderedParameter.length() + 1; 644 645 if (requiredLength > MAX_DIAGNOSTIC_PARAMETERS_LENGTH) { 646 int availableLength = MAX_DIAGNOSTIC_PARAMETERS_LENGTH 647 - parametersBuilder.length() 648 - separator.length() 649 - TRUNCATED_SUFFIX.length() 650 - 1; 651 652 parametersBuilder.append(separator); 653 654 if (availableLength > 0) 655 parametersBuilder.append(renderedParameter, 0, Math.min(availableLength, renderedParameter.length())); 656 657 parametersBuilder.append(TRUNCATED_SUFFIX).append(']'); 658 if (parametersBuilder.length() > MAX_DIAGNOSTIC_PARAMETERS_LENGTH) { 659 parametersBuilder.setLength(MAX_DIAGNOSTIC_PARAMETERS_LENGTH - 1); 660 parametersBuilder.append(']'); 661 } 662 return parametersBuilder.toString(); 663 } 664 665 parametersBuilder.append(separator).append(renderedParameter); 666 } 667 668 parametersBuilder.append(']'); 669 return parametersBuilder.toString(); 670 } 671 672 @NonNull 673 private static String boundedDiagnosticParameter(@Nullable Object parameter) { 674 String renderedParameter = String.valueOf(parameter); 675 String compactParameter = DIAGNOSTIC_WHITESPACE_PATTERN.matcher(renderedParameter).replaceAll(" ").trim(); 676 677 if (compactParameter.length() <= MAX_DIAGNOSTIC_PARAMETER_LENGTH) 678 return compactParameter; 679 680 int prefixLength = Math.max(0, MAX_DIAGNOSTIC_PARAMETER_LENGTH - TRUNCATED_SUFFIX.length()); 681 return compactParameter.substring(0, prefixLength) + TRUNCATED_SUFFIX; 682 } 683 684 /** 685 * Performs an operation in the context of a pre-existing transaction. 686 * <p> 687 * No commit or rollback on the transaction will occur when {@code transactionalOperation} completes. 688 * <p> 689 * However, if an exception bubbles out of {@code transactionalOperation}, the transaction will be marked as rollback-only. 690 * <p> 691 * The transaction must have been created by this {@link Database}, or by another {@link Database} using the same 692 * {@link DataSource} instance. 693 * <p> 694 * If this thread is interrupted while waiting for another participant to release the transaction connection, Pyranid 695 * restores the interrupt flag and throws {@link DatabaseException}. 696 * 697 * @param transaction the transaction in which to participate 698 * @param transactionalOperation the operation that should participate in the transaction 699 */ 700 public void participate(@NonNull Transaction transaction, 701 @NonNull TransactionalOperation transactionalOperation) { 702 requireNonNull(transaction); 703 requireNonNull(transactionalOperation); 704 705 participate(transaction, () -> { 706 transactionalOperation.perform(); 707 return Optional.empty(); 708 }); 709 } 710 711 /** 712 * Performs an operation in the context of a pre-existing transaction, optionally returning a value. 713 * <p> 714 * No commit or rollback on the transaction will occur when {@code transactionalOperation} completes. 715 * <p> 716 * However, if an exception bubbles out of {@code transactionalOperation}, the transaction will be marked as rollback-only. 717 * <p> 718 * The transaction must have been created by this {@link Database}, or by another {@link Database} using the same 719 * {@link DataSource} instance. 720 * <p> 721 * If this thread is interrupted while waiting for another participant to release the transaction connection, Pyranid 722 * restores the interrupt flag and throws {@link DatabaseException}. 723 * 724 * @param transaction the transaction in which to participate 725 * @param transactionalOperation the operation that should participate in the transaction 726 * @param <T> the type to be returned 727 * @return the result of the transactional operation 728 */ 729 @NonNull 730 public <T> Optional<T> participate(@NonNull Transaction transaction, 731 @NonNull ReturningTransactionalOperation<T> transactionalOperation) { 732 return this.transactions.participate(transaction, transactionalOperation); 733 } 734 735 /** 736 * Creates a fluent builder for executing SQL. 737 * <p> 738 * Named parameters use the {@code :paramName} syntax and are bound via {@link Query#bind(String, Object)}. 739 * Positional parameters via {@code ?} are not supported. 740 * Pyranid ignores parameter-looking text inside SQL string literals, quoted identifiers, comments, PostgreSQL 741 * dollar-quoted strings, and SQL Server-style bracket-quoted identifiers. PostgreSQL JSONB/hstore {@code ?}, 742 * {@code ?|}, and {@code ?&} operators are supported; when running against {@link DatabaseType#POSTGRESQL}, Pyranid 743 * emits pgjdbc's escaped {@code ??} form automatically. Unterminated quotes and comments fail fast. 744 * <p> 745 * Example: 746 * <pre>{@code 747 * Optional<Employee> employee = database.query("SELECT * FROM employee WHERE id = :id") 748 * .bind("id", 42) 749 * .fetchObject(Employee.class); 750 * }</pre> 751 * 752 * @param sql SQL containing {@code :paramName} placeholders 753 * @return a fluent builder for binding parameters and executing 754 * @since 4.0.0 755 */ 756 @NonNull 757 public Query query(@NonNull String sql) { 758 requireNonNull(sql); 759 return new DefaultQuery(this, sql, this.sqlCompiler, this.statementExecutor); 760 } 761 762 /** 763 * Performs a portable connectivity check using JDBC {@link Connection#isValid(int)}. 764 * <p> 765 * This method borrows a fresh connection from this database's {@link DataSource}, calls 766 * {@link Connection#isValid(int)}, and closes the connection before returning. It does <strong>not</strong> 767 * participate in an active Pyranid transaction, if one exists. 768 * <p> 769 * JDBC accepts timeout values in whole seconds. Positive sub-second durations are rounded up to one second; 770 * {@link Duration#ZERO} passes a timeout of {@code 0} to the driver. 771 * <p> 772 * If the failure this method propagates carries evidence of thread interruption, Pyranid restores the interrupt 773 * flag before throwing, as long as no Pyranid transaction is open on the calling thread. Inside a transaction this 774 * method restores nothing, and nothing restores the flag for a failure the caller handles there. A caller that 775 * handles one of these failures inside a transaction and wants to act on the cancellation must call 776 * {@link Thread#interrupt()} itself. 777 * 778 * @param timeout maximum time to wait for driver validation 779 * @throws IllegalArgumentException if {@code timeout} is negative or too large for JDBC's integer-second timeout 780 * @throws DatabaseException if connection acquisition fails, validation throws, or the driver reports the 781 * connection is not valid 782 * @since 4.2.0 783 */ 784 public void performHealthCheck(@NonNull Duration timeout) { 785 requireNonNull(timeout); 786 int timeoutSeconds = healthCheckTimeoutSeconds(timeout); 787 788 performRawConnectionOperation(connection -> { 789 boolean valid; 790 791 try { 792 valid = connection.isValid(timeoutSeconds); 793 } catch (SQLException e) { 794 throw DatabaseException.wrapSafely("Unable to perform database health check", e, 795 this.databaseDialect); 796 } 797 798 if (!valid) 799 throw new DatabaseException("Database health check failed: connection is not valid"); 800 801 return Optional.empty(); 802 }, false); 803 } 804 805 static void restoreInterruptIfNeeded(@NonNull Throwable throwable) { 806 requireNonNull(throwable); 807 // Failure inspection is best effort; it must never prevent cleanup or replace the primary exception. 808 if (ThrowableGraph.inspect(throwable, current -> current instanceof InterruptedException ? 1 : 0, 0) != 0) 809 Thread.currentThread().interrupt(); 810 } 811 812 static void lockInterruptibly(@NonNull ReentrantLock lock, 813 @NonNull String operation) { 814 requireNonNull(lock); 815 requireNonNull(operation); 816 817 try { 818 lock.lockInterruptibly(); 819 } catch (InterruptedException e) { 820 Thread.currentThread().interrupt(); 821 throw new DatabaseException(format("Interrupted while waiting to %s", operation), e); 822 } 823 } 824 825 @Nullable 826 static Throwable closeStatementContextResources(@NonNull StatementContext<?> statementContext, 827 @Nullable Throwable cleanupFailure) { 828 requireNonNull(statementContext); 829 830 Queue<AutoCloseable> cleanupOperations = statementContext.getCleanupOperations(); 831 AutoCloseable cleanupOperation; 832 833 while ((cleanupOperation = cleanupOperations.poll()) != null) { 834 try { 835 cleanupOperation.close(); 836 } catch (Throwable cleanupException) { 837 cleanupFailure = appendCleanupFailure(cleanupFailure, cleanupException); 838 } 839 } 840 841 return cleanupFailure; 842 } 843 844 /** Keeps the first failure while allowing cleanup to continue when a driver reuses an exception instance. */ 845 @NonNull 846 static Throwable appendCleanupFailure(@Nullable Throwable existing, @NonNull Throwable additional) { 847 requireNonNull(additional); 848 if (existing == null) 849 return additional; 850 if (existing != additional) 851 existing.addSuppressed(additional); 852 return existing; 853 } 854 855 static boolean isUnsupportedSqlFeature(@NonNull SQLException e) { 856 requireNonNull(e); 857 858 try { 859 String sqlState = e.getSQLState(); 860 if (sqlState != null && (sqlState.startsWith("0A") || "HYC00".equals(sqlState))) 861 return true; 862 863 Throwable cause = e.getCause(); 864 if (cause instanceof SQLFeatureNotSupportedException 865 || cause instanceof UnsupportedOperationException 866 || cause instanceof AbstractMethodError) 867 return true; 868 869 String message = e.getMessage(); 870 if (message == null) 871 return false; 872 873 String lower = message.toLowerCase(Locale.ROOT); 874 return lower.contains("not supported") 875 || lower.contains("unsupported") 876 || lower.contains("not implemented") 877 || lower.contains("feature not supported"); 878 } catch (Throwable diagnosticFailure) { 879 if (e != diagnosticFailure) e.addSuppressed(diagnosticFailure); 880 // Unreadable capability evidence cannot authorize executing the statement again. 881 return false; 882 } 883 } 884 885 @Nullable 886 static Duration validateQueryTimeout(@Nullable Duration queryTimeout) { 887 if (queryTimeout != null) { 888 if (queryTimeout.isNegative()) 889 throw new IllegalArgumentException("queryTimeout must be >= 0"); 890 891 queryTimeoutSeconds(queryTimeout); 892 } 893 894 return queryTimeout; 895 } 896 897 @Nullable 898 static Integer validateNonNegativeStatementSetting(@NonNull String name, 899 @Nullable Integer value) { 900 requireNonNull(name); 901 902 if (value != null && value < 0) 903 throw new IllegalArgumentException(format("%s must be >= 0", name)); 904 905 return value; 906 } 907 908 @Nullable 909 static Integer validatePositiveQuerySetting(@NonNull String name, 910 @Nullable Integer value) { 911 requireNonNull(name); 912 913 if (value != null && value <= 0) 914 throw new IllegalArgumentException(format("%s must be > 0", name)); 915 916 return value; 917 } 918 919 private static int queryTimeoutSeconds(@NonNull Duration queryTimeout) { 920 requireNonNull(queryTimeout); 921 922 long seconds = queryTimeout.getSeconds(); 923 924 if (queryTimeout.getNano() > 0) { 925 if (seconds == Long.MAX_VALUE) 926 throw new IllegalArgumentException(format("queryTimeout must be <= %s seconds", Integer.MAX_VALUE)); 927 928 ++seconds; 929 } 930 931 if (seconds > Integer.MAX_VALUE) 932 throw new IllegalArgumentException(format("queryTimeout must be <= %s seconds", Integer.MAX_VALUE)); 933 934 return (int) seconds; 935 } 936 937 private static int healthCheckTimeoutSeconds(@NonNull Duration timeout) { 938 requireNonNull(timeout); 939 940 if (timeout.isNegative()) 941 throw new IllegalArgumentException("timeout must be >= 0"); 942 943 long seconds = timeout.getSeconds(); 944 945 if (timeout.getNano() > 0) { 946 if (seconds == Long.MAX_VALUE) 947 throw new IllegalArgumentException(format("timeout must be <= %s seconds", Integer.MAX_VALUE)); 948 949 ++seconds; 950 } 951 952 if (seconds > Integer.MAX_VALUE) 953 throw new IllegalArgumentException(format("timeout must be <= %s seconds", Integer.MAX_VALUE)); 954 955 return (int) seconds; 956 } 957 958 /** 959 * Exposes a temporary handle to JDBC {@link DatabaseMetaData}, which provides comprehensive vendor-specific information about this database as a whole. 960 * <p> 961 * This method acquires {@link DatabaseMetaData} on its own newly-borrowed connection, which it manages internally. 962 * <p> 963 * It does <strong>not</strong> participate in the active transaction, if one exists. 964 * <p> 965 * The connection is closed as soon as {@link DatabaseMetaDataReader#read(DatabaseMetaData)} completes. 966 * <p> 967 * 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. 968 * <p> 969 * If the failure this method propagates carries evidence of thread interruption, Pyranid restores the interrupt 970 * flag before throwing, as long as no Pyranid transaction is open on the calling thread. Inside a transaction this 971 * method restores nothing, and nothing restores the flag for a failure the caller handles there. A caller that 972 * handles one of these failures inside a transaction and wants to act on the cancellation must call 973 * {@link Thread#interrupt()} itself. 974 */ 975 public void readDatabaseMetaData(@NonNull DatabaseMetaDataReader databaseMetaDataReader) { 976 requireNonNull(databaseMetaDataReader); 977 978 performRawConnectionOperation((connection -> { 979 databaseMetaDataReader.read(connection.getMetaData()); 980 return Optional.empty(); 981 }), false); 982 } 983 984 /** 985 * Performs raw JDBC work with a Pyranid-managed {@link Connection}. 986 * <p> 987 * If called inside a Pyranid transaction, this operation uses the transaction's connection and participates in that 988 * transaction. Otherwise, Pyranid borrows a connection for the duration of the callback and closes it afterwards. 989 * <p> 990 * The {@link Connection} passed to {@code rawConnectionOperation} is a guarded handle. Normal JDBC operations are 991 * delegated to the underlying driver connection, but lifecycle, transaction-management, and connection-wide state 992 * methods such as 993 * {@link Connection#close()}, {@link Connection#commit()}, {@link Connection#rollback()}, 994 * {@link Connection#setAutoCommit(boolean)}, {@link Connection#setCatalog(String)}, {@link Connection#setSchema(String)}, and 995 * {@link Connection#setNetworkTimeout(java.util.concurrent.Executor, int)} throw {@link IllegalStateException}. Use 996 * Pyranid transaction APIs instead. 997 * {@link Connection#unwrap(Class)} may return a guarded, callback-scoped proxy for a vendor interface, but never a 998 * castable physical {@link Connection}; the proxy blocks lifecycle methods and expires with the callback. 999 * JDBC objects created from this handle are also guarded: {@link java.sql.Statement#getConnection()} and 1000 * {@link java.sql.DatabaseMetaData#getConnection()} return the Pyranid-managed handle, and 1001 * {@link ResultSet#getStatement()} returns a guarded statement. Guarded statements, resultsets, and metadata refuse 1002 * driver-specific {@code unwrap(...)} calls that could expose the driver's underlying connection. 1003 * <p> 1004 * The connection handle is valid only for the duration of the callback. Do not close it, retain it, or use it after this 1005 * method returns. 1006 * <p> 1007 * If the failure this method propagates carries evidence of thread interruption, Pyranid restores the interrupt 1008 * flag before throwing, as long as no Pyranid transaction is open on the calling thread. Inside a transaction this 1009 * method restores nothing, and nothing restores the flag for a failure the caller handles there. A caller that 1010 * handles one of these failures inside a transaction and wants to act on the cancellation must call 1011 * {@link Thread#interrupt()} itself. 1012 * 1013 * @param rawConnectionOperation the raw JDBC operation to perform 1014 * @param <T> the type to be returned 1015 * @return the operation result 1016 * @throws DatabaseException if connection acquisition, callback execution, or cleanup fails 1017 * @since 4.2.0 1018 */ 1019 @NonNull 1020 public <T> Optional<T> useRawConnection(@NonNull RawConnectionOperation<Connection, T> rawConnectionOperation) { 1021 requireNonNull(rawConnectionOperation); 1022 1023 return performRawConnectionOperation(connection -> { 1024 PyranidRawConnection rawConnection = new PyranidRawConnection(connection); 1025 1026 try { 1027 Optional<T> result = rawConnectionOperation.perform(rawConnection); 1028 return result == null ? Optional.empty() : result; 1029 } finally { 1030 rawConnection.release(); 1031 } 1032 }, true); 1033 } 1034 1035 /** 1036 * Performs driver-specific raw JDBC work with a Pyranid-managed connection of the requested type. 1037 * <p> 1038 * If called inside a Pyranid transaction, this operation uses the transaction's connection. Standard JDBC work 1039 * participates in that transaction; driver-extension APIs may impose additional transaction-start or lifecycle rules. 1040 * Otherwise, Pyranid borrows a connection for the duration of the callback and closes it afterwards. 1041 * <p> 1042 * If the managed connection is already an instance of {@code connectionType}, it is passed directly to 1043 * {@code rawConnectionOperation}. Otherwise, Pyranid obtains the requested driver connection through 1044 * {@link Connection#unwrap(Class)}. Failure to obtain the requested type results in a {@link DatabaseException}. 1045 * <p> 1046 * <strong>Unlike {@link #useRawConnection(RawConnectionOperation)}, the connection supplied by this overload is 1047 * unguarded.</strong> This is necessary for concrete driver APIs, but it also means Pyranid cannot prevent operations 1048 * that interfere with its connection and transaction management. Guarding is selected by the overload, so the 1049 * connection remains unguarded when {@code connectionType} is {@code Connection.class}. Do not close the connection, 1050 * retain it, call 1051 * {@link Connection#commit()}, {@link Connection#rollback()}, or {@link Connection#setAutoCommit(boolean)}, or mutate 1052 * connection-wide state such as schema, catalog, client info, holdability, type map, or network timeout. 1053 * <p> 1054 * The connection and every object obtained from it are valid only for the duration of the callback. Statements, result 1055 * sets, appenders, readers, and other derived resources must be closed before this method returns. Retaining or using any 1056 * of these objects after the callback returns has undefined behavior. 1057 * <p> 1058 * If the failure this method propagates carries evidence of thread interruption, Pyranid restores the interrupt 1059 * flag before throwing, as long as no Pyranid transaction is open on the calling thread. Inside a transaction this 1060 * method restores nothing, and nothing restores the flag for a failure the caller handles there. A caller that 1061 * handles one of these failures inside a transaction and wants to act on the cancellation must call 1062 * {@link Thread#interrupt()} itself. 1063 * 1064 * @param connectionType driver connection class or interface to expose 1065 * @param rawConnectionOperation the driver-specific raw JDBC operation to perform 1066 * @param <C> the requested connection type 1067 * @param <T> the type to be returned 1068 * @return the operation result 1069 * @throws DatabaseException if connection acquisition, unwrapping, callback execution, or cleanup fails 1070 * @since 4.7.0 1071 */ 1072 @NonNull 1073 public <C extends Connection, T> Optional<T> useRawConnection(@NonNull Class<C> connectionType, 1074 @NonNull RawConnectionOperation<? super C, T> rawConnectionOperation) { 1075 requireNonNull(connectionType); 1076 requireNonNull(rawConnectionOperation); 1077 1078 return performRawConnectionOperation(connection -> { 1079 C rawConnection = connectionType.isInstance(connection) 1080 ? connectionType.cast(connection) 1081 : connection.unwrap(connectionType); 1082 1083 if (rawConnection == null) 1084 throw new SQLException(format("Connection.unwrap(%s) returned null", connectionType.getName())); 1085 1086 Optional<T> result = rawConnectionOperation.perform(rawConnection); 1087 return result == null ? Optional.empty() : result; 1088 }, true); 1089 } 1090 1091 /** 1092 * Performs an operation with one callback-scoped database-notification listener session. 1093 * <p> 1094 * This method is synchronous and blocking. It acquires at most one listener connection from this 1095 * {@code Database}'s configured {@link DataSource}, registers every requested channel, invokes {@code operation} 1096 * at most once, expires the supplied {@link NotificationSession}, and completes cleanup before returning or 1097 * throwing. It never reconnects. 1098 * <p> 1099 * The configured source must preserve one physical backend session for the entire checkout. For PostgreSQL, 1100 * direct connections and session pooling are suitable. Using PgBouncer transaction or statement pooling as the 1101 * listener source is unsupported: registration can appear to succeed before backend-session affinity is lost and 1102 * notification delivery silently stops. Pyranid does not inspect or validate proxy topology. Applications whose 1103 * ordinary source cannot provide the required affinity should construct a separate {@code Database} over a suitable 1104 * listener source and invoke this method on that instance. 1105 * <p> 1106 * Notifications are lossy hints. Durable applications should normally reconcile authoritative state as the first 1107 * callback action. The operation may use ordinary database methods, but those methods acquire or select their 1108 * connection normally and never reuse the listener connection. Pyranid transaction entry is interrupt-sensitive; 1109 * when a receive returns a batch with a racing interrupt, temporarily clear and remember the flag during bounded 1110 * reconciliation and restore it afterward as described by {@link NotificationSession#awaitNotifications(Duration)}. 1111 * <p> 1112 * A terminal receive failure is retained by the session and rethrown after cleanup even if {@code operation} catches 1113 * it and returns. A retained transport {@link Error} propagates as that exact, unwrapped instance. If the callback 1114 * instead throws a distinct {@code Error}, that callback error remains primary and the retained transport failure is 1115 * suppressed beneath it. 1116 * 1117 * @param channels fixed, nonempty set of nonblank channels to register 1118 * @param operation operation to invoke after every channel has been registered 1119 * @throws NullPointerException if {@code channels}, a channel, or {@code operation} is null 1120 * @throws IllegalArgumentException if the set is empty or a channel violates common or backend-specific limits 1121 * @throws IllegalStateException if any Pyranid transaction is active on the calling thread 1122 * @throws InterruptedException if cooperative interruption wins after any required cleanup 1123 * @throws UnsupportedOperationException if notification listening is unavailable through the database dialect or 1124 * runtime driver, or is unsupported or disabled for the current server version or configuration 1125 * @throws DatabaseException if connection acquisition, setup, receive, callback execution, or cleanup fails 1126 * @since 4.6.0 1127 */ 1128 public void withNotificationSession(@NonNull Set<@NonNull String> channels, 1129 @NonNull NotificationSessionOperation operation) 1130 throws InterruptedException { 1131 NotificationSessionRunner.run(this, channels, operation); 1132 } 1133 1134 /** 1135 * Performs an operation with one callback-scoped database-notification listener session for a single channel. 1136 * <p> 1137 * This is the single-channel convenience form of 1138 * {@link #withNotificationSession(Set, NotificationSessionOperation)}. The listener connection comes from this 1139 * {@code Database}'s configured {@link DataSource}, which must preserve physical backend-session affinity for the 1140 * entire checkout. For PostgreSQL, using PgBouncer transaction or statement pooling as the listener source is 1141 * unsupported: registration can appear to succeed before affinity is lost and delivery silently stops. Pyranid does 1142 * not inspect or validate proxy topology. 1143 * <p> 1144 * A terminal receive failure is retained and rethrown after cleanup even if {@code operation} catches it and returns. 1145 * A retained transport {@link Error} propagates as the exact, unwrapped instance unless a distinct callback 1146 * {@code Error} takes precedence as described by the set-based overload. 1147 * 1148 * @param channel nonblank channel to register 1149 * @param operation operation to invoke after the channel has been registered 1150 * @throws NullPointerException if {@code channel} or {@code operation} is null 1151 * @throws IllegalArgumentException if the channel violates common or backend-specific limits 1152 * @throws IllegalStateException if any Pyranid transaction is active on the calling thread 1153 * @throws InterruptedException if cooperative interruption wins after any required cleanup 1154 * @throws UnsupportedOperationException if notification listening is unavailable through the database dialect or 1155 * runtime driver, or is unsupported or disabled for the current server version or configuration 1156 * @throws DatabaseException if connection acquisition, setup, receive, callback execution, or cleanup fails 1157 * @since 4.6.0 1158 */ 1159 public void withNotificationSession(@NonNull String channel, 1160 @NonNull NotificationSessionOperation operation) 1161 throws InterruptedException { 1162 requireNonNull(channel); 1163 requireNonNull(operation); 1164 withNotificationSession(Set.of(channel), operation); 1165 } 1166 1167 /** 1168 * Sends a transient database notification without specifying a payload. 1169 * <p> 1170 * Sending follows ordinary Pyranid statement and transaction selection. On PostgreSQL, a send inside a Pyranid 1171 * transaction becomes visible only if that transaction commits. Payload representation is database-specific; 1172 * PostgreSQL converts the resulting null payload to the empty string. 1173 * 1174 * @param channel nonblank notification channel 1175 * @throws NullPointerException if {@code channel} is null 1176 * @throws IllegalArgumentException if the channel violates common or backend-specific limits 1177 * @throws UnsupportedOperationException if notification sending is unavailable through the database dialect, or is 1178 * unsupported or disabled for the current server version or configuration 1179 * @throws DatabaseException if the send fails 1180 * @since 4.6.0 1181 */ 1182 public void sendNotification(@NonNull String channel) { 1183 sendNotification(channel, null); 1184 } 1185 1186 /** 1187 * Sends a transient database notification. 1188 * <p> 1189 * Sending follows ordinary Pyranid statement and transaction selection, including connection ownership, 1190 * parameter binding and redaction, statement logging, timeout configuration, and metrics. Payload nullability 1191 * and null/empty-string handling are database-specific; Pyranid performs no generic normalization. 1192 * <p> 1193 * On PostgreSQL this executes bound {@code pg_notify(?, ?)} SQL. PostgreSQL converts a null payload to the empty 1194 * string. Delivery occurs only after commit and is discarded by rollback; notification delivery itself remains 1195 * non-durable and may be coalesced. 1196 * 1197 * @param channel nonblank notification channel 1198 * @param payload notification payload, possibly null or empty 1199 * @throws NullPointerException if {@code channel} is null 1200 * @throws IllegalArgumentException if the channel or payload violates common or backend-specific limits 1201 * @throws UnsupportedOperationException if notification sending is unavailable through the database dialect, or is 1202 * unsupported or disabled for the current server version or configuration 1203 * @throws DatabaseException if the send fails 1204 * @since 4.6.0 1205 */ 1206 public void sendNotification(@NonNull String channel, 1207 @Nullable String payload) { 1208 Notification.validateChannel(channel); 1209 DatabaseNotificationSupport notificationSupport = getDatabaseDialect().notificationSupport(); 1210 1211 notificationSupport.validateChannel(channel); 1212 notificationSupport.validatePayload(payload); 1213 1214 if (!notificationSupport.isSendSupported()) 1215 throw new UnsupportedOperationException(format( 1216 "Database type %s does not support notification sends", getDatabaseType())); 1217 1218 Statement statement = Statement.of(generateId(), notificationSupport.sendStatementSql()); 1219 StatementContext<Void> statementContext = StatementContext.with(statement, this) 1220 .parameters(channel, payload) 1221 .build(); 1222 1223 PreparedStatementCustomizer defaultPreparedStatementCustomizer = hasDefaultPreparedStatementSettings() 1224 ? (context, preparedStatement) -> applyDefaultPreparedStatementSettings(preparedStatement) 1225 : null; 1226 1227 // Scoped deliberately to PreparedStatement.execute(). Connection acquisition, result draining and 1228 // JDBC cleanup can all raise these same SQLStates for unrelated reasons. 1229 AtomicReference<SQLException> sendStatementFailure = new AtomicReference<>(); 1230 1231 try { 1232 this.statementExecutor.performDatabaseOperation(statementContext, Arrays.asList(channel, payload), 1233 defaultPreparedStatementCustomizer, (preparedStatement, timing) -> { 1234 long startTime = nanoTime(); 1235 1236 boolean resultAvailable; 1237 1238 try { 1239 resultAvailable = preparedStatement.execute(); 1240 } catch (SQLException e) { 1241 if (isNotificationStatementUnsupported(e)) 1242 sendStatementFailure.set(e); 1243 1244 throw e; 1245 } 1246 1247 drainStatementResults(preparedStatement, resultAvailable); 1248 1249 return new StatementExecutor.DatabaseOperationResult( 1250 Duration.ofNanos(nanoTime() - startTime), null, StatementResult.empty()); 1251 }); 1252 } catch (DatabaseException e) { 1253 // The send statement is Pyranid's own, so the server rejecting it as unsupported syntax or an 1254 // unavailable function is a server-capability issue rather than malformed caller SQL. 1255 if (sendStatementFailure.get() != null) 1256 throw new UnsupportedOperationException( 1257 notificationServerCapabilityUnavailableMessage(getDatabaseType()), e); 1258 1259 throw e; 1260 } 1261 } 1262 1263 /** 1264 * Reports whether the configured database dialect and currently loadable runtime adapter expose the APIs required 1265 * to attempt a notification-listening session. 1266 * <p> 1267 * This method uses the configured database type without acquiring a connection or inspecting JDBC metadata. 1268 * It does not verify whether notifications are enabled or supported by the current server version or configuration, 1269 * acquire a listener session, inspect pool or proxy mode, 1270 * prove backend-session affinity, unwrap a physical listener connection, or emit notification-session lifecycle 1271 * metrics. 1272 * 1273 * @return true if notification listening can be attempted with the current dialect and runtime 1274 * @since 4.6.0 1275 */ 1276 @NonNull 1277 public Boolean isNotificationListeningSupported() { 1278 return getDatabaseDialect().notificationSupport().isReceiveRuntimeAvailable(); 1279 } 1280 1281 static void drainStatementResults(@NonNull PreparedStatement preparedStatement, 1282 boolean resultAvailable) throws SQLException { 1283 requireNonNull(preparedStatement); 1284 1285 for (;;) { 1286 if (resultAvailable) { 1287 ResultSet resultSet = preparedStatement.getResultSet(); 1288 Throwable resultSetFailure = null; 1289 try { 1290 if (resultSet != null) { 1291 while (resultSet.next()) { 1292 // Drain every row before advancing to a possible subsequent result. 1293 } 1294 } 1295 } catch (SQLException | RuntimeException | Error failure) { 1296 resultSetFailure = failure; 1297 throw failure; 1298 } finally { 1299 JdbcResources.close(resultSet, resultSetFailure); 1300 } 1301 } else if (preparedStatement.getUpdateCount() == -1) { 1302 break; 1303 } 1304 1305 resultAvailable = preparedStatement.getMoreResults(java.sql.Statement.CLOSE_CURRENT_RESULT); 1306 } 1307 } 1308 1309 static boolean isNotificationStatementUnsupported(@Nullable Throwable throwable) { 1310 return NotificationSessionRunner.isNotificationStatementUnsupported(throwable); 1311 } 1312 1313 @NonNull 1314 private String notificationServerCapabilityUnavailableMessage(@NonNull DatabaseType databaseType) { 1315 return NotificationSessionRunner.notificationServerCapabilityUnavailableMessage(databaseType); 1316 } 1317 1318 boolean hasDefaultPreparedStatementSettings() { 1319 return this.queryTimeout != null || this.fetchSize != null || this.maxRows != null; 1320 } 1321 1322 void applyDefaultPreparedStatementSettings(@NonNull PreparedStatement preparedStatement) throws SQLException { 1323 requireNonNull(preparedStatement); 1324 applyPreparedStatementSettings(preparedStatement, this.queryTimeout, this.fetchSize, this.maxRows); 1325 } 1326 1327 static void applyPreparedStatementSettings(@NonNull PreparedStatement preparedStatement, 1328 @Nullable Duration queryTimeout, 1329 @Nullable Integer fetchSize, 1330 @Nullable Integer maxRows) throws SQLException { 1331 requireNonNull(preparedStatement); 1332 1333 if (queryTimeout != null) 1334 preparedStatement.setQueryTimeout(queryTimeoutSeconds(queryTimeout)); 1335 1336 if (fetchSize != null) 1337 preparedStatement.setFetchSize(fetchSize); 1338 1339 if (maxRows != null) 1340 preparedStatement.setMaxRows(maxRows); 1341 } 1342 1343 @FunctionalInterface 1344 interface InternalRawConnectionOperation<R> { 1345 @NonNull 1346 Optional<R> perform(@NonNull Connection connection) throws Exception; 1347 } 1348 1349 /** 1350 * Gets the database type configured when this database was built. 1351 * 1352 * @return the configured database type 1353 * @since 3.0.0 1354 */ 1355 @NonNull 1356 public DatabaseType getDatabaseType() { 1357 return this.databaseType; 1358 } 1359 1360 @NonNull 1361 DatabaseDialect getDatabaseDialect() { 1362 return this.databaseDialect; 1363 } 1364 1365 /** 1366 * @since 3.0.0 1367 */ 1368 @NonNull 1369 public ZoneId getTimeZone() { 1370 return this.timeZone; 1371 } 1372 1373 /** 1374 * How should Pyranid bind {@link java.time.Instant} and {@link java.time.OffsetDateTime} parameters when JDBC 1375 * parameter metadata cannot identify whether the target is {@code TIMESTAMP} or {@code TIMESTAMP WITH TIME ZONE}? 1376 * 1377 * @return behavior to use when timestamp target metadata is unavailable or non-identifying 1378 * @since 4.2.0 1379 */ 1380 @NonNull 1381 public AmbiguousTimestampBindingStrategy getAmbiguousTimestampBindingStrategy() { 1382 return this.ambiguousTimestampBindingStrategy; 1383 } 1384 1385 /** 1386 * Gets the configured redactor used for non-secure parameters in diagnostics. 1387 * 1388 * @return the configured parameter redactor 1389 * @since 4.4.0 1390 */ 1391 @NonNull 1392 public ParameterRedactor getParameterRedactor() { 1393 return this.parameterRedactor; 1394 } 1395 1396 /** 1397 * Useful for single-shot "utility" calls that operate outside of normal query operations, e.g. pulling DB metadata. 1398 * <p> 1399 * Example: {@link #readDatabaseMetaData(DatabaseMetaDataReader)}. 1400 */ 1401 @NonNull 1402 <R> Optional<R> performRawConnectionOperation(@NonNull InternalRawConnectionOperation<R> rawConnectionOperation, 1403 @NonNull Boolean shouldParticipateInExistingTransactionIfPossible) { 1404 requireNonNull(rawConnectionOperation); 1405 requireNonNull(shouldParticipateInExistingTransactionIfPossible); 1406 1407 if (shouldParticipateInExistingTransactionIfPossible) { 1408 Optional<Transaction> transaction = currentTransactionForDatabaseOperation(); 1409 ReentrantLock connectionLock = transaction.isPresent() ? transaction.get().getConnectionLock() : null; 1410 // Try to participate in txn if it's available 1411 Connection connection = null; 1412 Throwable thrown = null; 1413 boolean connectionLockAcquired = false; 1414 1415 try { 1416 if (connectionLock != null) { 1417 lockInterruptibly(connectionLock, "use the transaction connection for a raw connection operation"); 1418 connectionLockAcquired = true; 1419 } 1420 1421 if (transaction.isPresent()) { 1422 connection = transaction.get().getConnection(); 1423 } else { 1424 try { 1425 connection = getDataSource().getConnection(); 1426 } catch (SQLException e) { 1427 throw DatabaseException.wrapSafely("Unable to acquire database connection", e, 1428 this.databaseDialect); 1429 } 1430 } 1431 1432 return rawConnectionOperation.perform(connection); 1433 } catch (DatabaseException e) { 1434 thrown = e; 1435 throw e; 1436 } catch (Error e) { 1437 thrown = e; 1438 throw e; 1439 } catch (Exception e) { 1440 DatabaseException wrapped = databaseExceptionWithRawConnectionContext(e); 1441 thrown = wrapped; 1442 throw wrapped; 1443 } finally { 1444 Throwable cleanupFailure = null; 1445 1446 try { 1447 // If this was a single-shot operation (not in a transaction), close the connection 1448 if (connection != null && !transaction.isPresent()) { 1449 try { 1450 closeConnection(connection, this.databaseDialect); 1451 } catch (Throwable cleanupException) { 1452 cleanupFailure = cleanupException; 1453 } 1454 } 1455 } finally { 1456 if (connectionLockAcquired) 1457 connectionLock.unlock(); 1458 1459 Throwable exitThrown = thrown; 1460 1461 if (cleanupFailure != null) { 1462 if (thrown != null) { 1463 appendCleanupFailure(thrown, cleanupFailure); 1464 } else { 1465 exitThrown = cleanupFailure instanceof RuntimeException || cleanupFailure instanceof Error 1466 ? cleanupFailure : new RuntimeException(cleanupFailure); 1467 } 1468 } 1469 1470 // Closing the borrowed connection can introduce interruption after the operation's catch block, and a 1471 // cleanup failure can replace the primary failure outright. Restore from the failure that actually exits, 1472 // and only outside a transaction: publishing the interrupt here would abort an ambient transaction's next 1473 // statement on this thread, on a failure the caller handles. 1474 if (exitThrown != null && transaction.isEmpty()) 1475 restoreInterruptIfNeeded(exitThrown); 1476 1477 if (thrown == null && exitThrown != null) { 1478 if (exitThrown instanceof Error error) 1479 throw error; 1480 1481 throw (RuntimeException) exitThrown; 1482 } 1483 } 1484 } 1485 } else { 1486 boolean acquiredConnection = false; 1487 Connection connection = null; 1488 Throwable thrown = null; 1489 1490 // Always get a fresh connection no matter what and close it afterwards 1491 try { 1492 connection = getDataSource().getConnection(); 1493 acquiredConnection = true; 1494 return rawConnectionOperation.perform(connection); 1495 } catch (DatabaseException e) { 1496 thrown = e; 1497 throw e; 1498 } catch (Error e) { 1499 thrown = e; 1500 throw e; 1501 } catch (Exception e) { 1502 DatabaseException wrapped = acquiredConnection 1503 ? databaseExceptionWithRawConnectionContext(e) 1504 : DatabaseException.wrapSafely("Unable to acquire database connection", e, 1505 this.databaseDialect); 1506 thrown = wrapped; 1507 throw wrapped; 1508 } finally { 1509 Throwable exitThrown = thrown; 1510 1511 if (connection != null) { 1512 try { 1513 closeConnection(connection, this.databaseDialect); 1514 } catch (Throwable cleanupException) { 1515 if (thrown != null) { 1516 appendCleanupFailure(thrown, cleanupException); 1517 } else { 1518 exitThrown = cleanupException instanceof RuntimeException || cleanupException instanceof Error 1519 ? cleanupException : new RuntimeException(cleanupException); 1520 } 1521 } 1522 } 1523 1524 // Closing the borrowed connection can introduce interruption after the operation's catch block, and a 1525 // cleanup failure can replace the primary failure outright. Restore from the failure that actually exits, 1526 // and only outside a transaction: this branch never participates, but an ambient transaction on this thread 1527 // must not have its next statement aborted here. 1528 if (exitThrown != null && !hasAmbientTransaction()) 1529 restoreInterruptIfNeeded(exitThrown); 1530 1531 if (thrown == null && exitThrown != null) { 1532 if (exitThrown instanceof Error error) 1533 throw error; 1534 1535 throw (RuntimeException) exitThrown; 1536 } 1537 } 1538 } 1539 } 1540 1541 @NonNull 1542 DataSource getDataSource() { 1543 return this.dataSource; 1544 } 1545 1546 @NonNull 1547 InstanceProvider getInstanceProvider() { 1548 return this.instanceProvider; 1549 } 1550 1551 @NonNull 1552 ResultSetMapper getResultSetMapper() { 1553 return this.resultSetMapper; 1554 } 1555 1556 @NonNull 1557 public MetricsCollector getMetricsCollector() { 1558 return getMetricsCollectorDispatcher().getMetricsCollector(); 1559 } 1560 1561 @NonNull 1562 MetricsCollectorDispatcher getMetricsCollectorDispatcher() { 1563 return this.metricsCollectorDispatcher; 1564 } 1565 1566 @NonNull 1567 Object generateId() { 1568 // "Unique" keys 1569 return this.defaultIdGenerator.incrementAndGet(); 1570 } 1571 1572 /** 1573 * Builder used to construct instances of {@link Database}. 1574 * <p> 1575 * This class is intended for use by a single thread. 1576 * 1577 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1578 * @since 1.0.0 1579 */ 1580 @NotThreadSafe 1581 public static class Builder { 1582 @NonNull 1583 private final DataSource dataSource; 1584 @NonNull 1585 private final DatabaseType databaseType; 1586 @Nullable 1587 private ZoneId timeZone; 1588 @Nullable 1589 private AmbiguousTimestampBindingStrategy ambiguousTimestampBindingStrategy; 1590 @Nullable 1591 private InstanceProvider instanceProvider; 1592 @Nullable 1593 private PreparedStatementBinder preparedStatementBinder; 1594 @Nullable 1595 private ResultSetMapper resultSetMapper; 1596 @Nullable 1597 private StatementLogger statementLogger; 1598 @Nullable 1599 private ParameterRedactor parameterRedactor; 1600 @Nullable 1601 private MetricsCollector metricsCollector; 1602 @Nullable 1603 private Duration queryTimeout; 1604 @Nullable 1605 private Integer fetchSize; 1606 @Nullable 1607 private Integer maxRows; 1608 @Nullable 1609 private Integer parsedSqlCacheCapacity; 1610 1611 private Builder(@NonNull DataSource dataSource, @NonNull DatabaseType databaseType) { 1612 this.dataSource = requireNonNull(dataSource); 1613 this.databaseType = requireNonNull(databaseType); 1614 this.metricsCollector = null; 1615 this.queryTimeout = null; 1616 this.fetchSize = null; 1617 this.maxRows = null; 1618 this.parsedSqlCacheCapacity = null; 1619 } 1620 1621 /** 1622 * Configures the database time zone Pyranid should use when converting zone-less temporal values. 1623 * <p> 1624 * This value is used when mapping {@code TIMESTAMP} values to instant-based Java types, and when binding 1625 * {@link java.time.Instant} or {@link java.time.OffsetDateTime} parameters to known {@code TIMESTAMP} 1626 * targets. It also applies to ambiguous timestamp bindings if 1627 * {@link #ambiguousTimestampBindingStrategy(AmbiguousTimestampBindingStrategy)} is configured with 1628 * {@link AmbiguousTimestampBindingStrategy#TIMESTAMP_WITHOUT_TIME_ZONE}. 1629 * <p> 1630 * If {@code null}, Pyranid uses {@link ZoneId#systemDefault()}. 1631 * 1632 * @param timeZone database time zone to use, or {@code null} for the JVM default zone 1633 * @return this {@code Builder}, for chaining 1634 * @since 3.0.0 1635 */ 1636 @NonNull 1637 public Builder timeZone(@Nullable ZoneId timeZone) { 1638 this.timeZone = timeZone; 1639 return this; 1640 } 1641 1642 /** 1643 * Configures how Pyranid binds {@link java.time.Instant} and {@link java.time.OffsetDateTime} parameters 1644 * when JDBC parameter metadata cannot identify whether the target is {@code TIMESTAMP} or 1645 * {@code TIMESTAMP WITH TIME ZONE}. 1646 * <p> 1647 * The default, {@link AmbiguousTimestampBindingStrategy#TIMESTAMP_WITH_TIME_ZONE}, is appropriate for 1648 * timestamp-with-time-zone targets. Use 1649 * {@link AmbiguousTimestampBindingStrategy#TIMESTAMP_WITHOUT_TIME_ZONE} for drivers or proxies that 1650 * cannot provide identifying parameter metadata when your target columns are zone-less {@code TIMESTAMP} 1651 * values that should be interpreted in {@link #timeZone(ZoneId)}. 1652 * 1653 * @param ambiguousTimestampBindingStrategy strategy to use, or {@code null} for the default 1654 * @return this {@code Builder}, for chaining 1655 * @since 4.2.0 1656 */ 1657 @NonNull 1658 public Builder ambiguousTimestampBindingStrategy(@Nullable AmbiguousTimestampBindingStrategy ambiguousTimestampBindingStrategy) { 1659 this.ambiguousTimestampBindingStrategy = ambiguousTimestampBindingStrategy; 1660 return this; 1661 } 1662 1663 @NonNull 1664 public Builder instanceProvider(@Nullable InstanceProvider instanceProvider) { 1665 this.instanceProvider = instanceProvider; 1666 return this; 1667 } 1668 1669 @NonNull 1670 public Builder preparedStatementBinder(@Nullable PreparedStatementBinder preparedStatementBinder) { 1671 this.preparedStatementBinder = preparedStatementBinder; 1672 return this; 1673 } 1674 1675 @NonNull 1676 public Builder resultSetMapper(@Nullable ResultSetMapper resultSetMapper) { 1677 this.resultSetMapper = resultSetMapper; 1678 return this; 1679 } 1680 1681 /** 1682 * Configures the statement logger for the {@link Database} being built. 1683 * <p> 1684 * {@link StatementLogger} failures are fail-fast: a logger exception can make a successful statement operation throw, 1685 * and inside a Pyranid transaction it participates in normal rollback handling. If the database statement itself failed, 1686 * logger failures are suppressed onto the primary statement failure. 1687 * 1688 * @param statementLogger statement logger to use, or {@code null} for a no-op logger 1689 * @return this {@code Builder}, for chaining 1690 */ 1691 @NonNull 1692 public Builder statementLogger(@Nullable StatementLogger statementLogger) { 1693 this.statementLogger = statementLogger; 1694 return this; 1695 } 1696 1697 /** 1698 * Configures the redactor used for non-secure parameters in diagnostics. 1699 * <p> 1700 * {@link SecureParameter} values always render via {@link SecureParameter#getMask()} and are never passed to this 1701 * redactor. Batch executions render a bounded batch summary instead of invoking the redactor for each batch value. 1702 * Specify {@code null} or omit this setter to render non-secure, non-batch values verbatim. 1703 * 1704 * @param parameterRedactor parameter redactor to use, or {@code null} for the default 1705 * @return this {@code Builder}, for chaining 1706 * @since 4.4.0 1707 */ 1708 @NonNull 1709 public Builder parameterRedactor(@Nullable ParameterRedactor parameterRedactor) { 1710 this.parameterRedactor = parameterRedactor; 1711 return this; 1712 } 1713 1714 /** 1715 * Configures the metrics collector for the {@link Database} being built. 1716 * <p> 1717 * Like all {@code Database} configuration, this value is fixed at {@link #build()} time. Specify {@code null} 1718 * or omit this setter to disable metrics collection. 1719 * 1720 * @param metricsCollector metrics collector to use, or {@code null} to disable 1721 * @return this {@code Builder}, for chaining 1722 * @since 4.2.0 1723 */ 1724 @NonNull 1725 public Builder metricsCollector(@Nullable MetricsCollector metricsCollector) { 1726 this.metricsCollector = metricsCollector; 1727 return this; 1728 } 1729 1730 /** 1731 * Configures a database-wide JDBC query timeout default. 1732 * <p> 1733 * This maps to {@link java.sql.Statement#setQueryTimeout(int)}. {@code null} leaves the timeout unset. 1734 * {@link Duration#ZERO} disables the JDBC timeout. Positive sub-second durations are rounded up to one second 1735 * because JDBC accepts whole seconds. Per-query {@link Query#queryTimeout(Duration)} settings override this value, 1736 * and {@link Query#customize(PreparedStatementCustomizer)} can override both settings before parameter binding. 1737 * 1738 * @param queryTimeout timeout to apply by default, or {@code null} to leave unset 1739 * @return this {@code Builder}, for chaining 1740 * @since 4.2.0 1741 */ 1742 @NonNull 1743 public Builder queryTimeout(@Nullable Duration queryTimeout) { 1744 this.queryTimeout = validateQueryTimeout(queryTimeout); 1745 return this; 1746 } 1747 1748 /** 1749 * Configures a database-wide JDBC fetch size default. 1750 * <p> 1751 * This maps to {@link java.sql.Statement#setFetchSize(int)}. {@code null} leaves the fetch size unset. A value 1752 * of {@code 0} uses the driver's default fetch-size behavior. Per-query {@link Query#fetchSize(Integer)} 1753 * settings override this value, and {@link Query#customize(PreparedStatementCustomizer)} can override both 1754 * settings before parameter binding. 1755 * 1756 * @param fetchSize fetch size to apply by default, or {@code null} to leave unset 1757 * @return this {@code Builder}, for chaining 1758 * @since 4.2.0 1759 */ 1760 @NonNull 1761 public Builder fetchSize(@Nullable Integer fetchSize) { 1762 this.fetchSize = validateNonNegativeStatementSetting("fetchSize", fetchSize); 1763 return this; 1764 } 1765 1766 /** 1767 * Configures a database-wide JDBC maximum row count default. 1768 * <p> 1769 * This maps to {@link java.sql.Statement#setMaxRows(int)}. {@code null} leaves the maximum row count unset. A 1770 * value of {@code 0} disables the JDBC row limit. Per-query {@link Query#maxRows(Integer)} settings override 1771 * this value, and {@link Query#customize(PreparedStatementCustomizer)} can override both settings before parameter 1772 * binding. 1773 * 1774 * @param maxRows maximum rows to apply by default, or {@code null} to leave unset 1775 * @return this {@code Builder}, for chaining 1776 * @since 4.2.0 1777 */ 1778 @NonNull 1779 public Builder maxRows(@Nullable Integer maxRows) { 1780 this.maxRows = validateNonNegativeStatementSetting("maxRows", maxRows); 1781 return this; 1782 } 1783 1784 /** 1785 * Configures the size of the parsed SQL cache. 1786 * <p> 1787 * A value of {@code 0} disables caching. A value of {@code null} uses the default size. 1788 * 1789 * @param parsedSqlCacheCapacity cache size (0 disables caching, null uses default) 1790 * @return this {@code Builder}, for chaining 1791 */ 1792 @NonNull 1793 public Builder parsedSqlCacheCapacity(@Nullable Integer parsedSqlCacheCapacity) { 1794 if (parsedSqlCacheCapacity != null && parsedSqlCacheCapacity < 0) 1795 throw new IllegalArgumentException("parsedSqlCacheCapacity must be >= 0"); 1796 1797 this.parsedSqlCacheCapacity = parsedSqlCacheCapacity; 1798 return this; 1799 } 1800 1801 @NonNull 1802 public Database build() { 1803 return new Database(this); 1804 } 1805 } 1806 1807}