001package io.ebeaninternal.dbmigration.ddlgeneration.platform;
002
003import io.ebean.config.DatabaseConfig;
004import io.ebeaninternal.dbmigration.ddlgeneration.DdlAlterTable;
005import io.ebeaninternal.dbmigration.ddlgeneration.DdlBuffer;
006import io.ebeaninternal.dbmigration.ddlgeneration.DdlWrite;
007import io.ebeaninternal.dbmigration.migration.AddHistoryTable;
008import io.ebeaninternal.dbmigration.migration.DropHistoryTable;
009import io.ebeaninternal.dbmigration.model.MColumn;
010import io.ebeaninternal.dbmigration.model.MTable;
011
012import java.util.List;
013
014/**
015 * Uses DB triggers to maintain a history table.
016 */
017public abstract class DbTriggerBasedHistoryDdl extends DbTableBasedHistoryDdl implements PlatformHistoryDdl {
018
019  protected String sysPeriod;
020  protected String sysPeriodStart;
021  protected String sysPeriodEnd;
022  protected String viewSuffix;
023  protected String sysPeriodType = "datetime(6)";
024  protected String now = "now(6)";
025  protected String sysPeriodEndValue = "now(6)";
026
027  DbTriggerBasedHistoryDdl() {
028  }
029
030  @Override
031  public void configure(DatabaseConfig config, PlatformDdl platformDdl) {
032    super.configure(config, platformDdl);
033    this.sysPeriod = config.getAsOfSysPeriod();
034    this.viewSuffix = config.getAsOfViewSuffix();
035    this.sysPeriodStart = sysPeriod + "_start";
036    this.sysPeriodEnd = sysPeriod + "_end";
037  }
038
039  @Override
040  public void dropHistoryTable(DdlWrite writer, DropHistoryTable dropHistoryTable) {
041    String baseTable = dropHistoryTable.getBaseTable();
042
043    // drop in appropriate order
044    dropTriggers(writer.applyDropDependencies(), baseTable);
045    dropWithHistoryView(writer.applyDropDependencies(), baseTable);
046    dropHistoryTable(writer.applyDropDependencies(), baseTable);
047
048    dropSysPeriodColumns(writer, baseTable);
049  }
050
051  @Override
052  public void addHistoryTable(DdlWrite writer, AddHistoryTable addHistoryTable) {
053    String baseTable = addHistoryTable.getBaseTable();
054    MTable table = writer.getTable(baseTable);
055    if (table == null) {
056      throw new IllegalStateException("MTable " + baseTable + " not found in writer? (required for history DDL)");
057    }
058    createWithHistory(writer, table);
059  }
060
061  @Override
062  public void createWithHistory(DdlWrite writer, MTable table) {
063    String baseTable = quote(table.getName());
064
065    addSysPeriodColumns(writer, baseTable, table.getWhenCreatedColumn());
066    createHistoryTable(writer.applyPostAlter(), table);
067
068    createWithHistoryView(writer.applyPostAlter(), table.getName());
069    createTriggers(writer.applyPostAlter(), baseTable, columnNamesForApply(table));
070    writer.applyPostAlter().end();
071
072    // drop all scripts
073    dropTriggers(writer.dropAll(), baseTable);
074    dropWithHistoryView(writer.dropAll(), baseTable);
075    dropHistoryTable(writer.dropAll(), baseTable);
076    // no need to dropSysPeriodColumns as whole table will be deleted soon
077  }
078
079  @Override
080  public void updateTriggers(DdlWrite writer, String tableName) {
081    MTable table = writer.getTable(tableName);
082    if (table != null && table.isWithHistory()) {
083      DdlAlterTable alter = platformDdl.alterTable(writer, tableName);
084      if (!alter.isHistoryHandled()) {
085        // this code effectively disables history support before the table alter and enables it again
086        // immediately after the table alter. As all alters per table are altogether now, this can done here
087        dropTriggers(writer.apply(), tableName);
088        dropWithHistoryView(writer.apply(), tableName);
089        // here are the alter commands
090        createWithHistoryView(writer.applyPostAlter(), tableName);
091        createTriggers(writer.applyPostAlter(), quote(tableName), columnNamesForApply(table));
092        alter.setHistoryHandled();
093      }
094    }
095  }
096
097  /**
098   * Will add a history trigger to the buffer. The config
099   */
100  protected abstract void createTriggers(DdlBuffer buffer, String baseTable, List<String> columnNames);
101
102  protected abstract void dropTriggers(DdlBuffer buffer, String baseTable);
103
104  protected String historyViewName(String baseTableName) {
105    return normalise(baseTableName, viewSuffix);
106  }
107
108  protected String procedureName(String baseTableName) {
109    return normalise(baseTableName, "_history_version");
110  }
111
112  protected String triggerName(String baseTableName) {
113    return normalise(baseTableName, "_history_upd");
114  }
115
116  protected String updateTriggerName(String baseTableName) {
117    return normalise(baseTableName, "_history_upd");
118  }
119
120  protected String deleteTriggerName(String baseTableName) {
121    return normalise(baseTableName, "_history_del");
122  }
123
124  protected void addSysPeriodColumns(DdlWrite writer, String baseTableName, String whenCreatedColumn) {
125    platformDdl.alterTableAddColumn(writer, baseTableName, sysPeriodStart, sysPeriodType, now);
126    platformDdl.alterTableAddColumn(writer, baseTableName, sysPeriodEnd, sysPeriodType, null);
127    if (whenCreatedColumn != null) {
128      writer.applyPostAlter()
129        .append("update ").append(baseTableName).append(" set ").append(sysPeriodStart).append(" = ").append(whenCreatedColumn).endOfStatement();
130    }
131  }
132
133  protected void createHistoryTable(DdlBuffer apply, MTable table) {
134    createHistoryTableAs(apply, table);
135    createHistoryTableWithPeriod(apply);
136    // TODO: add tablespace here (currently no DbTriggerBased platforms with tablespace support)
137    apply.endOfStatement();
138  }
139
140  protected void createHistoryTableAs(DdlBuffer apply, MTable table) {
141    apply.append(platformDdl.getCreateTableCommandPrefix()).append(" ").append(historyTableName(table.getName())).append("(").newLine();
142    for (MColumn column : table.allColumns()) {
143      if (!column.isDraftOnly()) {
144        writeColumnDefinition(apply, column.getName(), column.getType());
145        apply.append(",").newLine();
146      }
147    }
148    // TODO: We must apply also pending dropped columns. Let's do that in a later step
149    if (table.hasDroppedColumns()) {
150      throw new IllegalStateException(table.getName() + " has dropped columns. Please generate drop script before enabling history");
151    }
152  }
153
154  protected void createHistoryTableWithPeriod(DdlBuffer apply) {
155    writeColumnDefinition(apply, sysPeriodStart, sysPeriodType);
156    apply.append(",").newLine();
157    writeColumnDefinition(apply, sysPeriodEnd, sysPeriodType);
158    apply.newLine().append(")");
159  }
160
161  /**
162   * Write the column definition to the create table statement.
163   */
164  protected void writeColumnDefinition(DdlBuffer buffer, String columnName, String type) {
165    String platformType = platformDdl.convert(type);
166    buffer.append("  ");
167    buffer.append(quote(columnName), 29);
168    buffer.append(platformType);
169  }
170
171  protected void createWithHistoryView(DdlBuffer apply, String baseTableName) {
172    apply
173      .append("create view ").append(historyViewName(baseTableName))
174      .append(" as select * from ").append(quote(baseTableName))
175      .append(" union all select * from ").append(historyTableName(baseTableName))
176      .endOfStatement();
177  }
178
179  protected void appendSysPeriodColumns(DdlBuffer apply, String prefix) {
180    appendColumnName(apply, prefix, sysPeriodStart);
181    appendColumnName(apply, prefix, sysPeriodEnd);
182  }
183
184  protected void dropWithHistoryView(DdlBuffer apply, String baseTableName) {
185    apply.append("drop view ").append(historyViewName(baseTableName)).endOfStatement();
186  }
187
188  protected void dropHistoryTable(DdlBuffer apply, String baseTableName) {
189    apply.append("drop table ").append(historyTableName(baseTableName)).endOfStatement().end();
190  }
191
192  protected void dropSysPeriodColumns(DdlWrite writer, String baseTableName) {
193    platformDdl.alterTableDropColumn(writer, baseTableName, sysPeriodStart);
194    platformDdl.alterTableDropColumn(writer, baseTableName, sysPeriodEnd);
195  }
196
197  protected void appendInsertIntoHistory(DdlBuffer buffer, String baseTable, List<String> columns) {
198    buffer.append("    insert into ").append(historyTableName(baseTable)).append(" (").append(sysPeriodStart).append(",").append(sysPeriodEnd).append(",");
199    appendColumnNames(buffer, columns, "");
200    buffer.append(") values (OLD.").append(sysPeriodStart).append(", ").append(sysPeriodEndValue).append(",");
201    appendColumnNames(buffer, columns, "OLD.");
202    buffer.append(");").newLine();
203  }
204
205  void appendColumnNames(DdlBuffer buffer, List<String> columns, String columnPrefix) {
206    for (int i = 0; i < columns.size(); i++) {
207      if (i > 0) {
208        buffer.append(", ");
209      }
210      buffer.append(columnPrefix);
211      buffer.append(quote(columns.get(i)));
212    }
213  }
214
215  /**
216   * Append a single column to the buffer if it is not null.
217   */
218  void appendColumnName(DdlBuffer buffer, String prefix, String columnName) {
219    if (columnName != null) {
220      buffer.append(prefix).append(columnName);
221    }
222  }
223
224  /**
225   * Return the column names included in history for the apply script.
226   * <p>
227   * Note that dropped columns are actually still included at this point as they are going
228   * to be removed from the history handling when the drop script runs that also deletes
229   * the column.
230   * </p>
231   */
232  List<String> columnNamesForApply(MTable table) {
233    return table.allHistoryColumns(true);
234  }
235
236
237}