PreparedStatementBatchBuilder.java

package org.itsallcode.jdbc.batch;

import static java.util.stream.Collectors.joining;

import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.logging.Logger;

import org.itsallcode.jdbc.SimpleConnection;
import org.itsallcode.jdbc.SimplePreparedStatement;
import org.itsallcode.jdbc.identifier.Identifier;

/**
 * Builder for {@link PreparedStatementBatch}. Create a new builder instance
 * using {@link SimpleConnection#preparedStatementBatch()}.
 */
public class PreparedStatementBatchBuilder {
    private static final Logger LOG = Logger.getLogger(PreparedStatementBatchBuilder.class.getName());
    /** Default maximum batch size. */
    public static final int DEFAULT_MAX_BATCH_SIZE = 200_000;
    private final Function<String, SimplePreparedStatement> statementFactory;
    private String sql;
    private int maxBatchSize = DEFAULT_MAX_BATCH_SIZE;

    /**
     * Create a new instance.
     * 
     * @param statementFactory factory for creating {@link SimplePreparedStatement}.
     */
    public PreparedStatementBatchBuilder(final Function<String, SimplePreparedStatement> statementFactory) {
        this.statementFactory = statementFactory;
    }

    /**
     * Define the SQL statement to be used for the batch job, e.g. {@code INSERT} or
     * {@code UPDATE}.
     * 
     * @param sql SQL statement
     * @return {@code this} for fluent programming
     */
    public PreparedStatementBatchBuilder sql(final String sql) {
        this.sql = sql;
        return this;
    }

    /**
     * Define table and column names used for generating the {@code INSERT}
     * statement.
     * 
     * @param tableName   table name
     * @param columnNames column names
     * @return {@code this} for fluent programming
     */
    @SuppressWarnings("java:S3242") // Using List instead of Collection to preserve column order
    public PreparedStatementBatchBuilder into(final Identifier tableName, final List<Identifier> columnNames) {
        this.sql = createInsertStatement(tableName, columnNames);
        return this;
    }

    /**
     * Define table and column names used for generating the {@code INSERT}
     * statement.
     * 
     * @param tableName   table name
     * @param columnNames column names
     * @return {@code this} for fluent programming
     */
    @SuppressWarnings("java:S3242") // Using List instead of Collection to preserve column order
    public PreparedStatementBatchBuilder into(final String tableName, final List<String> columnNames) {
        return into(Identifier.simple(tableName), columnNames.stream().map(Identifier::simple).toList());
    }

    /**
     * Define maximum batch size, using {@link #DEFAULT_MAX_BATCH_SIZE} as default.
     * 
     * @param maxBatchSize maximum batch size
     * @return {@code this} for fluent programming
     */
    public PreparedStatementBatchBuilder maxBatchSize(final int maxBatchSize) {
        this.maxBatchSize = maxBatchSize;
        return this;
    }

    private static String createInsertStatement(final Identifier table, final List<Identifier> columnNames) {
        final String columns = columnNames.stream().map(Identifier::quote).collect(joining(","));
        final String placeholders = columnNames.stream().map(n -> "?").collect(joining(","));
        return "insert into " + table.quote() + " (" + columns + ") values (" + placeholders + ")";
    }

    /**
     * Build the batch inserter.
     * 
     * @return the batch inserter
     */
    public PreparedStatementBatch build() {
        Objects.requireNonNull(this.sql, "sql");
        LOG.finest(() -> "Running insert statement '" + sql + "'...");
        final SimplePreparedStatement statement = statementFactory.apply(sql);
        return new PreparedStatementBatch(statement, this.maxBatchSize);
    }
}