All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.flywaydb.core.internal.database.base.Database Maven / Gradle / Ivy

There is a newer version: 10.15.2
Show newest version
/*
 * Copyright (C) Red Gate Software Ltd 2010-2024
 *
 * 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.
 */
package org.flywaydb.core.internal.database.base;

import lombok.CustomLog;
import lombok.Getter;
import org.flywaydb.core.api.CoreMigrationType;
import org.flywaydb.core.api.MigrationVersion;
import org.flywaydb.core.api.configuration.Configuration;
import org.flywaydb.core.extensibility.LicenseGuard;
import org.flywaydb.core.extensibility.Tier;
import org.flywaydb.core.internal.database.DatabaseType;
import org.flywaydb.core.internal.exception.FlywayDbUpgradeRequiredException;
import org.flywaydb.core.internal.exception.FlywaySqlException;
import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory;
import org.flywaydb.core.internal.jdbc.JdbcTemplate;
import org.flywaydb.core.internal.jdbc.JdbcUtils;
import org.flywaydb.core.internal.jdbc.StatementInterceptor;
import org.flywaydb.core.internal.resource.StringResource;
import org.flywaydb.core.internal.sqlscript.Delimiter;
import org.flywaydb.core.internal.sqlscript.SqlScript;
import org.flywaydb.core.internal.sqlscript.SqlScriptFactory;
import org.flywaydb.core.internal.util.AbbreviationUtils;
import org.flywaydb.core.internal.util.Pair;
import org.flywaydb.core.internal.util.StringUtils;

import java.io.Closeable;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.List;

import static org.flywaydb.core.internal.util.FlywayDbWebsiteLinks.COMMUNITY_SUPPORT;

/**
 * Abstraction for database-specific functionality.
 */
@CustomLog
public abstract class Database implements Closeable {
    protected final DatabaseType databaseType;
    @Getter
    protected final Configuration configuration;
    protected final StatementInterceptor statementInterceptor;
    protected final JdbcConnectionFactory jdbcConnectionFactory;
    protected final DatabaseMetaData jdbcMetaData;
    /**
     * The main JDBC connection, without any wrapping.
     */
    protected final java.sql.Connection rawMainJdbcConnection;
    protected JdbcTemplate jdbcTemplate;
    private C migrationConnection;
    private C eventConnection;
    private C mainConnection;
    /**
     * The 'major.minor' version of this database.
     */
    private MigrationVersion version;
    /**
     * The user who applied the migrations.
     */
    private String installedBy;

    public Database(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory, StatementInterceptor statementInterceptor) {
        this.databaseType = jdbcConnectionFactory.getDatabaseType();
        this.configuration = configuration;
        this.rawMainJdbcConnection = jdbcConnectionFactory.openConnection();
        try {
            this.jdbcMetaData = rawMainJdbcConnection.getMetaData();
        } catch (SQLException e) {
            throw new FlywaySqlException("Unable to get metadata for connection", e);
        }
        this.jdbcTemplate = new JdbcTemplate(rawMainJdbcConnection, databaseType);
        this.jdbcConnectionFactory = jdbcConnectionFactory;
        this.statementInterceptor = statementInterceptor;
    }

    /**
     * Retrieves a Flyway Connection for this JDBC connection.
     */
    private C getConnection(java.sql.Connection connection) {
        return doGetConnection(connection);
    }

    /**
     * Retrieves a Flyway Connection for this JDBC connection.
     */
    protected abstract C doGetConnection(java.sql.Connection connection);

    /**
     * Ensure Flyway supports this version of this database.
     */
    public abstract void ensureSupported(Configuration configuration);

    /**
     * @return The 'major.minor' version of this database.
     */
    public final MigrationVersion getVersion() {
        if (version == null) {
            version = determineVersion();
        }
        return version;
    }

    protected final void ensureDatabaseIsRecentEnough(String oldestSupportedVersion) {
        if (!getVersion().isAtLeast(oldestSupportedVersion)) {
            throw new FlywayDbUpgradeRequiredException(
                    databaseType,
                    computeVersionDisplayName(getVersion()),
                    computeVersionDisplayName(MigrationVersion.fromVersion(oldestSupportedVersion)));
        }
    }

    /**
     * Ensure this database it at least as recent as this version otherwise suggest upgrade to this higher edition of
     * Flyway.
     */
    protected final void ensureDatabaseNotOlderThanOtherwiseRecommendUpgradeToFlywayEdition(String oldestSupportedVersionInThisEdition,
                                                                                            List editionWhereStillSupported, Configuration configuration) {
        if (!LicenseGuard.isLicensed(configuration, editionWhereStillSupported) &&
                !getVersion().isAtLeast(oldestSupportedVersionInThisEdition)) {
            LOG.info(getDatabaseType().getName() + " " + computeVersionDisplayName(getVersion()) + " is outside of Redgate community support. See " + COMMUNITY_SUPPORT + " for details");
        }

    }

    protected final void recommendFlywayUpgradeIfNecessary(String newestSupportedVersion) {
        if (getVersion().isNewerThan(newestSupportedVersion)) {
            recommendFlywayUpgrade(newestSupportedVersion);
        }
    }

    protected final void recommendFlywayUpgradeIfNecessaryForMajorVersion(String newestSupportedVersion) {
        if (getVersion().isMajorNewerThan(newestSupportedVersion)) {
            recommendFlywayUpgrade(newestSupportedVersion);
        }
    }

    protected final void notifyDatabaseIsNotFormallySupported() {
        LOG.warn("Support for " + databaseType + " is provided only on a community-led basis, and is not formally supported by Redgate");
    }

    private void recommendFlywayUpgrade(String newestSupportedVersion) {
        String message = "Flyway upgrade recommended: " + databaseType + " " + computeVersionDisplayName(getVersion())
                + " is newer than this version of Flyway and support has not been tested."
                + " The latest supported version of " + databaseType + " is " + newestSupportedVersion + ".";
        LOG.warn(message);
    }

    /**
     * Compute the user-friendly display name for this database version.
     */
    protected String computeVersionDisplayName(MigrationVersion version) {
        return version.getVersion();
    }

    public Delimiter getDefaultDelimiter() {
        return Delimiter.SEMICOLON;
    }

    /**
     * @return The name of the database, by default as determined by JDBC.
     */
    public final String getCatalog() {
        try {
            return doGetCatalog();
        } catch (SQLException e) {
            throw new FlywaySqlException("Error retrieving the database name", e);
        }
    }

    protected String doGetCatalog() throws SQLException {
        return getMainConnection().getJdbcConnection().getCatalog();
    }

    public final String getCurrentUser() {
        try {
            return doGetCurrentUser();
        } catch (SQLException e) {
            throw new FlywaySqlException("Error retrieving the database user", e);
        }
    }

    protected String doGetCurrentUser() throws SQLException {
        return jdbcMetaData.getUserName();
    }

    public abstract boolean supportsDdlTransactions();

    /**
     * @return The representation of the value {@code true} in a boolean column.
     */
    public abstract String getBooleanTrue();

    /**
     * @return The representation of the value {@code false} in a boolean column.
     */
    public abstract String getBooleanFalse();

    /**
     * Quotes these identifiers for use in SQL queries. Multiple identifiers will be quoted and separated by a dot.
     */
    public final String quote(String... identifiers) {
        StringBuilder result = new StringBuilder();

        boolean first = true;
        for (String identifier : identifiers) {
            if (!first) {
                result.append(".");
            }
            first = false;
            result.append(doQuote(identifier));
        }

        return result.toString();
    }

    /**
     * Quotes this identifier for use in SQL queries.
     */
    public String doQuote(String identifier) {
        return getOpenQuote() + identifier + getCloseQuote();
    }

    protected String getOpenQuote() {
        return "\"";
    }

    protected String getCloseQuote() {
        return "\"";
    }

    protected String getEscapedQuote() {
        return "";
    }

    public String unQuote(String identifier) {
        String open = getOpenQuote();
        String close = getCloseQuote();

        if (!open.equals("") && !close.equals("") && identifier.startsWith(open) && identifier.endsWith(close)) {
            identifier = identifier.substring(open.length(), identifier.length() - close.length());
            if (!getEscapedQuote().equals("")) {
                identifier = StringUtils.replaceAll(identifier, getEscapedQuote(), close);
            }
        }

        return identifier;
    }

    /**
     * @return {@code true} if this database uses a catalog to represent a schema, or {@code false} if a schema is
     * simply a schema.
     */
    public abstract boolean catalogIsSchema();

    /**
     * @return Whether to use a single connection for both schema history table management and applying migrations.
     */
    public boolean useSingleConnection() {
        return false;
    }

    public DatabaseMetaData getJdbcMetaData() {
        return jdbcMetaData;
    }

    /**
     * @return The main connection used to manipulate the schema history.
     */
    public final C getMainConnection() {
        if (mainConnection == null) {
            this.mainConnection = getConnection(rawMainJdbcConnection);
        }
        return mainConnection;
    }

    /**
     * @return The migration connection used to apply migrations.
     */
    public final C getMigrationConnection() {
        if (migrationConnection == null) {
            if (useSingleConnection()) {
                this.migrationConnection = getMainConnection();
            } else {
                this.migrationConnection = getConnection(jdbcConnectionFactory.openConnection());
            }
        }
        return migrationConnection;
    }

    /**
     * @return The event connection used to handle event callbacks.
     * The reason for creating an event connection is that if using the migration connection instead, it may trigger an unwanted commit which breaks
     * any ongoing migration transaction.
     */
    public final C getEventConnection() {
        if (!hasEventConnection()) {
            eventConnection = getConnection(jdbcConnectionFactory.openConnection());
        }
        return eventConnection;
    }

    public final boolean hasEventConnection(){
        return eventConnection != null;
    }

    /**
     * An event connection should be disposed after usage to minimize long-standing connections.
     */
    public void disposeEventConnection() {
        if (hasEventConnection()) {
            eventConnection.close();
            eventConnection = null;
        }
    }

    /**
     * @return The major and minor version of the database.
     */
    protected MigrationVersion determineVersion() {
        try {
            return MigrationVersion.fromVersion(jdbcMetaData.getDatabaseMajorVersion() + "." + jdbcMetaData.getDatabaseMinorVersion());
        } catch (SQLException e) {
            throw new FlywaySqlException("Unable to determine the major version of the database", e);
        }
    }

    /**
     * Retrieves the script used to create the schema history table.
     *
     * @param sqlScriptFactory The factory used to create the SQL script.
     * @param table The table to create.
     * @param baseline Whether to include the creation of a baseline marker.
     */
    public final SqlScript getCreateScript(SqlScriptFactory sqlScriptFactory, Table table, boolean baseline) {
        return sqlScriptFactory.createSqlScript(new StringResource(getRawCreateScript(table, baseline)), false, null);
    }

    public abstract String getRawCreateScript(Table table, boolean baseline);

    public String getInsertStatement(Table table) {
        return "INSERT INTO " + table
                + " (" + quote("installed_rank")
                + ", " + quote("version")
                + ", " + quote("description")
                + ", " + quote("type")
                + ", " + quote("script")
                + ", " + quote("checksum")
                + ", " + quote("installed_by")
                + ", " + quote("execution_time")
                + ", " + quote("success")
                + ")"
                + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
    }

    public String getUpdateStatement(Table table) {
        return "UPDATE " + table
                + " SET "
                + quote("description") + "=? , "
                + quote("type") + "=? , "
                + quote("checksum") + "=?"
                + " WHERE " + quote("installed_rank") + "=?";
    }

    protected String getBaselineStatement(Table table) {
        return String.format(getInsertStatement(table).replace("?", "%s"),
                             1,
                             "'" + configuration.getBaselineVersion() + "'",
                             "'" + AbbreviationUtils.abbreviateDescription(configuration.getBaselineDescription()) + "'",
                             "'" + CoreMigrationType.BASELINE + "'",
                             "'" + AbbreviationUtils.abbreviateScript(configuration.getBaselineDescription()) + "'",
                             "NULL",
                             "'" + getInstalledBy() + "'",
                             0,
                             getBooleanTrue()
                            );
    }

    public String getSelectStatement(Table table) {
        return "SELECT " + quote("installed_rank")
                + "," + quote("version")
                + "," + quote("description")
                + "," + quote("type")
                + "," + quote("script")
                + "," + quote("checksum")
                + "," + quote("installed_on")
                + "," + quote("installed_by")
                + "," + quote("execution_time")
                + "," + quote("success")
                + " FROM " + table
                + " WHERE " + quote("installed_rank") + " > ?"
                + " ORDER BY " + quote("installed_rank");
    }

    public Pair getDeleteStatement(Table table, boolean version, String filter) {
        String deleteStatement = "DELETE FROM " + table +
            " WHERE " + quote("success") + " = " + getBooleanFalse() + " AND " +
            (version ?
                quote("version") + " = ?" :
                quote("description") + " = ?");

        return Pair.of(deleteStatement, filter);
    }

    public final String getInstalledBy() {
        if (installedBy == null) {
            installedBy = configuration.getInstalledBy() == null ? getCurrentUser() : configuration.getInstalledBy();
        }
        return installedBy;
    }

    public void close() {
        if (!useSingleConnection() && migrationConnection != null) {
            migrationConnection.close();
        }
        if (mainConnection != null) {
            mainConnection.close();
        }

        disposeEventConnection();

        if (rawMainJdbcConnection != null) {
            JdbcUtils.closeConnection(rawMainJdbcConnection);
        }
    }

    public DatabaseType getDatabaseType() {
        return databaseType;
    }

    public boolean supportsEmptyMigrationDescription() {
        return true;
    }

    public boolean supportsMultiStatementTransactions() {
        return true;
    }

    /**
     * Cleans all the objects in this database that need to be cleaned before each schema.
     */
    public void cleanPreSchemas() {
        try {
            doCleanPreSchemas();
        } catch (SQLException e) {
            throw new FlywaySqlException("Unable to clean database " + this, e);
        }
    }

    /**
     * Cleans all the objects in this database that need to be cleaned before each schema.
     *
     * @throws SQLException when the clean failed.
     */
    protected void doCleanPreSchemas() throws SQLException {}

    /**
     * Cleans all the objects in this database that need to be cleaned after each schema.
     *
     * @param schemas The list of schemas managed by Flyway.
     */
    public void cleanPostSchemas(Schema[] schemas) {
        try {
            doCleanPostSchemas(schemas);
        } catch (SQLException e) {
            throw new FlywaySqlException("Unable to clean schema " + this, e);
        }
    }

    /**
     * Cleans all the objects in this database that need to be cleaned after each schema.
     *
     * @param schemas The list of schemas managed by Flyway.
     * @throws SQLException when the clean failed.
     */
    protected void doCleanPostSchemas(Schema[] schemas) throws SQLException {}

    public Schema[] getAllSchemas() {
        throw new UnsupportedOperationException("Getting all schemas not supported for " + getDatabaseType().getName());
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy