BaselineVerbExtension.java

/*-
 * ========================LICENSE_START=================================
 * flyway-verb-baseline
 * ========================================================================
 * 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.verb.baseline;

import lombok.CustomLog;
import org.flywaydb.core.api.CoreMigrationType;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.MigrationVersion;
import org.flywaydb.core.api.configuration.Configuration;
import org.flywaydb.core.api.output.BaselineResult;
import org.flywaydb.core.internal.nc.NativeConnectorsDatabase;
import org.flywaydb.core.internal.nc.schemahistory.SchemaHistoryItem;
import org.flywaydb.core.extensibility.CachingVerbExtension;
import org.flywaydb.core.internal.license.VersionPrinter;
import org.flywaydb.core.internal.util.FlywayDbWebsiteLinks;
import org.flywaydb.nc.preparation.PreparationContext;
import org.flywaydb.verb.schemas.SchemasVerbExtension;

@CustomLog
public class BaselineVerbExtension extends CachingVerbExtension {

    @Override
    public boolean handlesVerb(final String verb) {
        return "baseline".equals(verb);
    }

    @Override
    public Object executeVerb(final Configuration configuration) {
        final PreparationContext context = PreparationContext.get(configuration, cached);
        final NativeConnectorsDatabase database = context.getDatabase();

        final BaselineResult baselineResult = new BaselineResult(VersionPrinter.getVersion(),
            database.getDatabaseMetaData().databaseName());
        final MigrationVersion baselineVersion = configuration.getBaselineVersion();

        final String schemaHistoryName = configuration.getTable();
        final boolean schemaHistoryTableExists = database.schemaHistoryTableExists(schemaHistoryName);

        if (configuration.isCreateSchemas()) {
            final SchemasVerbExtension schemasVerbExtension = new SchemasVerbExtension();
            schemasVerbExtension.useCaching();
            schemasVerbExtension.executeVerb(configuration);
        } else {
            LOG.warn("""
                     The configuration option 'createSchemas' is false.
                     Even though Flyway is configured not to create any schemas, the schema history table still needs a schema to reside in.
                     You must manually create a schema for the schema history table to reside in.
                     See\s""" + FlywayDbWebsiteLinks.MIGRATIONS);
        }

        try {
            if (!schemaHistoryTableExists) {
                createBaselineMarker(configuration, database, baselineResult);
                return baselineResult;
            }

            final String schemaHistoryText = database.quote(database.getCurrentSchema()) + "." + database.quote(
                schemaHistoryName);
            final boolean baselinePresent = context.getSchemaHistoryModel()
                .getSchemaHistoryItems()
                .stream()
                .anyMatch(x -> CoreMigrationType.BASELINE.name().equals(x.getType()));

            final boolean onlySchemas = context.getSchemaHistoryModel()
                .getSchemaHistoryItems()
                .stream()
                .allMatch(x -> CoreMigrationType.SCHEMA.name().equals(x.getType()));

            if (baselinePresent) {
                final String baselineDescription = configuration.getBaselineDescription();
                final SchemaHistoryItem baselineMarker = context.getSchemaHistoryModel()
                    .getSchemaHistoryItems()
                    .stream()
                    .filter(x -> CoreMigrationType.BASELINE.name().equals(x.getType()))
                    .findFirst()
                    .get();

                if (baselineVersion.getVersion().equals(baselineMarker.getVersion()) && baselineDescription.equals(
                    baselineMarker.getDescription())) {
                    LOG.info("Schema history table "
                        + schemaHistoryText
                        + " already initialized with ("
                        + baselineVersion
                        + ","
                        + baselineDescription
                        + "). Skipping.");
                    baselineResult.successfullyBaselined = true;
                    baselineResult.baselineVersion = baselineVersion.toString();
                } else {
                    throw new FlywayException("Unable to baseline schema history table "
                        + schemaHistoryText
                        + " with ("
                        + baselineVersion
                        + ","
                        + baselineDescription
                        + ") as it has already been baselined with ("
                        + baselineMarker.getVersion()
                        + ","
                        + baselineMarker.getDescription()
                        + ")\n"
                        + "Need to reset your baseline? Learn more: "
                        + FlywayDbWebsiteLinks.RESET_THE_BASELINE_MIGRATION);
                }
            } else {
                final boolean schemaPresent = context.getSchemaHistoryModel()
                    .getSchemaHistoryItems()
                    .stream()
                    .anyMatch(x -> CoreMigrationType.SCHEMA.name().equals(x.getType()));

                if (schemaPresent && baselineVersion.equals(MigrationVersion.fromVersion("0"))) {
                    throw new FlywayException("Unable to baseline schema history table "
                        + schemaHistoryText
                        + " with version 0 as this version was used for schema creation");
                }
                final boolean nonSyntheticMigrations = context.getSchemaHistoryModel()
                    .getSchemaHistoryItems()
                    .stream()
                    .anyMatch(x -> !CoreMigrationType.fromString(x.getType()).isSynthetic());
                if (nonSyntheticMigrations) {
                    throw new FlywayException("Unable to baseline schema history table "
                        + schemaHistoryText
                        + " as it already contains migrations\n"
                        + "Need to reset your baseline? Learn more: "
                        + FlywayDbWebsiteLinks.RESET_THE_BASELINE_MIGRATION);
                }
                if (context.getSchemaHistoryModel().getSchemaHistoryItems().isEmpty()) {
                    throw new FlywayException("Unable to baseline schema history table "
                        + schemaHistoryText
                        + " as it already exists, and is empty.\n"
                        + "Delete the schema history table, and run baseline again.");
                }

                if (onlySchemas) {
                    createBaselineMarker(configuration, database, baselineResult);
                } else {
                    throw new FlywayException("Unable to baseline schema history table "
                        + schemaHistoryText
                        + " as it already contains migrations.\n"
                        + "Delete the schema history table, and run baseline again.\n"
                        + "Need to reset your baseline? Learn more: "
                        + FlywayDbWebsiteLinks.RESET_THE_BASELINE_MIGRATION);
                }
            }
        } catch (final FlywayException e) {
            baselineResult.successfullyBaselined = false;
            throw e;
        }

        return baselineResult;
    }

    private static void createBaselineMarker(final Configuration configuration,
        final NativeConnectorsDatabase database,
        final BaselineResult baselineResult) {

        database.createSchemaHistoryTableIfNotExists(configuration);

        final String baselineVersion = configuration.getBaselineVersion().getVersion();
        database.appendSchemaHistoryItem(SchemaHistoryItem.builder()
            .description(configuration.getBaselineDescription())
            .installedRank(1)
            .type("BASELINE")
            .script("<< Flyway Baseline >>")
            .installedBy(database.getInstalledBy(configuration))
            .version(baselineVersion)
            .executionTime(0)
            .success(true)
            .build(), configuration.getTable());

        LOG.info("Successfully baselined schema with version: " + baselineVersion);
        baselineResult.successfullyBaselined = true;
        baselineResult.baselineVersion = baselineVersion;
    }
}