Flyway.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;

import static org.flywaydb.core.internal.nc.NativeConnectorsModeUtils.canUseNativeConnectors;
import static org.flywaydb.core.internal.logging.PreviewFeatureWarning.NATIVE_CONNECTORS;
import static org.flywaydb.core.internal.logging.PreviewFeatureWarning.logPreviewFeature;

import lombok.CustomLog;
import lombok.Setter;
import lombok.SneakyThrows;
import org.flywaydb.core.api.CoreErrorCode;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.MigrationInfoService;
import org.flywaydb.core.api.callback.Callback;
import org.flywaydb.core.api.callback.Event;
import org.flywaydb.core.api.configuration.ClassicConfiguration;
import org.flywaydb.core.api.configuration.Configuration;
import org.flywaydb.core.api.configuration.FluentConfiguration;
import org.flywaydb.core.api.exception.FlywayValidateException;
import org.flywaydb.core.api.logging.LogFactory;
import org.flywaydb.core.api.output.*;
import org.flywaydb.core.api.pattern.ValidatePattern;
import org.flywaydb.core.extensibility.ConfigurationExtension;
import org.flywaydb.core.extensibility.EventTelemetryModel;
import org.flywaydb.core.extensibility.LicenseGuard;
import org.flywaydb.core.extensibility.Tier;
import org.flywaydb.core.extensibility.VerbExtension;
import org.flywaydb.core.internal.callback.CallbackExecutor;
import org.flywaydb.core.internal.command.*;
import org.flywaydb.core.internal.command.clean.DbClean;
import org.flywaydb.core.internal.database.base.Database;
import org.flywaydb.core.internal.database.base.Schema;
import org.flywaydb.core.internal.resolver.CompositeMigrationResolver;
import org.flywaydb.core.internal.schemahistory.SchemaHistory;
import org.flywaydb.core.internal.util.CommandExtensionUtils;
import org.flywaydb.core.internal.util.FlywayDbWebsiteLinks;
import org.flywaydb.core.internal.util.StringUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;









/**
 * This is the centre point of Flyway, and for most users, the only class they will ever have to deal with.
 *
 * It is THE public API from which all important Flyway functions such as clean, validate and migrate can be called.
 *
 * To get started all you need to do is create a configured Flyway object and then invoke its principal methods.
 * <pre>
 * Flyway flyway = Flyway.configure().dataSource(url, user, password).load();
 * flyway.migrate();
 * </pre>
 * Note that a configured Flyway object is immutable. If you change the configuration you will end up creating a new Flyway object.
 */
@CustomLog
public class Flyway {
    private final ClassicConfiguration configuration;
    private final FlywayExecutor flywayExecutor;

    @Deprecated
    @Setter
    private FlywayTelemetryManager flywayTelemetryManager;

    /**
     * This is your starting point. This creates a configuration which can be customized to your needs before being
     * loaded into a new Flyway instance using the load() method.
     *
     * In its simplest form, this is how you configure Flyway with all defaults to get started:
     * <pre>Flyway flyway = Flyway.configure().dataSource(url, user, password).load();</pre>
     *
     * After that you have a fully-configured Flyway instance at your disposal which can be used to invoke Flyway
     * functionality such as migrate() or clean().
     *
     * @return A new configuration from which Flyway can be loaded.
     */
    public static FluentConfiguration configure() {
        return new FluentConfiguration();
    }

    /**
     * This is your starting point. This creates a configuration which can be customized to your needs before being
     * loaded into a new Flyway instance using the load() method.
     *
     * In its simplest form, this is how you configure Flyway with all defaults to get started:
     * <pre>Flyway flyway = Flyway.configure().dataSource(url, user, password).load();</pre>
     *
     * After that you have a fully-configured Flyway instance at your disposal which can be used to invoke Flyway
     * functionality such as migrate() or clean().
     *
     * @param classLoader The class loader to use when loading classes and resources.
     *
     * @return A new configuration from which Flyway can be loaded.
     */
    public static FluentConfiguration configure(ClassLoader classLoader) {
        return new FluentConfiguration(classLoader);
    }

    /**
     * Creates a new instance of Flyway with this configuration. In general the Flyway.configure() factory method should
     * be preferred over this constructor, unless you need to create or reuse separate Configuration objects.
     *
     * @param configuration The configuration to use.
     */
    public Flyway(Configuration configuration) {
        this.configuration = new ClassicConfiguration(configuration);
        List<Callback> callbacks = this.configuration.loadCallbackLocation("db/callback", false);
        if (!callbacks.isEmpty()) {
            this.configuration.setCallbacks(callbacks.toArray(new Callback[0]));
        }
        this.flywayExecutor = new FlywayExecutor(this.configuration);

        LogFactory.setConfiguration(this.configuration);

        if (LicenseGuard.isLicensed(this.configuration, List.of(Tier.ENTERPRISE))) {
            FlywayDbWebsiteLinks.FEEDBACK_SURVEY_LINK = FlywayDbWebsiteLinks.FEEDBACK_SURVEY_LINK_ENTERPRISE;
        } else {
            FlywayDbWebsiteLinks.FEEDBACK_SURVEY_LINK = FlywayDbWebsiteLinks.FEEDBACK_SURVEY_LINK_COMMUNITY;
        }
    }

    /**
     * @return The configuration that Flyway is using.
     */
    public Configuration getConfiguration() {
        return configuration;
    }

    /**
     * @return The configuration extension type requested from the plugin register.
     */
    public <T extends ConfigurationExtension> T getConfigurationExtension(Class<T> configClass) {
        return getConfiguration().getPluginRegister().getPlugin(configClass);
    }

    /**
     * Starts the database migration. All pending migrations will be applied in order.
     * Calling migrate on an up-to-date database has no effect.
     * <img src="https://flyway.github.io/flyway/assets/command-migrate.png" alt="migrate">
     *
     * @return An object summarising the successfully applied migrations.
     *
     * @throws FlywayException when the migration failed.
     */
    @SneakyThrows
    public MigrateResult migrate() throws FlywayException {
        try (MigrateTelemetryModel telemetryModel = new MigrateTelemetryModel(flywayTelemetryManager)) {
            if (canUseNativeConnectors(configuration, "migrate")) {
                logPreviewFeature(NATIVE_CONNECTORS);
                final var verb = configuration.getPluginRegister().getPlugins(VerbExtension.class).stream().filter(verbExtension -> verbExtension.handlesVerb("migrate")).findFirst();
                if (verb.isPresent()) {
                    LOG.debug("Native Connectors for migrate is set and a verb is present");
                    final var result = (MigrateResult) verb.get().executeVerb(configuration);
                    telemetryModel.setFromMigrateResult(result);
                    return result;
                } else {
                    LOG.warn("Native Connectors for migrate is set but no verb is present");
                }
            }

            try {
                return flywayExecutor.execute((migrationResolver, schemaHistory, database, defaultSchema, schemas, callbackExecutor, statementInterceptor) -> {







                    if (configuration.isValidateOnMigrate()) {
                        List<ValidatePattern> ignorePatterns = new ArrayList<>(Arrays.asList(configuration.getIgnoreMigrationPatterns()));
                        ignorePatterns.add(ValidatePattern.fromPattern("*:pending"));
                        ValidateResult validateResult = doValidate(database, migrationResolver, schemaHistory, defaultSchema, schemas, callbackExecutor, ignorePatterns.toArray(new ValidatePattern[0]));
                        if (!validateResult.validationSuccessful) {
                            throw new FlywayValidateException(validateResult.errorDetails, validateResult.getAllErrorMessages());
                        }
                    }

                    if (configuration.isCreateSchemas()) {
                        new DbSchemas(database, schemas, schemaHistory, callbackExecutor).create(false);
                    } else if (!defaultSchema.exists()) {
                        LOG.warn("The configuration option 'createSchemas' is false.\n" +
                                         "However, the schema history table still needs a schema to reside in.\n" +
                                         "You must manually create a schema for the schema history table to reside in.\n" +
                                         "See " + FlywayDbWebsiteLinks.MIGRATIONS);
                    }

                    if (!schemaHistory.exists()) {
                        List<Schema> nonEmptySchemas = new ArrayList<>();
                        for (Schema schema : schemas) {
                            if (schema.exists() && !schema.empty()) {
                                nonEmptySchemas.add(schema);
                            }
                        }

                        if (nonEmptySchemas.isEmpty() && configuration.isBaselineOnMigrate()) {
                            LOG.info("All configured schemas are empty; baseline operation skipped. "
                                + "A baseline or migration script with a lower version than the baseline version may execute if available. Check the Schemas parameter if this is not intended.");
                        }

                        if (!nonEmptySchemas.isEmpty() && !configuration.isSkipExecutingMigrations()) {
                            if (configuration.isBaselineOnMigrate()) {
                                doBaseline(schemaHistory, callbackExecutor, database);



                            } else {
                                // Second check for MySQL which is sometimes flaky otherwise
                                if (!schemaHistory.exists()) {
                                    throw new FlywayException("Found non-empty schema(s) "
                                                                      + StringUtils.collectionToCommaDelimitedString(nonEmptySchemas)
                                                                      + " but no schema history table. Use baseline()"
                                                                      + " or set baselineOnMigrate to true to initialize the schema history table.", CoreErrorCode.NON_EMPTY_SCHEMA_WITHOUT_SCHEMA_HISTORY_TABLE);
                                }
                            }
                        }

                        schemaHistory.create(false);
                    }

                    MigrateResult result = new DbMigrate(database, schemaHistory, defaultSchema, migrationResolver, configuration, callbackExecutor).migrate();
                    telemetryModel.setFromMigrateResult(result);
                    callbackExecutor.onOperationFinishEvent(Event.AFTER_MIGRATE_OPERATION_FINISH, result);

                    return result;
                }, true, flywayTelemetryManager);
            } catch (Exception e) {
                telemetryModel.setException(e);
                throw e;
            }
        }
    }

    /**
     * Retrieves the complete information about all the migrations including applied, pending and current migrations with
     * details and status.
     * <img src="https://flyway.github.io/flyway/assets/command-info.png" alt="info">
     *
     * @return All migrations sorted by version, oldest first.
     *
     * @throws FlywayException when the info retrieval failed.
     */
    public MigrationInfoService info() {
        if (canUseNativeConnectors(configuration, "info")) {
            logPreviewFeature(NATIVE_CONNECTORS);
            final var verb = configuration.getPluginRegister().getPlugins(VerbExtension.class).stream().filter(verbExtension -> verbExtension.handlesVerb("info")).findFirst();
            if (verb.isPresent()) {
                LOG.debug("Native Connectors for info is set and a verb is present");
                return (MigrationInfoService) verb.get().executeVerb(configuration);
            } else {
                LOG.warn("Native Connectors for info is set but no verb is present");
            }
        }
        return flywayExecutor.execute((migrationResolver, schemaHistory, database, defaultSchema, schemas, callbackExecutor, statementInterceptor) -> {
            MigrationInfoService migrationInfoService = new DbInfo(migrationResolver, schemaHistory, configuration, database, callbackExecutor, schemas).info();

            callbackExecutor.onOperationFinishEvent(Event.AFTER_INFO_OPERATION_FINISH, migrationInfoService.getInfoResult());

            return migrationInfoService;
        }, true, flywayTelemetryManager);
    }

    /**
     * Drops all objects (tables, views, procedures, triggers, ...) in the configured schemas.
     * The schemas are cleaned in the order specified by the {@code schemas} property.
     * <img src="https://flyway.github.io/flyway/assets/command-clean.png" alt="clean">
     *
     * @return An object summarising the actions taken
     *
     * @throws FlywayException when the clean fails.
     */
    @SneakyThrows
    public CleanResult clean() {
        try (EventTelemetryModel telemetryModel = new EventTelemetryModel("clean", flywayTelemetryManager)) {
            if (canUseNativeConnectors(configuration, "clean")) {
                logPreviewFeature(NATIVE_CONNECTORS);
                final var verb = configuration.getPluginRegister().getPlugins(VerbExtension.class).stream().filter(verbExtension -> verbExtension.handlesVerb("clean")).findFirst();
                if (verb.isPresent()) {
                    LOG.debug("Native Connectors for clean is set and a verb is present");
                    return (CleanResult) verb.get().executeVerb(configuration);
                } else {
                    LOG.warn("Native Connectors for clean is set but no verb is present");
                }
            }

            try {
                return flywayExecutor.execute((migrationResolver, schemaHistory, database, defaultSchema, schemas, callbackExecutor, statementInterceptor) -> {
                    CleanResult cleanResult = doClean(database, schemaHistory, defaultSchema, schemas, callbackExecutor);





                    callbackExecutor.onOperationFinishEvent(Event.AFTER_CLEAN_OPERATION_FINISH, cleanResult);

                    return cleanResult;
                }, false, flywayTelemetryManager);
            } catch (Exception e) {
                telemetryModel.setException(e);
                throw e;
            }
        }
    }

    /**
     * Validate applied migrations against resolved ones (on the filesystem or classpath)
     * to detect accidental changes that may prevent the schema(s) from being recreated exactly.
     * Validation fails if:
     * <ul>
     * <li>differences in migration names, types or checksums are found</li>
     * <li>versions have been applied that aren't resolved locally anymore</li>
     * <li>versions have been resolved that haven't been applied yet</li>
     * </ul>
     *
     * <img src="https://flyway.github.io/flyway/assets/command-validate.png" alt="validate">
     *
     * @throws FlywayException when something went wrong during validation.
     * @throws FlywayValidateException when the validation failed.
     */
    public void validate() throws FlywayException {
        final ValidateResult validateResult = validateWithResult();
        if (!validateResult.validationSuccessful) {
            throw new FlywayValidateException(validateResult.errorDetails, validateResult.getAllErrorMessages());
        }
    }

    /**
     * Validate applied migrations against resolved ones (on the filesystem or classpath)
     * to detect accidental changes that may prevent the schema(s) from being recreated exactly.
     * Validation fails if:
     * <ul>
     * <li>differences in migration names, types or checksums are found</li>
     * <li>versions have been applied that aren't resolved locally anymore</li>
     * <li>versions have been resolved that haven't been applied yet</li>
     * </ul>
     *
     * <img src="https://flyway.github.io/flyway/assets/command-validate.png" alt="validate">
     *
     * @return An object summarising the validation results
     *
     * @throws FlywayException when something went wrong during validation.
     */
    public ValidateResult validateWithResult() throws FlywayException {
        if (canUseNativeConnectors(configuration, "validate")) {
            logPreviewFeature(NATIVE_CONNECTORS);
            final var verb = configuration.getPluginRegister().getPlugins(VerbExtension.class).stream().filter(verbExtension -> verbExtension.handlesVerb("validate")).findFirst();
            if (verb.isPresent()) {
                LOG.debug("Native Connectors for validate is set and a verb is present");
                return (ValidateResult) verb.get().executeVerb(configuration);
            } else {
                LOG.warn("Native Connectors for validate is set but no verb is present");
            }
        }
        return flywayExecutor.execute((migrationResolver, schemaHistory, database, defaultSchema, schemas, callbackExecutor, statementInterceptor) -> {
            ValidateResult validateResult = doValidate(database, migrationResolver, schemaHistory, defaultSchema, schemas, callbackExecutor, configuration.getIgnoreMigrationPatterns());

            callbackExecutor.onOperationFinishEvent(Event.AFTER_VALIDATE_OPERATION_FINISH, validateResult);

            return validateResult;
        }, true, flywayTelemetryManager);
    }

    /**
     * Baselines an existing database, excluding all migrations up to and including baselineVersion.
     *
     * <img src="https://flyway.github.io/flyway/assets/command-baseline.png" alt="baseline">
     *
     * @return An object summarising the actions taken
     *
     * @throws FlywayException when the schema baseline failed.
     */
    @SneakyThrows
    public BaselineResult baseline() throws FlywayException {
        try (EventTelemetryModel telemetryModel = new EventTelemetryModel("baseline", flywayTelemetryManager)) {
            if (canUseNativeConnectors(configuration, "baseline")) {
                logPreviewFeature(NATIVE_CONNECTORS);
                final var verb = configuration.getPluginRegister().getPlugins(VerbExtension.class).stream().filter(verbExtension -> verbExtension.handlesVerb("baseline")).findFirst();
                if (verb.isPresent()) {
                    LOG.debug("Native Connectors for baseline is set and a verb is present");
                    return (BaselineResult) verb.get().executeVerb(configuration);
                } else {
                    LOG.warn("Native Connectors for baseline is set but no verb is present");
                }
            }

            try {
                return flywayExecutor.execute((migrationResolver, schemaHistory, database, defaultSchema, schemas, callbackExecutor, statementInterceptor) -> {
                    if (configuration.isCreateSchemas()) {
                        new DbSchemas(database, schemas, schemaHistory, callbackExecutor).create(true);
                    } else {
                        LOG.warn("The configuration option 'createSchemas' is false.\n" +
                                         "Even though Flyway is configured not to create any schemas, the schema history table still needs a schema to reside in.\n" +
                                         "You must manually create a schema for the schema history table to reside in.\n" +
                                         "See " + FlywayDbWebsiteLinks.MIGRATIONS);
                    }

                    BaselineResult baselineResult = doBaseline(schemaHistory, callbackExecutor, database);





                    callbackExecutor.onOperationFinishEvent(Event.AFTER_BASELINE_OPERATION_FINISH, baselineResult);

                    return baselineResult;
                }, false, flywayTelemetryManager);
            } catch (Exception e) {
                telemetryModel.setException(e);
                throw e;
            }
        }
    }

    /**
     * Repairs the Flyway schema history table. This will perform the following actions:
     * <ul>
     * <li>Remove any failed migrations on databases without DDL transactions (User objects left behind must still be cleaned up manually)</li>
     * <li>Realign the checksums, descriptions and types of the applied migrations with the ones of the available migrations</li>
     * </ul>
     * <img src="https://flyway.github.io/flyway/assets/command-repair.png" alt="repair">
     *
     * @return An object summarising the actions taken
     *
     * @throws FlywayException when the schema history table repair failed.
     */
    @SneakyThrows
    public RepairResult repair() throws FlywayException {
        try (EventTelemetryModel telemetryModel = new EventTelemetryModel("repair", flywayTelemetryManager)) {
            if (canUseNativeConnectors(configuration, "repair")) {
                logPreviewFeature(NATIVE_CONNECTORS);
                final var verb = configuration.getPluginRegister().getPlugins(VerbExtension.class).stream().filter(verbExtension -> verbExtension.handlesVerb("repair")).findFirst();
                if (verb.isPresent()) {
                    LOG.debug("Native Connectors for repair is set and a verb is present");
                    return (RepairResult) verb.get().executeVerb(configuration);
                } else {
                    LOG.warn("Native Connectors for repair is set but no verb is present");
                }
            }

            try {
                return flywayExecutor.execute((migrationResolver, schemaHistory, database, defaultSchema, schemas, callbackExecutor, statementInterceptor) -> {
                    RepairResult repairResult = new DbRepair(database, migrationResolver, schemaHistory, callbackExecutor, configuration).repair();





                    callbackExecutor.onOperationFinishEvent(Event.AFTER_REPAIR_OPERATION_FINISH, repairResult);

                    return repairResult;
                }, true, flywayTelemetryManager);
            } catch (Exception e) {
                telemetryModel.setException(e);
                throw e;
            }
        }
    }

    /**
     * Undoes the most recently applied versioned migration. If target is specified, Flyway will attempt to undo
     * versioned migrations in the order they were applied until it hits one with a version below the target. If there
     * is no versioned migration to undo, calling undo has no effect.
     * <i>Flyway Teams only</i>
     * <img src="https://flyway.github.io/flyway/assets/command-undo.png" alt="undo">
     *
     * @return An object summarising the successfully undone migrations.
     *
     * @throws FlywayException when undo failed.
     */
    public OperationResult undo() throws FlywayException {
        if (canUseNativeConnectors(configuration, "undo")) {
            logPreviewFeature(NATIVE_CONNECTORS);
            final var verb = configuration.getPluginRegister().getPlugins(VerbExtension.class).stream().filter(verbExtension -> verbExtension.handlesVerb("undo")).findFirst();
            if (verb.isPresent()) {
                try (EventTelemetryModel telemetryModel = new EventTelemetryModel("undo", flywayTelemetryManager)) {
                    LOG.debug("Native Connectors for undo is set and a verb is present");
                    return (OperationResult) verb.get().executeVerb(configuration);
                }
            } else {
                LOG.warn("Native Connectors for undo is set but no verb is present");
            }
        }
        try {
            return runCommand("undo", Collections.emptyList());
        } catch (FlywayException e) {
            if (e.getMessage().startsWith("No command extension found")) {
                throw new FlywayException("The command 'undo' was not recognized. Make sure you have added 'flyway-proprietary' as a dependency.", e);
            }
            throw e;
        }
    }

    private OperationResult runCommand(String command, List<String> flags) {
        return CommandExtensionUtils.runCommandExtension(configuration, command, flags, flywayTelemetryManager);
    }

    private CleanResult doClean(Database database, SchemaHistory schemaHistory, Schema defaultSchema, Schema[] schemas, CallbackExecutor callbackExecutor) {
        return new DbClean(database, schemaHistory, defaultSchema, schemas, callbackExecutor, configuration).clean();
    }

    private ValidateResult doValidate(Database database, CompositeMigrationResolver migrationResolver, SchemaHistory schemaHistory,
                                      Schema defaultSchema, Schema[] schemas, CallbackExecutor callbackExecutor, ValidatePattern[] ignorePatterns) {
        ValidateResult validateResult = new DbValidate(database, schemaHistory, defaultSchema, migrationResolver, configuration, callbackExecutor, ignorePatterns).validate();

        if (configuration.isCleanOnValidationError()) {
            throw new FlywayException("cleanOnValidationError has been removed");
        }
        return validateResult;
    }

    private BaselineResult doBaseline(SchemaHistory schemaHistory, CallbackExecutor callbackExecutor, Database database) {
        return new DbBaseline(schemaHistory, configuration.getBaselineVersion(), configuration.getBaselineDescription(), callbackExecutor, database).baseline();
    }
}