001/*
002 * Copyright 2015-2022 Transmogrify LLC, 2022-2025 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 javax.annotation.Nonnull;
020import javax.annotation.Nullable;
021import javax.annotation.concurrent.NotThreadSafe;
022import javax.annotation.concurrent.ThreadSafe;
023import javax.sql.DataSource;
024import java.sql.Connection;
025import java.sql.DatabaseMetaData;
026import java.sql.PreparedStatement;
027import java.sql.ResultSet;
028import java.sql.SQLException;
029import java.time.Duration;
030import java.time.ZoneId;
031import java.util.ArrayDeque;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Deque;
035import java.util.List;
036import java.util.Optional;
037import java.util.concurrent.atomic.AtomicInteger;
038import java.util.function.Consumer;
039import java.util.logging.Logger;
040import java.util.stream.Collectors;
041
042import static java.lang.String.format;
043import static java.lang.System.nanoTime;
044import static java.util.Objects.requireNonNull;
045import static java.util.logging.Level.WARNING;
046
047/**
048 * Main class for performing database access operations.
049 *
050 * @author <a href="https://www.revetkn.com">Mark Allen</a>
051 * @since 1.0.0
052 */
053@ThreadSafe
054public final class Database {
055        @Nonnull
056        private static final ThreadLocal<Deque<Transaction>> TRANSACTION_STACK_HOLDER;
057
058        static {
059                TRANSACTION_STACK_HOLDER = ThreadLocal.withInitial(() -> new ArrayDeque<>());
060        }
061
062        @Nonnull
063        private final DataSource dataSource;
064        @Nonnull
065        private final DatabaseType databaseType;
066        @Nonnull
067        private final ZoneId timeZone;
068        @Nonnull
069        private final InstanceProvider instanceProvider;
070        @Nonnull
071        private final PreparedStatementBinder preparedStatementBinder;
072        @Nonnull
073        private final ResultSetMapper resultSetMapper;
074        @Nonnull
075        private final StatementLogger statementLogger;
076        @Nonnull
077        private final AtomicInteger defaultIdGenerator;
078        @Nonnull
079        private final Logger logger;
080
081        @Nonnull
082        private volatile DatabaseOperationSupportStatus executeLargeBatchSupported;
083        @Nonnull
084        private volatile DatabaseOperationSupportStatus executeLargeUpdateSupported;
085
086        protected Database(@Nonnull Builder builder) {
087                requireNonNull(builder);
088
089                this.dataSource = requireNonNull(builder.dataSource);
090                this.databaseType = requireNonNull(builder.databaseType);
091                this.timeZone = builder.timeZone == null ? ZoneId.systemDefault() : builder.timeZone;
092                this.instanceProvider = builder.instanceProvider == null ? new InstanceProvider() {} : builder.instanceProvider;
093                this.preparedStatementBinder = builder.preparedStatementBinder == null ? PreparedStatementBinder.withDefaultConfiguration() : builder.preparedStatementBinder;
094                this.resultSetMapper = builder.resultSetMapper == null ? ResultSetMapper.withDefaultConfiguration() : builder.resultSetMapper;
095                this.statementLogger = builder.statementLogger == null ? (statementLog) -> {} : builder.statementLogger;
096                this.defaultIdGenerator = new AtomicInteger();
097                this.logger = Logger.getLogger(getClass().getName());
098                this.executeLargeBatchSupported = DatabaseOperationSupportStatus.UNKNOWN;
099                this.executeLargeUpdateSupported = DatabaseOperationSupportStatus.UNKNOWN;
100        }
101
102        /**
103         * Provides a {@link Database} builder for the given {@link DataSource}.
104         *
105         * @param dataSource data source used to create the {@link Database} builder
106         * @return a {@link Database} builder
107         */
108        @Nonnull
109        public static Builder withDataSource(@Nonnull DataSource dataSource) {
110                requireNonNull(dataSource);
111                return new Builder(dataSource);
112        }
113
114        /**
115         * Gets a reference to the current transaction, if any.
116         *
117         * @return the current transaction
118         */
119        @Nonnull
120        public Optional<Transaction> currentTransaction() {
121                Deque<Transaction> transactionStack = TRANSACTION_STACK_HOLDER.get();
122                return Optional.ofNullable(transactionStack.isEmpty() ? null : transactionStack.peek());
123        }
124
125        /**
126         * Performs an operation transactionally.
127         * <p>
128         * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}.
129         *
130         * @param transactionalOperation the operation to perform transactionally
131         */
132        public void transaction(@Nonnull TransactionalOperation transactionalOperation) {
133                requireNonNull(transactionalOperation);
134
135                transaction(() -> {
136                        transactionalOperation.perform();
137                        return Optional.empty();
138                });
139        }
140
141        /**
142         * Performs an operation transactionally with the given isolation level.
143         * <p>
144         * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}.
145         *
146         * @param transactionIsolation   the desired database transaction isolation level
147         * @param transactionalOperation the operation to perform transactionally
148         */
149        public void transaction(@Nonnull TransactionIsolation transactionIsolation,
150                                                                                                        @Nonnull TransactionalOperation transactionalOperation) {
151                requireNonNull(transactionIsolation);
152                requireNonNull(transactionalOperation);
153
154                transaction(transactionIsolation, () -> {
155                        transactionalOperation.perform();
156                        return Optional.empty();
157                });
158        }
159
160        /**
161         * Performs an operation transactionally and optionally returns a value.
162         * <p>
163         * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}.
164         *
165         * @param transactionalOperation the operation to perform transactionally
166         * @param <T>                    the type to be returned
167         * @return the result of the transactional operation
168         */
169        @Nonnull
170        public <T> Optional<T> transaction(@Nonnull ReturningTransactionalOperation<T> transactionalOperation) {
171                requireNonNull(transactionalOperation);
172                return transaction(TransactionIsolation.DEFAULT, transactionalOperation);
173        }
174
175        /**
176         * Performs an operation transactionally with the given isolation level, optionally returning a value.
177         * <p>
178         * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}.
179         *
180         * @param transactionIsolation   the desired database transaction isolation level
181         * @param transactionalOperation the operation to perform transactionally
182         * @param <T>                    the type to be returned
183         * @return the result of the transactional operation
184         */
185        @Nonnull
186        public <T> Optional<T> transaction(@Nonnull TransactionIsolation transactionIsolation,
187                                                                                                                                                 @Nonnull ReturningTransactionalOperation<T> transactionalOperation) {
188                requireNonNull(transactionIsolation);
189                requireNonNull(transactionalOperation);
190
191                Transaction transaction = new Transaction(dataSource, transactionIsolation);
192                TRANSACTION_STACK_HOLDER.get().push(transaction);
193                boolean committed = false;
194
195                try {
196                        Optional<T> returnValue = transactionalOperation.perform();
197
198                        // Safeguard in case user code accidentally returns null instead of Optional.empty()
199                        if (returnValue == null)
200                                returnValue = Optional.empty();
201
202                        if (transaction.isRollbackOnly()) {
203                                transaction.rollback();
204                        } else {
205                                transaction.commit();
206                                committed = true;
207                        }
208
209                        return returnValue;
210                } catch (RuntimeException e) {
211                        try {
212                                transaction.rollback();
213                        } catch (Exception rollbackException) {
214                                logger.log(WARNING, "Unable to roll back transaction", rollbackException);
215                        }
216
217                        throw e;
218                } catch (Throwable t) {
219                        try {
220                                transaction.rollback();
221                        } catch (Exception rollbackException) {
222                                logger.log(WARNING, "Unable to roll back transaction", rollbackException);
223                        }
224
225                        throw new RuntimeException(t);
226                } finally {
227                        Deque<Transaction> transactionStack = TRANSACTION_STACK_HOLDER.get();
228
229                        transactionStack.pop();
230
231                        // Ensure txn stack is fully cleaned up
232                        if (transactionStack.isEmpty())
233                                TRANSACTION_STACK_HOLDER.remove();
234
235                        try {
236                                try {
237                                        transaction.restoreTransactionIsolationIfNeeded();
238
239                                        if (transaction.getInitialAutoCommit().isPresent() && transaction.getInitialAutoCommit().get())
240                                                // Autocommit was true initially, so restoring to true now that transaction has completed
241                                                transaction.setAutoCommit(true);
242                                } finally {
243                                        if (transaction.hasConnection())
244                                                closeConnection(transaction.getConnection());
245                                }
246                        } finally {
247                                // Execute any user-supplied post-execution hooks
248                                for (Consumer<TransactionResult> postTransactionOperation : transaction.getPostTransactionOperations())
249                                        postTransactionOperation.accept(committed ? TransactionResult.COMMITTED : TransactionResult.ROLLED_BACK);
250                        }
251                }
252        }
253
254        protected void closeConnection(@Nonnull Connection connection) {
255                requireNonNull(connection);
256
257                try {
258                        connection.close();
259                } catch (SQLException e) {
260                        throw new DatabaseException("Unable to close database connection", e);
261                }
262        }
263
264        /**
265         * Performs an operation in the context of a pre-existing transaction.
266         * <p>
267         * No commit or rollback on the transaction will occur when {@code transactionalOperation} completes.
268         * <p>
269         * However, if an exception bubbles out of {@code transactionalOperation}, the transaction will be marked as rollback-only.
270         *
271         * @param transaction            the transaction in which to participate
272         * @param transactionalOperation the operation that should participate in the transaction
273         */
274        public void participate(@Nonnull Transaction transaction,
275                                                                                                        @Nonnull TransactionalOperation transactionalOperation) {
276                requireNonNull(transaction);
277                requireNonNull(transactionalOperation);
278
279                participate(transaction, () -> {
280                        transactionalOperation.perform();
281                        return Optional.empty();
282                });
283        }
284
285        /**
286         * Performs an operation in the context of a pre-existing transaction, optionall returning a value.
287         * <p>
288         * No commit or rollback on the transaction will occur when {@code transactionalOperation} completes.
289         * <p>
290         * However, if an exception bubbles out of {@code transactionalOperation}, the transaction will be marked as rollback-only.
291         *
292         * @param transaction            the transaction in which to participate
293         * @param transactionalOperation the operation that should participate in the transaction
294         * @param <T>                    the type to be returned
295         * @return the result of the transactional operation
296         */
297        @Nonnull
298        public <T> Optional<T> participate(@Nonnull Transaction transaction,
299                                                                                                                                                 @Nonnull ReturningTransactionalOperation<T> transactionalOperation) {
300                requireNonNull(transaction);
301                requireNonNull(transactionalOperation);
302
303                TRANSACTION_STACK_HOLDER.get().push(transaction);
304
305                try {
306                        Optional<T> returnValue = transactionalOperation.perform();
307                        return returnValue == null ? Optional.empty() : returnValue;
308                } catch (RuntimeException e) {
309                        transaction.setRollbackOnly(true);
310                        throw e;
311                } catch (Throwable t) {
312                        transaction.setRollbackOnly(true);
313                        throw new RuntimeException(t);
314                } finally {
315                        TRANSACTION_STACK_HOLDER.get().pop();
316                }
317        }
318
319        /**
320         * Performs a SQL query that is expected to return 0 or 1 result rows.
321         *
322         * @param sql              the SQL query to execute
323         * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled
324         * @param parameters       {@link PreparedStatement} parameters, if any
325         * @param <T>              the type to be returned
326         * @return a single result (or no result)
327         * @throws DatabaseException if > 1 row is returned
328         */
329        @Nonnull
330        public <T> Optional<T> queryForObject(@Nonnull String sql,
331                                                                                                                                                                @Nonnull Class<T> resultSetRowType,
332                                                                                                                                                                @Nullable Object... parameters) {
333                requireNonNull(sql);
334                requireNonNull(resultSetRowType);
335
336                return queryForObject(Statement.of(generateId(), sql), resultSetRowType, parameters);
337        }
338
339        /**
340         * Performs a SQL query that is expected to return 0 or 1 result rows.
341         *
342         * @param statement        the SQL statement to execute
343         * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled
344         * @param parameters       {@link PreparedStatement} parameters, if any
345         * @param <T>              the type to be returned
346         * @return a single result (or no result)
347         * @throws DatabaseException if > 1 row is returned
348         */
349        public <T> Optional<T> queryForObject(@Nonnull Statement statement,
350                                                                                                                                                                @Nonnull Class<T> resultSetRowType,
351                                                                                                                                                                @Nullable Object... parameters) {
352                requireNonNull(statement);
353                requireNonNull(resultSetRowType);
354
355                List<T> list = queryForList(statement, resultSetRowType, parameters);
356
357                if (list.size() > 1)
358                        throw new DatabaseException(format("Expected 1 row in resultset but got %s instead", list.size()));
359
360                return Optional.ofNullable(list.isEmpty() ? null : list.get(0));
361        }
362
363        /**
364         * Performs a SQL query that is expected to return any number of result rows.
365         *
366         * @param sql              the SQL query to execute
367         * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled
368         * @param parameters       {@link PreparedStatement} parameters, if any
369         * @param <T>              the type to be returned
370         * @return a list of results
371         */
372        @Nonnull
373        public <T> List<T> queryForList(@Nonnull String sql,
374                                                                                                                                        @Nonnull Class<T> resultSetRowType,
375                                                                                                                                        @Nullable Object... parameters) {
376                requireNonNull(sql);
377                requireNonNull(resultSetRowType);
378
379                return queryForList(Statement.of(generateId(), sql), resultSetRowType, parameters);
380        }
381
382        /**
383         * Performs a SQL query that is expected to return any number of result rows.
384         *
385         * @param statement        the SQL statement to execute
386         * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled
387         * @param parameters       {@link PreparedStatement} parameters, if any
388         * @param <T>              the type to be returned
389         * @return a list of results
390         */
391        @Nonnull
392        public <T> List<T> queryForList(@Nonnull Statement statement,
393                                                                                                                                        @Nonnull Class<T> resultSetRowType,
394                                                                                                                                        @Nullable Object... parameters) {
395                requireNonNull(statement);
396                requireNonNull(resultSetRowType);
397
398                List<T> list = new ArrayList<>();
399                StatementContext<T> statementContext = StatementContext.<T>with(statement, this)
400                                .resultSetRowType(resultSetRowType)
401                                .parameters(parameters)
402                                .build();
403
404                List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters);
405
406                performDatabaseOperation(statementContext, parametersAsList, (PreparedStatement preparedStatement) -> {
407                        long startTime = nanoTime();
408
409                        try (ResultSet resultSet = preparedStatement.executeQuery()) {
410                                Duration executionDuration = Duration.ofNanos(nanoTime() - startTime);
411                                startTime = nanoTime();
412
413                                while (resultSet.next()) {
414                                        try {
415                                                T listElement = getResultSetMapper().map(statementContext, resultSet, statementContext.getResultSetRowType().get(), getInstanceProvider()).orElse(null);
416                                                list.add(listElement);
417                                        } catch (SQLException e) {
418                                                throw new DatabaseException(format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), statementContext.getResultSetRowType().get()), e);
419                                        }
420                                }
421
422                                Duration resultSetMappingDuration = Duration.ofNanos(nanoTime() - startTime);
423                                return new DatabaseOperationResult(executionDuration, resultSetMappingDuration);
424                        }
425                });
426
427                return list;
428        }
429
430        /**
431         * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE};
432         * or a SQL statement that returns nothing, such as a DDL statement.
433         *
434         * @param sql        the SQL to execute
435         * @param parameters {@link PreparedStatement} parameters, if any
436         * @return the number of rows affected by the SQL statement
437         */
438        @Nonnull
439        public Long execute(@Nonnull String sql,
440                                                                                        @Nullable Object... parameters) {
441                requireNonNull(sql);
442                return execute(Statement.of(generateId(), sql), parameters);
443        }
444
445        /**
446         * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE};
447         * or a SQL statement that returns nothing, such as a DDL statement.
448         *
449         * @param statement  the SQL statement to execute
450         * @param parameters {@link PreparedStatement} parameters, if any
451         * @return the number of rows affected by the SQL statement
452         */
453        @Nonnull
454        public Long execute(@Nonnull Statement statement,
455                                                                                        @Nullable Object... parameters) {
456                requireNonNull(statement);
457
458                ResultHolder<Long> resultHolder = new ResultHolder<>();
459                StatementContext<Void> statementContext = StatementContext.with(statement, this)
460                                .parameters(parameters)
461                                .build();
462
463                List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters);
464
465                performDatabaseOperation(statementContext, parametersAsList, (PreparedStatement preparedStatement) -> {
466                        long startTime = nanoTime();
467
468                        DatabaseOperationSupportStatus executeLargeUpdateSupported = getExecuteLargeUpdateSupported();
469
470                        // Use the appropriate "large" value if we know it.
471                        // If we don't know it, detect it and store it.
472                        if (executeLargeUpdateSupported == DatabaseOperationSupportStatus.YES) {
473                                resultHolder.value = preparedStatement.executeLargeUpdate();
474                        } else if (executeLargeUpdateSupported == DatabaseOperationSupportStatus.NO) {
475                                resultHolder.value = (long) preparedStatement.executeUpdate();
476                        } else {
477                                // If the driver doesn't support executeLargeUpdate, then UnsupportedOperationException is thrown.
478                                try {
479                                        resultHolder.value = preparedStatement.executeLargeUpdate();
480                                        setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.YES);
481                                } catch (UnsupportedOperationException e) {
482                                        setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.NO);
483                                        resultHolder.value = (long) preparedStatement.executeUpdate();
484                                }
485                        }
486
487                        Duration executionDuration = Duration.ofNanos(nanoTime() - startTime);
488                        return new DatabaseOperationResult(executionDuration, null);
489                });
490
491                return resultHolder.value;
492        }
493
494        /**
495         * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE},
496         * which returns 0 or 1 rows, e.g. with Postgres/Oracle's {@code RETURNING} clause.
497         *
498         * @param sql              the SQL query to execute
499         * @param resultSetRowType the type to which the {@link ResultSet} row should be marshaled
500         * @param parameters       {@link PreparedStatement} parameters, if any
501         * @param <T>              the type to be returned
502         * @return a single result (or no result)
503         * @throws DatabaseException if > 1 row is returned
504         */
505        @Nonnull
506        public <T> Optional<T> executeForObject(@Nonnull String sql,
507                                                                                                                                                                        @Nonnull Class<T> resultSetRowType,
508                                                                                                                                                                        @Nullable Object... parameters) {
509                requireNonNull(sql);
510                requireNonNull(resultSetRowType);
511
512                return executeForObject(Statement.of(generateId(), sql), resultSetRowType, parameters);
513        }
514
515        /**
516         * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE},
517         * which returns 0 or 1 rows, e.g. with Postgres/Oracle's {@code RETURNING} clause.
518         *
519         * @param statement        the SQL statement to execute
520         * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled
521         * @param parameters       {@link PreparedStatement} parameters, if any
522         * @param <T>              the type to be returned
523         * @return a single result (or no result)
524         * @throws DatabaseException if > 1 row is returned
525         */
526        public <T> Optional<T> executeForObject(@Nonnull Statement statement,
527                                                                                                                                                                        @Nonnull Class<T> resultSetRowType,
528                                                                                                                                                                        @Nullable Object... parameters) {
529                requireNonNull(statement);
530                requireNonNull(resultSetRowType);
531
532                // Ultimately we just delegate to queryForObject.
533                // Having `executeForList` is to allow for users to explicitly express intent
534                // and make static analysis of code easier (e.g. maybe you'd like to hook all of your "execute" statements for
535                // logging, or delegation to a writable master as opposed to a read replica)
536                return queryForObject(statement, resultSetRowType, parameters);
537        }
538
539        /**
540         * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE},
541         * which returns any number of rows, e.g. with Postgres/Oracle's {@code RETURNING} clause.
542         *
543         * @param sql              the SQL to execute
544         * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled
545         * @param parameters       {@link PreparedStatement} parameters, if any
546         * @param <T>              the type to be returned
547         * @return a list of results
548         */
549        @Nonnull
550        public <T> List<T> executeForList(@Nonnull String sql,
551                                                                                                                                                @Nonnull Class<T> resultSetRowType,
552                                                                                                                                                @Nullable Object... parameters) {
553                requireNonNull(sql);
554                requireNonNull(resultSetRowType);
555
556                return executeForList(Statement.of(generateId(), sql), resultSetRowType, parameters);
557        }
558
559        /**
560         * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE},
561         * which returns any number of rows, e.g. with Postgres/Oracle's {@code RETURNING} clause.
562         *
563         * @param statement        the SQL statement to execute
564         * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled
565         * @param parameters       {@link PreparedStatement} parameters, if any
566         * @param <T>              the type to be returned
567         * @return a list of results
568         */
569        @Nonnull
570        public <T> List<T> executeForList(@Nonnull Statement statement,
571                                                                                                                                                @Nonnull Class<T> resultSetRowType,
572                                                                                                                                                @Nullable Object... parameters) {
573                requireNonNull(statement);
574                requireNonNull(resultSetRowType);
575
576                // Ultimately we just delegate to queryForList.
577                // Having `executeForList` is to allow for users to explicitly express intent
578                // and make static analysis of code easier (e.g. maybe you'd like to hook all of your "execute" statements for
579                // logging, or delegation to a writable master as opposed to a read replica)
580                return queryForList(statement, resultSetRowType, parameters);
581        }
582
583        /**
584         * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}
585         * in "batch" over a set of parameter groups.
586         * <p>
587         * Useful for bulk-inserting or updating large amounts of data.
588         *
589         * @param sql             the SQL to execute
590         * @param parameterGroups Groups of {@link PreparedStatement} parameters
591         * @return the number of rows affected by the SQL statement per-group
592         */
593        @Nonnull
594        public List<Long> executeBatch(@Nonnull String sql,
595                                                                                                                                 @Nonnull List<List<Object>> parameterGroups) {
596                requireNonNull(sql);
597                requireNonNull(parameterGroups);
598
599                return executeBatch(Statement.of(generateId(), sql), parameterGroups);
600        }
601
602        /**
603         * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}
604         * in "batch" over a set of parameter groups.
605         * <p>
606         * Useful for bulk-inserting or updating large amounts of data.
607         *
608         * @param statement       the SQL statement to execute
609         * @param parameterGroups Groups of {@link PreparedStatement} parameters
610         * @return the number of rows affected by the SQL statement per-group
611         */
612        @Nonnull
613        public List<Long> executeBatch(@Nonnull Statement statement,
614                                                                                                                                 @Nonnull List<List<Object>> parameterGroups) {
615                requireNonNull(statement);
616                requireNonNull(parameterGroups);
617
618                ResultHolder<List<Long>> resultHolder = new ResultHolder<>();
619                StatementContext<List<Long>> statementContext = StatementContext.with(statement, this)
620                                .parameters((List) parameterGroups)
621                                .resultSetRowType(List.class)
622                                .build();
623
624                performDatabaseOperation(statementContext, (preparedStatement) -> {
625                        for (List<Object> parameterGroup : parameterGroups) {
626                                if (parameterGroup != null && parameterGroup.size() > 0)
627                                        performPreparedStatementBinding(statementContext, preparedStatement, parameterGroup);
628
629                                preparedStatement.addBatch();
630                        }
631                }, (PreparedStatement preparedStatement) -> {
632                        long startTime = nanoTime();
633                        List<Long> result;
634
635                        DatabaseOperationSupportStatus executeLargeBatchSupported = getExecuteLargeBatchSupported();
636
637                        // Use the appropriate "large" value if we know it.
638                        // If we don't know it, detect it and store it.
639                        if (executeLargeBatchSupported == DatabaseOperationSupportStatus.YES) {
640                                long[] resultArray = preparedStatement.executeLargeBatch();
641                                result = Arrays.stream(resultArray).boxed().collect(Collectors.toList());
642                        } else if (executeLargeBatchSupported == DatabaseOperationSupportStatus.NO) {
643                                int[] resultArray = preparedStatement.executeBatch();
644                                result = Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList());
645                        } else {
646                                // If the driver doesn't support executeLargeBatch, then UnsupportedOperationException is thrown.
647                                try {
648                                        long[] resultArray = preparedStatement.executeLargeBatch();
649                                        result = Arrays.stream(resultArray).boxed().collect(Collectors.toList());
650                                        setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.YES);
651                                } catch (UnsupportedOperationException e) {
652                                        setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.NO);
653                                        int[] resultArray = preparedStatement.executeBatch();
654                                        result = Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList());
655                                }
656                        }
657
658                        resultHolder.value = result;
659                        Duration executionDuration = Duration.ofNanos(nanoTime() - startTime);
660                        return new DatabaseOperationResult(executionDuration, null);
661                });
662
663                return resultHolder.value;
664        }
665
666        /**
667         * Exposes a temporary handle to JDBC {@link DatabaseMetaData}, which provides comprehensive vendor-specific information about this database as a whole.
668         * <p>
669         * This method acquires {@link DatabaseMetaData} on its own newly-borrowed connection, which it manages internally.
670         * <p>
671         * It does <strong>not</strong> participate in the active transaction, if one exists.
672         * <p>
673         * The connection is closed as soon as {@link DatabaseMetaDataReader#read(DatabaseMetaData)} completes.
674         * <p>
675         * 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.
676         */
677        public void readDatabaseMetaData(@Nonnull DatabaseMetaDataReader databaseMetaDataReader) {
678                requireNonNull(databaseMetaDataReader);
679
680                performRawConnectionOperation((connection -> {
681                        databaseMetaDataReader.read(connection.getMetaData());
682                        return Optional.empty();
683                }), false);
684        }
685
686        protected <T> void performDatabaseOperation(@Nonnull StatementContext<T> statementContext,
687                                                                                                                                                                                        @Nonnull List<Object> parameters,
688                                                                                                                                                                                        @Nonnull DatabaseOperation databaseOperation) {
689                requireNonNull(statementContext);
690                requireNonNull(parameters);
691                requireNonNull(databaseOperation);
692
693                performDatabaseOperation(statementContext, (preparedStatement) -> {
694                        if (parameters.size() > 0)
695                                performPreparedStatementBinding(statementContext, preparedStatement, parameters);
696                }, databaseOperation);
697        }
698
699        protected <T> void performPreparedStatementBinding(@Nonnull StatementContext<T> statementContext,
700                                                                                                                                                                                                                 @Nonnull PreparedStatement preparedStatement,
701                                                                                                                                                                                                                 @Nonnull List<Object> parameters) {
702                requireNonNull(statementContext);
703                requireNonNull(preparedStatement);
704                requireNonNull(parameters);
705
706                try {
707                        for (int i = 0; i < parameters.size(); ++i) {
708                                Object parameter = parameters.get(i);
709
710                                if (parameter != null)
711                                        getPreparedStatementBinder().bindParameter(statementContext, preparedStatement, i + 1, parameter);
712                                else
713                                        preparedStatement.setObject(i + 1, parameter);
714                        }
715                } catch (Exception e) {
716                        throw new DatabaseException(e);
717                }
718        }
719
720        @FunctionalInterface
721        protected interface RawConnectionOperation<R> {
722                @Nonnull
723                Optional<R> perform(@Nonnull Connection connection) throws Exception;
724        }
725
726        /**
727         * @since 3.0.0
728         */
729        @Nonnull
730        public DatabaseType getDatabaseType() {
731                return this.databaseType;
732        }
733
734        /**
735         * @since 3.0.0
736         */
737        @Nonnull
738        public ZoneId getTimeZone() {
739                return this.timeZone;
740        }
741
742        /**
743         * Useful for single-shot "utility" calls that operate outside of normal query operations, e.g. pulling DB metadata.
744         * <p>
745         * Example: {@link #readDatabaseMetaData(DatabaseMetaDataReader)}.
746         */
747        @Nonnull
748        protected <R> Optional<R> performRawConnectionOperation(@Nonnull RawConnectionOperation<R> rawConnectionOperation,
749                                                                                                                                                                                                                                        @Nonnull Boolean shouldParticipateInExistingTransactionIfPossible) {
750                requireNonNull(rawConnectionOperation);
751                requireNonNull(shouldParticipateInExistingTransactionIfPossible);
752
753                if (shouldParticipateInExistingTransactionIfPossible) {
754                        // Try to participate in txn if it's available
755                        Connection connection = null;
756
757                        try {
758                                connection = acquireConnection();
759                                return rawConnectionOperation.perform(connection);
760                        } catch (DatabaseException e) {
761                                throw e;
762                        } catch (Exception e) {
763                                throw new DatabaseException(e);
764                        } finally {
765                                // If this was a single-shot operation (not in a transaction), close the connection
766                                if (connection != null && !currentTransaction().isPresent())
767                                        closeConnection(connection);
768                        }
769                } else {
770                        boolean acquiredConnection = false;
771
772                        // Always get a fresh connection no matter what and close it afterwards
773                        try (Connection connection = getDataSource().getConnection()) {
774                                acquiredConnection = true;
775                                return rawConnectionOperation.perform(connection);
776                        } catch (Exception e) {
777                                if (acquiredConnection)
778                                        throw new DatabaseException(e);
779                                else
780                                        throw new DatabaseException("Unable to acquire database connection", e);
781                        }
782                }
783        }
784
785        protected <T> void performDatabaseOperation(@Nonnull StatementContext<T> statementContext,
786                                                                                                                                                                                        @Nonnull PreparedStatementBindingOperation preparedStatementBindingOperation,
787                                                                                                                                                                                        @Nonnull DatabaseOperation databaseOperation) {
788                requireNonNull(statementContext);
789                requireNonNull(preparedStatementBindingOperation);
790                requireNonNull(databaseOperation);
791
792                long startTime = nanoTime();
793                Duration connectionAcquisitionDuration = null;
794                Duration preparationDuration = null;
795                Duration executionDuration = null;
796                Duration resultSetMappingDuration = null;
797                Exception exception = null;
798                Connection connection = null;
799
800                try {
801                        boolean alreadyHasConnection = currentTransaction().isPresent() && currentTransaction().get().hasConnection();
802                        connection = acquireConnection();
803                        connectionAcquisitionDuration = alreadyHasConnection ? null : Duration.ofNanos(nanoTime() - startTime);
804                        startTime = nanoTime();
805
806                        try (PreparedStatement preparedStatement = connection.prepareStatement(statementContext.getStatement().getSql())) {
807                                preparedStatementBindingOperation.perform(preparedStatement);
808                                preparationDuration = Duration.ofNanos(nanoTime() - startTime);
809
810                                DatabaseOperationResult databaseOperationResult = databaseOperation.perform(preparedStatement);
811                                executionDuration = databaseOperationResult.getExecutionDuration().orElse(null);
812                                resultSetMappingDuration = databaseOperationResult.getResultSetMappingDuration().orElse(null);
813                        }
814                } catch (DatabaseException e) {
815                        exception = e;
816                        throw e;
817                } catch (Exception e) {
818                        exception = e;
819                        throw new DatabaseException(e);
820                } finally {
821                        try {
822                                // If this was a single-shot operation (not in a transaction), close the connection
823                                if (connection != null && !currentTransaction().isPresent())
824                                        closeConnection(connection);
825                        } finally {
826                                StatementLog statementLog =
827                                                StatementLog.withStatementContext(statementContext)
828                                                                .connectionAcquisitionDuration(connectionAcquisitionDuration)
829                                                                .preparationDuration(preparationDuration)
830                                                                .executionDuration(executionDuration)
831                                                                .resultSetMappingDuration(resultSetMappingDuration)
832                                                                .exception(exception)
833                                                                .build();
834
835                                getStatementLogger().log(statementLog);
836                        }
837                }
838        }
839
840        @Nonnull
841        protected Connection acquireConnection() {
842                Optional<Transaction> transaction = currentTransaction();
843
844                if (transaction.isPresent())
845                        return transaction.get().getConnection();
846
847                try {
848                        return getDataSource().getConnection();
849                } catch (SQLException e) {
850                        throw new DatabaseException("Unable to acquire database connection", e);
851                }
852        }
853
854        @Nonnull
855        protected DataSource getDataSource() {
856                return this.dataSource;
857        }
858
859        @Nonnull
860        protected InstanceProvider getInstanceProvider() {
861                return this.instanceProvider;
862        }
863
864        @Nonnull
865        protected PreparedStatementBinder getPreparedStatementBinder() {
866                return this.preparedStatementBinder;
867        }
868
869        @Nonnull
870        protected ResultSetMapper getResultSetMapper() {
871                return this.resultSetMapper;
872        }
873
874        @Nonnull
875        protected StatementLogger getStatementLogger() {
876                return this.statementLogger;
877        }
878
879        @Nonnull
880        protected DatabaseOperationSupportStatus getExecuteLargeBatchSupported() {
881                return this.executeLargeBatchSupported;
882        }
883
884        protected void setExecuteLargeBatchSupported(@Nonnull DatabaseOperationSupportStatus executeLargeBatchSupported) {
885                requireNonNull(executeLargeBatchSupported);
886                this.executeLargeBatchSupported = executeLargeBatchSupported;
887        }
888
889        @Nonnull
890        protected DatabaseOperationSupportStatus getExecuteLargeUpdateSupported() {
891                return this.executeLargeUpdateSupported;
892        }
893
894        protected void setExecuteLargeUpdateSupported(@Nonnull DatabaseOperationSupportStatus executeLargeUpdateSupported) {
895                requireNonNull(executeLargeUpdateSupported);
896                this.executeLargeUpdateSupported = executeLargeUpdateSupported;
897        }
898
899        @Nonnull
900        protected Object generateId() {
901                // "Unique" keys
902                return format("com.pyranid.%s", this.defaultIdGenerator.incrementAndGet());
903        }
904
905        @FunctionalInterface
906        protected interface DatabaseOperation {
907                @Nonnull
908                DatabaseOperationResult perform(@Nonnull PreparedStatement preparedStatement) throws Exception;
909        }
910
911        @FunctionalInterface
912        protected interface PreparedStatementBindingOperation {
913                void perform(@Nonnull PreparedStatement preparedStatement) throws Exception;
914        }
915
916        /**
917         * Builder used to construct instances of {@link Database}.
918         * <p>
919         * This class is intended for use by a single thread.
920         *
921         * @author <a href="https://www.revetkn.com">Mark Allen</a>
922         * @since 1.0.0
923         */
924        @NotThreadSafe
925        public static class Builder {
926                @Nonnull
927                private final DataSource dataSource;
928                @Nonnull
929                private final DatabaseType databaseType;
930                @Nullable
931                private ZoneId timeZone;
932                @Nullable
933                private InstanceProvider instanceProvider;
934                @Nullable
935                private PreparedStatementBinder preparedStatementBinder;
936                @Nullable
937                private ResultSetMapper resultSetMapper;
938                @Nullable
939                private StatementLogger statementLogger;
940
941                private Builder(@Nonnull DataSource dataSource) {
942                        this.dataSource = requireNonNull(dataSource);
943                        this.databaseType = DatabaseType.fromDataSource(dataSource);
944                }
945
946                @Nonnull
947                public Builder timeZone(@Nullable ZoneId timeZone) {
948                        this.timeZone = timeZone;
949                        return this;
950                }
951
952                @Nonnull
953                public Builder instanceProvider(@Nullable InstanceProvider instanceProvider) {
954                        this.instanceProvider = instanceProvider;
955                        return this;
956                }
957
958                @Nonnull
959                public Builder preparedStatementBinder(@Nullable PreparedStatementBinder preparedStatementBinder) {
960                        this.preparedStatementBinder = preparedStatementBinder;
961                        return this;
962                }
963
964                @Nonnull
965                public Builder resultSetMapper(@Nullable ResultSetMapper resultSetMapper) {
966                        this.resultSetMapper = resultSetMapper;
967                        return this;
968                }
969
970                @Nonnull
971                public Builder statementLogger(@Nullable StatementLogger statementLogger) {
972                        this.statementLogger = statementLogger;
973                        return this;
974                }
975
976                @Nonnull
977                public Database build() {
978                        return new Database(this);
979                }
980        }
981
982        @ThreadSafe
983        static class DatabaseOperationResult {
984                @Nullable
985                private final Duration executionDuration;
986                @Nullable
987                private final Duration resultSetMappingDuration;
988
989                public DatabaseOperationResult(@Nullable Duration executionDuration,
990                                                                                                                                         @Nullable Duration resultSetMappingDuration) {
991                        this.executionDuration = executionDuration;
992                        this.resultSetMappingDuration = resultSetMappingDuration;
993                }
994
995                @Nonnull
996                public Optional<Duration> getExecutionDuration() {
997                        return Optional.ofNullable(this.executionDuration);
998                }
999
1000                @Nonnull
1001                public Optional<Duration> getResultSetMappingDuration() {
1002                        return Optional.ofNullable(this.resultSetMappingDuration);
1003                }
1004        }
1005
1006        @NotThreadSafe
1007        static class ResultHolder<T> {
1008                T value;
1009        }
1010
1011        enum DatabaseOperationSupportStatus {
1012                UNKNOWN,
1013                YES,
1014                NO
1015        }
1016}