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