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