NativeConnectorsJdbc.java

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

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.configuration.Configuration;
import org.flywaydb.core.internal.nc.AbstractNativeConnectorsDatabase;
import org.flywaydb.core.internal.nc.ConnectionType;
import org.flywaydb.core.internal.nc.DatabaseVersion;
import org.flywaydb.core.internal.nc.DatabaseVersionImpl;
import org.flywaydb.core.internal.nc.MetaData;
import org.flywaydb.core.internal.nc.schemahistory.SchemaHistoryItem;
import org.flywaydb.core.internal.nc.schemahistory.SchemaHistoryModel;
import org.flywaydb.core.internal.configuration.ConfigUtils;
import org.flywaydb.core.internal.configuration.models.ResolvedEnvironment;
import org.flywaydb.core.internal.jdbc.JdbcUtils;
import org.flywaydb.core.internal.jdbc.Result;
import org.flywaydb.core.internal.util.AsciiTable;

public abstract class NativeConnectorsJdbc<T> extends AbstractNativeConnectorsDatabase<T> {
    protected Connection connection;

    @Override
    public void initialize(final ResolvedEnvironment environment, final Configuration configuration) {
        final int connectRetries = environment.getConnectRetries() != null ? environment.getConnectRetries() : 0;
        final int connectRetriesInterval = environment.getConnectRetriesInterval() != null
            ? environment.getConnectRetriesInterval()
            : 0;
        connection = JdbcUtils.openConnection(configuration.getDataSource(), connectRetries, connectRetriesInterval);
        initializeConnectionType(environment, configuration);
        currentSchema = getDefaultSchema(configuration);
        metaData = getDatabaseMetaData();
    }

    @Override
    public boolean canCreateJdbcDataSource() {
        return true;
    }

    @Override
    public void doExecute(final T executionUnit, final boolean outputQueryResults) {
        try (final Statement statement = connection.createStatement()) {
            final boolean hasResult = statement.execute((String) executionUnit);
            parseResults(hasResult, statement, outputQueryResults);
        } catch (SQLException e) {
            throw new FlywayException(e);
        }
    }

    @Override
    public void doExecuteBatch() {
        if (batch.isEmpty()) {
            return;
        }
        try (final Statement statement = connection.createStatement()) {
            for (final String sql : batch) {
                statement.addBatch(sql);
            }
            statement.executeBatch();
            batch.clear();
        } catch (SQLException e) {
            throw new FlywayException(e);
        }
    }

    @Override
    public String getCurrentUser() {
        try {
            return JdbcUtils.getDatabaseMetaData(connection).getUserName();
        } catch (final SQLException e) {
            throw new FlywayException(e);
        }
    }

    @Override
    public final MetaData getDatabaseMetaData() {
        if (this.metaData != null) {
            return metaData;
        }

        final DatabaseMetaData databaseMetaData = JdbcUtils.getDatabaseMetaData(connection);
        final String productName = JdbcUtils.getDatabaseProductName(databaseMetaData);
        final String productVersion = JdbcUtils.getDatabaseProductVersion(databaseMetaData);
        final DatabaseVersion version = new DatabaseVersionImpl(JdbcUtils.getDatabaseVersion(databaseMetaData));
        final String databaseType = getDatabaseType();

        final String databaseName = supportsCatalog() ? getCatalog()
            : supportsSchema() ? getCurrentSchema() : null;


        return new MetaData(databaseType,
            productName,
            version,
            productVersion,
            databaseName,
            getConnectionType());
    }

    @Override
    public boolean isClosed() {
        try {
            return connection != null && connection.isClosed();
        } catch (SQLException e) {
            throw new FlywayException(e);
        }
    }

    @Override
    public void close() throws Exception {
        if (!isClosed()) {
            connection.close();
        }
    }

    protected boolean queryBoolean(final String sql) {
        try (final Statement statement = connection.createStatement()) {
            final ResultSet resultSet = statement.executeQuery(sql);
            resultSet.next();
            return resultSet.getBoolean(1);
        } catch (final SQLException e) {
            throw new FlywayException(e);
        }
    }

    public List<String> queryForStringList(final String query) throws SQLException {
        ResultSet resultSet = null;

        List<String> result;
        try (final Statement statement = connection.createStatement()) {
            resultSet = statement.executeQuery(query);

            result = new ArrayList<>();
            while (resultSet.next()) {
                result.add(resultSet.getString(1));
            }
        } finally {
            JdbcUtils.closeResultSet(resultSet);
        }

        return result;
    }

    private void parseResults(boolean hasResults, final Statement statement, final boolean outputQueryResult) throws SQLException {
        if (outputQueryResult) {
            while (hasResults || (statement.getUpdateCount()) != -1) {
                final List<String> columns;
                final List<List<String>> data;
                if (hasResults) {
                    try (final ResultSet resultSet = statement.getResultSet()) {
                        columns = new ArrayList<>();
                        final ResultSetMetaData metadata = resultSet.getMetaData();
                        final int columnCount = metadata.getColumnCount();
                        for (int i = 1; i <= columnCount; i++) {
                            columns.add(metadata.getColumnName(i));
                        }

                        data = new ArrayList<>();

                        while (resultSet.next()) {
                            final List<String> row = new ArrayList<>();
                            for (int i = 1; i <= columnCount; i++) {
                                row.add(resultSet.getString(i));
                            }
                            data.add(row);
                        }
                    }
                    outputResult(new Result(-1, columns, data, ""));
                }
                hasResults = statement.getMoreResults();
            }
        }
    }

    private void outputResult(final Result result) {
        if (result.columns() != null && !result.columns().isEmpty()) {
            LOG.info(new AsciiTable(result.columns(), result.data(),
                true, "", "No rows returned").render());
        }
    }

    @Override
    public SchemaHistoryModel getSchemaHistoryModel(final String table) {
        try (final Statement statement = connection.createStatement()) {
            final String querySql = "SELECT " + doQuote("installed_rank")
                + ", " + doQuote("version")
                + ", " + doQuote("description")
                + ", " + doQuote("type")
                + ", " + doQuote("script")
                + ", " + doQuote("checksum")
                + ", " + doQuote("installed_on")
                + ", " + doQuote("installed_by")
                + ", " + doQuote("execution_time")
                + ", " + doQuote("success")
                + " FROM "
                + getTableNameWithSchema(table)
                + " WHERE "+ doQuote("installed_rank") + " >= 0"
                + " ORDER BY " + doQuote("installed_rank");
            final ResultSet resultSet = statement.executeQuery(querySql);
            final ArrayList<SchemaHistoryItem> items = new ArrayList<>();
            while (resultSet.next()) {
                items.add(SchemaHistoryItem.builder()
                    .installedRank(resultSet.getInt("installed_rank"))
                    .version(resultSet.getString("version"))
                    .description(resultSet.getString("description"))
                    .type(resultSet.getString("type"))
                    .script(resultSet.getString("script"))
                    .checksum(resultSet.getInt("checksum"))
                    .installedOn(resultSet.getTimestamp("installed_on").toLocalDateTime())
                    .installedBy(resultSet.getString("installed_by"))
                    .executionTime(resultSet.getInt("execution_time"))
                    .success(resultSet.getBoolean("success"))
                    .build());
            }
            return new SchemaHistoryModel(items);
        } catch (final SQLException e) {
            return new SchemaHistoryModel();
        }
    }

    @Override
    public void createSchemaHistoryTable(final Configuration configuration) {
        try (final Statement statement = connection.createStatement()) {
            final String createSql = "CREATE TABLE "
                + getTableNameWithSchema(configuration.getTable())
                + " (\n"
                + doQuote("installed_rank") + " INT NOT NULL PRIMARY KEY,\n"
                + doQuote("version") + " VARCHAR(50),\n"
                + doQuote("description") + " VARCHAR(200) NOT NULL,\n"
                + doQuote("type") + " VARCHAR(20) NOT NULL,\n"
                + doQuote("script") + " VARCHAR(1000) NOT NULL,\n"
                + doQuote("checksum") + " INT,\n"
                + doQuote("installed_by") + " VARCHAR(100) NOT NULL,\n"
                + doQuote("installed_on") + " TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now')),\n"
                + doQuote("execution_time") + " INT NOT NULL,\n"
                + doQuote("success") + " BOOLEAN NOT NULL\n"
                + " );\n";
            statement.executeUpdate(createSql);
        } catch (SQLException e) {
            throw new FlywayException(e);
        }
    }

    @Override
    public void appendSchemaHistoryItem(final SchemaHistoryItem item, final String tableName) {
        try (final Statement statement = connection.createStatement()) {
            final StringBuilder insertSql = new StringBuilder().append("INSERT INTO ")
                .append(getTableNameWithSchema(tableName))
                .append(" (")
                .append(doQuote("installed_rank")  + ", ")
                .append(item.getVersion() == null ? "" : doQuote("version") + ", ")
                .append(doQuote("description")  + ", ")
                .append(doQuote("type")  + ", ")
                .append(doQuote("script")  + ", ")
                .append(doQuote("checksum")  + ", ")
                .append(doQuote("installed_by")  + ", ")
                .append(doQuote("execution_time")  + ", ")
                .append(doQuote("success"))
                .append(")");

            insertSql.append(" VALUES (")
                .append(item.getInstalledRank())
                .append(", ");
            if (item.getVersion() != null) {
                insertSql.append("'").append(item.getVersion()).append("', ");
            }
            insertSql.append("'")
                .append(item.getDescription())
                .append("', ")
                .append("'")
                .append(item.getType())
                .append("', ")
                .append("'")
                .append(item.getScript())
                .append("', ")
                .append(item.getChecksum())
                .append(", ")
                .append("'")
                .append(item.getInstalledBy() == null ? "" : item.getInstalledBy())
                .append("', ")
                .append(item.getExecutionTime())
                .append(", ")
                .append(supportsBoolean() ? item.isSuccess() : item.isSuccess() ? "1" : "0")
                .append(")");
            statement.execute(insertSql.toString());
        } catch (SQLException e) {
            throw new FlywayException(e);
        }
    }

    @Override
    public void removeFailedSchemaHistoryItems(final String tableName) {
        try {
            try (final Statement statement = connection.createStatement()) {
                statement.execute("DELETE FROM " + getTableNameWithSchema(tableName) + " WHERE " + doQuote("success") + " = 0");
            }
        } catch (SQLException e) {
            throw new FlywayException(e);
        }
    }

    @Override
    public void updateSchemaHistoryItem(final SchemaHistoryItem item, final String tableName) {
        try {
            final String sql = new StringBuilder().append("UPDATE ")
                .append(getTableNameWithSchema(tableName))
                .append(" SET ")
                .append(doQuote("description"))
                .append("=? , ")
                .append(doQuote("type"))
                .append("=? , ")
                .append(doQuote("checksum"))
                .append("=?")
                .append(" WHERE ")
                .append(doQuote("installed_rank"))
                .append("=?")
                .toString();
            try (final PreparedStatement statement = connection.prepareStatement(sql)) {
                statement.setString(1, item.getDescription());
                statement.setString(2, item.getType());
                statement.setInt(3, item.getChecksum());
                statement.setInt(4, item.getInstalledRank());
                statement.execute();
            }
        } catch (SQLException e) {
            throw new FlywayException(e);
        }
    }

    @Override
    protected final String getDefaultSchema(final Configuration configuration) {
        if (!supportsSchema()) {
            return getSchemaPlaceHolder();
        }

        final String schema = ConfigUtils.getCalculatedDefaultSchema(configuration);
        if (schema == null) {
            try {
                return connection.getSchema();
            } catch (SQLException e) {
                throw new FlywayException(e);
            }
        }
        return schema;
    }

    protected String getTableNameWithSchema(final String table) {
        return quote(getCurrentSchema(), table);
    }

    protected boolean supportsSchema() {
        return true;
    }

    protected String getSchemaPlaceHolder() {
        return null;
    }

    protected boolean supportsBoolean() {
        return true;
    }

    protected boolean supportsCatalog() {
        return true;
    }

    protected void initializeConnectionType(final ResolvedEnvironment environment, final Configuration configuration) {
        connectionType = ConnectionType.JDBC;
    }

    protected String getCatalog() {
        try {
            return connection.getCatalog();
        } catch (final SQLException e) {
            throw new FlywayException(e);
        }
    }

    public abstract boolean handlesProductName(final Connection connection, final String databaseProductName);
}