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