JdbcUtils.java

/*-
 * ========================LICENSE_START=================================
 * flyway-core
 * ========================================================================
 * Copyright (C) 2010 - 2025 Red Gate Software Ltd
 * ========================================================================
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * =========================LICENSE_END==================================
 */
package org.flywaydb.core.internal.jdbc;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import javax.sql.DataSource;
import lombok.AccessLevel;
import lombok.CustomLog;
import lombok.RequiredArgsConstructor;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.internal.exception.FlywaySqlException;
import org.flywaydb.core.internal.exception.sqlExceptions.FlywaySqlUnableToConnectToDbException;
import org.flywaydb.core.internal.strategy.BackoffStrategy;
import org.flywaydb.core.internal.util.ExceptionUtils;

@CustomLog
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class JdbcUtils {
    /**
     * Opens a new connection from this DataSource.
     *
     * @param dataSource             The DataSource to obtain the connection from.
     * @param connectRetries         The maximum number of retries when attempting to connect to the database.
     * @param connectRetriesInterval The maximum time between retries in seconds.
     * @return The new connection.
     * @throws FlywayException when the connection could not be opened.
     */

    public static Connection openConnection(final DataSource dataSource,
        final int connectRetries,
        final int connectRetriesInterval)
        throws FlywayException {
        final BackoffStrategy backoffStrategy = new BackoffStrategy(1, 2, connectRetriesInterval);

        final Properties systemProperties = System.getProperties();
        if (!systemProperties.containsKey("polyglot.engine.WarnInterpreterOnly")) {
            systemProperties.setProperty("polyglot.engine.WarnInterpreterOnly", "false");
        }

        int retries = 0;
        while (true) {
            try {
                return dataSource.getConnection();
            } catch (final SQLException sqlException) {
                FlywaySqlException.throwFlywayExceptionIfPossible(sqlException, dataSource);

                if (++retries > connectRetries) {
                    throw new FlywaySqlUnableToConnectToDbException(sqlException, dataSource);
                }
                final Throwable rootCause = ExceptionUtils.getRootCause(sqlException);
                String message = "Connection error: " + sqlException.getMessage();
                if (rootCause != null && rootCause != sqlException && rootCause.getMessage() != null) {
                    message += "\n(Caused by " + rootCause.getMessage() + ")";
                }
                LOG.warn(message + "\nRetrying in " + backoffStrategy.peek() + " sec...");
                try {
                    Thread.sleep(backoffStrategy.next() * 1000L);
                } catch (final InterruptedException e1) {
                    throw new FlywaySqlUnableToConnectToDbException(sqlException, dataSource);
                }
            }
        }
    }

    /**
     * Safely closes this Connection. This method never fails.
     */
    public static void closeConnection(final Connection connection) {
        if (connection == null) {
            return;
        }
        try {
            if (!connection.isClosed()) {
                connection.close();
            }
        } catch (final Exception e) {
            LOG.error("Error while closing database Connection: " + e.getMessage(), e);
        }
    }

    /**
     * Safely closes this Statement. This method never fails.
     */
    public static void closeStatement(final Statement statement) {
        if (statement == null) {
            return;
        }
        try {
            statement.close();
        } catch (final SQLException e) {
            LOG.error("Error while closing JDBC Statement", e);
        }
    }

    /**
     * Safely closes this ResultSet. This method never fails.
     */
    public static void closeResultSet(final ResultSet resultSet) {
        if (resultSet == null) {
            return;
        }
        try {
            resultSet.close();
        } catch (final SQLException e) {
            LOG.error("Error while closing JDBC ResultSet", e);
        }
    }

    public static DatabaseMetaData getDatabaseMetaData(final Connection connection) {
        final DatabaseMetaData databaseMetaData;
        try {
            databaseMetaData = connection.getMetaData();
        } catch (final SQLException e) {
            throw new FlywaySqlException("Unable to read database connection metadata: " + e.getMessage(), e);
        }
        if (databaseMetaData == null) {
            throw new FlywayException("Unable to read database connection metadata while it is null!");
        }
        return databaseMetaData;
    }

    /**
     * @return The name of the database product. Example: Oracle, MySQL, ...
     */
    public static String getDatabaseProductName(final DatabaseMetaData databaseMetaData) {
        try {
            final String databaseProductName = databaseMetaData.getDatabaseProductName();
            if (databaseProductName == null) {
                throw new FlywayException("Unable to determine database. Product name is null.");
            }

            return databaseProductName
                + " "
                + databaseMetaData.getDatabaseMajorVersion()
                + "."
                + databaseMetaData.getDatabaseMinorVersion();
        } catch (final SQLException e) {
            throw new FlywaySqlException("Error while determining database product name", e);
        }
    }

    /**
     * @return The version of the database product. Example: MariaDB 10.3, ...
     */
    public static String getDatabaseProductVersion(final DatabaseMetaData databaseMetaData) {
        try {
            return databaseMetaData.getDatabaseProductVersion();
        } catch (final SQLException e) {
            throw new FlywaySqlException("Error while determining database product version", e);
        }
    }

    public static String getDatabaseVersion(final DatabaseMetaData databaseMetaData) {
        try {
            return databaseMetaData.getDatabaseMajorVersion() + "." + databaseMetaData.getDatabaseMinorVersion();
        } catch (final SQLException e) {
            throw new FlywaySqlException("Error while determining database version", e);
        }
    }
}