001package io.ebeaninternal.dbmigration.ddlgeneration.platform;
002
003import io.ebeaninternal.dbmigration.ddlgeneration.DdlBuffer;
004import io.ebeaninternal.dbmigration.ddlgeneration.DdlWrite;
005import io.ebeaninternal.dbmigration.model.MTable;
006
007import java.util.List;
008
009/**
010 * Uses DB triggers to maintain a history table.
011 */
012public class PostgresHistoryDdl extends DbTriggerBasedHistoryDdl {
013
014  PostgresHistoryDdl() {
015    this.now = "current_timestamp";
016    this.sysPeriodEndValue = "current_timestamp";
017  }
018
019  /**
020   * Use Postgres create table like to create the history table.
021   */
022  @Override
023  protected void createHistoryTable(DdlBuffer apply, MTable table) {
024    apply.append("create table ").append(historyTableName(table.getName()))
025      .append("(like ").append(table.getName()).append(")").endOfStatement();
026  }
027
028  /**
029   * Use Postgres range type rather than start and end timestamps.
030   */
031  @Override
032  protected void addSysPeriodColumns(DdlWrite writer, String baseTableName, String whenCreatedColumn) {
033    platformDdl.alterTableAddColumn(writer, baseTableName, sysPeriod, "tstzrange not null", "tstzrange(" + now + ", null)");
034    if (whenCreatedColumn != null) {
035      writer.applyPostAlter()
036        .append("update ").append(baseTableName).append(" set ")
037        .append(sysPeriod).append(" = tstzrange(").append(whenCreatedColumn).append(", null)").endOfStatement();
038    }
039  }
040
041  @Override
042  protected void appendSysPeriodColumns(DdlBuffer apply, String prefix) {
043    appendColumnName(apply, prefix, sysPeriod);
044  }
045
046  @Override
047  protected void dropSysPeriodColumns(DdlWrite writer, String baseTableName) {
048    platformDdl.alterTableDropColumn(writer, baseTableName, sysPeriod);
049  }
050
051  @Override
052  protected void createTriggers(DdlBuffer buffer, String baseTableName, List<String> columnNames) {
053    String procedureName = procedureName(baseTableName);
054    String triggerName = triggerName(baseTableName);
055    createOrReplaceFunction(buffer, procedureName, historyTableName(baseTableName), columnNames);
056    buffer
057      .append("create trigger ").append(triggerName).newLine()
058      .append("  before update or delete on ").append(baseTableName).newLine()
059      .append("  for each row execute procedure ").append(procedureName).append("();").newLine().newLine();
060  }
061
062  @Override
063  protected void dropTriggers(DdlBuffer buffer, String baseTable) {
064    // rollback trigger then function
065    buffer.append("drop trigger if exists ").append(triggerName(baseTable)).append(" on ").append(baseTable).append(" cascade").endOfStatement();
066    buffer.append("drop function if exists ").append(procedureName(baseTable)).append("()").endOfStatement();
067    buffer.end();
068  }
069
070  protected void createOrReplaceFunction(DdlBuffer apply, String procedureName, String historyTable, List<String> includedColumns) {
071    apply
072      .append("create or replace function ").append(procedureName).append("() returns trigger as $$").newLine();
073
074    apply.append("declare").newLine()
075      .append("  lowerTs timestamptz;").newLine()
076      .append("  upperTs timestamptz;").newLine();
077
078    apply.append("begin").newLine()
079      .append("  lowerTs = lower(OLD.sys_period);").newLine()
080      .append("  upperTs = greatest(lowerTs + '1 microsecond',current_timestamp);").newLine();
081
082    apply
083      .append("  if (TG_OP = 'UPDATE') then").newLine();
084    appendInsertIntoHistory(apply, historyTable, includedColumns);
085    apply
086      .append("    NEW.").append(sysPeriod).append(" = tstzrange(upperTs,null);").newLine()
087      .append("    return new;").newLine();
088    apply
089      .append("  elsif (TG_OP = 'DELETE') then").newLine();
090    appendInsertIntoHistory(apply, historyTable, includedColumns);
091    apply
092      .append("    return old;").newLine();
093    apply
094      .append("  end if;").newLine()
095      .append("end;").newLine()
096      .append("$$ LANGUAGE plpgsql;").newLine();
097
098    apply.end();
099  }
100
101  @Override
102  protected void appendInsertIntoHistory(DdlBuffer buffer, String historyTable, List<String> columns) {
103    buffer.append("    insert into ").append(historyTable).append(" (").append(sysPeriod).append(",");
104    appendColumnNames(buffer, columns, "");
105    buffer.append(") values (tstzrange(lowerTs,upperTs), ");
106    appendColumnNames(buffer, columns, "OLD.");
107    buffer.append(");").newLine();
108  }
109
110  @Override
111  protected String procedureName(String baseTableName) {
112    return normalise(baseTableName) + "_history_version";
113  }
114
115  @Override
116  protected String triggerName(String baseTableName) {
117    return normalise(baseTableName) + "_history_upd";
118  }
119
120  @Override
121  protected String updateTriggerName(String baseTableName) {
122    return normalise(baseTableName) + "_history_upd";
123  }
124
125  @Override
126  protected String deleteTriggerName(String baseTableName) {
127    return normalise(baseTableName) + "_history_del";
128  }
129}