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.ParameterMetaData; 028import java.sql.PreparedStatement; 029import java.sql.ResultSet; 030import java.sql.SQLException; 031import java.sql.SQLFeatureNotSupportedException; 032import java.sql.Types; 033import java.time.Duration; 034import java.time.ZoneId; 035import java.util.ArrayDeque; 036import java.util.ArrayList; 037import java.util.Arrays; 038import java.util.Collection; 039import java.util.Deque; 040import java.util.HashSet; 041import java.util.LinkedHashMap; 042import java.util.List; 043import java.util.Locale; 044import java.util.Map; 045import java.util.Optional; 046import java.util.OptionalDouble; 047import java.util.OptionalInt; 048import java.util.OptionalLong; 049import java.util.Queue; 050import java.util.Set; 051import java.util.Spliterator; 052import java.util.Spliterators; 053import java.util.concurrent.atomic.AtomicReference; 054import java.util.concurrent.atomic.AtomicLong; 055import java.util.concurrent.locks.ReentrantLock; 056import java.util.function.Consumer; 057import java.util.function.Function; 058import java.util.logging.Logger; 059import java.util.regex.Pattern; 060import java.util.stream.Collectors; 061import java.util.stream.Stream; 062import java.util.stream.StreamSupport; 063 064import static java.lang.String.format; 065import static java.lang.System.nanoTime; 066import static java.util.Objects.requireNonNull; 067 068/** 069 * Main class for performing database access operations. 070 * 071 * @author <a href="https://www.revetkn.com">Mark Allen</a> 072 * @since 1.0.0 073 */ 074@ThreadSafe 075public final class Database { 076 @NonNull 077 private static final ThreadLocal<Deque<Transaction>> TRANSACTION_STACK_HOLDER; 078 private static final int DEFAULT_PARSED_SQL_CACHE_CAPACITY = 1024; 079 private static final int MAX_DIAGNOSTIC_MESSAGE_LENGTH = 1024; 080 private static final int MAX_DIAGNOSTIC_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 = ThreadLocal.withInitial(() -> new ArrayDeque<>()); 088 } 089 090 @NonNull 091 private final DataSource dataSource; 092 @NonNull 093 private final AtomicReference<@Nullable DatabaseType> databaseType; 094 @NonNull 095 private final ThreadLocal<Connection> databaseTypeDetectionConnectionHolder; 096 @NonNull 097 private final ZoneId timeZone; 098 @NonNull 099 private final InstanceProvider instanceProvider; 100 @NonNull 101 private final PreparedStatementBinder preparedStatementBinder; 102 @NonNull 103 private final ResultSetMapper resultSetMapper; 104 @NonNull 105 private final StatementLogger statementLogger; 106 @Nullable 107 private final Map<String, ParsedSql> parsedSqlCache; 108 @NonNull 109 private final AtomicLong defaultIdGenerator; 110 @NonNull 111 private final Logger logger; 112 113 @NonNull 114 private volatile DatabaseOperationSupportStatus executeLargeBatchSupported; 115 @NonNull 116 private volatile DatabaseOperationSupportStatus executeLargeUpdateSupported; 117 118 protected Database(@NonNull Builder builder) { 119 requireNonNull(builder); 120 121 this.dataSource = requireNonNull(builder.dataSource); 122 this.databaseType = new AtomicReference<>(builder.databaseType); 123 this.databaseTypeDetectionConnectionHolder = new ThreadLocal<>(); 124 this.timeZone = builder.timeZone == null ? ZoneId.systemDefault() : builder.timeZone; 125 this.instanceProvider = builder.instanceProvider == null ? new InstanceProvider() {} : builder.instanceProvider; 126 this.preparedStatementBinder = builder.preparedStatementBinder == null ? PreparedStatementBinder.withDefaultConfiguration() : builder.preparedStatementBinder; 127 this.resultSetMapper = builder.resultSetMapper == null ? ResultSetMapper.withDefaultConfiguration() : builder.resultSetMapper; 128 this.statementLogger = builder.statementLogger == null ? (statementLog) -> {} : builder.statementLogger; 129 if (builder.parsedSqlCacheCapacity != null && builder.parsedSqlCacheCapacity < 0) 130 throw new IllegalArgumentException("parsedSqlCacheCapacity must be >= 0"); 131 132 int parsedSqlCacheCapacity = builder.parsedSqlCacheCapacity == null 133 ? DEFAULT_PARSED_SQL_CACHE_CAPACITY 134 : builder.parsedSqlCacheCapacity; 135 this.parsedSqlCache = parsedSqlCacheCapacity == 0 ? null : new ConcurrentLruMap<>(parsedSqlCacheCapacity); 136 this.defaultIdGenerator = new AtomicLong(); 137 this.logger = Logger.getLogger(getClass().getName()); 138 this.executeLargeBatchSupported = DatabaseOperationSupportStatus.UNKNOWN; 139 this.executeLargeUpdateSupported = DatabaseOperationSupportStatus.UNKNOWN; 140 } 141 142 /** 143 * Provides a {@link Database} builder for the given {@link DataSource}. 144 * 145 * @param dataSource data source used to create the {@link Database} builder 146 * @return a {@link Database} builder 147 */ 148 @NonNull 149 public static Builder withDataSource(@NonNull DataSource dataSource) { 150 requireNonNull(dataSource); 151 return new Builder(dataSource); 152 } 153 154 /** 155 * Gets a reference to the current transaction, if any. 156 * 157 * @return the current transaction 158 */ 159 @NonNull 160 public Optional<Transaction> currentTransaction() { 161 Deque<Transaction> transactionStack = TRANSACTION_STACK_HOLDER.get(); 162 return Optional.ofNullable(transactionStack.isEmpty() ? null : transactionStack.peek()); 163 } 164 165 /** 166 * Performs an operation transactionally. 167 * <p> 168 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 169 * <p> 170 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 171 * not automatically join an outer transaction. Use {@link #participate(Transaction, TransactionalOperation)} to join an 172 * existing transaction explicitly. 173 * 174 * @param transactionalOperation the operation to perform transactionally 175 */ 176 public void transaction(@NonNull TransactionalOperation transactionalOperation) { 177 requireNonNull(transactionalOperation); 178 179 transaction(() -> { 180 transactionalOperation.perform(); 181 return Optional.empty(); 182 }); 183 } 184 185 /** 186 * Performs an operation transactionally with the given isolation level. 187 * <p> 188 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 189 * <p> 190 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 191 * not automatically join an outer transaction. Use {@link #participate(Transaction, TransactionalOperation)} to join an 192 * existing transaction explicitly. 193 * 194 * @param transactionIsolation the desired database transaction isolation level 195 * @param transactionalOperation the operation to perform transactionally 196 */ 197 public void transaction(@NonNull TransactionIsolation transactionIsolation, 198 @NonNull TransactionalOperation transactionalOperation) { 199 requireNonNull(transactionIsolation); 200 requireNonNull(transactionalOperation); 201 202 transaction(transactionIsolation, () -> { 203 transactionalOperation.perform(); 204 return Optional.empty(); 205 }); 206 } 207 208 /** 209 * Performs an operation transactionally and optionally returns a value. 210 * <p> 211 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 212 * <p> 213 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 214 * not automatically join an outer transaction. Use {@link #participate(Transaction, ReturningTransactionalOperation)} to 215 * join an existing transaction explicitly. 216 * 217 * @param transactionalOperation the operation to perform transactionally 218 * @param <T> the type to be returned 219 * @return the result of the transactional operation 220 */ 221 @NonNull 222 public <T> Optional<T> transaction(@NonNull ReturningTransactionalOperation<T> transactionalOperation) { 223 requireNonNull(transactionalOperation); 224 return transaction(TransactionIsolation.DEFAULT, transactionalOperation); 225 } 226 227 /** 228 * Performs an operation transactionally with the given isolation level, optionally returning a value. 229 * <p> 230 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 231 * <p> 232 * Nested calls to {@code transaction(...)} are independent transactions with independent JDBC connections; they do 233 * not automatically join an outer transaction. Use {@link #participate(Transaction, ReturningTransactionalOperation)} to 234 * join an existing transaction explicitly. 235 * 236 * @param transactionIsolation the desired database transaction isolation level 237 * @param transactionalOperation the operation to perform transactionally 238 * @param <T> the type to be returned 239 * @return the result of the transactional operation 240 */ 241 @NonNull 242 public <T> Optional<T> transaction(@NonNull TransactionIsolation transactionIsolation, 243 @NonNull ReturningTransactionalOperation<T> transactionalOperation) { 244 requireNonNull(transactionIsolation); 245 requireNonNull(transactionalOperation); 246 247 Transaction transaction = new Transaction(dataSource, transactionIsolation); 248 TRANSACTION_STACK_HOLDER.get().push(transaction); 249 boolean committed = false; 250 Throwable thrown = null; 251 252 try { 253 Optional<T> returnValue = transactionalOperation.perform(); 254 255 // Safeguard in case user code accidentally returns null instead of Optional.empty() 256 if (returnValue == null) 257 returnValue = Optional.empty(); 258 259 if (transaction.isRollbackOnly()) { 260 transaction.rollback(); 261 } else { 262 transaction.commit(); 263 committed = true; 264 } 265 266 return returnValue; 267 } catch (RuntimeException e) { 268 thrown = e; 269 try { 270 transaction.rollback(); 271 } catch (Exception rollbackException) { 272 e.addSuppressed(rollbackException); 273 } 274 275 restoreInterruptIfNeeded(e); 276 throw e; 277 } catch (Error e) { 278 thrown = e; 279 try { 280 transaction.rollback(); 281 } catch (Exception rollbackException) { 282 e.addSuppressed(rollbackException); 283 } 284 285 restoreInterruptIfNeeded(e); 286 throw e; 287 } catch (Throwable t) { 288 RuntimeException wrapped = new RuntimeException(t); 289 thrown = wrapped; 290 try { 291 transaction.rollback(); 292 } catch (Exception rollbackException) { 293 wrapped.addSuppressed(rollbackException); 294 } 295 296 restoreInterruptIfNeeded(t); 297 throw wrapped; 298 } finally { 299 Deque<Transaction> transactionStack = TRANSACTION_STACK_HOLDER.get(); 300 301 transactionStack.pop(); 302 303 // Ensure txn stack is fully cleaned up 304 if (transactionStack.isEmpty()) 305 TRANSACTION_STACK_HOLDER.remove(); 306 307 Throwable cleanupFailure = null; 308 309 try { 310 try { 311 transaction.restoreTransactionIsolationIfNeeded(); 312 313 if (transaction.getInitialAutoCommit().isPresent() && transaction.getInitialAutoCommit().get()) 314 // Autocommit was true initially, so restoring to true now that transaction has completed 315 transaction.setAutoCommit(true); 316 } catch (Throwable cleanupException) { 317 cleanupFailure = cleanupException; 318 } finally { 319 Connection connection = transaction.getExistingConnection().orElse(null); 320 321 if (connection != null) { 322 try { 323 closeConnection(connection); 324 transaction.clearConnection(); 325 } catch (Throwable cleanupException) { 326 if (cleanupFailure == null) 327 cleanupFailure = cleanupException; 328 else 329 cleanupFailure.addSuppressed(cleanupException); 330 } 331 } 332 } 333 } finally { 334 transaction.markCompleted(); 335 336 // Execute any user-supplied post-execution hooks 337 for (Consumer<TransactionResult> postTransactionOperation : transaction.getPostTransactionOperations()) { 338 try { 339 postTransactionOperation.accept(committed ? TransactionResult.COMMITTED : TransactionResult.ROLLED_BACK); 340 } catch (Throwable cleanupException) { 341 if (cleanupFailure == null) 342 cleanupFailure = cleanupException; 343 else 344 cleanupFailure.addSuppressed(cleanupException); 345 } 346 } 347 } 348 349 if (cleanupFailure != null) { 350 if (thrown != null) { 351 thrown.addSuppressed(cleanupFailure); 352 } else if (cleanupFailure instanceof RuntimeException) { 353 throw (RuntimeException) cleanupFailure; 354 } else if (cleanupFailure instanceof Error) { 355 throw (Error) cleanupFailure; 356 } else { 357 throw new RuntimeException(cleanupFailure); 358 } 359 } 360 } 361 } 362 363 protected void closeConnection(@NonNull Connection connection) { 364 requireNonNull(connection); 365 366 try { 367 connection.close(); 368 } catch (SQLException e) { 369 throw new DatabaseException("Unable to close database connection", e); 370 } 371 } 372 373 @NonNull 374 private static DatabaseException databaseExceptionWithStatementContext(@NonNull StatementContext<?> statementContext, 375 @NonNull Throwable cause) { 376 requireNonNull(statementContext); 377 requireNonNull(cause); 378 379 String message = cause.getMessage(); 380 381 if (message == null || message.trim().length() == 0) 382 message = "Database operation failed"; 383 384 return databaseExceptionWithStatementContext(statementContext, message, cause); 385 } 386 387 @NonNull 388 private static DatabaseException databaseExceptionWithStatementContext(@NonNull StatementContext<?> statementContext, 389 @NonNull String message, 390 @NonNull Throwable cause) { 391 requireNonNull(statementContext); 392 requireNonNull(message); 393 requireNonNull(cause); 394 395 return new DatabaseException(format("%s [%s]", boundedDiagnosticMessage(message), statementDiagnostic(statementContext)), cause); 396 } 397 398 @NonNull 399 private static String statementDiagnostic(@NonNull StatementContext<?> statementContext) { 400 requireNonNull(statementContext); 401 402 Statement statement = statementContext.getStatement(); 403 return format("statementId=%s, sql=%s, parameterCount=%s", 404 statement.getId(), boundedSql(statement.getSql()), statementContext.getParameters().size()); 405 } 406 407 @NonNull 408 private static String boundedDiagnosticMessage(@NonNull String message) { 409 requireNonNull(message); 410 411 String compactMessage = DIAGNOSTIC_WHITESPACE_PATTERN.matcher(message).replaceAll(" ").trim(); 412 413 if (compactMessage.length() <= MAX_DIAGNOSTIC_MESSAGE_LENGTH) 414 return compactMessage; 415 416 int prefixLength = Math.max(0, MAX_DIAGNOSTIC_MESSAGE_LENGTH - TRUNCATED_SUFFIX.length()); 417 return compactMessage.substring(0, prefixLength) + TRUNCATED_SUFFIX; 418 } 419 420 @NonNull 421 private static String boundedSql(@NonNull String sql) { 422 requireNonNull(sql); 423 424 String compactSql = DIAGNOSTIC_WHITESPACE_PATTERN.matcher(sql).replaceAll(" ").trim(); 425 426 if (compactSql.length() <= MAX_DIAGNOSTIC_SQL_LENGTH) 427 return compactSql; 428 429 int prefixLength = Math.max(0, MAX_DIAGNOSTIC_SQL_LENGTH - TRUNCATED_SUFFIX.length()); 430 return compactSql.substring(0, prefixLength) + TRUNCATED_SUFFIX; 431 } 432 433 /** 434 * Performs an operation in the context of a pre-existing transaction. 435 * <p> 436 * No commit or rollback on the transaction will occur when {@code transactionalOperation} completes. 437 * <p> 438 * However, if an exception bubbles out of {@code transactionalOperation}, the transaction will be marked as rollback-only. 439 * 440 * @param transaction the transaction in which to participate 441 * @param transactionalOperation the operation that should participate in the transaction 442 */ 443 public void participate(@NonNull Transaction transaction, 444 @NonNull TransactionalOperation transactionalOperation) { 445 requireNonNull(transaction); 446 requireNonNull(transactionalOperation); 447 448 participate(transaction, () -> { 449 transactionalOperation.perform(); 450 return Optional.empty(); 451 }); 452 } 453 454 /** 455 * Performs an operation in the context of a pre-existing transaction, optionall returning a value. 456 * <p> 457 * No commit or rollback on the transaction will occur when {@code transactionalOperation} completes. 458 * <p> 459 * However, if an exception bubbles out of {@code transactionalOperation}, the transaction will be marked as rollback-only. 460 * 461 * @param transaction the transaction in which to participate 462 * @param transactionalOperation the operation that should participate in the transaction 463 * @param <T> the type to be returned 464 * @return the result of the transactional operation 465 */ 466 @NonNull 467 public <T> Optional<T> participate(@NonNull Transaction transaction, 468 @NonNull ReturningTransactionalOperation<T> transactionalOperation) { 469 requireNonNull(transaction); 470 requireNonNull(transactionalOperation); 471 472 Deque<Transaction> transactionStack = TRANSACTION_STACK_HOLDER.get(); 473 transactionStack.push(transaction); 474 475 try { 476 Optional<T> returnValue = transactionalOperation.perform(); 477 return returnValue == null ? Optional.empty() : returnValue; 478 } catch (RuntimeException e) { 479 if (!(e instanceof StatementLoggerFailureException)) 480 transaction.setRollbackOnly(true); 481 restoreInterruptIfNeeded(e); 482 throw e; 483 } catch (Error e) { 484 transaction.setRollbackOnly(true); 485 restoreInterruptIfNeeded(e); 486 throw e; 487 } catch (Throwable t) { 488 transaction.setRollbackOnly(true); 489 restoreInterruptIfNeeded(t); 490 throw new RuntimeException(t); 491 } finally { 492 try { 493 transactionStack.pop(); 494 } finally { 495 if (transactionStack.isEmpty()) 496 TRANSACTION_STACK_HOLDER.remove(); 497 } 498 } 499 } 500 501 /** 502 * Creates a fluent builder for executing SQL. 503 * <p> 504 * Named parameters use the {@code :paramName} syntax and are bound via {@link Query#bind(String, Object)}. 505 * Positional parameters via {@code ?} are not supported. 506 * <p> 507 * Example: 508 * <pre>{@code 509 * Optional<Employee> employee = database.query("SELECT * FROM employee WHERE id = :id") 510 * .bind("id", 42) 511 * .fetchObject(Employee.class); 512 * }</pre> 513 * 514 * @param sql SQL containing {@code :paramName} placeholders 515 * @return a fluent builder for binding parameters and executing 516 * @since 4.0.0 517 */ 518 @NonNull 519 public Query query(@NonNull String sql) { 520 requireNonNull(sql); 521 return new DefaultQuery(this, sql); 522 } 523 524 private static void restoreInterruptIfNeeded(@NonNull Throwable throwable) { 525 requireNonNull(throwable); 526 527 Throwable current = throwable; 528 529 while (current != null) { 530 if (current instanceof InterruptedException) { 531 Thread.currentThread().interrupt(); 532 return; 533 } 534 535 current = current.getCause(); 536 } 537 } 538 539 @Nullable 540 private static Object unwrapOptionalValue(@Nullable Object value) { 541 if (value == null) 542 return null; 543 544 if (value instanceof Optional<?> optional) 545 return optional.orElse(null); 546 if (value instanceof OptionalInt optionalInt) 547 return optionalInt.isPresent() ? optionalInt.getAsInt() : null; 548 if (value instanceof OptionalLong optionalLong) 549 return optionalLong.isPresent() ? optionalLong.getAsLong() : null; 550 if (value instanceof OptionalDouble optionalDouble) 551 return optionalDouble.isPresent() ? optionalDouble.getAsDouble() : null; 552 553 return value; 554 } 555 556 @Nullable 557 private static Throwable closeStatementContextResources(@NonNull StatementContext<?> statementContext, 558 @Nullable Throwable cleanupFailure) { 559 requireNonNull(statementContext); 560 561 Queue<AutoCloseable> cleanupOperations = statementContext.getCleanupOperations(); 562 AutoCloseable cleanupOperation; 563 564 while ((cleanupOperation = cleanupOperations.poll()) != null) { 565 try { 566 cleanupOperation.close(); 567 } catch (Throwable cleanupException) { 568 if (cleanupFailure == null) 569 cleanupFailure = cleanupException; 570 else 571 cleanupFailure.addSuppressed(cleanupException); 572 } 573 } 574 575 return cleanupFailure; 576 } 577 578 private static boolean isUnsupportedSqlFeature(@NonNull SQLException e) { 579 requireNonNull(e); 580 581 String sqlState = e.getSQLState(); 582 if (sqlState != null) { 583 if (sqlState.startsWith("0A") || "HYC00".equals(sqlState)) 584 return true; 585 } 586 587 Throwable cause = e.getCause(); 588 if (cause instanceof SQLFeatureNotSupportedException 589 || cause instanceof UnsupportedOperationException 590 || cause instanceof AbstractMethodError) 591 return true; 592 593 String message = e.getMessage(); 594 if (message == null) 595 return false; 596 597 String lower = message.toLowerCase(Locale.ROOT); 598 return lower.contains("not supported") 599 || lower.contains("unsupported") 600 || lower.contains("not implemented") 601 || lower.contains("feature not supported"); 602 } 603 604 @NonNull 605 private ParsedSql getParsedSql(@NonNull String sql) { 606 requireNonNull(sql); 607 608 if (this.parsedSqlCache == null) 609 return parseNamedParameterSql(sql); 610 611 return this.parsedSqlCache.computeIfAbsent(sql, Database::parseNamedParameterSql); 612 } 613 614 /** 615 * Default internal implementation of {@link Query}. 616 * <p> 617 * This class is intended for use by a single thread. 618 */ 619 @NotThreadSafe 620 private static final class DefaultQuery implements Query { 621 @NonNull 622 private final Database database; 623 @NonNull 624 private final String originalSql; 625 @NonNull 626 private final List<String> sqlFragments; 627 @NonNull 628 private final List<String> parameterNames; 629 @NonNull 630 private final Set<String> distinctParameterNames; 631 @NonNull 632 private final Map<String, Object> bindings; 633 @Nullable 634 private PreparedStatementCustomizer preparedStatementCustomizer; 635 @Nullable 636 private Object id; 637 638 private DefaultQuery(@NonNull Database database, 639 @NonNull String sql) { 640 requireNonNull(database); 641 requireNonNull(sql); 642 643 this.database = database; 644 this.originalSql = sql; 645 646 ParsedSql parsedSql = database.getParsedSql(sql); 647 this.sqlFragments = parsedSql.sqlFragments; 648 this.parameterNames = parsedSql.parameterNames; 649 this.distinctParameterNames = parsedSql.distinctParameterNames; 650 651 this.bindings = new LinkedHashMap<>(Math.max(8, this.distinctParameterNames.size())); 652 this.preparedStatementCustomizer = null; 653 } 654 655 @NonNull 656 @Override 657 public Query bind(@NonNull String name, 658 @Nullable Object value) { 659 requireNonNull(name); 660 661 if (!this.distinctParameterNames.contains(name)) 662 throw new IllegalArgumentException(format("Unknown named parameter '%s' for SQL: %s", name, this.originalSql)); 663 664 this.bindings.put(name, value); 665 return this; 666 } 667 668 @NonNull 669 @Override 670 public Query bindAll(@NonNull Map<@NonNull String, @Nullable Object> parameters) { 671 requireNonNull(parameters); 672 673 for (Map.Entry<@NonNull String, @Nullable Object> entry : parameters.entrySet()) 674 bind(entry.getKey(), entry.getValue()); 675 676 return this; 677 } 678 679 @NonNull 680 @Override 681 public Query id(@Nullable Object id) { 682 this.id = id; 683 return this; 684 } 685 686 @NonNull 687 @Override 688 public Query customize(@NonNull PreparedStatementCustomizer preparedStatementCustomizer) { 689 requireNonNull(preparedStatementCustomizer); 690 this.preparedStatementCustomizer = preparedStatementCustomizer; 691 692 return this; 693 } 694 695 @NonNull 696 @Override 697 public <T> Optional<T> fetchObject(@NonNull Class<T> resultType) { 698 requireNonNull(resultType); 699 PreparedQuery preparedQuery = prepare(this.bindings); 700 return this.database.queryForObject(preparedQuery.statement, resultType, this.preparedStatementCustomizer, preparedQuery.parameters); 701 } 702 703 @NonNull 704 @Override 705 public <T> List<@Nullable T> fetchList(@NonNull Class<T> resultType) { 706 requireNonNull(resultType); 707 PreparedQuery preparedQuery = prepare(this.bindings); 708 return this.database.queryForList(preparedQuery.statement, resultType, this.preparedStatementCustomizer, preparedQuery.parameters); 709 } 710 711 @Nullable 712 @Override 713 public <T, R> R fetchStream(@NonNull Class<T> resultType, 714 @NonNull Function<Stream<@Nullable T>, R> streamFunction) { 715 requireNonNull(resultType); 716 requireNonNull(streamFunction); 717 PreparedQuery preparedQuery = prepare(this.bindings); 718 return this.database.queryForStream(preparedQuery.statement, resultType, this.preparedStatementCustomizer, streamFunction, preparedQuery.parameters); 719 } 720 721 722 @NonNull 723 @Override 724 public Long execute() { 725 PreparedQuery preparedQuery = prepare(this.bindings); 726 return this.database.execute(preparedQuery.statement, this.preparedStatementCustomizer, preparedQuery.parameters); 727 } 728 729 @NonNull 730 @Override 731 public List<Long> executeBatch(@NonNull List<@NonNull Map<@NonNull String, @Nullable Object>> parameterGroups) { 732 requireNonNull(parameterGroups); 733 if (parameterGroups.isEmpty()) 734 return List.of(); 735 736 List<List<Object>> parametersAsList = new ArrayList<>(parameterGroups.size()); 737 Object statementId = this.id == null ? this.database.generateId() : this.id; 738 Statement statement = null; 739 String expandedSql = null; 740 741 for (Map<@NonNull String, @Nullable Object> parameterGroup : parameterGroups) { 742 requireNonNull(parameterGroup); 743 744 for (String parameterName : parameterGroup.keySet()) 745 if (!this.distinctParameterNames.contains(parameterName)) 746 throw new IllegalArgumentException(format("Unknown named parameter '%s' for SQL: %s", parameterName, this.originalSql)); 747 748 Map<String, Object> mergedBindings; 749 if (this.bindings.isEmpty()) { 750 mergedBindings = parameterGroup; 751 } else if (parameterGroup.isEmpty()) { 752 mergedBindings = this.bindings; 753 } else { 754 Map<String, Object> combinedBindings = new LinkedHashMap<>(this.bindings); 755 combinedBindings.putAll(parameterGroup); 756 mergedBindings = combinedBindings; 757 } 758 759 PreparedQuery preparedQuery = prepare(mergedBindings, statementId); 760 761 if (expandedSql == null) { 762 expandedSql = preparedQuery.statement.getSql(); 763 statement = preparedQuery.statement; 764 } else if (!expandedSql.equals(preparedQuery.statement.getSql())) { 765 throw new IllegalArgumentException(format( 766 "Inconsistent SQL after expanding parameters for batch execution; ensure collection sizes are consistent. SQL: %s", 767 this.originalSql)); 768 } 769 770 parametersAsList.add(Arrays.asList(preparedQuery.parameters)); 771 } 772 773 if (statement == null) 774 statement = Statement.of(statementId, buildPlaceholderSql()); 775 776 return this.database.executeBatch(statement, parametersAsList, this.preparedStatementCustomizer); 777 } 778 779 @NonNull 780 @Override 781 public <T> Optional<T> executeForObject(@NonNull Class<T> resultType) { 782 requireNonNull(resultType); 783 PreparedQuery preparedQuery = prepare(this.bindings); 784 return this.database.executeForObject(preparedQuery.statement, resultType, this.preparedStatementCustomizer, preparedQuery.parameters); 785 } 786 787 @NonNull 788 @Override 789 public <T> List<@Nullable T> executeForList(@NonNull Class<T> resultType) { 790 requireNonNull(resultType); 791 PreparedQuery preparedQuery = prepare(this.bindings); 792 return this.database.executeForList(preparedQuery.statement, resultType, this.preparedStatementCustomizer, preparedQuery.parameters); 793 } 794 795 @NonNull 796 private PreparedQuery prepare(@NonNull Map<String, Object> bindings) { 797 Object statementId = this.id == null ? this.database.generateId() : this.id; 798 return prepare(bindings, statementId); 799 } 800 801 @NonNull 802 private PreparedQuery prepare(@NonNull Map<String, Object> bindings, 803 @NonNull Object statementId) { 804 requireNonNull(bindings); 805 requireNonNull(statementId); 806 807 if (this.parameterNames.isEmpty()) 808 return new PreparedQuery(Statement.of(statementId, this.originalSql), new Object[0]); 809 810 StringBuilder sql = new StringBuilder(this.originalSql.length() + this.parameterNames.size() * 2); 811 List<String> missingParameterNames = null; 812 List<Object> parameters = new ArrayList<>(this.parameterNames.size()); 813 814 for (int i = 0; i < this.parameterNames.size(); ++i) { 815 String parameterName = this.parameterNames.get(i); 816 sql.append(this.sqlFragments.get(i)); 817 818 if (!bindings.containsKey(parameterName)) { 819 if (missingParameterNames == null) 820 missingParameterNames = new ArrayList<>(); 821 822 missingParameterNames.add(parameterName); 823 sql.append('?'); 824 continue; 825 } 826 827 Object value = unwrapOptionalValue(bindings.get(parameterName)); 828 829 if (value instanceof InListParameter inListParameter) { 830 Object[] elements = inListParameter.getElements(); 831 832 if (elements.length == 0) 833 throw new IllegalArgumentException(format("IN-list parameter '%s' for SQL: %s is empty", parameterName, this.originalSql)); 834 835 appendPlaceholders(sql, elements.length); 836 parameters.addAll(Arrays.asList(elements)); 837 } else if (value instanceof Collection<?>) { 838 throw new IllegalArgumentException(format( 839 "Collection parameter '%s' for SQL: %s must be wrapped with %s.inList(...) or %s.listOf/%s.setOf(...)", 840 parameterName, this.originalSql, 841 Parameters.class.getSimpleName(), 842 Parameters.class.getSimpleName(), Parameters.class.getSimpleName())); 843 } else if (value != null && value.getClass().isArray() && !(value instanceof byte[])) { 844 throw new IllegalArgumentException(format( 845 "Array parameter '%s' for SQL: %s must be wrapped with %s.inList(...), %s.sqlArrayOf(...), or %s.arrayOf(Class, ...)", 846 parameterName, this.originalSql, 847 Parameters.class.getSimpleName(), Parameters.class.getSimpleName(), Parameters.class.getSimpleName())); 848 } else { 849 sql.append('?'); 850 parameters.add(value); 851 } 852 } 853 854 sql.append(this.sqlFragments.get(this.sqlFragments.size() - 1)); 855 856 if (missingParameterNames != null) 857 throw new IllegalArgumentException(format("Missing required named parameters %s for SQL: %s", missingParameterNames, this.originalSql)); 858 859 return new PreparedQuery(Statement.of(statementId, sql.toString()), parameters.toArray()); 860 } 861 862 @NonNull 863 private String buildPlaceholderSql() { 864 if (this.parameterNames.isEmpty()) 865 return this.originalSql; 866 867 StringBuilder sql = new StringBuilder(this.originalSql.length() + this.parameterNames.size() * 2); 868 869 for (int i = 0; i < this.parameterNames.size(); ++i) 870 sql.append(this.sqlFragments.get(i)).append('?'); 871 872 sql.append(this.sqlFragments.get(this.sqlFragments.size() - 1)); 873 return sql.toString(); 874 } 875 876 private void appendPlaceholders(@NonNull StringBuilder sql, 877 int count) { 878 requireNonNull(sql); 879 880 for (int i = 0; i < count; ++i) { 881 if (i > 0) 882 sql.append(", "); 883 sql.append('?'); 884 } 885 } 886 887 private static final class PreparedQuery { 888 @NonNull 889 private final Statement statement; 890 @NonNull 891 private final Object @NonNull [] parameters; 892 893 private PreparedQuery(@NonNull Statement statement, 894 Object @NonNull [] parameters) { 895 this.statement = requireNonNull(statement); 896 this.parameters = requireNonNull(parameters); 897 } 898 } 899 900 } 901 902 static final class ParsedSql { 903 @NonNull 904 private final List<String> sqlFragments; 905 @NonNull 906 private final List<String> parameterNames; 907 @NonNull 908 private final Set<String> distinctParameterNames; 909 910 private ParsedSql(@NonNull List<String> sqlFragments, 911 @NonNull List<String> parameterNames, 912 @NonNull Set<String> distinctParameterNames) { 913 requireNonNull(sqlFragments); 914 requireNonNull(parameterNames); 915 requireNonNull(distinctParameterNames); 916 917 this.sqlFragments = sqlFragments; 918 this.parameterNames = parameterNames; 919 this.distinctParameterNames = distinctParameterNames; 920 } 921 } 922 923 @NonNull 924 static ParsedSql parseNamedParameterSql(@NonNull String sql) { 925 requireNonNull(sql); 926 927 List<String> sqlFragments = new ArrayList<>(); 928 StringBuilder sqlFragment = new StringBuilder(sql.length()); 929 List<String> parameterNames = new ArrayList<>(); 930 Set<String> distinctParameterNames = new HashSet<>(); 931 932 boolean inSingleQuote = false; 933 boolean inSingleQuoteEscapesBackslash = false; 934 boolean inDoubleQuote = false; 935 boolean inBacktickQuote = false; 936 boolean inBracketQuote = false; 937 boolean inLineComment = false; 938 int blockCommentDepth = 0; 939 String dollarQuoteDelimiter = null; 940 941 for (int i = 0; i < sql.length(); ) { 942 if (dollarQuoteDelimiter != null) { 943 if (sql.startsWith(dollarQuoteDelimiter, i)) { 944 sqlFragment.append(dollarQuoteDelimiter); 945 i += dollarQuoteDelimiter.length(); 946 dollarQuoteDelimiter = null; 947 } else { 948 sqlFragment.append(sql.charAt(i)); 949 ++i; 950 } 951 952 continue; 953 } 954 955 char c = sql.charAt(i); 956 957 if (inLineComment) { 958 sqlFragment.append(c); 959 ++i; 960 961 if (c == '\n' || c == '\r') 962 inLineComment = false; 963 964 continue; 965 } 966 967 if (blockCommentDepth > 0) { 968 if (c == '/' && i + 1 < sql.length() && sql.charAt(i + 1) == '*') { 969 sqlFragment.append("/*"); 970 i += 2; 971 ++blockCommentDepth; 972 } else if (c == '*' && i + 1 < sql.length() && sql.charAt(i + 1) == '/') { 973 sqlFragment.append("*/"); 974 i += 2; 975 --blockCommentDepth; 976 } else { 977 sqlFragment.append(c); 978 ++i; 979 } 980 981 continue; 982 } 983 984 if (inSingleQuote) { 985 sqlFragment.append(c); 986 987 if (inSingleQuoteEscapesBackslash && c == '\\' && i + 1 < sql.length()) { 988 sqlFragment.append(sql.charAt(i + 1)); 989 i += 2; 990 continue; 991 } 992 993 if (c == '\'') { 994 // Escaped quote: '' 995 if (i + 1 < sql.length() && sql.charAt(i + 1) == '\'') { 996 sqlFragment.append('\''); 997 i += 2; 998 continue; 999 } 1000 1001 inSingleQuote = false; 1002 inSingleQuoteEscapesBackslash = false; 1003 } 1004 1005 ++i; 1006 continue; 1007 } 1008 1009 if (inDoubleQuote) { 1010 sqlFragment.append(c); 1011 1012 if (c == '"') { 1013 // Escaped quote: "" 1014 if (i + 1 < sql.length() && sql.charAt(i + 1) == '"') { 1015 sqlFragment.append('"'); 1016 i += 2; 1017 continue; 1018 } 1019 1020 inDoubleQuote = false; 1021 } 1022 1023 ++i; 1024 continue; 1025 } 1026 1027 if (inBacktickQuote) { 1028 sqlFragment.append(c); 1029 1030 if (c == '`') 1031 inBacktickQuote = false; 1032 1033 ++i; 1034 continue; 1035 } 1036 1037 if (inBracketQuote) { 1038 sqlFragment.append(c); 1039 1040 if (c == ']') 1041 inBracketQuote = false; 1042 1043 ++i; 1044 continue; 1045 } 1046 1047 // Not inside string/comment 1048 if (c == '-' && i + 1 < sql.length() && sql.charAt(i + 1) == '-') { 1049 sqlFragment.append("--"); 1050 i += 2; 1051 inLineComment = true; 1052 continue; 1053 } 1054 1055 if (c == '/' && i + 1 < sql.length() && sql.charAt(i + 1) == '*') { 1056 sqlFragment.append("/*"); 1057 i += 2; 1058 blockCommentDepth = 1; 1059 continue; 1060 } 1061 1062 if ((c == 'U' || c == 'u') && i + 2 < sql.length() && sql.charAt(i + 1) == '&' && sql.charAt(i + 2) == '\'') { 1063 inSingleQuote = true; 1064 inSingleQuoteEscapesBackslash = true; 1065 sqlFragment.append(c).append("&'"); 1066 i += 3; 1067 continue; 1068 } 1069 1070 if ((c == 'E' || c == 'e') && i + 1 < sql.length() && sql.charAt(i + 1) == '\'') { 1071 inSingleQuote = true; 1072 inSingleQuoteEscapesBackslash = true; 1073 sqlFragment.append(c).append('\''); 1074 i += 2; 1075 continue; 1076 } 1077 1078 if (c == '\'') { 1079 inSingleQuote = true; 1080 inSingleQuoteEscapesBackslash = false; 1081 sqlFragment.append(c); 1082 ++i; 1083 continue; 1084 } 1085 1086 if (c == '"') { 1087 inDoubleQuote = true; 1088 sqlFragment.append(c); 1089 ++i; 1090 continue; 1091 } 1092 1093 if (c == '`') { 1094 inBacktickQuote = true; 1095 sqlFragment.append(c); 1096 ++i; 1097 continue; 1098 } 1099 1100 if (c == '[') { 1101 inBracketQuote = true; 1102 sqlFragment.append(c); 1103 ++i; 1104 continue; 1105 } 1106 1107 if (c == '$' && !isIdentifierContinuation(sql, i)) { 1108 String delimiter = parseDollarQuoteDelimiter(sql, i); 1109 1110 if (delimiter != null) { 1111 sqlFragment.append(delimiter); 1112 i += delimiter.length(); 1113 dollarQuoteDelimiter = delimiter; 1114 continue; 1115 } 1116 } 1117 1118 if (c == '?') { 1119 if (isAllowedQuestionMarkOperator(sql, i)) { 1120 sqlFragment.append(c); 1121 ++i; 1122 continue; 1123 } 1124 1125 throw new IllegalArgumentException(format("Positional parameters ('?') are not supported. Use named parameters (e.g. ':id') and %s#bind. SQL: %s", 1126 Query.class.getSimpleName(), sql)); 1127 } 1128 1129 if (c == ':' && i + 1 < sql.length() && sql.charAt(i + 1) == ':') { 1130 // Postgres type-cast operator (::), do not treat second ':' as a parameter prefix. 1131 sqlFragment.append("::"); 1132 i += 2; 1133 continue; 1134 } 1135 1136 if (c == ':' && i + 1 < sql.length() && Character.isJavaIdentifierStart(sql.charAt(i + 1))) { 1137 int nameStartIndex = i + 1; 1138 int nameEndIndex = nameStartIndex + 1; 1139 1140 while (nameEndIndex < sql.length() && Character.isJavaIdentifierPart(sql.charAt(nameEndIndex))) 1141 ++nameEndIndex; 1142 1143 String parameterName = sql.substring(nameStartIndex, nameEndIndex); 1144 parameterNames.add(parameterName); 1145 distinctParameterNames.add(parameterName); 1146 sqlFragments.add(sqlFragment.toString()); 1147 sqlFragment.setLength(0); 1148 i = nameEndIndex; 1149 continue; 1150 } 1151 1152 sqlFragment.append(c); 1153 ++i; 1154 } 1155 1156 sqlFragments.add(sqlFragment.toString()); 1157 1158 return new ParsedSql(List.copyOf(sqlFragments), List.copyOf(parameterNames), Set.copyOf(distinctParameterNames)); 1159 } 1160 1161 @Nullable 1162 private static String parseDollarQuoteDelimiter(@NonNull String sql, 1163 int startIndex) { 1164 requireNonNull(sql); 1165 1166 if (startIndex < 0 || startIndex >= sql.length()) 1167 return null; 1168 1169 if (sql.charAt(startIndex) != '$') 1170 return null; 1171 1172 int i = startIndex + 1; 1173 1174 if (i >= sql.length()) 1175 return null; 1176 1177 char firstTagCharacter = sql.charAt(i); 1178 1179 if (firstTagCharacter == '$') 1180 return "$$"; 1181 1182 if (!isDollarQuoteTagStart(firstTagCharacter)) 1183 return null; 1184 1185 ++i; 1186 1187 while (i < sql.length()) { 1188 char c = sql.charAt(i); 1189 1190 if (c == '$') 1191 return sql.substring(startIndex, i + 1); 1192 1193 if (!isDollarQuoteTagPart(c)) 1194 return null; 1195 1196 ++i; 1197 } 1198 1199 return null; 1200 } 1201 1202 private static boolean isDollarQuoteTagStart(char character) { 1203 return Character.isLetter(character) || character == '_'; 1204 } 1205 1206 private static boolean isDollarQuoteTagPart(char character) { 1207 return Character.isLetterOrDigit(character) || character == '_'; 1208 } 1209 1210 private static boolean isIdentifierContinuation(@NonNull String sql, 1211 int startIndex) { 1212 requireNonNull(sql); 1213 1214 if (startIndex <= 0) 1215 return false; 1216 1217 return Character.isJavaIdentifierPart(sql.charAt(startIndex - 1)); 1218 } 1219 1220 @NonNull 1221 private static final Set<@NonNull String> QUESTION_MARK_PREFIX_KEYWORDS = Set.of( 1222 "SELECT", "WHERE", "AND", "OR", "ON", "HAVING", "WHEN", "THEN", "ELSE", "IN", 1223 "VALUES", "SET", "RETURNING", "USING", "LIKE", "BETWEEN", "IS", "NOT", "NULL", 1224 "JOIN", "FROM" 1225 ); 1226 1227 @NonNull 1228 private static final Set<@NonNull String> QUESTION_MARK_SUFFIX_KEYWORDS = Set.of( 1229 "FROM", "WHERE", "AND", "OR", "GROUP", "ORDER", "HAVING", "LIMIT", "OFFSET", 1230 "UNION", "EXCEPT", "INTERSECT", "RETURNING", "JOIN", "ON" 1231 ); 1232 1233 private static boolean isAllowedQuestionMarkOperator(@NonNull String sql, 1234 int questionMarkIndex) { 1235 requireNonNull(sql); 1236 1237 if (questionMarkIndex + 1 < sql.length()) { 1238 char nextChar = sql.charAt(questionMarkIndex + 1); 1239 if (nextChar == '|' || nextChar == '&') 1240 return true; 1241 } 1242 1243 int previousIndex = previousNonWhitespaceIndex(sql, questionMarkIndex - 1); 1244 int nextIndex = nextNonWhitespaceIndex(sql, questionMarkIndex + 1); 1245 1246 if (previousIndex < 0 || nextIndex < 0) 1247 return false; 1248 1249 char previousChar = sql.charAt(previousIndex); 1250 char nextChar = sql.charAt(nextIndex); 1251 1252 if (isOperatorBeforeQuestionMark(previousChar)) 1253 return false; 1254 1255 if (isTerminatorAfterQuestionMark(nextChar)) 1256 return false; 1257 1258 String previousKeyword = keywordBefore(sql, previousIndex); 1259 if (previousKeyword != null && QUESTION_MARK_PREFIX_KEYWORDS.contains(previousKeyword)) 1260 return false; 1261 1262 String nextKeyword = keywordAfter(sql, nextIndex); 1263 if (nextKeyword != null && QUESTION_MARK_SUFFIX_KEYWORDS.contains(nextKeyword)) 1264 return false; 1265 1266 return true; 1267 } 1268 1269 private static boolean isOperatorBeforeQuestionMark(char c) { 1270 return switch (c) { 1271 case '=', '<', '>', '!', '+', '-', '*', '/', '%', ',', '(' -> true; 1272 default -> false; 1273 }; 1274 } 1275 1276 private static boolean isTerminatorAfterQuestionMark(char c) { 1277 return switch (c) { 1278 case ')', ',', ';' -> true; 1279 default -> false; 1280 }; 1281 } 1282 1283 private static int previousNonWhitespaceIndex(@NonNull String sql, 1284 int startIndex) { 1285 for (int i = startIndex; i >= 0; i--) 1286 if (!Character.isWhitespace(sql.charAt(i))) 1287 return i; 1288 return -1; 1289 } 1290 1291 private static int nextNonWhitespaceIndex(@NonNull String sql, 1292 int startIndex) { 1293 for (int i = startIndex; i < sql.length(); i++) 1294 if (!Character.isWhitespace(sql.charAt(i))) 1295 return i; 1296 return -1; 1297 } 1298 1299 @Nullable 1300 private static String keywordBefore(@NonNull String sql, 1301 int index) { 1302 char c = sql.charAt(index); 1303 if (!Character.isJavaIdentifierPart(c)) 1304 return null; 1305 1306 int endIndex = index + 1; 1307 int startIndex = index; 1308 while (startIndex >= 0 && Character.isJavaIdentifierPart(sql.charAt(startIndex))) 1309 --startIndex; 1310 1311 return sql.substring(startIndex + 1, endIndex).toUpperCase(Locale.ROOT); 1312 } 1313 1314 @Nullable 1315 private static String keywordAfter(@NonNull String sql, 1316 int index) { 1317 char c = sql.charAt(index); 1318 if (!Character.isJavaIdentifierPart(c)) 1319 return null; 1320 1321 int endIndex = index + 1; 1322 while (endIndex < sql.length() && Character.isJavaIdentifierPart(sql.charAt(endIndex))) 1323 ++endIndex; 1324 1325 return sql.substring(index, endIndex).toUpperCase(Locale.ROOT); 1326 } 1327 1328 /** 1329 * Performs a SQL query that is expected to return 0 or 1 result rows. 1330 * 1331 * @param sql the SQL query to execute 1332 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1333 * @param parameters {@link PreparedStatement} parameters, if any 1334 * @param <T> the type to be returned 1335 * @return a single result (or no result) 1336 * @throws DatabaseException if > 1 row is returned 1337 */ 1338 @NonNull 1339 private <T> Optional<T> queryForObject(@NonNull String sql, 1340 @NonNull Class<T> resultSetRowType, 1341 Object @Nullable ... parameters) { 1342 requireNonNull(sql); 1343 requireNonNull(resultSetRowType); 1344 1345 return queryForObject(Statement.of(generateId(), sql), resultSetRowType, parameters); 1346 } 1347 1348 /** 1349 * Performs a SQL query that is expected to return 0 or 1 result rows. 1350 * 1351 * @param statement the SQL statement to execute 1352 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1353 * @param parameters {@link PreparedStatement} parameters, if any 1354 * @param <T> the type to be returned 1355 * @return a single result (or no result) 1356 * @throws DatabaseException if > 1 row is returned 1357 */ 1358 private <T> Optional<T> queryForObject(@NonNull Statement statement, 1359 @NonNull Class<T> resultSetRowType, 1360 Object @Nullable ... parameters) { 1361 requireNonNull(statement); 1362 requireNonNull(resultSetRowType); 1363 1364 return queryForObject(statement, resultSetRowType, null, parameters); 1365 } 1366 1367 private <T> Optional<T> queryForObject(@NonNull Statement statement, 1368 @NonNull Class<T> resultSetRowType, 1369 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1370 Object @Nullable ... parameters) { 1371 requireNonNull(statement); 1372 requireNonNull(resultSetRowType); 1373 1374 ResultHolder<Optional<T>> resultHolder = new ResultHolder<>(); 1375 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 1376 .resultSetRowType(resultSetRowType) 1377 .parameters(parameters) 1378 .build(); 1379 1380 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 1381 1382 performDatabaseOperation(statementContext, parametersAsList, preparedStatementCustomizer, (PreparedStatement preparedStatement) -> { 1383 long startTime = nanoTime(); 1384 1385 try (ResultSet resultSet = preparedStatement.executeQuery()) { 1386 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 1387 startTime = nanoTime(); 1388 1389 Optional<T> result = Optional.empty(); 1390 1391 if (resultSet.next()) { 1392 try { 1393 T value = getResultSetMapper().map(statementContext, resultSet, statementContext.getResultSetRowType().get(), getInstanceProvider()).orElse(null); 1394 result = Optional.ofNullable(value); 1395 } catch (SQLException e) { 1396 throw databaseExceptionWithStatementContext(statementContext, 1397 format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), statementContext.getResultSetRowType().get()), e); 1398 } 1399 1400 if (resultSet.next()) 1401 throw new DatabaseException("Expected 1 row in resultset but got more than 1 instead"); 1402 } 1403 1404 resultHolder.value = result; 1405 Duration resultSetMappingDuration = Duration.ofNanos(nanoTime() - startTime); 1406 return new DatabaseOperationResult(executionDuration, resultSetMappingDuration); 1407 } 1408 }); 1409 1410 return resultHolder.value; 1411 } 1412 1413 /** 1414 * Performs a SQL query that is expected to return any number of result rows. 1415 * 1416 * @param sql the SQL query to execute 1417 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1418 * @param parameters {@link PreparedStatement} parameters, if any 1419 * @param <T> the type to be returned 1420 * @return a list of results 1421 */ 1422 @NonNull 1423 private <T> List<@Nullable T> queryForList(@NonNull String sql, 1424 @NonNull Class<T> resultSetRowType, 1425 Object @Nullable ... parameters) { 1426 requireNonNull(sql); 1427 requireNonNull(resultSetRowType); 1428 1429 return queryForList(Statement.of(generateId(), sql), resultSetRowType, parameters); 1430 } 1431 1432 /** 1433 * Performs a SQL query that is expected to return any number of result rows. 1434 * 1435 * @param statement the SQL statement to execute 1436 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1437 * @param parameters {@link PreparedStatement} parameters, if any 1438 * @param <T> the type to be returned 1439 * @return a list of results 1440 */ 1441 @NonNull 1442 private <T> List<@Nullable T> queryForList(@NonNull Statement statement, 1443 @NonNull Class<T> resultSetRowType, 1444 Object @Nullable ... parameters) { 1445 requireNonNull(statement); 1446 requireNonNull(resultSetRowType); 1447 1448 return queryForList(statement, resultSetRowType, null, parameters); 1449 } 1450 1451 private <T> List<@Nullable T> queryForList(@NonNull Statement statement, 1452 @NonNull Class<T> resultSetRowType, 1453 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1454 Object @Nullable ... parameters) { 1455 requireNonNull(statement); 1456 requireNonNull(resultSetRowType); 1457 1458 List<T> list = new ArrayList<>(); 1459 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 1460 .resultSetRowType(resultSetRowType) 1461 .parameters(parameters) 1462 .build(); 1463 1464 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 1465 1466 performDatabaseOperation(statementContext, parametersAsList, preparedStatementCustomizer, (PreparedStatement preparedStatement) -> { 1467 long startTime = nanoTime(); 1468 1469 try (ResultSet resultSet = preparedStatement.executeQuery()) { 1470 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 1471 startTime = nanoTime(); 1472 1473 while (resultSet.next()) { 1474 try { 1475 T listElement = getResultSetMapper().map(statementContext, resultSet, statementContext.getResultSetRowType().get(), getInstanceProvider()).orElse(null); 1476 list.add(listElement); 1477 } catch (SQLException e) { 1478 throw databaseExceptionWithStatementContext(statementContext, 1479 format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), statementContext.getResultSetRowType().get()), e); 1480 } 1481 } 1482 1483 Duration resultSetMappingDuration = Duration.ofNanos(nanoTime() - startTime); 1484 return new DatabaseOperationResult(executionDuration, resultSetMappingDuration); 1485 } 1486 }); 1487 1488 return list; 1489 } 1490 1491 @Nullable 1492 private <T, R> R queryForStream(@NonNull Statement statement, 1493 @NonNull Class<T> resultSetRowType, 1494 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1495 @NonNull Function<Stream<@Nullable T>, R> streamFunction, 1496 Object @Nullable ... parameters) { 1497 requireNonNull(statement); 1498 requireNonNull(resultSetRowType); 1499 requireNonNull(streamFunction); 1500 1501 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 1502 .resultSetRowType(resultSetRowType) 1503 .parameters(parameters) 1504 .build(); 1505 1506 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 1507 StreamingResultSet<T> iterator = new StreamingResultSet<>(this, statementContext, parametersAsList, preparedStatementCustomizer); 1508 1509 try (Stream<@Nullable T> stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false) 1510 .onClose(iterator::close)) { 1511 return streamFunction.apply(stream); 1512 } 1513 } 1514 1515 /** 1516 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}; 1517 * or a SQL statement that returns nothing, such as a DDL statement. 1518 * 1519 * @param sql the SQL to execute 1520 * @param parameters {@link PreparedStatement} parameters, if any 1521 * @return the number of rows affected by the SQL statement 1522 */ 1523 @NonNull 1524 private Long execute(@NonNull String sql, 1525 Object @Nullable ... parameters) { 1526 requireNonNull(sql); 1527 return execute(Statement.of(generateId(), sql), parameters); 1528 } 1529 1530 /** 1531 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}; 1532 * or a SQL statement that returns nothing, such as a DDL statement. 1533 * 1534 * @param statement the SQL statement to execute 1535 * @param parameters {@link PreparedStatement} parameters, if any 1536 * @return the number of rows affected by the SQL statement 1537 */ 1538 @NonNull 1539 private Long execute(@NonNull Statement statement, 1540 Object @Nullable ... parameters) { 1541 requireNonNull(statement); 1542 1543 return execute(statement, null, parameters); 1544 } 1545 1546 private Long execute(@NonNull Statement statement, 1547 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1548 Object @Nullable ... parameters) { 1549 requireNonNull(statement); 1550 1551 ResultHolder<Long> resultHolder = new ResultHolder<>(); 1552 StatementContext<Void> statementContext = StatementContext.with(statement, this) 1553 .parameters(parameters) 1554 .build(); 1555 1556 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 1557 1558 performDatabaseOperation(statementContext, parametersAsList, preparedStatementCustomizer, (PreparedStatement preparedStatement) -> { 1559 long startTime = nanoTime(); 1560 1561 DatabaseOperationSupportStatus executeLargeUpdateSupported = getExecuteLargeUpdateSupported(); 1562 1563 // Use the appropriate "large" value if we know it. 1564 // If we don't know it, detect it and store it. 1565 if (executeLargeUpdateSupported == DatabaseOperationSupportStatus.YES) { 1566 resultHolder.value = preparedStatement.executeLargeUpdate(); 1567 } else if (executeLargeUpdateSupported == DatabaseOperationSupportStatus.NO) { 1568 resultHolder.value = (long) preparedStatement.executeUpdate(); 1569 } else { 1570 // If the driver doesn't support executeLargeUpdate, then UnsupportedOperationException is thrown. 1571 try { 1572 resultHolder.value = preparedStatement.executeLargeUpdate(); 1573 setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.YES); 1574 } catch (SQLFeatureNotSupportedException | UnsupportedOperationException | AbstractMethodError e) { 1575 setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.NO); 1576 resultHolder.value = (long) preparedStatement.executeUpdate(); 1577 } catch (SQLException e) { 1578 if (isUnsupportedSqlFeature(e)) { 1579 setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.NO); 1580 resultHolder.value = (long) preparedStatement.executeUpdate(); 1581 } else { 1582 throw e; 1583 } 1584 } 1585 } 1586 1587 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 1588 return new DatabaseOperationResult(executionDuration, null); 1589 }); 1590 1591 return resultHolder.value; 1592 } 1593 1594 /** 1595 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 1596 * which returns 0 or 1 rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 1597 * 1598 * @param sql the SQL query to execute 1599 * @param resultSetRowType the type to which the {@link ResultSet} row should be marshaled 1600 * @param parameters {@link PreparedStatement} parameters, if any 1601 * @param <T> the type to be returned 1602 * @return a single result (or no result) 1603 * @throws DatabaseException if > 1 row is returned 1604 */ 1605 @NonNull 1606 private <T> Optional<T> executeForObject(@NonNull String sql, 1607 @NonNull Class<T> resultSetRowType, 1608 Object @Nullable ... parameters) { 1609 requireNonNull(sql); 1610 requireNonNull(resultSetRowType); 1611 1612 return executeForObject(Statement.of(generateId(), sql), resultSetRowType, parameters); 1613 } 1614 1615 /** 1616 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 1617 * which returns 0 or 1 rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 1618 * 1619 * @param statement the SQL statement to execute 1620 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1621 * @param parameters {@link PreparedStatement} parameters, if any 1622 * @param <T> the type to be returned 1623 * @return a single result (or no result) 1624 * @throws DatabaseException if > 1 row is returned 1625 */ 1626 private <T> Optional<T> executeForObject(@NonNull Statement statement, 1627 @NonNull Class<T> resultSetRowType, 1628 Object @Nullable ... parameters) { 1629 requireNonNull(statement); 1630 requireNonNull(resultSetRowType); 1631 1632 return executeForObject(statement, resultSetRowType, null, parameters); 1633 } 1634 1635 private <T> Optional<T> executeForObject(@NonNull Statement statement, 1636 @NonNull Class<T> resultSetRowType, 1637 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1638 Object @Nullable ... parameters) { 1639 requireNonNull(statement); 1640 requireNonNull(resultSetRowType); 1641 1642 // Ultimately we just delegate to queryForObject. 1643 // Having `executeForList` is to allow for users to explicitly express intent 1644 // and make static analysis of code easier (e.g. maybe you'd like to hook all of your "execute" statements for 1645 // logging, or delegation to a writable master as opposed to a read replica) 1646 return queryForObject(statement, resultSetRowType, preparedStatementCustomizer, parameters); 1647 } 1648 1649 /** 1650 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 1651 * which returns any number of rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 1652 * 1653 * @param sql the SQL to execute 1654 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1655 * @param parameters {@link PreparedStatement} parameters, if any 1656 * @param <T> the type to be returned 1657 * @return a list of results 1658 */ 1659 @NonNull 1660 private <T> List<@Nullable T> executeForList(@NonNull String sql, 1661 @NonNull Class<T> resultSetRowType, 1662 Object @Nullable ... parameters) { 1663 requireNonNull(sql); 1664 requireNonNull(resultSetRowType); 1665 1666 return executeForList(Statement.of(generateId(), sql), resultSetRowType, parameters); 1667 } 1668 1669 /** 1670 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 1671 * which returns any number of rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 1672 * 1673 * @param statement the SQL statement to execute 1674 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1675 * @param parameters {@link PreparedStatement} parameters, if any 1676 * @param <T> the type to be returned 1677 * @return a list of results 1678 */ 1679 @NonNull 1680 private <T> List<@Nullable T> executeForList(@NonNull Statement statement, 1681 @NonNull Class<T> resultSetRowType, 1682 Object @Nullable ... parameters) { 1683 requireNonNull(statement); 1684 requireNonNull(resultSetRowType); 1685 1686 return executeForList(statement, resultSetRowType, null, parameters); 1687 } 1688 1689 private <T> List<@Nullable T> executeForList(@NonNull Statement statement, 1690 @NonNull Class<T> resultSetRowType, 1691 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1692 Object @Nullable ... parameters) { 1693 requireNonNull(statement); 1694 requireNonNull(resultSetRowType); 1695 1696 // Ultimately we just delegate to queryForList. 1697 // Having `executeForList` is to allow for users to explicitly express intent 1698 // and make static analysis of code easier (e.g. maybe you'd like to hook all of your "execute" statements for 1699 // logging, or delegation to a writable master as opposed to a read replica) 1700 return queryForList(statement, resultSetRowType, preparedStatementCustomizer, parameters); 1701 } 1702 1703 /** 1704 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE} 1705 * in "batch" over a set of parameter groups. 1706 * <p> 1707 * Useful for bulk-inserting or updating large amounts of data. 1708 * 1709 * @param sql the SQL to execute 1710 * @param parameterGroups Groups of {@link PreparedStatement} parameters 1711 * @return the number of rows affected by the SQL statement per-group 1712 */ 1713 @NonNull 1714 private List<Long> executeBatch(@NonNull String sql, 1715 @NonNull List<List<Object>> parameterGroups) { 1716 requireNonNull(sql); 1717 requireNonNull(parameterGroups); 1718 1719 return executeBatch(Statement.of(generateId(), sql), parameterGroups); 1720 } 1721 1722 /** 1723 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE} 1724 * in "batch" over a set of parameter groups. 1725 * <p> 1726 * Useful for bulk-inserting or updating large amounts of data. 1727 * 1728 * @param statement the SQL statement to execute 1729 * @param parameterGroups Groups of {@link PreparedStatement} parameters 1730 * @return the number of rows affected by the SQL statement per-group 1731 */ 1732 @NonNull 1733 private List<Long> executeBatch(@NonNull Statement statement, 1734 @NonNull List<List<Object>> parameterGroups) { 1735 requireNonNull(statement); 1736 requireNonNull(parameterGroups); 1737 1738 return executeBatch(statement, parameterGroups, null); 1739 } 1740 1741 private List<Long> executeBatch(@NonNull Statement statement, 1742 @NonNull List<List<Object>> parameterGroups, 1743 @Nullable PreparedStatementCustomizer preparedStatementCustomizer) { 1744 requireNonNull(statement); 1745 requireNonNull(parameterGroups); 1746 if (parameterGroups.isEmpty()) 1747 return List.of(); 1748 1749 Integer expectedParameterCount = null; 1750 1751 for (int i = 0; i < parameterGroups.size(); i++) { 1752 List<Object> parameterGroup = parameterGroups.get(i); 1753 1754 if (parameterGroup == null) 1755 throw new IllegalArgumentException(format("Parameter group at index %s is null", i)); 1756 1757 int parameterCount = parameterGroup.size(); 1758 if (expectedParameterCount == null) { 1759 expectedParameterCount = parameterCount; 1760 } else if (parameterCount != expectedParameterCount) { 1761 throw new IllegalArgumentException(format( 1762 "Inconsistent parameter group size at index %s: expected %s but found %s", 1763 i, expectedParameterCount, parameterCount)); 1764 } 1765 } 1766 1767 ResultHolder<List<Long>> resultHolder = new ResultHolder<>(); 1768 StatementContext<List<Long>> statementContext = StatementContext.with(statement, this) 1769 .parameters((List) parameterGroups) 1770 .resultSetRowType(List.class) 1771 .build(); 1772 1773 performDatabaseOperation(statementContext, (preparedStatement) -> { 1774 applyPreparedStatementCustomizer(statementContext, preparedStatement, preparedStatementCustomizer); 1775 1776 for (List<Object> parameterGroup : parameterGroups) { 1777 if (parameterGroup.size() > 0) 1778 performPreparedStatementBinding(statementContext, preparedStatement, parameterGroup); 1779 1780 preparedStatement.addBatch(); 1781 } 1782 }, (PreparedStatement preparedStatement) -> { 1783 long startTime = nanoTime(); 1784 List<Long> result; 1785 1786 DatabaseOperationSupportStatus executeLargeBatchSupported = getExecuteLargeBatchSupported(); 1787 1788 // Use the appropriate "large" value if we know it. 1789 // If we don't know it, detect it and store it. 1790 if (executeLargeBatchSupported == DatabaseOperationSupportStatus.YES) { 1791 long[] resultArray = preparedStatement.executeLargeBatch(); 1792 result = Arrays.stream(resultArray).boxed().collect(Collectors.toList()); 1793 } else if (executeLargeBatchSupported == DatabaseOperationSupportStatus.NO) { 1794 int[] resultArray = preparedStatement.executeBatch(); 1795 result = Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList()); 1796 } else { 1797 // If the driver doesn't support executeLargeBatch, then UnsupportedOperationException is thrown. 1798 try { 1799 long[] resultArray = preparedStatement.executeLargeBatch(); 1800 result = Arrays.stream(resultArray).boxed().collect(Collectors.toList()); 1801 setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.YES); 1802 } catch (SQLFeatureNotSupportedException | UnsupportedOperationException | AbstractMethodError e) { 1803 setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.NO); 1804 int[] resultArray = preparedStatement.executeBatch(); 1805 result = Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList()); 1806 } catch (SQLException e) { 1807 if (isUnsupportedSqlFeature(e)) { 1808 setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.NO); 1809 int[] resultArray = preparedStatement.executeBatch(); 1810 result = Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList()); 1811 } else { 1812 throw e; 1813 } 1814 } 1815 } 1816 1817 resultHolder.value = result; 1818 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 1819 return new DatabaseOperationResult(executionDuration, null); 1820 }, parameterGroups.size()); 1821 1822 return resultHolder.value; 1823 } 1824 1825 /** 1826 * Exposes a temporary handle to JDBC {@link DatabaseMetaData}, which provides comprehensive vendor-specific information about this database as a whole. 1827 * <p> 1828 * This method acquires {@link DatabaseMetaData} on its own newly-borrowed connection, which it manages internally. 1829 * <p> 1830 * It does <strong>not</strong> participate in the active transaction, if one exists. 1831 * <p> 1832 * The connection is closed as soon as {@link DatabaseMetaDataReader#read(DatabaseMetaData)} completes. 1833 * <p> 1834 * 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. 1835 */ 1836 public void readDatabaseMetaData(@NonNull DatabaseMetaDataReader databaseMetaDataReader) { 1837 requireNonNull(databaseMetaDataReader); 1838 1839 performRawConnectionOperation((connection -> { 1840 databaseMetaDataReader.read(connection.getMetaData()); 1841 return Optional.empty(); 1842 }), false); 1843 } 1844 1845 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 1846 @NonNull List<Object> parameters, 1847 @NonNull DatabaseOperation databaseOperation) { 1848 requireNonNull(statementContext); 1849 requireNonNull(parameters); 1850 requireNonNull(databaseOperation); 1851 1852 performDatabaseOperation(statementContext, parameters, null, databaseOperation); 1853 } 1854 1855 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 1856 @NonNull List<Object> parameters, 1857 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1858 @NonNull DatabaseOperation databaseOperation) { 1859 requireNonNull(statementContext); 1860 requireNonNull(parameters); 1861 requireNonNull(databaseOperation); 1862 1863 performDatabaseOperation(statementContext, (preparedStatement) -> { 1864 applyPreparedStatementCustomizer(statementContext, preparedStatement, preparedStatementCustomizer); 1865 if (parameters.size() > 0) 1866 performPreparedStatementBinding(statementContext, preparedStatement, parameters); 1867 }, databaseOperation); 1868 } 1869 1870 protected <T> void performPreparedStatementBinding(@NonNull StatementContext<T> statementContext, 1871 @NonNull PreparedStatement preparedStatement, 1872 @NonNull List<Object> parameters) { 1873 requireNonNull(statementContext); 1874 requireNonNull(preparedStatement); 1875 requireNonNull(parameters); 1876 1877 try { 1878 for (int i = 0; i < parameters.size(); ++i) { 1879 Object parameter = parameters.get(i); 1880 1881 if (parameter != null) { 1882 getPreparedStatementBinder().bindParameter(statementContext, preparedStatement, i + 1, parameter); 1883 } else { 1884 try { 1885 ParameterMetaData parameterMetaData = preparedStatement.getParameterMetaData(); 1886 1887 if (parameterMetaData != null) { 1888 preparedStatement.setNull(i + 1, parameterMetaData.getParameterType(i + 1)); 1889 } else { 1890 preparedStatement.setNull(i + 1, Types.NULL); 1891 } 1892 } catch (SQLException | AbstractMethodError e) { 1893 preparedStatement.setNull(i + 1, Types.NULL); 1894 } 1895 } 1896 } 1897 } catch (Exception e) { 1898 throw databaseExceptionWithStatementContext(statementContext, e); 1899 } 1900 } 1901 1902 protected void applyPreparedStatementCustomizer(@NonNull StatementContext<?> statementContext, 1903 @NonNull PreparedStatement preparedStatement, 1904 @Nullable PreparedStatementCustomizer preparedStatementCustomizer) throws SQLException { 1905 requireNonNull(statementContext); 1906 requireNonNull(preparedStatement); 1907 1908 if (preparedStatementCustomizer == null) 1909 return; 1910 1911 preparedStatementCustomizer.customize(statementContext, preparedStatement); 1912 } 1913 1914 @FunctionalInterface 1915 protected interface RawConnectionOperation<R> { 1916 @NonNull 1917 Optional<R> perform(@NonNull Connection connection) throws Exception; 1918 } 1919 1920 /** 1921 * Gets the database type for this database. 1922 * <p> 1923 * If {@link Builder#databaseType(DatabaseType)} was not configured and the database type has not already been detected, 1924 * this method may acquire a connection and inspect {@link DatabaseMetaData}. Configure an explicit database type to avoid 1925 * runtime detection. 1926 * 1927 * @return the database type 1928 * @throws DatabaseException if automatic database type detection fails 1929 * @since 3.0.0 1930 */ 1931 @NonNull 1932 public DatabaseType getDatabaseType() { 1933 return getDatabaseType(this.databaseTypeDetectionConnectionHolder.get()); 1934 } 1935 1936 @NonNull 1937 private DatabaseType getDatabaseType(@Nullable Connection connection) { 1938 DatabaseType cachedDatabaseType = this.databaseType.get(); 1939 1940 if (cachedDatabaseType != null) 1941 return cachedDatabaseType; 1942 1943 DatabaseType detectedDatabaseType; 1944 1945 try { 1946 detectedDatabaseType = connection == null 1947 ? DatabaseType.fromDataSource(getDataSource()) 1948 : DatabaseType.fromConnection(connection); 1949 } catch (DatabaseException e) { 1950 throw new DatabaseException(format( 1951 "Unable to determine database type automatically. Configure %s.%s(%s) explicitly to avoid runtime detection.", 1952 Builder.class.getSimpleName(), "databaseType", DatabaseType.class.getSimpleName()), e); 1953 } 1954 1955 if (this.databaseType.compareAndSet(null, detectedDatabaseType)) 1956 return detectedDatabaseType; 1957 1958 return this.databaseType.get(); 1959 } 1960 1961 /** 1962 * @since 3.0.0 1963 */ 1964 @NonNull 1965 public ZoneId getTimeZone() { 1966 return this.timeZone; 1967 } 1968 1969 /** 1970 * Useful for single-shot "utility" calls that operate outside of normal query operations, e.g. pulling DB metadata. 1971 * <p> 1972 * Example: {@link #readDatabaseMetaData(DatabaseMetaDataReader)}. 1973 */ 1974 @NonNull 1975 protected <R> Optional<R> performRawConnectionOperation(@NonNull RawConnectionOperation<R> rawConnectionOperation, 1976 @NonNull Boolean shouldParticipateInExistingTransactionIfPossible) { 1977 requireNonNull(rawConnectionOperation); 1978 requireNonNull(shouldParticipateInExistingTransactionIfPossible); 1979 1980 if (shouldParticipateInExistingTransactionIfPossible) { 1981 Optional<Transaction> transaction = currentTransaction(); 1982 ReentrantLock connectionLock = transaction.isPresent() ? transaction.get().getConnectionLock() : null; 1983 // Try to participate in txn if it's available 1984 Connection connection = null; 1985 Throwable thrown = null; 1986 1987 if (connectionLock != null) 1988 connectionLock.lock(); 1989 1990 try { 1991 connection = transaction.isPresent() ? transaction.get().getConnection() : acquireConnection(); 1992 return rawConnectionOperation.perform(connection); 1993 } catch (DatabaseException e) { 1994 thrown = e; 1995 throw e; 1996 } catch (Exception e) { 1997 DatabaseException wrapped = new DatabaseException(e); 1998 thrown = wrapped; 1999 throw wrapped; 2000 } finally { 2001 Throwable cleanupFailure = null; 2002 2003 try { 2004 // If this was a single-shot operation (not in a transaction), close the connection 2005 if (connection != null && !transaction.isPresent()) { 2006 try { 2007 closeConnection(connection); 2008 } catch (Throwable cleanupException) { 2009 cleanupFailure = cleanupException; 2010 } 2011 } 2012 } finally { 2013 if (connectionLock != null) 2014 connectionLock.unlock(); 2015 2016 if (cleanupFailure != null) { 2017 if (thrown != null) { 2018 thrown.addSuppressed(cleanupFailure); 2019 } else if (cleanupFailure instanceof RuntimeException) { 2020 throw (RuntimeException) cleanupFailure; 2021 } else if (cleanupFailure instanceof Error) { 2022 throw (Error) cleanupFailure; 2023 } else { 2024 throw new RuntimeException(cleanupFailure); 2025 } 2026 } 2027 } 2028 } 2029 } else { 2030 boolean acquiredConnection = false; 2031 Connection connection = null; 2032 Throwable thrown = null; 2033 2034 // Always get a fresh connection no matter what and close it afterwards 2035 try { 2036 connection = getDataSource().getConnection(); 2037 acquiredConnection = true; 2038 return rawConnectionOperation.perform(connection); 2039 } catch (DatabaseException e) { 2040 thrown = e; 2041 throw e; 2042 } catch (Exception e) { 2043 DatabaseException wrapped = acquiredConnection 2044 ? new DatabaseException(e) 2045 : new DatabaseException("Unable to acquire database connection", e); 2046 thrown = wrapped; 2047 throw wrapped; 2048 } finally { 2049 if (connection != null) { 2050 try { 2051 closeConnection(connection); 2052 } catch (Throwable cleanupException) { 2053 if (thrown != null) { 2054 thrown.addSuppressed(cleanupException); 2055 } else if (cleanupException instanceof RuntimeException) { 2056 throw (RuntimeException) cleanupException; 2057 } else if (cleanupException instanceof Error) { 2058 throw (Error) cleanupException; 2059 } else { 2060 throw new RuntimeException(cleanupException); 2061 } 2062 } 2063 } 2064 } 2065 } 2066 } 2067 2068 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 2069 @NonNull PreparedStatementBindingOperation preparedStatementBindingOperation, 2070 @NonNull DatabaseOperation databaseOperation) { 2071 performDatabaseOperation(statementContext, preparedStatementBindingOperation, databaseOperation, null); 2072 } 2073 2074 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 2075 @NonNull PreparedStatementBindingOperation preparedStatementBindingOperation, 2076 @NonNull DatabaseOperation databaseOperation, 2077 @Nullable Integer batchSize) { 2078 requireNonNull(statementContext); 2079 requireNonNull(preparedStatementBindingOperation); 2080 requireNonNull(databaseOperation); 2081 2082 long startTime = nanoTime(); 2083 Duration connectionAcquisitionDuration = null; 2084 Duration preparationDuration = null; 2085 Duration executionDuration = null; 2086 Duration resultSetMappingDuration = null; 2087 Exception exception = null; 2088 Throwable thrown = null; 2089 Connection connection = null; 2090 Optional<Transaction> transaction = currentTransaction(); 2091 ReentrantLock connectionLock = transaction.isPresent() ? transaction.get().getConnectionLock() : null; 2092 2093 if (connectionLock != null) 2094 connectionLock.lock(); 2095 2096 try { 2097 boolean alreadyHasConnection = transaction.isPresent() && transaction.get().hasConnection(); 2098 connection = transaction.isPresent() ? transaction.get().getConnection() : acquireConnection(); 2099 connectionAcquisitionDuration = alreadyHasConnection ? null : Duration.ofNanos(nanoTime() - startTime); 2100 startTime = nanoTime(); 2101 2102 try (PreparedStatement preparedStatement = connection.prepareStatement(statementContext.getStatement().getSql())) { 2103 Connection previousDatabaseTypeDetectionConnection = this.databaseTypeDetectionConnectionHolder.get(); 2104 this.databaseTypeDetectionConnectionHolder.set(connection); 2105 2106 try { 2107 preparedStatementBindingOperation.perform(preparedStatement); 2108 preparationDuration = Duration.ofNanos(nanoTime() - startTime); 2109 2110 DatabaseOperationResult databaseOperationResult = databaseOperation.perform(preparedStatement); 2111 executionDuration = databaseOperationResult.getExecutionDuration().orElse(null); 2112 resultSetMappingDuration = databaseOperationResult.getResultSetMappingDuration().orElse(null); 2113 } finally { 2114 if (previousDatabaseTypeDetectionConnection == null) 2115 this.databaseTypeDetectionConnectionHolder.remove(); 2116 else 2117 this.databaseTypeDetectionConnectionHolder.set(previousDatabaseTypeDetectionConnection); 2118 } 2119 } 2120 } catch (DatabaseException e) { 2121 exception = e; 2122 thrown = e; 2123 throw e; 2124 } catch (Error e) { 2125 exception = databaseExceptionWithStatementContext(statementContext, e); 2126 thrown = e; 2127 throw e; 2128 } catch (Exception e) { 2129 DatabaseException wrapped = databaseExceptionWithStatementContext(statementContext, e); 2130 exception = e; 2131 thrown = wrapped; 2132 throw wrapped; 2133 } finally { 2134 Throwable cleanupFailure = null; 2135 2136 try { 2137 cleanupFailure = closeStatementContextResources(statementContext, cleanupFailure); 2138 2139 // If this was a single-shot operation (not in a transaction), close the connection 2140 if (connection != null && !transaction.isPresent()) { 2141 try { 2142 closeConnection(connection); 2143 } catch (Throwable cleanupException) { 2144 if (cleanupFailure == null) 2145 cleanupFailure = cleanupException; 2146 else 2147 cleanupFailure.addSuppressed(cleanupException); 2148 } 2149 } 2150 } finally { 2151 if (connectionLock != null) 2152 connectionLock.unlock(); 2153 2154 StatementLog statementLog = 2155 StatementLog.withStatementContext(statementContext) 2156 .connectionAcquisitionDuration(connectionAcquisitionDuration) 2157 .preparationDuration(preparationDuration) 2158 .executionDuration(executionDuration) 2159 .resultSetMappingDuration(resultSetMappingDuration) 2160 .batchSize(batchSize) 2161 .exception(exception) 2162 .build(); 2163 2164 try { 2165 getStatementLogger().log(statementLog); 2166 } catch (Throwable cleanupException) { 2167 if (transaction.isPresent() && thrown == null && cleanupFailure == null) { 2168 Throwable loggerFailure = cleanupException; 2169 Transaction currentTransaction = transaction.get(); 2170 2171 if (!currentTransaction.isOwnedByCurrentThread()) { 2172 cleanupFailure = new StatementLoggerFailureException(loggerFailure); 2173 } else { 2174 currentTransaction.addPostTransactionOperation(result -> { 2175 if (loggerFailure instanceof RuntimeException runtimeException) 2176 throw runtimeException; 2177 if (loggerFailure instanceof Error error) 2178 throw error; 2179 throw new RuntimeException(loggerFailure); 2180 }); 2181 } 2182 } else { 2183 if (cleanupFailure == null) 2184 cleanupFailure = cleanupException; 2185 else 2186 cleanupFailure.addSuppressed(cleanupException); 2187 } 2188 } 2189 } 2190 2191 if (cleanupFailure != null) { 2192 if (thrown != null) { 2193 thrown.addSuppressed(cleanupFailure); 2194 } else if (cleanupFailure instanceof RuntimeException) { 2195 throw (RuntimeException) cleanupFailure; 2196 } else if (cleanupFailure instanceof Error) { 2197 throw (Error) cleanupFailure; 2198 } else { 2199 throw new RuntimeException(cleanupFailure); 2200 } 2201 } 2202 } 2203 } 2204 2205 @NonNull 2206 protected Connection acquireConnection() { 2207 Optional<Transaction> transaction = currentTransaction(); 2208 2209 if (transaction.isPresent()) 2210 return transaction.get().getConnection(); 2211 2212 try { 2213 return getDataSource().getConnection(); 2214 } catch (SQLException e) { 2215 throw new DatabaseException("Unable to acquire database connection", e); 2216 } 2217 } 2218 2219 @NonNull 2220 protected DataSource getDataSource() { 2221 return this.dataSource; 2222 } 2223 2224 @NonNull 2225 protected InstanceProvider getInstanceProvider() { 2226 return this.instanceProvider; 2227 } 2228 2229 @NonNull 2230 protected PreparedStatementBinder getPreparedStatementBinder() { 2231 return this.preparedStatementBinder; 2232 } 2233 2234 @NonNull 2235 protected ResultSetMapper getResultSetMapper() { 2236 return this.resultSetMapper; 2237 } 2238 2239 @NonNull 2240 protected StatementLogger getStatementLogger() { 2241 return this.statementLogger; 2242 } 2243 2244 @NonNull 2245 protected DatabaseOperationSupportStatus getExecuteLargeBatchSupported() { 2246 return this.executeLargeBatchSupported; 2247 } 2248 2249 protected void setExecuteLargeBatchSupported(@NonNull DatabaseOperationSupportStatus executeLargeBatchSupported) { 2250 requireNonNull(executeLargeBatchSupported); 2251 this.executeLargeBatchSupported = executeLargeBatchSupported; 2252 } 2253 2254 @NonNull 2255 protected DatabaseOperationSupportStatus getExecuteLargeUpdateSupported() { 2256 return this.executeLargeUpdateSupported; 2257 } 2258 2259 protected void setExecuteLargeUpdateSupported(@NonNull DatabaseOperationSupportStatus executeLargeUpdateSupported) { 2260 requireNonNull(executeLargeUpdateSupported); 2261 this.executeLargeUpdateSupported = executeLargeUpdateSupported; 2262 } 2263 2264 @NonNull 2265 protected Object generateId() { 2266 // "Unique" keys 2267 return format("com.pyranid.%s", this.defaultIdGenerator.incrementAndGet()); 2268 } 2269 2270 @FunctionalInterface 2271 protected interface DatabaseOperation { 2272 @NonNull 2273 DatabaseOperationResult perform(@NonNull PreparedStatement preparedStatement) throws Exception; 2274 } 2275 2276 @FunctionalInterface 2277 protected interface PreparedStatementBindingOperation { 2278 void perform(@NonNull PreparedStatement preparedStatement) throws Exception; 2279 } 2280 2281 @NotThreadSafe 2282 private static final class StreamingResultSet<T> implements java.util.Iterator<T>, AutoCloseable { 2283 private final Database database; 2284 private final StatementContext<T> statementContext; 2285 private final List<Object> parameters; 2286 @Nullable 2287 private final PreparedStatementCustomizer preparedStatementCustomizer; 2288 @NonNull 2289 private final Optional<Transaction> transaction; 2290 @Nullable 2291 private final ReentrantLock connectionLock; 2292 @Nullable 2293 private Connection connection; 2294 @Nullable 2295 private PreparedStatement preparedStatement; 2296 @Nullable 2297 private ResultSet resultSet; 2298 private boolean closed; 2299 private boolean hasNextEvaluated; 2300 private boolean hasNext; 2301 @Nullable 2302 private Duration connectionAcquisitionDuration; 2303 @Nullable 2304 private Duration preparationDuration; 2305 @Nullable 2306 private Duration executionDuration; 2307 private long resultSetMappingNanos; 2308 @Nullable 2309 private Exception exception; 2310 @Nullable 2311 private Throwable thrown; 2312 2313 private StreamingResultSet(@NonNull Database database, 2314 @NonNull StatementContext<T> statementContext, 2315 @NonNull List<Object> parameters, 2316 @Nullable PreparedStatementCustomizer preparedStatementCustomizer) { 2317 this.database = requireNonNull(database); 2318 this.statementContext = requireNonNull(statementContext); 2319 this.parameters = requireNonNull(parameters); 2320 this.preparedStatementCustomizer = preparedStatementCustomizer; 2321 this.transaction = database.currentTransaction(); 2322 this.connectionLock = this.transaction.isPresent() ? this.transaction.get().getConnectionLock() : null; 2323 2324 open(); 2325 } 2326 2327 private void open() { 2328 long startTime = nanoTime(); 2329 2330 if (this.connectionLock != null) 2331 this.connectionLock.lock(); 2332 2333 try { 2334 boolean alreadyHasConnection = this.transaction.isPresent() && this.transaction.get().hasConnection(); 2335 this.connection = this.transaction.isPresent() ? this.transaction.get().getConnection() : this.database.acquireConnection(); 2336 this.connectionAcquisitionDuration = alreadyHasConnection ? null : Duration.ofNanos(nanoTime() - startTime); 2337 startTime = nanoTime(); 2338 2339 this.preparedStatement = this.connection.prepareStatement(this.statementContext.getStatement().getSql()); 2340 Connection previousDatabaseTypeDetectionConnection = this.database.databaseTypeDetectionConnectionHolder.get(); 2341 this.database.databaseTypeDetectionConnectionHolder.set(this.connection); 2342 2343 try { 2344 this.database.applyPreparedStatementCustomizer(this.statementContext, this.preparedStatement, this.preparedStatementCustomizer); 2345 if (this.parameters.size() > 0) 2346 this.database.performPreparedStatementBinding(this.statementContext, this.preparedStatement, this.parameters); 2347 this.preparationDuration = Duration.ofNanos(nanoTime() - startTime); 2348 2349 startTime = nanoTime(); 2350 this.resultSet = this.preparedStatement.executeQuery(); 2351 this.executionDuration = Duration.ofNanos(nanoTime() - startTime); 2352 } finally { 2353 if (previousDatabaseTypeDetectionConnection == null) 2354 this.database.databaseTypeDetectionConnectionHolder.remove(); 2355 else 2356 this.database.databaseTypeDetectionConnectionHolder.set(previousDatabaseTypeDetectionConnection); 2357 } 2358 } catch (DatabaseException e) { 2359 this.exception = e; 2360 this.thrown = e; 2361 close(); 2362 throw e; 2363 } catch (Exception e) { 2364 DatabaseException wrapped = databaseExceptionWithStatementContext(this.statementContext, e); 2365 this.exception = e; 2366 this.thrown = wrapped; 2367 close(); 2368 throw wrapped; 2369 } 2370 } 2371 2372 @Override 2373 public boolean hasNext() { 2374 if (this.closed) 2375 return false; 2376 2377 if (!this.hasNextEvaluated) { 2378 try { 2379 this.hasNext = this.resultSet != null && this.resultSet.next(); 2380 this.hasNextEvaluated = true; 2381 if (!this.hasNext) 2382 close(); 2383 } catch (SQLException e) { 2384 DatabaseException wrapped = databaseExceptionWithStatementContext(this.statementContext, e); 2385 this.exception = e; 2386 this.thrown = wrapped; 2387 close(); 2388 throw wrapped; 2389 } 2390 } 2391 2392 return this.hasNext; 2393 } 2394 2395 @Override 2396 public T next() { 2397 if (!hasNext()) 2398 throw new java.util.NoSuchElementException(); 2399 2400 this.hasNextEvaluated = false; 2401 long startTime = nanoTime(); 2402 2403 try { 2404 Connection previousDatabaseTypeDetectionConnection = this.database.databaseTypeDetectionConnectionHolder.get(); 2405 this.database.databaseTypeDetectionConnectionHolder.set(requireNonNull(this.connection)); 2406 T value; 2407 2408 try { 2409 value = this.database.getResultSetMapper() 2410 .map(this.statementContext, requireNonNull(this.resultSet), this.statementContext.getResultSetRowType().get(), this.database.getInstanceProvider()) 2411 .orElse(null); 2412 } finally { 2413 if (previousDatabaseTypeDetectionConnection == null) 2414 this.database.databaseTypeDetectionConnectionHolder.remove(); 2415 else 2416 this.database.databaseTypeDetectionConnectionHolder.set(previousDatabaseTypeDetectionConnection); 2417 } 2418 2419 this.resultSetMappingNanos += nanoTime() - startTime; 2420 return value; 2421 } catch (SQLException e) { 2422 DatabaseException wrapped = databaseExceptionWithStatementContext(this.statementContext, 2423 format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), this.statementContext.getResultSetRowType().get()), e); 2424 this.exception = e; 2425 this.thrown = wrapped; 2426 close(); 2427 throw wrapped; 2428 } catch (DatabaseException e) { 2429 this.exception = e; 2430 this.thrown = e; 2431 close(); 2432 throw e; 2433 } 2434 } 2435 2436 @Override 2437 public void close() { 2438 if (this.closed) 2439 return; 2440 2441 this.closed = true; 2442 Throwable cleanupFailure = null; 2443 2444 try { 2445 cleanupFailure = closeStatementContextResources(this.statementContext, cleanupFailure); 2446 2447 if (this.resultSet != null) { 2448 try { 2449 this.resultSet.close(); 2450 } catch (Throwable cleanupException) { 2451 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 2452 } 2453 } 2454 2455 if (this.preparedStatement != null) { 2456 try { 2457 this.preparedStatement.close(); 2458 } catch (Throwable cleanupException) { 2459 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 2460 } 2461 } 2462 2463 if (this.connection != null && this.transaction.isEmpty()) { 2464 try { 2465 this.database.closeConnection(this.connection); 2466 } catch (Throwable cleanupException) { 2467 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 2468 } 2469 } 2470 } finally { 2471 if (this.connectionLock != null) 2472 this.connectionLock.unlock(); 2473 2474 Duration mappingDuration = this.resultSetMappingNanos == 0L ? null : Duration.ofNanos(this.resultSetMappingNanos); 2475 2476 StatementLog statementLog = 2477 StatementLog.withStatementContext(this.statementContext) 2478 .connectionAcquisitionDuration(this.connectionAcquisitionDuration) 2479 .preparationDuration(this.preparationDuration) 2480 .executionDuration(this.executionDuration) 2481 .resultSetMappingDuration(mappingDuration) 2482 .exception(this.exception) 2483 .build(); 2484 2485 try { 2486 this.database.getStatementLogger().log(statementLog); 2487 } catch (Throwable cleanupException) { 2488 if (this.transaction.isPresent() && this.thrown == null && cleanupFailure == null) { 2489 Throwable loggerFailure = cleanupException; 2490 Transaction currentTransaction = this.transaction.get(); 2491 2492 if (!currentTransaction.isOwnedByCurrentThread()) { 2493 cleanupFailure = new StatementLoggerFailureException(loggerFailure); 2494 } else { 2495 currentTransaction.addPostTransactionOperation(result -> { 2496 if (loggerFailure instanceof RuntimeException runtimeException) 2497 throw runtimeException; 2498 if (loggerFailure instanceof Error error) 2499 throw error; 2500 throw new RuntimeException(loggerFailure); 2501 }); 2502 } 2503 } else { 2504 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 2505 } 2506 } 2507 } 2508 2509 if (cleanupFailure != null) { 2510 if (this.thrown != null) { 2511 this.thrown.addSuppressed(cleanupFailure); 2512 } else if (cleanupFailure instanceof RuntimeException) { 2513 throw (RuntimeException) cleanupFailure; 2514 } else if (cleanupFailure instanceof Error) { 2515 throw (Error) cleanupFailure; 2516 } else { 2517 throw new RuntimeException(cleanupFailure); 2518 } 2519 } 2520 } 2521 2522 @NonNull 2523 private static Throwable addSuppressed(@NonNull Throwable existing, 2524 @NonNull Throwable additional) { 2525 existing.addSuppressed(additional); 2526 return existing; 2527 } 2528 } 2529 2530 /** 2531 * Builder used to construct instances of {@link Database}. 2532 * <p> 2533 * This class is intended for use by a single thread. 2534 * 2535 * @author <a href="https://www.revetkn.com">Mark Allen</a> 2536 * @since 1.0.0 2537 */ 2538 @NotThreadSafe 2539 public static class Builder { 2540 @NonNull 2541 private final DataSource dataSource; 2542 @Nullable 2543 private DatabaseType databaseType; 2544 @Nullable 2545 private ZoneId timeZone; 2546 @Nullable 2547 private InstanceProvider instanceProvider; 2548 @Nullable 2549 private PreparedStatementBinder preparedStatementBinder; 2550 @Nullable 2551 private ResultSetMapper resultSetMapper; 2552 @Nullable 2553 private StatementLogger statementLogger; 2554 @Nullable 2555 private Integer parsedSqlCacheCapacity; 2556 2557 private Builder(@NonNull DataSource dataSource) { 2558 this.dataSource = requireNonNull(dataSource); 2559 this.databaseType = null; 2560 this.parsedSqlCacheCapacity = null; 2561 } 2562 2563 /** 2564 * Overrides automatic database type detection. 2565 * <p> 2566 * If {@code null}, the database type is detected lazily when database-type-specific behavior is first needed. 2567 * Supplying a non-null value avoids automatic detection and its metadata lookup entirely. 2568 * 2569 * @param databaseType the database type to use (null to enable auto-detection) 2570 * @return this {@code Builder}, for chaining 2571 * @since 4.0.0 2572 */ 2573 @NonNull 2574 public Builder databaseType(@Nullable DatabaseType databaseType) { 2575 this.databaseType = databaseType; 2576 return this; 2577 } 2578 2579 @NonNull 2580 public Builder timeZone(@Nullable ZoneId timeZone) { 2581 this.timeZone = timeZone; 2582 return this; 2583 } 2584 2585 @NonNull 2586 public Builder instanceProvider(@Nullable InstanceProvider instanceProvider) { 2587 this.instanceProvider = instanceProvider; 2588 return this; 2589 } 2590 2591 @NonNull 2592 public Builder preparedStatementBinder(@Nullable PreparedStatementBinder preparedStatementBinder) { 2593 this.preparedStatementBinder = preparedStatementBinder; 2594 return this; 2595 } 2596 2597 @NonNull 2598 public Builder resultSetMapper(@Nullable ResultSetMapper resultSetMapper) { 2599 this.resultSetMapper = resultSetMapper; 2600 return this; 2601 } 2602 2603 @NonNull 2604 public Builder statementLogger(@Nullable StatementLogger statementLogger) { 2605 this.statementLogger = statementLogger; 2606 return this; 2607 } 2608 2609 /** 2610 * Configures the size of the parsed SQL cache. 2611 * <p> 2612 * A value of {@code 0} disables caching. A value of {@code null} uses the default size. 2613 * 2614 * @param parsedSqlCacheCapacity cache size (0 disables caching, null uses default) 2615 * @return this {@code Builder}, for chaining 2616 */ 2617 @NonNull 2618 public Builder parsedSqlCacheCapacity(@Nullable Integer parsedSqlCacheCapacity) { 2619 if (parsedSqlCacheCapacity != null && parsedSqlCacheCapacity < 0) 2620 throw new IllegalArgumentException("parsedSqlCacheCapacity must be >= 0"); 2621 2622 this.parsedSqlCacheCapacity = parsedSqlCacheCapacity; 2623 return this; 2624 } 2625 2626 @NonNull 2627 public Database build() { 2628 return new Database(this); 2629 } 2630 } 2631 2632 @ThreadSafe 2633 static class DatabaseOperationResult { 2634 @Nullable 2635 private final Duration executionDuration; 2636 @Nullable 2637 private final Duration resultSetMappingDuration; 2638 2639 public DatabaseOperationResult(@Nullable Duration executionDuration, 2640 @Nullable Duration resultSetMappingDuration) { 2641 this.executionDuration = executionDuration; 2642 this.resultSetMappingDuration = resultSetMappingDuration; 2643 } 2644 2645 @NonNull 2646 public Optional<Duration> getExecutionDuration() { 2647 return Optional.ofNullable(this.executionDuration); 2648 } 2649 2650 @NonNull 2651 public Optional<Duration> getResultSetMappingDuration() { 2652 return Optional.ofNullable(this.resultSetMappingDuration); 2653 } 2654 } 2655 2656 @NotThreadSafe 2657 static class ResultHolder<T> { 2658 T value; 2659 } 2660 2661 enum DatabaseOperationSupportStatus { 2662 UNKNOWN, 2663 YES, 2664 NO 2665 } 2666 2667 private static final class StatementLoggerFailureException extends RuntimeException { 2668 private StatementLoggerFailureException(@NonNull Throwable cause) { 2669 super("Statement logger failed", cause); 2670 } 2671 } 2672}