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