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