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 boolean inBlockComment = false; 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 (inBlockComment) { 877 sqlFragment.append(c); 878 879 if (c == '*' && i + 1 < sql.length() && sql.charAt(i + 1) == '/') { 880 sqlFragment.append('/'); 881 i += 2; 882 inBlockComment = false; 883 } else { 884 ++i; 885 } 886 887 continue; 888 } 889 890 if (inSingleQuote) { 891 sqlFragment.append(c); 892 893 if (inSingleQuoteEscapesBackslash && c == '\\' && i + 1 < sql.length()) { 894 sqlFragment.append(sql.charAt(i + 1)); 895 i += 2; 896 continue; 897 } 898 899 if (c == '\'') { 900 // Escaped quote: '' 901 if (i + 1 < sql.length() && sql.charAt(i + 1) == '\'') { 902 sqlFragment.append('\''); 903 i += 2; 904 continue; 905 } 906 907 inSingleQuote = false; 908 inSingleQuoteEscapesBackslash = false; 909 } 910 911 ++i; 912 continue; 913 } 914 915 if (inDoubleQuote) { 916 sqlFragment.append(c); 917 918 if (c == '"') { 919 // Escaped quote: "" 920 if (i + 1 < sql.length() && sql.charAt(i + 1) == '"') { 921 sqlFragment.append('"'); 922 i += 2; 923 continue; 924 } 925 926 inDoubleQuote = false; 927 } 928 929 ++i; 930 continue; 931 } 932 933 if (inBacktickQuote) { 934 sqlFragment.append(c); 935 936 if (c == '`') 937 inBacktickQuote = false; 938 939 ++i; 940 continue; 941 } 942 943 if (inBracketQuote) { 944 sqlFragment.append(c); 945 946 if (c == ']') 947 inBracketQuote = false; 948 949 ++i; 950 continue; 951 } 952 953 // Not inside string/comment 954 if (c == '-' && i + 1 < sql.length() && sql.charAt(i + 1) == '-') { 955 sqlFragment.append("--"); 956 i += 2; 957 inLineComment = true; 958 continue; 959 } 960 961 if (c == '/' && i + 1 < sql.length() && sql.charAt(i + 1) == '*') { 962 sqlFragment.append("/*"); 963 i += 2; 964 inBlockComment = true; 965 continue; 966 } 967 968 if ((c == 'U' || c == 'u') && i + 2 < sql.length() && sql.charAt(i + 1) == '&' && sql.charAt(i + 2) == '\'') { 969 inSingleQuote = true; 970 inSingleQuoteEscapesBackslash = true; 971 sqlFragment.append(c).append("&'"); 972 i += 3; 973 continue; 974 } 975 976 if ((c == 'E' || c == 'e') && i + 1 < sql.length() && sql.charAt(i + 1) == '\'') { 977 inSingleQuote = true; 978 inSingleQuoteEscapesBackslash = true; 979 sqlFragment.append(c).append('\''); 980 i += 2; 981 continue; 982 } 983 984 if (c == '\'') { 985 inSingleQuote = true; 986 inSingleQuoteEscapesBackslash = false; 987 sqlFragment.append(c); 988 ++i; 989 continue; 990 } 991 992 if (c == '"') { 993 inDoubleQuote = true; 994 sqlFragment.append(c); 995 ++i; 996 continue; 997 } 998 999 if (c == '`') { 1000 inBacktickQuote = true; 1001 sqlFragment.append(c); 1002 ++i; 1003 continue; 1004 } 1005 1006 if (c == '[') { 1007 inBracketQuote = true; 1008 sqlFragment.append(c); 1009 ++i; 1010 continue; 1011 } 1012 1013 if (c == '$') { 1014 String delimiter = parseDollarQuoteDelimiter(sql, i); 1015 1016 if (delimiter != null) { 1017 sqlFragment.append(delimiter); 1018 i += delimiter.length(); 1019 dollarQuoteDelimiter = delimiter; 1020 continue; 1021 } 1022 } 1023 1024 if (c == '?') { 1025 if (isAllowedQuestionMarkOperator(sql, i)) { 1026 sqlFragment.append(c); 1027 ++i; 1028 continue; 1029 } 1030 1031 throw new IllegalArgumentException(format("Positional parameters ('?') are not supported. Use named parameters (e.g. ':id') and %s#bind. SQL: %s", 1032 Query.class.getSimpleName(), sql)); 1033 } 1034 1035 if (c == ':' && i + 1 < sql.length() && sql.charAt(i + 1) == ':') { 1036 // Postgres type-cast operator (::), do not treat second ':' as a parameter prefix. 1037 sqlFragment.append("::"); 1038 i += 2; 1039 continue; 1040 } 1041 1042 if (c == ':' && i + 1 < sql.length() && Character.isJavaIdentifierStart(sql.charAt(i + 1))) { 1043 int nameStartIndex = i + 1; 1044 int nameEndIndex = nameStartIndex + 1; 1045 1046 while (nameEndIndex < sql.length() && Character.isJavaIdentifierPart(sql.charAt(nameEndIndex))) 1047 ++nameEndIndex; 1048 1049 String parameterName = sql.substring(nameStartIndex, nameEndIndex); 1050 parameterNames.add(parameterName); 1051 distinctParameterNames.add(parameterName); 1052 sqlFragments.add(sqlFragment.toString()); 1053 sqlFragment.setLength(0); 1054 i = nameEndIndex; 1055 continue; 1056 } 1057 1058 sqlFragment.append(c); 1059 ++i; 1060 } 1061 1062 sqlFragments.add(sqlFragment.toString()); 1063 1064 return new ParsedSql(List.copyOf(sqlFragments), List.copyOf(parameterNames), Set.copyOf(distinctParameterNames)); 1065 } 1066 1067 @Nullable 1068 private static String parseDollarQuoteDelimiter(@NonNull String sql, 1069 int startIndex) { 1070 requireNonNull(sql); 1071 1072 if (startIndex < 0 || startIndex >= sql.length()) 1073 return null; 1074 1075 if (sql.charAt(startIndex) != '$') 1076 return null; 1077 1078 int i = startIndex + 1; 1079 1080 while (i < sql.length()) { 1081 char c = sql.charAt(i); 1082 1083 if (c == '$') 1084 return sql.substring(startIndex, i + 1); 1085 1086 if (Character.isWhitespace(c)) 1087 return null; 1088 1089 ++i; 1090 } 1091 1092 return null; 1093 } 1094 1095 @NonNull 1096 private static final Set<@NonNull String> QUESTION_MARK_PREFIX_KEYWORDS = Set.of( 1097 "SELECT", "WHERE", "AND", "OR", "ON", "HAVING", "WHEN", "THEN", "ELSE", "IN", 1098 "VALUES", "SET", "RETURNING", "USING", "LIKE", "BETWEEN", "IS", "NOT", "NULL", 1099 "JOIN", "FROM" 1100 ); 1101 1102 @NonNull 1103 private static final Set<@NonNull String> QUESTION_MARK_SUFFIX_KEYWORDS = Set.of( 1104 "FROM", "WHERE", "AND", "OR", "GROUP", "ORDER", "HAVING", "LIMIT", "OFFSET", 1105 "UNION", "EXCEPT", "INTERSECT", "RETURNING", "JOIN", "ON" 1106 ); 1107 1108 private static boolean isAllowedQuestionMarkOperator(@NonNull String sql, 1109 int questionMarkIndex) { 1110 requireNonNull(sql); 1111 1112 if (questionMarkIndex + 1 < sql.length()) { 1113 char nextChar = sql.charAt(questionMarkIndex + 1); 1114 if (nextChar == '|' || nextChar == '&') 1115 return true; 1116 } 1117 1118 int previousIndex = previousNonWhitespaceIndex(sql, questionMarkIndex - 1); 1119 int nextIndex = nextNonWhitespaceIndex(sql, questionMarkIndex + 1); 1120 1121 if (previousIndex < 0 || nextIndex < 0) 1122 return false; 1123 1124 char previousChar = sql.charAt(previousIndex); 1125 char nextChar = sql.charAt(nextIndex); 1126 1127 if (isOperatorBeforeQuestionMark(previousChar)) 1128 return false; 1129 1130 if (isTerminatorAfterQuestionMark(nextChar)) 1131 return false; 1132 1133 String previousKeyword = keywordBefore(sql, previousIndex); 1134 if (previousKeyword != null && QUESTION_MARK_PREFIX_KEYWORDS.contains(previousKeyword)) 1135 return false; 1136 1137 String nextKeyword = keywordAfter(sql, nextIndex); 1138 if (nextKeyword != null && QUESTION_MARK_SUFFIX_KEYWORDS.contains(nextKeyword)) 1139 return false; 1140 1141 return true; 1142 } 1143 1144 private static boolean isOperatorBeforeQuestionMark(char c) { 1145 return switch (c) { 1146 case '=', '<', '>', '!', '+', '-', '*', '/', '%', ',', '(' -> true; 1147 default -> false; 1148 }; 1149 } 1150 1151 private static boolean isTerminatorAfterQuestionMark(char c) { 1152 return switch (c) { 1153 case ')', ',', ';' -> true; 1154 default -> false; 1155 }; 1156 } 1157 1158 private static int previousNonWhitespaceIndex(@NonNull String sql, 1159 int startIndex) { 1160 for (int i = startIndex; i >= 0; i--) 1161 if (!Character.isWhitespace(sql.charAt(i))) 1162 return i; 1163 return -1; 1164 } 1165 1166 private static int nextNonWhitespaceIndex(@NonNull String sql, 1167 int startIndex) { 1168 for (int i = startIndex; i < sql.length(); i++) 1169 if (!Character.isWhitespace(sql.charAt(i))) 1170 return i; 1171 return -1; 1172 } 1173 1174 @Nullable 1175 private static String keywordBefore(@NonNull String sql, 1176 int index) { 1177 char c = sql.charAt(index); 1178 if (!Character.isJavaIdentifierPart(c)) 1179 return null; 1180 1181 int endIndex = index + 1; 1182 int startIndex = index; 1183 while (startIndex >= 0 && Character.isJavaIdentifierPart(sql.charAt(startIndex))) 1184 --startIndex; 1185 1186 return sql.substring(startIndex + 1, endIndex).toUpperCase(Locale.ROOT); 1187 } 1188 1189 @Nullable 1190 private static String keywordAfter(@NonNull String sql, 1191 int index) { 1192 char c = sql.charAt(index); 1193 if (!Character.isJavaIdentifierPart(c)) 1194 return null; 1195 1196 int endIndex = index + 1; 1197 while (endIndex < sql.length() && Character.isJavaIdentifierPart(sql.charAt(endIndex))) 1198 ++endIndex; 1199 1200 return sql.substring(index, endIndex).toUpperCase(Locale.ROOT); 1201 } 1202 1203 /** 1204 * Performs a SQL query that is expected to return 0 or 1 result rows. 1205 * 1206 * @param sql the SQL query to execute 1207 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1208 * @param parameters {@link PreparedStatement} parameters, if any 1209 * @param <T> the type to be returned 1210 * @return a single result (or no result) 1211 * @throws DatabaseException if > 1 row is returned 1212 */ 1213 @NonNull 1214 private <T> Optional<T> queryForObject(@NonNull String sql, 1215 @NonNull Class<T> resultSetRowType, 1216 Object @Nullable ... parameters) { 1217 requireNonNull(sql); 1218 requireNonNull(resultSetRowType); 1219 1220 return queryForObject(Statement.of(generateId(), sql), resultSetRowType, parameters); 1221 } 1222 1223 /** 1224 * Performs a SQL query that is expected to return 0 or 1 result rows. 1225 * 1226 * @param statement the SQL statement to execute 1227 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1228 * @param parameters {@link PreparedStatement} parameters, if any 1229 * @param <T> the type to be returned 1230 * @return a single result (or no result) 1231 * @throws DatabaseException if > 1 row is returned 1232 */ 1233 private <T> Optional<T> queryForObject(@NonNull Statement statement, 1234 @NonNull Class<T> resultSetRowType, 1235 Object @Nullable ... parameters) { 1236 requireNonNull(statement); 1237 requireNonNull(resultSetRowType); 1238 1239 return queryForObject(statement, resultSetRowType, null, parameters); 1240 } 1241 1242 private <T> Optional<T> queryForObject(@NonNull Statement statement, 1243 @NonNull Class<T> resultSetRowType, 1244 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1245 Object @Nullable ... parameters) { 1246 requireNonNull(statement); 1247 requireNonNull(resultSetRowType); 1248 1249 ResultHolder<Optional<T>> resultHolder = new ResultHolder<>(); 1250 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 1251 .resultSetRowType(resultSetRowType) 1252 .parameters(parameters) 1253 .build(); 1254 1255 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 1256 1257 performDatabaseOperation(statementContext, parametersAsList, preparedStatementCustomizer, (PreparedStatement preparedStatement) -> { 1258 long startTime = nanoTime(); 1259 1260 try (ResultSet resultSet = preparedStatement.executeQuery()) { 1261 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 1262 startTime = nanoTime(); 1263 1264 Optional<T> result = Optional.empty(); 1265 1266 if (resultSet.next()) { 1267 try { 1268 T value = getResultSetMapper().map(statementContext, resultSet, statementContext.getResultSetRowType().get(), getInstanceProvider()).orElse(null); 1269 result = Optional.ofNullable(value); 1270 } catch (SQLException e) { 1271 throw new DatabaseException(format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), statementContext.getResultSetRowType().get()), e); 1272 } 1273 1274 if (resultSet.next()) 1275 throw new DatabaseException("Expected 1 row in resultset but got more than 1 instead"); 1276 } 1277 1278 resultHolder.value = result; 1279 Duration resultSetMappingDuration = Duration.ofNanos(nanoTime() - startTime); 1280 return new DatabaseOperationResult(executionDuration, resultSetMappingDuration); 1281 } 1282 }); 1283 1284 return resultHolder.value; 1285 } 1286 1287 /** 1288 * Performs a SQL query that is expected to return any number of result rows. 1289 * 1290 * @param sql the SQL query to execute 1291 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1292 * @param parameters {@link PreparedStatement} parameters, if any 1293 * @param <T> the type to be returned 1294 * @return a list of results 1295 */ 1296 @NonNull 1297 private <T> List<@Nullable T> queryForList(@NonNull String sql, 1298 @NonNull Class<T> resultSetRowType, 1299 Object @Nullable ... parameters) { 1300 requireNonNull(sql); 1301 requireNonNull(resultSetRowType); 1302 1303 return queryForList(Statement.of(generateId(), sql), resultSetRowType, parameters); 1304 } 1305 1306 /** 1307 * Performs a SQL query that is expected to return any number of result rows. 1308 * 1309 * @param statement the SQL statement to execute 1310 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1311 * @param parameters {@link PreparedStatement} parameters, if any 1312 * @param <T> the type to be returned 1313 * @return a list of results 1314 */ 1315 @NonNull 1316 private <T> List<@Nullable T> queryForList(@NonNull Statement statement, 1317 @NonNull Class<T> resultSetRowType, 1318 Object @Nullable ... parameters) { 1319 requireNonNull(statement); 1320 requireNonNull(resultSetRowType); 1321 1322 return queryForList(statement, resultSetRowType, null, parameters); 1323 } 1324 1325 private <T> List<@Nullable T> queryForList(@NonNull Statement statement, 1326 @NonNull Class<T> resultSetRowType, 1327 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1328 Object @Nullable ... parameters) { 1329 requireNonNull(statement); 1330 requireNonNull(resultSetRowType); 1331 1332 List<T> list = new ArrayList<>(); 1333 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 1334 .resultSetRowType(resultSetRowType) 1335 .parameters(parameters) 1336 .build(); 1337 1338 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 1339 1340 performDatabaseOperation(statementContext, parametersAsList, preparedStatementCustomizer, (PreparedStatement preparedStatement) -> { 1341 long startTime = nanoTime(); 1342 1343 try (ResultSet resultSet = preparedStatement.executeQuery()) { 1344 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 1345 startTime = nanoTime(); 1346 1347 while (resultSet.next()) { 1348 try { 1349 T listElement = getResultSetMapper().map(statementContext, resultSet, statementContext.getResultSetRowType().get(), getInstanceProvider()).orElse(null); 1350 list.add(listElement); 1351 } catch (SQLException e) { 1352 throw new DatabaseException(format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), statementContext.getResultSetRowType().get()), e); 1353 } 1354 } 1355 1356 Duration resultSetMappingDuration = Duration.ofNanos(nanoTime() - startTime); 1357 return new DatabaseOperationResult(executionDuration, resultSetMappingDuration); 1358 } 1359 }); 1360 1361 return list; 1362 } 1363 1364 @Nullable 1365 private <T, R> R queryForStream(@NonNull Statement statement, 1366 @NonNull Class<T> resultSetRowType, 1367 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1368 @NonNull Function<Stream<@Nullable T>, R> streamFunction, 1369 Object @Nullable ... parameters) { 1370 requireNonNull(statement); 1371 requireNonNull(resultSetRowType); 1372 requireNonNull(streamFunction); 1373 1374 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 1375 .resultSetRowType(resultSetRowType) 1376 .parameters(parameters) 1377 .build(); 1378 1379 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 1380 StreamingResultSet<T> iterator = new StreamingResultSet<>(this, statementContext, parametersAsList, preparedStatementCustomizer); 1381 1382 try (Stream<@Nullable T> stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false) 1383 .onClose(iterator::close)) { 1384 return streamFunction.apply(stream); 1385 } 1386 } 1387 1388 /** 1389 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}; 1390 * or a SQL statement that returns nothing, such as a DDL statement. 1391 * 1392 * @param sql the SQL to execute 1393 * @param parameters {@link PreparedStatement} parameters, if any 1394 * @return the number of rows affected by the SQL statement 1395 */ 1396 @NonNull 1397 private Long execute(@NonNull String sql, 1398 Object @Nullable ... parameters) { 1399 requireNonNull(sql); 1400 return execute(Statement.of(generateId(), sql), parameters); 1401 } 1402 1403 /** 1404 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}; 1405 * or a SQL statement that returns nothing, such as a DDL statement. 1406 * 1407 * @param statement the SQL statement to execute 1408 * @param parameters {@link PreparedStatement} parameters, if any 1409 * @return the number of rows affected by the SQL statement 1410 */ 1411 @NonNull 1412 private Long execute(@NonNull Statement statement, 1413 Object @Nullable ... parameters) { 1414 requireNonNull(statement); 1415 1416 return execute(statement, null, parameters); 1417 } 1418 1419 private Long execute(@NonNull Statement statement, 1420 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1421 Object @Nullable ... parameters) { 1422 requireNonNull(statement); 1423 1424 ResultHolder<Long> resultHolder = new ResultHolder<>(); 1425 StatementContext<Void> statementContext = StatementContext.with(statement, this) 1426 .parameters(parameters) 1427 .build(); 1428 1429 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 1430 1431 performDatabaseOperation(statementContext, parametersAsList, preparedStatementCustomizer, (PreparedStatement preparedStatement) -> { 1432 long startTime = nanoTime(); 1433 1434 DatabaseOperationSupportStatus executeLargeUpdateSupported = getExecuteLargeUpdateSupported(); 1435 1436 // Use the appropriate "large" value if we know it. 1437 // If we don't know it, detect it and store it. 1438 if (executeLargeUpdateSupported == DatabaseOperationSupportStatus.YES) { 1439 resultHolder.value = preparedStatement.executeLargeUpdate(); 1440 } else if (executeLargeUpdateSupported == DatabaseOperationSupportStatus.NO) { 1441 resultHolder.value = (long) preparedStatement.executeUpdate(); 1442 } else { 1443 // If the driver doesn't support executeLargeUpdate, then UnsupportedOperationException is thrown. 1444 try { 1445 resultHolder.value = preparedStatement.executeLargeUpdate(); 1446 setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.YES); 1447 } catch (SQLFeatureNotSupportedException | UnsupportedOperationException | AbstractMethodError e) { 1448 setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.NO); 1449 resultHolder.value = (long) preparedStatement.executeUpdate(); 1450 } catch (SQLException e) { 1451 if (isUnsupportedSqlFeature(e)) { 1452 setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.NO); 1453 resultHolder.value = (long) preparedStatement.executeUpdate(); 1454 } else { 1455 throw e; 1456 } 1457 } 1458 } 1459 1460 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 1461 return new DatabaseOperationResult(executionDuration, null); 1462 }); 1463 1464 return resultHolder.value; 1465 } 1466 1467 /** 1468 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 1469 * which returns 0 or 1 rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 1470 * 1471 * @param sql the SQL query to execute 1472 * @param resultSetRowType the type to which the {@link ResultSet} row should be marshaled 1473 * @param parameters {@link PreparedStatement} parameters, if any 1474 * @param <T> the type to be returned 1475 * @return a single result (or no result) 1476 * @throws DatabaseException if > 1 row is returned 1477 */ 1478 @NonNull 1479 private <T> Optional<T> executeForObject(@NonNull String sql, 1480 @NonNull Class<T> resultSetRowType, 1481 Object @Nullable ... parameters) { 1482 requireNonNull(sql); 1483 requireNonNull(resultSetRowType); 1484 1485 return executeForObject(Statement.of(generateId(), sql), resultSetRowType, parameters); 1486 } 1487 1488 /** 1489 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 1490 * which returns 0 or 1 rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 1491 * 1492 * @param statement the SQL statement to execute 1493 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1494 * @param parameters {@link PreparedStatement} parameters, if any 1495 * @param <T> the type to be returned 1496 * @return a single result (or no result) 1497 * @throws DatabaseException if > 1 row is returned 1498 */ 1499 private <T> Optional<T> executeForObject(@NonNull Statement statement, 1500 @NonNull Class<T> resultSetRowType, 1501 Object @Nullable ... parameters) { 1502 requireNonNull(statement); 1503 requireNonNull(resultSetRowType); 1504 1505 return executeForObject(statement, resultSetRowType, null, parameters); 1506 } 1507 1508 private <T> Optional<T> executeForObject(@NonNull Statement statement, 1509 @NonNull Class<T> resultSetRowType, 1510 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1511 Object @Nullable ... parameters) { 1512 requireNonNull(statement); 1513 requireNonNull(resultSetRowType); 1514 1515 // Ultimately we just delegate to queryForObject. 1516 // Having `executeForList` is to allow for users to explicitly express intent 1517 // and make static analysis of code easier (e.g. maybe you'd like to hook all of your "execute" statements for 1518 // logging, or delegation to a writable master as opposed to a read replica) 1519 return queryForObject(statement, resultSetRowType, preparedStatementCustomizer, 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 any number of rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 1525 * 1526 * @param sql the SQL 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 list of results 1531 */ 1532 @NonNull 1533 private <T> List<@Nullable T> executeForList(@NonNull String sql, 1534 @NonNull Class<T> resultSetRowType, 1535 Object @Nullable ... parameters) { 1536 requireNonNull(sql); 1537 requireNonNull(resultSetRowType); 1538 1539 return executeForList(Statement.of(generateId(), sql), resultSetRowType, parameters); 1540 } 1541 1542 /** 1543 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 1544 * which returns any number of rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 1545 * 1546 * @param statement the SQL statement to execute 1547 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 1548 * @param parameters {@link PreparedStatement} parameters, if any 1549 * @param <T> the type to be returned 1550 * @return a list of results 1551 */ 1552 @NonNull 1553 private <T> List<@Nullable T> executeForList(@NonNull Statement statement, 1554 @NonNull Class<T> resultSetRowType, 1555 Object @Nullable ... parameters) { 1556 requireNonNull(statement); 1557 requireNonNull(resultSetRowType); 1558 1559 return executeForList(statement, resultSetRowType, null, parameters); 1560 } 1561 1562 private <T> List<@Nullable T> executeForList(@NonNull Statement statement, 1563 @NonNull Class<T> resultSetRowType, 1564 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1565 Object @Nullable ... parameters) { 1566 requireNonNull(statement); 1567 requireNonNull(resultSetRowType); 1568 1569 // Ultimately we just delegate to queryForList. 1570 // Having `executeForList` is to allow for users to explicitly express intent 1571 // and make static analysis of code easier (e.g. maybe you'd like to hook all of your "execute" statements for 1572 // logging, or delegation to a writable master as opposed to a read replica) 1573 return queryForList(statement, resultSetRowType, preparedStatementCustomizer, parameters); 1574 } 1575 1576 /** 1577 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE} 1578 * in "batch" over a set of parameter groups. 1579 * <p> 1580 * Useful for bulk-inserting or updating large amounts of data. 1581 * 1582 * @param sql the SQL to execute 1583 * @param parameterGroups Groups of {@link PreparedStatement} parameters 1584 * @return the number of rows affected by the SQL statement per-group 1585 */ 1586 @NonNull 1587 private List<Long> executeBatch(@NonNull String sql, 1588 @NonNull List<List<Object>> parameterGroups) { 1589 requireNonNull(sql); 1590 requireNonNull(parameterGroups); 1591 1592 return executeBatch(Statement.of(generateId(), sql), parameterGroups); 1593 } 1594 1595 /** 1596 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE} 1597 * in "batch" over a set of parameter groups. 1598 * <p> 1599 * Useful for bulk-inserting or updating large amounts of data. 1600 * 1601 * @param statement the SQL statement to execute 1602 * @param parameterGroups Groups of {@link PreparedStatement} parameters 1603 * @return the number of rows affected by the SQL statement per-group 1604 */ 1605 @NonNull 1606 private List<Long> executeBatch(@NonNull Statement statement, 1607 @NonNull List<List<Object>> parameterGroups) { 1608 requireNonNull(statement); 1609 requireNonNull(parameterGroups); 1610 1611 return executeBatch(statement, parameterGroups, null); 1612 } 1613 1614 private List<Long> executeBatch(@NonNull Statement statement, 1615 @NonNull List<List<Object>> parameterGroups, 1616 @Nullable PreparedStatementCustomizer preparedStatementCustomizer) { 1617 requireNonNull(statement); 1618 requireNonNull(parameterGroups); 1619 if (parameterGroups.isEmpty()) 1620 return List.of(); 1621 1622 Integer expectedParameterCount = null; 1623 1624 for (int i = 0; i < parameterGroups.size(); i++) { 1625 List<Object> parameterGroup = parameterGroups.get(i); 1626 1627 if (parameterGroup == null) 1628 throw new IllegalArgumentException(format("Parameter group at index %s is null", i)); 1629 1630 int parameterCount = parameterGroup.size(); 1631 if (expectedParameterCount == null) { 1632 expectedParameterCount = parameterCount; 1633 } else if (parameterCount != expectedParameterCount) { 1634 throw new IllegalArgumentException(format( 1635 "Inconsistent parameter group size at index %s: expected %s but found %s", 1636 i, expectedParameterCount, parameterCount)); 1637 } 1638 } 1639 1640 ResultHolder<List<Long>> resultHolder = new ResultHolder<>(); 1641 StatementContext<List<Long>> statementContext = StatementContext.with(statement, this) 1642 .parameters((List) parameterGroups) 1643 .resultSetRowType(List.class) 1644 .build(); 1645 1646 performDatabaseOperation(statementContext, (preparedStatement) -> { 1647 applyPreparedStatementCustomizer(statementContext, preparedStatement, preparedStatementCustomizer); 1648 1649 for (List<Object> parameterGroup : parameterGroups) { 1650 if (parameterGroup.size() > 0) 1651 performPreparedStatementBinding(statementContext, preparedStatement, parameterGroup); 1652 1653 preparedStatement.addBatch(); 1654 } 1655 }, (PreparedStatement preparedStatement) -> { 1656 long startTime = nanoTime(); 1657 List<Long> result; 1658 1659 DatabaseOperationSupportStatus executeLargeBatchSupported = getExecuteLargeBatchSupported(); 1660 1661 // Use the appropriate "large" value if we know it. 1662 // If we don't know it, detect it and store it. 1663 if (executeLargeBatchSupported == DatabaseOperationSupportStatus.YES) { 1664 long[] resultArray = preparedStatement.executeLargeBatch(); 1665 result = Arrays.stream(resultArray).boxed().collect(Collectors.toList()); 1666 } else if (executeLargeBatchSupported == DatabaseOperationSupportStatus.NO) { 1667 int[] resultArray = preparedStatement.executeBatch(); 1668 result = Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList()); 1669 } else { 1670 // If the driver doesn't support executeLargeBatch, then UnsupportedOperationException is thrown. 1671 try { 1672 long[] resultArray = preparedStatement.executeLargeBatch(); 1673 result = Arrays.stream(resultArray).boxed().collect(Collectors.toList()); 1674 setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.YES); 1675 } catch (SQLFeatureNotSupportedException | UnsupportedOperationException | AbstractMethodError e) { 1676 setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.NO); 1677 int[] resultArray = preparedStatement.executeBatch(); 1678 result = Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList()); 1679 } catch (SQLException e) { 1680 if (isUnsupportedSqlFeature(e)) { 1681 setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.NO); 1682 int[] resultArray = preparedStatement.executeBatch(); 1683 result = Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList()); 1684 } else { 1685 throw e; 1686 } 1687 } 1688 } 1689 1690 resultHolder.value = result; 1691 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 1692 return new DatabaseOperationResult(executionDuration, null); 1693 }, parameterGroups.size()); 1694 1695 return resultHolder.value; 1696 } 1697 1698 /** 1699 * Exposes a temporary handle to JDBC {@link DatabaseMetaData}, which provides comprehensive vendor-specific information about this database as a whole. 1700 * <p> 1701 * This method acquires {@link DatabaseMetaData} on its own newly-borrowed connection, which it manages internally. 1702 * <p> 1703 * It does <strong>not</strong> participate in the active transaction, if one exists. 1704 * <p> 1705 * The connection is closed as soon as {@link DatabaseMetaDataReader#read(DatabaseMetaData)} completes. 1706 * <p> 1707 * 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. 1708 */ 1709 public void readDatabaseMetaData(@NonNull DatabaseMetaDataReader databaseMetaDataReader) { 1710 requireNonNull(databaseMetaDataReader); 1711 1712 performRawConnectionOperation((connection -> { 1713 databaseMetaDataReader.read(connection.getMetaData()); 1714 return Optional.empty(); 1715 }), false); 1716 } 1717 1718 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 1719 @NonNull List<Object> parameters, 1720 @NonNull DatabaseOperation databaseOperation) { 1721 requireNonNull(statementContext); 1722 requireNonNull(parameters); 1723 requireNonNull(databaseOperation); 1724 1725 performDatabaseOperation(statementContext, parameters, null, databaseOperation); 1726 } 1727 1728 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 1729 @NonNull List<Object> parameters, 1730 @Nullable PreparedStatementCustomizer preparedStatementCustomizer, 1731 @NonNull DatabaseOperation databaseOperation) { 1732 requireNonNull(statementContext); 1733 requireNonNull(parameters); 1734 requireNonNull(databaseOperation); 1735 1736 performDatabaseOperation(statementContext, (preparedStatement) -> { 1737 applyPreparedStatementCustomizer(statementContext, preparedStatement, preparedStatementCustomizer); 1738 if (parameters.size() > 0) 1739 performPreparedStatementBinding(statementContext, preparedStatement, parameters); 1740 }, databaseOperation); 1741 } 1742 1743 protected <T> void performPreparedStatementBinding(@NonNull StatementContext<T> statementContext, 1744 @NonNull PreparedStatement preparedStatement, 1745 @NonNull List<Object> parameters) { 1746 requireNonNull(statementContext); 1747 requireNonNull(preparedStatement); 1748 requireNonNull(parameters); 1749 1750 try { 1751 for (int i = 0; i < parameters.size(); ++i) { 1752 Object parameter = parameters.get(i); 1753 1754 if (parameter != null) { 1755 getPreparedStatementBinder().bindParameter(statementContext, preparedStatement, i + 1, parameter); 1756 } else { 1757 try { 1758 ParameterMetaData parameterMetaData = preparedStatement.getParameterMetaData(); 1759 1760 if (parameterMetaData != null) { 1761 preparedStatement.setNull(i + 1, parameterMetaData.getParameterType(i + 1)); 1762 } else { 1763 preparedStatement.setNull(i + 1, Types.NULL); 1764 } 1765 } catch (SQLException | AbstractMethodError e) { 1766 preparedStatement.setNull(i + 1, Types.NULL); 1767 } 1768 } 1769 } 1770 } catch (Exception e) { 1771 throw new DatabaseException(e); 1772 } 1773 } 1774 1775 protected void applyPreparedStatementCustomizer(@NonNull StatementContext<?> statementContext, 1776 @NonNull PreparedStatement preparedStatement, 1777 @Nullable PreparedStatementCustomizer preparedStatementCustomizer) throws SQLException { 1778 requireNonNull(statementContext); 1779 requireNonNull(preparedStatement); 1780 1781 if (preparedStatementCustomizer == null) 1782 return; 1783 1784 preparedStatementCustomizer.customize(statementContext, preparedStatement); 1785 } 1786 1787 @FunctionalInterface 1788 protected interface RawConnectionOperation<R> { 1789 @NonNull 1790 Optional<R> perform(@NonNull Connection connection) throws Exception; 1791 } 1792 1793 /** 1794 * @since 3.0.0 1795 */ 1796 @NonNull 1797 public DatabaseType getDatabaseType() { 1798 return this.databaseType; 1799 } 1800 1801 /** 1802 * @since 3.0.0 1803 */ 1804 @NonNull 1805 public ZoneId getTimeZone() { 1806 return this.timeZone; 1807 } 1808 1809 /** 1810 * Useful for single-shot "utility" calls that operate outside of normal query operations, e.g. pulling DB metadata. 1811 * <p> 1812 * Example: {@link #readDatabaseMetaData(DatabaseMetaDataReader)}. 1813 */ 1814 @NonNull 1815 protected <R> Optional<R> performRawConnectionOperation(@NonNull RawConnectionOperation<R> rawConnectionOperation, 1816 @NonNull Boolean shouldParticipateInExistingTransactionIfPossible) { 1817 requireNonNull(rawConnectionOperation); 1818 requireNonNull(shouldParticipateInExistingTransactionIfPossible); 1819 1820 if (shouldParticipateInExistingTransactionIfPossible) { 1821 Optional<Transaction> transaction = currentTransaction(); 1822 ReentrantLock connectionLock = transaction.isPresent() ? transaction.get().getConnectionLock() : null; 1823 // Try to participate in txn if it's available 1824 Connection connection = null; 1825 Throwable thrown = null; 1826 1827 if (connectionLock != null) 1828 connectionLock.lock(); 1829 1830 try { 1831 connection = transaction.isPresent() ? transaction.get().getConnection() : acquireConnection(); 1832 return rawConnectionOperation.perform(connection); 1833 } catch (DatabaseException e) { 1834 thrown = e; 1835 throw e; 1836 } catch (Exception e) { 1837 DatabaseException wrapped = new DatabaseException(e); 1838 thrown = wrapped; 1839 throw wrapped; 1840 } finally { 1841 Throwable cleanupFailure = null; 1842 1843 try { 1844 // If this was a single-shot operation (not in a transaction), close the connection 1845 if (connection != null && !transaction.isPresent()) { 1846 try { 1847 closeConnection(connection); 1848 } catch (Throwable cleanupException) { 1849 cleanupFailure = cleanupException; 1850 } 1851 } 1852 } finally { 1853 if (connectionLock != null) 1854 connectionLock.unlock(); 1855 1856 if (cleanupFailure != null) { 1857 if (thrown != null) { 1858 thrown.addSuppressed(cleanupFailure); 1859 } else if (cleanupFailure instanceof RuntimeException) { 1860 throw (RuntimeException) cleanupFailure; 1861 } else if (cleanupFailure instanceof Error) { 1862 throw (Error) cleanupFailure; 1863 } else { 1864 throw new RuntimeException(cleanupFailure); 1865 } 1866 } 1867 } 1868 } 1869 } else { 1870 boolean acquiredConnection = false; 1871 Connection connection = null; 1872 Throwable thrown = null; 1873 1874 // Always get a fresh connection no matter what and close it afterwards 1875 try { 1876 connection = getDataSource().getConnection(); 1877 acquiredConnection = true; 1878 return rawConnectionOperation.perform(connection); 1879 } catch (DatabaseException e) { 1880 thrown = e; 1881 throw e; 1882 } catch (Exception e) { 1883 DatabaseException wrapped = acquiredConnection 1884 ? new DatabaseException(e) 1885 : new DatabaseException("Unable to acquire database connection", e); 1886 thrown = wrapped; 1887 throw wrapped; 1888 } finally { 1889 if (connection != null) { 1890 try { 1891 closeConnection(connection); 1892 } catch (Throwable cleanupException) { 1893 if (thrown != null) { 1894 thrown.addSuppressed(cleanupException); 1895 } else if (cleanupException instanceof RuntimeException) { 1896 throw (RuntimeException) cleanupException; 1897 } else if (cleanupException instanceof Error) { 1898 throw (Error) cleanupException; 1899 } else { 1900 throw new RuntimeException(cleanupException); 1901 } 1902 } 1903 } 1904 } 1905 } 1906 } 1907 1908 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 1909 @NonNull PreparedStatementBindingOperation preparedStatementBindingOperation, 1910 @NonNull DatabaseOperation databaseOperation) { 1911 performDatabaseOperation(statementContext, preparedStatementBindingOperation, databaseOperation, null); 1912 } 1913 1914 protected <T> void performDatabaseOperation(@NonNull StatementContext<T> statementContext, 1915 @NonNull PreparedStatementBindingOperation preparedStatementBindingOperation, 1916 @NonNull DatabaseOperation databaseOperation, 1917 @Nullable Integer batchSize) { 1918 requireNonNull(statementContext); 1919 requireNonNull(preparedStatementBindingOperation); 1920 requireNonNull(databaseOperation); 1921 1922 long startTime = nanoTime(); 1923 Duration connectionAcquisitionDuration = null; 1924 Duration preparationDuration = null; 1925 Duration executionDuration = null; 1926 Duration resultSetMappingDuration = null; 1927 Exception exception = null; 1928 Throwable thrown = null; 1929 Connection connection = null; 1930 Optional<Transaction> transaction = currentTransaction(); 1931 ReentrantLock connectionLock = transaction.isPresent() ? transaction.get().getConnectionLock() : null; 1932 1933 if (connectionLock != null) 1934 connectionLock.lock(); 1935 1936 try { 1937 boolean alreadyHasConnection = transaction.isPresent() && transaction.get().hasConnection(); 1938 connection = transaction.isPresent() ? transaction.get().getConnection() : acquireConnection(); 1939 connectionAcquisitionDuration = alreadyHasConnection ? null : Duration.ofNanos(nanoTime() - startTime); 1940 startTime = nanoTime(); 1941 1942 try (PreparedStatement preparedStatement = connection.prepareStatement(statementContext.getStatement().getSql())) { 1943 preparedStatementBindingOperation.perform(preparedStatement); 1944 preparationDuration = Duration.ofNanos(nanoTime() - startTime); 1945 1946 DatabaseOperationResult databaseOperationResult = databaseOperation.perform(preparedStatement); 1947 executionDuration = databaseOperationResult.getExecutionDuration().orElse(null); 1948 resultSetMappingDuration = databaseOperationResult.getResultSetMappingDuration().orElse(null); 1949 } 1950 } catch (DatabaseException e) { 1951 exception = e; 1952 thrown = e; 1953 throw e; 1954 } catch (Error e) { 1955 exception = new DatabaseException(e); 1956 thrown = e; 1957 throw e; 1958 } catch (Exception e) { 1959 exception = e; 1960 DatabaseException wrapped = new DatabaseException(e); 1961 thrown = wrapped; 1962 throw wrapped; 1963 } finally { 1964 Throwable cleanupFailure = null; 1965 1966 try { 1967 cleanupFailure = closeStatementContextResources(statementContext, cleanupFailure); 1968 1969 // If this was a single-shot operation (not in a transaction), close the connection 1970 if (connection != null && !transaction.isPresent()) { 1971 try { 1972 closeConnection(connection); 1973 } catch (Throwable cleanupException) { 1974 cleanupFailure = cleanupException; 1975 } 1976 } 1977 } finally { 1978 if (connectionLock != null) 1979 connectionLock.unlock(); 1980 1981 StatementLog statementLog = 1982 StatementLog.withStatementContext(statementContext) 1983 .connectionAcquisitionDuration(connectionAcquisitionDuration) 1984 .preparationDuration(preparationDuration) 1985 .executionDuration(executionDuration) 1986 .resultSetMappingDuration(resultSetMappingDuration) 1987 .batchSize(batchSize) 1988 .exception(exception) 1989 .build(); 1990 1991 try { 1992 getStatementLogger().log(statementLog); 1993 } catch (Throwable cleanupException) { 1994 if (transaction.isPresent() && thrown == null && cleanupFailure == null) { 1995 Throwable loggerFailure = cleanupException; 1996 Transaction currentTransaction = transaction.get(); 1997 1998 if (!currentTransaction.isOwnedByCurrentThread()) { 1999 cleanupFailure = new StatementLoggerFailureException(loggerFailure); 2000 } else { 2001 currentTransaction.addPostTransactionOperation(result -> { 2002 if (loggerFailure instanceof RuntimeException runtimeException) 2003 throw runtimeException; 2004 if (loggerFailure instanceof Error error) 2005 throw error; 2006 throw new RuntimeException(loggerFailure); 2007 }); 2008 } 2009 } else { 2010 if (cleanupFailure == null) 2011 cleanupFailure = cleanupException; 2012 else 2013 cleanupFailure.addSuppressed(cleanupException); 2014 } 2015 } 2016 } 2017 2018 if (cleanupFailure != null) { 2019 if (thrown != null) { 2020 thrown.addSuppressed(cleanupFailure); 2021 } else if (cleanupFailure instanceof RuntimeException) { 2022 throw (RuntimeException) cleanupFailure; 2023 } else if (cleanupFailure instanceof Error) { 2024 throw (Error) cleanupFailure; 2025 } else { 2026 throw new RuntimeException(cleanupFailure); 2027 } 2028 } 2029 } 2030 } 2031 2032 @NonNull 2033 protected Connection acquireConnection() { 2034 Optional<Transaction> transaction = currentTransaction(); 2035 2036 if (transaction.isPresent()) 2037 return transaction.get().getConnection(); 2038 2039 try { 2040 return getDataSource().getConnection(); 2041 } catch (SQLException e) { 2042 throw new DatabaseException("Unable to acquire database connection", e); 2043 } 2044 } 2045 2046 @NonNull 2047 protected DataSource getDataSource() { 2048 return this.dataSource; 2049 } 2050 2051 @NonNull 2052 protected InstanceProvider getInstanceProvider() { 2053 return this.instanceProvider; 2054 } 2055 2056 @NonNull 2057 protected PreparedStatementBinder getPreparedStatementBinder() { 2058 return this.preparedStatementBinder; 2059 } 2060 2061 @NonNull 2062 protected ResultSetMapper getResultSetMapper() { 2063 return this.resultSetMapper; 2064 } 2065 2066 @NonNull 2067 protected StatementLogger getStatementLogger() { 2068 return this.statementLogger; 2069 } 2070 2071 @NonNull 2072 protected DatabaseOperationSupportStatus getExecuteLargeBatchSupported() { 2073 return this.executeLargeBatchSupported; 2074 } 2075 2076 protected void setExecuteLargeBatchSupported(@NonNull DatabaseOperationSupportStatus executeLargeBatchSupported) { 2077 requireNonNull(executeLargeBatchSupported); 2078 this.executeLargeBatchSupported = executeLargeBatchSupported; 2079 } 2080 2081 @NonNull 2082 protected DatabaseOperationSupportStatus getExecuteLargeUpdateSupported() { 2083 return this.executeLargeUpdateSupported; 2084 } 2085 2086 protected void setExecuteLargeUpdateSupported(@NonNull DatabaseOperationSupportStatus executeLargeUpdateSupported) { 2087 requireNonNull(executeLargeUpdateSupported); 2088 this.executeLargeUpdateSupported = executeLargeUpdateSupported; 2089 } 2090 2091 @NonNull 2092 protected Object generateId() { 2093 // "Unique" keys 2094 return format("com.pyranid.%s", this.defaultIdGenerator.incrementAndGet()); 2095 } 2096 2097 @FunctionalInterface 2098 protected interface DatabaseOperation { 2099 @NonNull 2100 DatabaseOperationResult perform(@NonNull PreparedStatement preparedStatement) throws Exception; 2101 } 2102 2103 @FunctionalInterface 2104 protected interface PreparedStatementBindingOperation { 2105 void perform(@NonNull PreparedStatement preparedStatement) throws Exception; 2106 } 2107 2108 @NotThreadSafe 2109 private static final class StreamingResultSet<T> implements java.util.Iterator<T>, AutoCloseable { 2110 private final Database database; 2111 private final StatementContext<T> statementContext; 2112 private final List<Object> parameters; 2113 @Nullable 2114 private final PreparedStatementCustomizer preparedStatementCustomizer; 2115 @NonNull 2116 private final Optional<Transaction> transaction; 2117 @Nullable 2118 private final ReentrantLock connectionLock; 2119 @Nullable 2120 private Connection connection; 2121 @Nullable 2122 private PreparedStatement preparedStatement; 2123 @Nullable 2124 private ResultSet resultSet; 2125 private boolean closed; 2126 private boolean hasNextEvaluated; 2127 private boolean hasNext; 2128 @Nullable 2129 private Duration connectionAcquisitionDuration; 2130 @Nullable 2131 private Duration preparationDuration; 2132 @Nullable 2133 private Duration executionDuration; 2134 private long resultSetMappingNanos; 2135 @Nullable 2136 private Exception exception; 2137 @Nullable 2138 private Throwable thrown; 2139 2140 private StreamingResultSet(@NonNull Database database, 2141 @NonNull StatementContext<T> statementContext, 2142 @NonNull List<Object> parameters, 2143 @Nullable PreparedStatementCustomizer preparedStatementCustomizer) { 2144 this.database = requireNonNull(database); 2145 this.statementContext = requireNonNull(statementContext); 2146 this.parameters = requireNonNull(parameters); 2147 this.preparedStatementCustomizer = preparedStatementCustomizer; 2148 this.transaction = database.currentTransaction(); 2149 this.connectionLock = this.transaction.isPresent() ? this.transaction.get().getConnectionLock() : null; 2150 2151 open(); 2152 } 2153 2154 private void open() { 2155 long startTime = nanoTime(); 2156 2157 if (this.connectionLock != null) 2158 this.connectionLock.lock(); 2159 2160 try { 2161 boolean alreadyHasConnection = this.transaction.isPresent() && this.transaction.get().hasConnection(); 2162 this.connection = this.transaction.isPresent() ? this.transaction.get().getConnection() : this.database.acquireConnection(); 2163 this.connectionAcquisitionDuration = alreadyHasConnection ? null : Duration.ofNanos(nanoTime() - startTime); 2164 startTime = nanoTime(); 2165 2166 this.preparedStatement = this.connection.prepareStatement(this.statementContext.getStatement().getSql()); 2167 this.database.applyPreparedStatementCustomizer(this.statementContext, this.preparedStatement, this.preparedStatementCustomizer); 2168 if (this.parameters.size() > 0) 2169 this.database.performPreparedStatementBinding(this.statementContext, this.preparedStatement, this.parameters); 2170 this.preparationDuration = Duration.ofNanos(nanoTime() - startTime); 2171 2172 startTime = nanoTime(); 2173 this.resultSet = this.preparedStatement.executeQuery(); 2174 this.executionDuration = Duration.ofNanos(nanoTime() - startTime); 2175 } catch (DatabaseException e) { 2176 this.exception = e; 2177 this.thrown = e; 2178 close(); 2179 throw e; 2180 } catch (Exception e) { 2181 this.exception = e; 2182 DatabaseException wrapped = new DatabaseException(e); 2183 this.thrown = wrapped; 2184 close(); 2185 throw wrapped; 2186 } 2187 } 2188 2189 @Override 2190 public boolean hasNext() { 2191 if (this.closed) 2192 return false; 2193 2194 if (!this.hasNextEvaluated) { 2195 try { 2196 this.hasNext = this.resultSet != null && this.resultSet.next(); 2197 this.hasNextEvaluated = true; 2198 if (!this.hasNext) 2199 close(); 2200 } catch (SQLException e) { 2201 this.exception = e; 2202 this.thrown = new DatabaseException(e); 2203 close(); 2204 throw (DatabaseException) this.thrown; 2205 } 2206 } 2207 2208 return this.hasNext; 2209 } 2210 2211 @Override 2212 public T next() { 2213 if (!hasNext()) 2214 throw new java.util.NoSuchElementException(); 2215 2216 this.hasNextEvaluated = false; 2217 long startTime = nanoTime(); 2218 2219 try { 2220 T value = this.database.getResultSetMapper() 2221 .map(this.statementContext, requireNonNull(this.resultSet), this.statementContext.getResultSetRowType().get(), this.database.getInstanceProvider()) 2222 .orElse(null); 2223 this.resultSetMappingNanos += nanoTime() - startTime; 2224 return value; 2225 } catch (SQLException e) { 2226 this.exception = e; 2227 this.thrown = new DatabaseException(format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), this.statementContext.getResultSetRowType().get()), e); 2228 close(); 2229 throw (DatabaseException) this.thrown; 2230 } catch (DatabaseException e) { 2231 this.exception = e; 2232 this.thrown = e; 2233 close(); 2234 throw e; 2235 } 2236 } 2237 2238 @Override 2239 public void close() { 2240 if (this.closed) 2241 return; 2242 2243 this.closed = true; 2244 Throwable cleanupFailure = null; 2245 2246 try { 2247 cleanupFailure = closeStatementContextResources(this.statementContext, cleanupFailure); 2248 2249 if (this.resultSet != null) { 2250 try { 2251 this.resultSet.close(); 2252 } catch (Throwable cleanupException) { 2253 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 2254 } 2255 } 2256 2257 if (this.preparedStatement != null) { 2258 try { 2259 this.preparedStatement.close(); 2260 } catch (Throwable cleanupException) { 2261 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 2262 } 2263 } 2264 2265 if (this.connection != null && this.transaction.isEmpty()) { 2266 try { 2267 this.database.closeConnection(this.connection); 2268 } catch (Throwable cleanupException) { 2269 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 2270 } 2271 } 2272 } finally { 2273 if (this.connectionLock != null) 2274 this.connectionLock.unlock(); 2275 2276 Duration mappingDuration = this.resultSetMappingNanos == 0L ? null : Duration.ofNanos(this.resultSetMappingNanos); 2277 2278 StatementLog statementLog = 2279 StatementLog.withStatementContext(this.statementContext) 2280 .connectionAcquisitionDuration(this.connectionAcquisitionDuration) 2281 .preparationDuration(this.preparationDuration) 2282 .executionDuration(this.executionDuration) 2283 .resultSetMappingDuration(mappingDuration) 2284 .exception(this.exception) 2285 .build(); 2286 2287 try { 2288 this.database.getStatementLogger().log(statementLog); 2289 } catch (Throwable cleanupException) { 2290 if (this.transaction.isPresent() && this.thrown == null && cleanupFailure == null) { 2291 Throwable loggerFailure = cleanupException; 2292 Transaction currentTransaction = this.transaction.get(); 2293 2294 if (!currentTransaction.isOwnedByCurrentThread()) { 2295 cleanupFailure = new StatementLoggerFailureException(loggerFailure); 2296 } else { 2297 currentTransaction.addPostTransactionOperation(result -> { 2298 if (loggerFailure instanceof RuntimeException runtimeException) 2299 throw runtimeException; 2300 if (loggerFailure instanceof Error error) 2301 throw error; 2302 throw new RuntimeException(loggerFailure); 2303 }); 2304 } 2305 } else { 2306 cleanupFailure = cleanupFailure == null ? cleanupException : addSuppressed(cleanupFailure, cleanupException); 2307 } 2308 } 2309 } 2310 2311 if (cleanupFailure != null) { 2312 if (this.thrown != null) { 2313 this.thrown.addSuppressed(cleanupFailure); 2314 } else if (cleanupFailure instanceof RuntimeException) { 2315 throw (RuntimeException) cleanupFailure; 2316 } else if (cleanupFailure instanceof Error) { 2317 throw (Error) cleanupFailure; 2318 } else { 2319 throw new RuntimeException(cleanupFailure); 2320 } 2321 } 2322 } 2323 2324 @NonNull 2325 private static Throwable addSuppressed(@NonNull Throwable existing, 2326 @NonNull Throwable additional) { 2327 existing.addSuppressed(additional); 2328 return existing; 2329 } 2330 } 2331 2332 /** 2333 * Builder used to construct instances of {@link Database}. 2334 * <p> 2335 * This class is intended for use by a single thread. 2336 * 2337 * @author <a href="https://www.revetkn.com">Mark Allen</a> 2338 * @since 1.0.0 2339 */ 2340 @NotThreadSafe 2341 public static class Builder { 2342 @NonNull 2343 private final DataSource dataSource; 2344 @Nullable 2345 private DatabaseType databaseType; 2346 @Nullable 2347 private ZoneId timeZone; 2348 @Nullable 2349 private InstanceProvider instanceProvider; 2350 @Nullable 2351 private PreparedStatementBinder preparedStatementBinder; 2352 @Nullable 2353 private ResultSetMapper resultSetMapper; 2354 @Nullable 2355 private StatementLogger statementLogger; 2356 @Nullable 2357 private Integer parsedSqlCacheCapacity; 2358 2359 private Builder(@NonNull DataSource dataSource) { 2360 this.dataSource = requireNonNull(dataSource); 2361 this.databaseType = null; 2362 this.parsedSqlCacheCapacity = null; 2363 } 2364 2365 /** 2366 * Overrides automatic database type detection. 2367 * 2368 * @param databaseType the database type to use (null to enable auto-detection) 2369 * @return this {@code Builder}, for chaining 2370 * @since 4.0.0 2371 */ 2372 @NonNull 2373 public Builder databaseType(@Nullable DatabaseType databaseType) { 2374 this.databaseType = databaseType; 2375 return this; 2376 } 2377 2378 @NonNull 2379 public Builder timeZone(@Nullable ZoneId timeZone) { 2380 this.timeZone = timeZone; 2381 return this; 2382 } 2383 2384 @NonNull 2385 public Builder instanceProvider(@Nullable InstanceProvider instanceProvider) { 2386 this.instanceProvider = instanceProvider; 2387 return this; 2388 } 2389 2390 @NonNull 2391 public Builder preparedStatementBinder(@Nullable PreparedStatementBinder preparedStatementBinder) { 2392 this.preparedStatementBinder = preparedStatementBinder; 2393 return this; 2394 } 2395 2396 @NonNull 2397 public Builder resultSetMapper(@Nullable ResultSetMapper resultSetMapper) { 2398 this.resultSetMapper = resultSetMapper; 2399 return this; 2400 } 2401 2402 @NonNull 2403 public Builder statementLogger(@Nullable StatementLogger statementLogger) { 2404 this.statementLogger = statementLogger; 2405 return this; 2406 } 2407 2408 /** 2409 * Configures the size of the parsed SQL cache. 2410 * <p> 2411 * A value of {@code 0} disables caching. A value of {@code null} uses the default size. 2412 * 2413 * @param parsedSqlCacheCapacity cache size (0 disables caching, null uses default) 2414 * @return this {@code Builder}, for chaining 2415 */ 2416 @NonNull 2417 public Builder parsedSqlCacheCapacity(@Nullable Integer parsedSqlCacheCapacity) { 2418 if (parsedSqlCacheCapacity != null && parsedSqlCacheCapacity < 0) 2419 throw new IllegalArgumentException("parsedSqlCacheCapacity must be >= 0"); 2420 2421 this.parsedSqlCacheCapacity = parsedSqlCacheCapacity; 2422 return this; 2423 } 2424 2425 @NonNull 2426 public Database build() { 2427 return new Database(this); 2428 } 2429 } 2430 2431 @ThreadSafe 2432 static class DatabaseOperationResult { 2433 @Nullable 2434 private final Duration executionDuration; 2435 @Nullable 2436 private final Duration resultSetMappingDuration; 2437 2438 public DatabaseOperationResult(@Nullable Duration executionDuration, 2439 @Nullable Duration resultSetMappingDuration) { 2440 this.executionDuration = executionDuration; 2441 this.resultSetMappingDuration = resultSetMappingDuration; 2442 } 2443 2444 @NonNull 2445 public Optional<Duration> getExecutionDuration() { 2446 return Optional.ofNullable(this.executionDuration); 2447 } 2448 2449 @NonNull 2450 public Optional<Duration> getResultSetMappingDuration() { 2451 return Optional.ofNullable(this.resultSetMappingDuration); 2452 } 2453 } 2454 2455 @NotThreadSafe 2456 static class ResultHolder<T> { 2457 T value; 2458 } 2459 2460 enum DatabaseOperationSupportStatus { 2461 UNKNOWN, 2462 YES, 2463 NO 2464 } 2465 2466 private static final class StatementLoggerFailureException extends RuntimeException { 2467 private StatementLoggerFailureException(@NonNull Throwable cause) { 2468 super("Statement logger failed", cause); 2469 } 2470 } 2471}