001package io.ebean.config.dbplatform.h2;
002
003import org.h2.api.Trigger;
004import org.slf4j.Logger;
005import org.slf4j.LoggerFactory;
006
007import java.sql.*;
008import java.util.Arrays;
009
010/**
011 * H2 database trigger used to populate history tables to support the @History feature.
012 */
013public class H2HistoryTrigger implements Trigger {
014
015  private static final Logger logger = LoggerFactory.getLogger(H2HistoryTrigger.class);
016
017  /**
018   * Hardcoding the column and history table suffix for now. Not sure how to get that
019   * configuration into the trigger instance nicely as it is instantiated by H2.
020   */
021  private static final String SYS_PERIOD_START = "SYS_PERIOD_START";
022  private static final String SYS_PERIOD_END = "SYS_PERIOD_END";
023  private static final String HISTORY_SUFFIX = "_history";
024
025  /**
026   * SQL to insert into the history table.
027   */
028  private String insertHistorySql;
029
030  /**
031   * Position of SYS_PERIOD_START column in the Object[].
032   */
033  private int effectStartPosition;
034
035  /**
036   * Position of SYS_PERIOD_END column in the Object[].
037   */
038  private int effectEndPosition;
039
040  @Override
041  public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException {
042    // get the columns for the table
043    ResultSet rs = conn.getMetaData().getColumns(null, schemaName, tableName, null);
044
045    // build the insert into history table SQL
046    StringBuilder insertSql = new StringBuilder(150);
047    insertSql.append("insert into ").append(schemaName).append(".").append(tableName).append(HISTORY_SUFFIX).append(" (");
048
049    int count = 0;
050    while (rs.next()) {
051      if (++count > 1) {
052        insertSql.append(",");
053      }
054      String columnName = rs.getString("COLUMN_NAME");
055      if (columnName.equalsIgnoreCase(SYS_PERIOD_START)) {
056        this.effectStartPosition = count - 1;
057      } else if (columnName.equalsIgnoreCase(SYS_PERIOD_END)) {
058        this.effectEndPosition = count - 1;
059      }
060      insertSql.append(columnName);
061    }
062    insertSql.append(") values (");
063    for (int i = 0; i < count; i++) {
064      if (i > 0) {
065        insertSql.append(",");
066      }
067      insertSql.append("?");
068    }
069    insertSql.append(");");
070
071    this.insertHistorySql = insertSql.toString();
072    logger.debug("History table insert sql: {}", insertHistorySql);
073  }
074
075  @Override
076  public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException {
077    if (oldRow != null) {
078      // a delete or update event
079      Timestamp now = new Timestamp(System.currentTimeMillis());
080      oldRow[effectEndPosition] = now;
081      if (newRow != null) {
082        // update event. Set the effective start timestamp to now.
083        newRow[effectStartPosition] = now;
084      }
085      if (logger.isDebugEnabled()) {
086        logger.debug("History insert: {}", Arrays.toString(oldRow));
087      }
088      insertIntoHistory(connection, oldRow);
089    }
090  }
091
092  /**
093   * Insert the data into the history table.
094   */
095  private void insertIntoHistory(Connection connection, Object[] oldRow) throws SQLException {
096    try (PreparedStatement stmt = connection.prepareStatement(insertHistorySql)) {
097      for (int i = 0; i < oldRow.length; i++) {
098        stmt.setObject(i + 1, oldRow[i]);
099      }
100      stmt.executeUpdate();
101    }
102  }
103
104  @Override
105  public void close() throws SQLException {
106    // do nothing
107  }
108
109  @Override
110  public void remove() throws SQLException {
111    // do nothing
112  }
113}