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