From 98ec00a2d63e654a58a5221847859984de473cda Mon Sep 17 00:00:00 2001
From: Robin Bygrave <robin.bygrave@gmail.com>
Date: Mon, 17 Aug 2015 22:12:06 +1200
Subject: [PATCH] #374 - Add @History support with MySql - refactor extracting
 DbTriggerBasedHistoryDdl

---
 .../ddlgeneration/platform/BaseTableDdl.java  |  44 +---
 .../platform/DbTriggerBasedHistoryDdl.java    | 241 ++++++++++++++++++
 .../ddlgeneration/platform/MySqlDdl.java      |   1 +
 .../platform/MySqlHistoryDdl.java             |  94 +++++++
 .../platform/NoHistorySupportDdl.java         |   2 +-
 .../ddlgeneration/platform/PlatformDdl.java   |  43 +++-
 .../platform/PlatformHistoryDdl.java          |   2 +-
 .../platform/PostgresHistoryDdl.java          | 224 +++++-----------
 8 files changed, 444 insertions(+), 207 deletions(-)
 create mode 100644 src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/DbTriggerBasedHistoryDdl.java
 create mode 100644 src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/MySqlHistoryDdl.java

diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/BaseTableDdl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/BaseTableDdl.java
index 79b1e519bf..e596e4decf 100644
--- a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/BaseTableDdl.java
+++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/BaseTableDdl.java
@@ -64,7 +64,7 @@ public class BaseTableDdl implements TableDdl {
    * Base tables that have associated history tables that need their triggers/functions regenerated as
    * columns have been added, removed, included or excluded.
    */
-  protected Map<String,HistoryTableUpdate> regenerateHistoryTriggers = new LinkedHashMap<String,HistoryTableUpdate>();
+  protected Map<String, HistoryTableUpdate> regenerateHistoryTriggers = new LinkedHashMap<String, HistoryTableUpdate>();
 
   /**
    * Construct with a naming convention and platform specific DDL.
@@ -114,14 +114,7 @@ public void generate(DdlWrite writer, CreateTable createTable) throws IOExceptio
 
     DdlBuffer apply = writer.apply();
     apply.append("create table ").append(tableName).append(" (");
-    for (int i = 0; i < columns.size(); i++) {
-      apply.newLine();
-      writeColumnDefinition(apply, columns.get(i), useIdentity);
-      if (i < columns.size() - 1) {
-        apply.append(",");
-      }
-    }
-
+    writeTableColumns(apply, columns, useIdentity);
     writeCheckConstraints(apply, createTable);
     writeUniqueConstraints(apply, createTable);
     writeCompoundUniqueConstraints(apply, createTable);
@@ -157,6 +150,10 @@ public void generate(DdlWrite writer, CreateTable createTable) throws IOExceptio
 
   }
 
+  private void writeTableColumns(DdlBuffer apply, List<Column> columns, boolean useIdentity) throws IOException {
+    platformDdl.writeTableColumns(apply, columns, useIdentity);
+  }
+
   /**
    * Specific handling of OneToOne unique constraints for MsSqlServer.
    * For all other DB platforms these unique constraints are done inline as per normal.
@@ -415,35 +412,6 @@ protected String lowerName(String name) {
     return naming.lowerName(name);
   }
 
-  /**
-   * Write the column definition to the create table statement.
-   */
-  protected void writeColumnDefinition(DdlBuffer buffer, Column column, boolean useIdentity) throws IOException {
-
-    boolean identityColumn = useIdentity && isTrue(column.isPrimaryKey());
-    String platformType = convertToPlatformType(column.getType(), identityColumn);
-
-    buffer.append("  ");
-    buffer.append(lowerName(column.getName()), 30);
-    buffer.append(platformType);
-    if (isTrue(column.isNotnull()) || isTrue(column.isPrimaryKey())) {
-      buffer.append(" not null");
-    }
-
-    // add check constraints later as we really want to give them a nice name
-    // so that the database can potentially provide a nice SQL error
-  }
-
-  /**
-   * Convert the expected logical type into a platform specific one.
-   * <p>
-   * For example clob -> text for postgres.
-   * </p>
-   */
-  protected String convertToPlatformType(String type, boolean identity) {
-    return platformDdl.convert(type, identity);
-  }
-
   /**
    * Return the list of columns that make the primary key.
    */
diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/DbTriggerBasedHistoryDdl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/DbTriggerBasedHistoryDdl.java
new file mode 100644
index 0000000000..47e400ad78
--- /dev/null
+++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/DbTriggerBasedHistoryDdl.java
@@ -0,0 +1,241 @@
+package com.avaje.ebean.dbmigration.ddlgeneration.platform;
+
+import com.avaje.ebean.config.DbConstraintNaming;
+import com.avaje.ebean.config.ServerConfig;
+import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
+import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
+import com.avaje.ebean.dbmigration.migration.AddHistoryTable;
+import com.avaje.ebean.dbmigration.migration.DropHistoryTable;
+import com.avaje.ebean.dbmigration.model.MColumn;
+import com.avaje.ebean.dbmigration.model.MTable;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Uses DB triggers to maintain a history table.
+ */
+public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
+
+  protected DbConstraintNaming constraintNaming;
+
+  protected PlatformDdl platformDdl;
+
+  protected String sysPeriod;
+  protected String sysPeriodStart;
+  protected String sysPeriodEnd;
+
+  protected String viewSuffix;
+  protected String historySuffix;
+
+
+  protected String currentTimestamp = "now(6)";
+  protected String sysPeriodType = "datetime(6)";
+
+  public DbTriggerBasedHistoryDdl() {
+  }
+
+  @Override
+  public void configure(ServerConfig serverConfig, PlatformDdl platformDdl) {
+    this.platformDdl = platformDdl;
+    this.sysPeriod = serverConfig.getAsOfSysPeriod();
+    this.viewSuffix = serverConfig.getAsOfViewSuffix();
+    this.historySuffix = serverConfig.getHistoryTableSuffix();
+    this.constraintNaming = serverConfig.getConstraintNaming();
+
+    this.sysPeriodStart = sysPeriod+"_start";
+    this.sysPeriodEnd = sysPeriod+"_end";
+  }
+
+  @Override
+  public void regenerateHistoryTriggers(DdlWrite writer, HistoryTableUpdate update) throws IOException {
+
+    MTable table = writer.getTable(update.getBaseTable());
+    if (table == null) {
+      throw new IllegalStateException("MTable "+update.getBaseTable()+" not found in writer? (required for history DDL)");
+    }
+    regenerateHistoryTriggers(writer, table, update);
+  }
+
+  protected abstract void regenerateHistoryTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException;
+
+  @Override
+  public void dropHistoryTable(DdlWrite writer, DropHistoryTable dropHistoryTable) throws IOException {
+
+    String baseTable = dropHistoryTable.getBaseTable();
+
+    // drop in appropriate order
+    dropTriggers(writer.dropHistory(), baseTable);
+    dropHistoryTableEtc(writer.dropHistory(), baseTable);
+  }
+
+
+  @Override
+  public void addHistoryTable(DdlWrite writer, AddHistoryTable addHistoryTable) throws IOException {
+
+    String baseTable = addHistoryTable.getBaseTable();
+    MTable table = writer.getTable(baseTable);
+    if (table == null) {
+      throw new IllegalStateException("MTable "+baseTable+" not found in writer? (required for history DDL)");
+    }
+
+    createWithHistory(writer, table);
+  }
+
+  @Override
+  public void createWithHistory(DdlWrite writer, MTable table) throws IOException {
+
+    String baseTable = table.getName();
+    String whenCreatedColumn = table.getWhenCreatedColumn();
+
+    // rollback changes in appropriate order
+    dropTriggers(writer.rollback(), baseTable);
+    dropHistoryTableEtc(writer.rollback(), baseTable);
+
+    addHistoryTable(writer, table, whenCreatedColumn);
+    addStoredFunction(writer, table, null);
+    createTriggers(writer, table);
+  }
+
+  protected abstract void createTriggers(DdlWrite writer, MTable table) throws IOException;
+
+  protected abstract void dropTriggers(DdlBuffer buffer, String baseTable) throws IOException;
+
+  protected void addStoredFunction(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
+    // do nothing
+  }
+
+  protected String normalise(String tableName) {
+    return constraintNaming.normaliseTable(tableName);
+  }
+
+  protected String historyTableName(String baseTableName) {
+    return normalise(baseTableName) + historySuffix;
+  }
+
+  protected String procedureName(String baseTableName) {
+    return normalise(baseTableName) + "_history_version";
+  }
+
+  protected String triggerName(String baseTableName) {
+    return normalise(baseTableName) + "_history_upd";
+  }
+
+  protected String updateTriggerName(String baseTableName) {
+    return normalise(baseTableName) + "_history_upd";
+  }
+
+  protected String deleteTriggerName(String baseTableName) {
+    return normalise(baseTableName) + "_history_del";
+  }
+
+  protected void addHistoryTable(DdlWrite writer, MTable table, String whenCreatedColumn) throws IOException {
+
+    String baseTableName = table.getName();
+
+    DdlBuffer apply = writer.applyHistory();
+
+    addSysPeriodColumns(apply, baseTableName, whenCreatedColumn);
+    createHistoryTable(apply, table);
+    createWithHistoryView(apply, baseTableName);
+  }
+
+  protected void addSysPeriodColumns(DdlBuffer apply, String baseTableName, String whenCreatedColumn) throws IOException {
+
+    apply.append("alter table ").append(baseTableName).append(" add column ")
+        .append(sysPeriodStart).append(" ").append(sysPeriodType).append(" default ").append(currentTimestamp).endOfStatement();
+    apply.append("alter table ").append(baseTableName).append(" add column ")
+        .append(sysPeriodEnd).append(" ").append(sysPeriodType).endOfStatement();
+
+    if (whenCreatedColumn != null) {
+      apply.append("update ").append(baseTableName).append(" set ").append(sysPeriodStart).append(" = ").append(whenCreatedColumn).endOfStatement();
+    }
+  }
+
+  protected void createHistoryTable(DdlBuffer apply, MTable table) throws IOException {
+
+    apply.append("create table ").append(table.getName()).append(historySuffix).append("(").newLine();
+
+    Collection<MColumn> cols = table.getColumns().values();
+    for (MColumn column : cols) {
+      writeColumnDefinition(apply, column.getName(), column.getType());
+      apply.append(",").newLine();
+    }
+    writeColumnDefinition(apply, sysPeriodStart, sysPeriodType);
+    apply.append(",").newLine();
+    writeColumnDefinition(apply, sysPeriodEnd, sysPeriodType);
+    apply.newLine().append(")").endOfStatement();
+  }
+
+  /**
+   * Write the column definition to the create table statement.
+   */
+  protected void writeColumnDefinition(DdlBuffer buffer, String columnName, String type) throws IOException {
+
+    String platformType = platformDdl.convert(type, false);
+    buffer.append("  ");
+    buffer.append(platformDdl.lowerName(columnName), 30);
+    buffer.append(platformType);
+  }
+
+  protected void createWithHistoryView(DdlBuffer apply, String baseTableName) throws IOException {
+
+    apply
+        .append("create view ").append(baseTableName).append(viewSuffix)
+        .append(" as select * from ").append(baseTableName)
+        .append(" union all select * from ").append(baseTableName).append(historySuffix)
+        .endOfStatement().end();
+  }
+
+  protected void dropHistoryTableEtc(DdlBuffer buffer, String baseTableName) throws IOException {
+
+    buffer.append("drop view ").append(baseTableName).append(viewSuffix).endOfStatement();
+    dropSysPeriodColumns(buffer, baseTableName);
+    buffer.append("drop table ").append(baseTableName).append(historySuffix).endOfStatement().end();
+  }
+
+  protected void dropSysPeriodColumns(DdlBuffer buffer, String baseTableName) throws IOException {
+    buffer.append("alter table ").append(baseTableName).append(" drop column ").append(sysPeriodStart).endOfStatement();
+    buffer.append("alter table ").append(baseTableName).append(" drop column ").append(sysPeriodEnd).endOfStatement();
+  }
+
+  //protected abstract void addFunction(DdlBuffer apply, String procedureName, String historyTable, List<String> includedColumns) throws IOException;
+
+  protected void appendInsertIntoHistory(DdlBuffer buffer, String historyTable, List<String> columns) throws IOException {
+
+    buffer.append("    insert into ").append(historyTable).append(" (").append(sysPeriodStart).append(",").append(sysPeriodEnd).append(",");
+    appendColumnNames(buffer, columns, "");
+    buffer.append(") values (OLD.").append(sysPeriodStart).append(", ").append(currentTimestamp).append(",");
+    appendColumnNames(buffer, columns, "OLD.");
+    buffer.append(");").newLine();
+  }
+
+  protected void appendColumnNames(DdlBuffer buffer, List<String> columns, String columnPrefix) throws IOException {
+
+    for (int i=0; i< columns.size(); i++) {
+      if (i > 0) {
+        buffer.append(", ");
+      }
+      buffer.append(columnPrefix);
+      buffer.append(columns.get(i));
+    }
+  }
+
+  /**
+   * Return the list of included columns in order.
+   */
+  protected List<String> includedColumnNames(MTable table) throws IOException {
+
+    Collection<MColumn> columns = table.getColumns().values();
+    List<String> includedColumns = new ArrayList<String>(columns.size());
+
+    for (MColumn column : columns) {
+      if (!column.isHistoryExclude()) {
+        includedColumns.add(column.getName());
+      }
+    }
+    return includedColumns;
+  }
+}
diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/MySqlDdl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/MySqlDdl.java
index 38d56b7069..e7c4be7328 100644
--- a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/MySqlDdl.java
+++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/MySqlDdl.java
@@ -13,6 +13,7 @@ public MySqlDdl(DbTypeMap platformTypes, DbIdentity dbIdentity) {
     super(platformTypes, dbIdentity);
     this.alterColumn =  "modify";
     this.dropUniqueConstraint = "drop index";
+    this.historyDdl = new MySqlHistoryDdl();
   }
 
   /**
diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/MySqlHistoryDdl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/MySqlHistoryDdl.java
new file mode 100644
index 0000000000..9f3b4fbf3b
--- /dev/null
+++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/MySqlHistoryDdl.java
@@ -0,0 +1,94 @@
+package com.avaje.ebean.dbmigration.ddlgeneration.platform;
+
+import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
+import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
+import com.avaje.ebean.dbmigration.model.MTable;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * MySql history support using DB triggers to maintain a history table.
+ */
+public class MySqlHistoryDdl extends DbTriggerBasedHistoryDdl {
+
+
+  public MySqlHistoryDdl() {
+  }
+
+  @Override
+  protected void dropTriggers(DdlBuffer buffer, String baseTable) throws IOException {
+
+    buffer.append("drop trigger ").append(updateTriggerName(baseTable)).endOfStatement();
+    buffer.append("drop trigger ").append(deleteTriggerName(baseTable)).endOfStatement();
+  }
+
+  @Override
+  protected void createTriggers(DdlWrite writer, MTable table) throws IOException {
+
+    String baseTableName = table.getName();
+    String historyTableName = historyTableName(baseTableName);
+    List<String> includedColumns = includedColumnNames(table);
+
+    DdlBuffer apply = writer.applyHistory();
+
+    addBeforeUpdate(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
+    addBeforeDelete(apply, deleteTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
+  }
+
+  @Override
+  protected void regenerateHistoryTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
+
+    String baseTableName = table.getName();
+    String historyTableName = historyTableName(baseTableName);
+    List<String> includedColumns = includedColumnNames(table);
+
+    DdlBuffer apply = writer.applyHistory();
+
+    apply.append("-- Regenerated ").newLine();
+    apply.append("-- changes: ").append(update.description()).newLine();
+    // lock the base table while we drop and recreate the triggers
+    apply.append("lock tables ").append(baseTableName).append(" write").endOfStatement();
+    dropTriggers(apply, baseTableName);
+    addBeforeUpdate(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
+    addBeforeDelete(apply, deleteTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
+    apply.append("unlock tables").endOfStatement();
+
+    // put a reverted version into the rollback buffer
+    update.toRevertedColumns(includedColumns);
+
+    DdlBuffer rollback = writer.rollback();
+    rollback.append("-- Revert regenerated ").newLine();
+    rollback.append("-- revert changes: ").append(update.description()).newLine();
+    // lock the base table while we drop and recreate the triggers
+    rollback.append("lock tables ").append(baseTableName).append(" write").endOfStatement();
+    dropTriggers(rollback, baseTableName);
+    addBeforeUpdate(rollback, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
+    addBeforeDelete(rollback, deleteTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
+    rollback.append("unlock tables").endOfStatement();
+
+  }
+
+  private void addBeforeUpdate(DdlBuffer apply, String triggerName, String baseTable, String historyTable, List<String> includedColumns) throws IOException {
+
+    apply
+        .append("delimiter $$").newLine()
+        .append("create trigger ").append(triggerName).append(" before update on ").append(baseTable)
+        .append(" for each row begin").newLine();
+    appendInsertIntoHistory(apply, historyTable, includedColumns);
+    apply
+        .append("    set NEW.").append(sysPeriod).append("_start = now(6)").endOfStatement().newLine()
+        .append("end$$").newLine();
+  }
+
+  private void addBeforeDelete(DdlBuffer apply, String triggerName, String baseTable, String historyTable, List<String> includedColumns) throws IOException {
+
+    apply
+        .append("delimiter $$").newLine()
+        .append("create trigger ").append(triggerName).append(" before delete on ").append(baseTable)
+        .append(" for each row begin").newLine();
+    appendInsertIntoHistory(apply, historyTable, includedColumns);
+    apply.append("end$$").newLine();
+  }
+
+}
diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/NoHistorySupportDdl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/NoHistorySupportDdl.java
index ef697a7792..259bc6fa7b 100644
--- a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/NoHistorySupportDdl.java
+++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/NoHistorySupportDdl.java
@@ -15,7 +15,7 @@
 public class NoHistorySupportDdl implements PlatformHistoryDdl {
 
   @Override
-  public void configure(ServerConfig serverConfig) {
+  public void configure(ServerConfig serverConfig, PlatformDdl platformDdl) {
     // does nothing
   }
 
diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformDdl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformDdl.java
index b8d5b31f8c..3e0e5caf8e 100644
--- a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformDdl.java
+++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformDdl.java
@@ -6,16 +6,19 @@
 import com.avaje.ebean.config.dbplatform.DbTypeMap;
 import com.avaje.ebean.config.dbplatform.IdType;
 import com.avaje.ebean.dbmigration.ddlgeneration.BaseDdlHandler;
+import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
 import com.avaje.ebean.dbmigration.ddlgeneration.DdlHandler;
 import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
 import com.avaje.ebean.dbmigration.ddlgeneration.platform.util.PlatformTypeConverter;
 import com.avaje.ebean.dbmigration.migration.AddHistoryTable;
 import com.avaje.ebean.dbmigration.migration.AlterColumn;
+import com.avaje.ebean.dbmigration.migration.Column;
 import com.avaje.ebean.dbmigration.migration.DropHistoryTable;
 import com.avaje.ebean.dbmigration.migration.IdentityType;
 import com.avaje.ebean.dbmigration.model.MTable;
 
 import java.io.IOException;
+import java.util.List;
 
 /**
  * Controls the DDL generation for a specific database platform.
@@ -84,7 +87,7 @@ public PlatformDdl(DbTypeMap platformTypes, DbIdentity dbIdentity) {
    * Set configuration options.
    */
   public void configure(ServerConfig serverConfig) {
-    historyDdl.configure(serverConfig);
+    historyDdl.configure(serverConfig, this);
     naming = serverConfig.getConstraintNaming();
   }
 
@@ -111,6 +114,38 @@ public String asIdentityColumn(String columnDefn) {
     return columnDefn + identitySuffix;
   }
 
+  /**
+   * Write all the table columns converting to platform types as necessary.
+   */
+  public void writeTableColumns(DdlBuffer apply, List<Column> columns, boolean useIdentity) throws IOException {
+    for (int i = 0; i < columns.size(); i++) {
+      apply.newLine();
+      writeColumnDefinition(apply, columns.get(i), useIdentity);
+      if (i < columns.size() - 1) {
+        apply.append(",");
+      }
+    }
+  }
+
+  /**
+   * Write the column definition to the create table statement.
+   */
+  protected void writeColumnDefinition(DdlBuffer buffer, Column column, boolean useIdentity) throws IOException {
+
+    boolean identityColumn = useIdentity && isTrue(column.isPrimaryKey());
+    String platformType = convert(column.getType(), identityColumn);
+
+    buffer.append("  ");
+    buffer.append(lowerName(column.getName()), 30);
+    buffer.append(platformType);
+    if (isTrue(column.isNotnull()) || isTrue(column.isPrimaryKey())) {
+      buffer.append(" not null");
+    }
+
+    // add check constraints later as we really want to give them a nice name
+    // so that the database can potentially provide a nice SQL error
+  }
+
   /**
    * Return the drop foreign key clause.
    */
@@ -333,4 +368,10 @@ protected String lowerName(String name) {
     return naming.lowerName(name);
   }
 
+  /**
+   * Null safe Boolean true test.
+   */
+  protected boolean isTrue(Boolean value) {
+    return Boolean.TRUE.equals(value);
+  }
 }
diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformHistoryDdl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformHistoryDdl.java
index 7797717656..48a92c3658 100644
--- a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformHistoryDdl.java
+++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformHistoryDdl.java
@@ -16,7 +16,7 @@ public interface PlatformHistoryDdl {
   /**
    * Configure typically reading the
    */
-  void configure(ServerConfig serverConfig);
+  void configure(ServerConfig serverConfig, PlatformDdl platformDdl);
 
   /**
    * Add history support to the table using platform specific mechanism.
diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PostgresHistoryDdl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PostgresHistoryDdl.java
index 412927b0b2..c53d902cb7 100644
--- a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PostgresHistoryDdl.java
+++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PostgresHistoryDdl.java
@@ -1,163 +1,108 @@
 package com.avaje.ebean.dbmigration.ddlgeneration.platform;
 
-import com.avaje.ebean.config.DbConstraintNaming;
-import com.avaje.ebean.config.ServerConfig;
 import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
 import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
-import com.avaje.ebean.dbmigration.migration.AddHistoryTable;
-import com.avaje.ebean.dbmigration.migration.DropHistoryTable;
-import com.avaje.ebean.dbmigration.model.MColumn;
 import com.avaje.ebean.dbmigration.model.MTable;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 
 /**
  * Uses DB triggers to maintain a history table.
  */
-public class PostgresHistoryDdl implements PlatformHistoryDdl {
+public class PostgresHistoryDdl extends DbTriggerBasedHistoryDdl {
 
-  private DbConstraintNaming constraintNaming;
-
-  private String sysPeriod;
-
-  private String viewSuffix;
-
-  private String historySuffix;
 
   public PostgresHistoryDdl() {
+    this.currentTimestamp = "current_timestamp";
   }
 
+  /**
+   * Use Postgres create table like to create the history table.
+   */
   @Override
-  public void configure(ServerConfig serverConfig) {
-    this.sysPeriod = serverConfig.getAsOfSysPeriod();
-    this.viewSuffix = serverConfig.getAsOfViewSuffix();
-    this.historySuffix = serverConfig.getHistoryTableSuffix();
-    this.constraintNaming = serverConfig.getConstraintNaming();
+  protected void createHistoryTable(DdlBuffer apply, MTable table) throws IOException {
+
+    String baseTable = table.getName();
+    apply
+        .append("create table ").append(baseTable).append(historySuffix)
+        .append("(like ").append(baseTable).append(")").endOfStatement();
   }
 
+  /**
+   * Use Postgres range type rather than start and end timestamps.
+   */
   @Override
-  public void regenerateHistoryTriggers(DdlWrite writer, HistoryTableUpdate update) throws IOException {
+  protected void addSysPeriodColumns(DdlBuffer apply, String baseTableName, String whenCreatedColumn) throws IOException {
+    apply
+        .append("alter table ").append(baseTableName)
+        .append(" add column ").append(sysPeriod).append(" tstzrange not null default tstzrange(").append(currentTimestamp).append(", null)")
+        .endOfStatement();
 
-    MTable table = writer.getTable(update.getBaseTable());
-    if (table == null) {
-      throw new IllegalStateException("MTable "+update.getBaseTable()+" not found in writer? (required for history DDL)");
+    if (whenCreatedColumn != null) {
+      apply.append("update table ").append(baseTableName).append(" set ")
+          .append(sysPeriod).append(" = tstzrange(").append(whenCreatedColumn).append(", null)").endOfStatement();
     }
-    addStoredFunction(writer, table, update);
   }
 
   @Override
-  public void dropHistoryTable(DdlWrite writer, DropHistoryTable dropHistoryTable) throws IOException {
-
-    String baseTable = dropHistoryTable.getBaseTable();
-
-    // drop in appropriate order
-    dropTriggersEtc(writer.dropHistory(), baseTable);
-    dropHistoryTableEtc(writer.dropHistory(), baseTable);
+  protected void dropSysPeriodColumns(DdlBuffer buffer, String baseTableName) throws IOException {
+    buffer.append("alter table ").append(baseTableName).append(" drop column ").append(sysPeriod).endOfStatement();
   }
 
-
   @Override
-  public void addHistoryTable(DdlWrite writer, AddHistoryTable addHistoryTable) throws IOException {
+  protected void createTriggers(DdlWrite writer, MTable table) throws IOException {
 
-    String baseTable = addHistoryTable.getBaseTable();
-    MTable table = writer.getTable(baseTable);
-    if (table == null) {
-      throw new IllegalStateException("MTable "+baseTable+" not found in writer? (required for history DDL)");
-    }
+    String baseTableName = table.getName();
+    String procedureName = procedureName(baseTableName);
+    String triggerName = triggerName(baseTableName);
 
-    createWithHistory(writer, table);
+    DdlBuffer apply = writer.applyHistory();
+    apply
+        .append("create trigger ").append(triggerName).newLine()
+        .append("  before update or delete on ").append(baseTableName).newLine()
+        .append("  for each row execute procedure ").append(procedureName).append("();").newLine().newLine();
   }
 
   @Override
-  public void createWithHistory(DdlWrite writer, MTable table) throws IOException {
-
-    String baseTable = table.getName();
-    String whenCreatedColumn = table.getWhenCreatedColumn();
-
-    // rollback changes in appropriate order
-    dropTriggersEtc(writer.rollback(), baseTable);
-    dropHistoryTableEtc(writer.rollback(), baseTable);
-
-    addHistoryTable(writer, table, whenCreatedColumn);
-    addStoredFunction(writer, table, null);
-    addTrigger(writer, table);
-  }
-
-  protected String normalise(String tableName) {
-    return constraintNaming.normaliseTable(tableName);
-  }
-
-  protected String historyTableName(String baseTableName) {
-    return normalise(baseTableName) + historySuffix;
-  }
-
-  protected String procedureName(String baseTableName) {
-    return normalise(baseTableName) + "_history_version";
-  }
-
-  protected String triggerName(String baseTableName) {
-    return normalise(baseTableName) + "_history_upd";
-  }
-
-  protected void dropTriggersEtc(DdlBuffer buffer, String baseTable) throws IOException {
-
+  protected void dropTriggers(DdlBuffer buffer, String baseTable) throws IOException {
     // rollback trigger then function
     buffer.append("drop trigger if exists ").append(triggerName(baseTable)).append(" on ").append(baseTable).append(" cascade").endOfStatement();
     buffer.append("drop function if exists ").append(procedureName(baseTable)).append("()").endOfStatement();
     buffer.end();
   }
 
-  protected void addHistoryTable(DdlWrite writer, MTable table, String whenCreatedColumn) throws IOException {
-
-    String baseTableName = table.getName();
-
-    DdlBuffer apply = writer.applyHistory();
-
-    if (whenCreatedColumn == null) {
-      // effective history start as at current timestamp
-      whenCreatedColumn = "current_timestamp";
-    }
-
+  protected void addFunction(DdlBuffer apply, String procedureName, String historyTable, List<String> includedColumns) throws IOException {
     apply
-        .append("alter table ").append(baseTableName)
-        .append(" add column ").append(sysPeriod).append(" tstzrange not null default tstzrange(").append(whenCreatedColumn).append(", null)")
-        .endOfStatement();
-
+        .append("create or replace function ").append(procedureName).append("() returns trigger as $$").newLine()
+        .append("begin").newLine();
     apply
-        .append("create table ").append(baseTableName).append(historySuffix)
-        .append(" (like ").append(baseTableName).append(")")
-        .endOfStatement();
-
+        .append("  if (TG_OP = 'UPDATE') then").newLine();
+    appendInsertIntoHistory(apply, historyTable, includedColumns);
     apply
-        .append("create view ").append(baseTableName).append(viewSuffix)
-        .append(" as select * from ").append(baseTableName)
-        .append(" union all select * from ").append(baseTableName).append(historySuffix)
-        .endOfStatement().end();
-  }
-
-  protected void dropHistoryTableEtc(DdlBuffer buffer, String baseTableName) throws IOException {
+        .append("    NEW.").append(sysPeriod).append(" = tstzrange(CURRENT_TIMESTAMP,null);").newLine()
+        .append("    return new;").newLine().newLine();
+    apply
+        .append("  elsif (TG_OP = 'DELETE') then").newLine();
+    appendInsertIntoHistory(apply, historyTable, includedColumns);
+    apply
+        .append("    return old;").newLine().newLine();
+    apply
+        .append("  end if;").newLine()
+        .append("end;").newLine()
+        .append("$$ LANGUAGE plpgsql;").newLine();
 
-    buffer.append("drop view ").append(baseTableName).append(viewSuffix).endOfStatement();
-    buffer.append("alter table ").append(baseTableName).append(" drop column ").append(sysPeriod).endOfStatement();
-    buffer.append("drop table ").append(baseTableName).append(historySuffix).endOfStatement().end();
+    apply.end();
   }
 
-  protected void addTrigger(DdlWrite writer, MTable table) throws IOException {
-
-    String baseTableName = table.getName();
-    String procedureName = procedureName(baseTableName);
-    String triggerName = triggerName(baseTableName);
+  @Override
+  protected void regenerateHistoryTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
 
-    DdlBuffer apply = writer.applyHistory();
-    apply
-        .append("create trigger ").append(triggerName).newLine()
-        .append("  before insert or update or delete on ").append(baseTableName).newLine()
-        .append("  for each row execute procedure ").append(procedureName).append("();").newLine().newLine();
+    // just replace the stored function with 'create or replace'
+    addStoredFunction(writer, table, update);
   }
 
+  @Override
   protected void addStoredFunction(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
 
     String procedureName = procedureName(table.getName());
@@ -187,67 +132,14 @@ protected void addStoredFunction(DdlWrite writer, MTable table, HistoryTableUpda
     }
   }
 
-  private void addFunction(DdlBuffer apply, String procedureName, String historyTable, List<String> includedColumns) throws IOException {
-    apply
-        .append("create or replace function ").append(procedureName).append("() returns trigger as $$").newLine()
-        .append("begin").newLine();
-    apply
-        .append("  if (TG_OP = 'INSERT') then").newLine()
-        .append("    NEW.").append(sysPeriod).append(" = tstzrange(CURRENT_TIMESTAMP,null);").newLine()
-        .append("    return new;").newLine().newLine();
-    apply
-        .append("  elsif (TG_OP = 'UPDATE') then").newLine();
-    appendInsertIntoHistory(apply, historyTable, includedColumns);
-    apply
-        .append("    NEW.").append(sysPeriod).append(" = tstzrange(CURRENT_TIMESTAMP,null);").newLine()
-        .append("    return new;").newLine().newLine();
-    apply
-        .append("  elsif (TG_OP = 'DELETE') then").newLine();
-    appendInsertIntoHistory(apply, historyTable, includedColumns);
-    apply
-        .append("    return old;").newLine().newLine();
-
-    apply
-        .append("  end if;").newLine()
-        .append("end;").newLine()
-        .append("$$ LANGUAGE plpgsql;").newLine();
-
-    apply.end();
-  }
-
+  @Override
   protected void appendInsertIntoHistory(DdlBuffer buffer, String historyTable, List<String> columns) throws IOException {
 
     buffer.append("    insert into ").append(historyTable).append(" (").append(sysPeriod).append(",");
     appendColumnNames(buffer, columns, "");
-    buffer.append(") values (tstzrange(lower(OLD.").append(sysPeriod).append("), CURRENT_TIMESTAMP), ");
+    buffer.append(") values (tstzrange(lower(OLD.").append(sysPeriod).append("), current_timestamp), ");
     appendColumnNames(buffer, columns, "OLD.");
     buffer.append(");").newLine();
   }
 
-  protected void appendColumnNames(DdlBuffer buffer, List<String> columns, String columnPrefix) throws IOException {
-
-    for (int i=0; i< columns.size(); i++) {
-      if (i > 0) {
-        buffer.append(", ");
-      }
-      buffer.append(columnPrefix);
-      buffer.append(columns.get(i));
-    }
-  }
-
-  /**
-   * Return the list of included columns in order.
-   */
-  protected List<String> includedColumnNames(MTable table) throws IOException {
-
-    Collection<MColumn> columns = table.getColumns().values();
-    List<String> includedColumns = new ArrayList<String>(columns.size());
-
-    for (MColumn column : columns) {
-      if (!column.isHistoryExclude()) {
-        includedColumns.add(column.getName());
-      }
-    }
-    return includedColumns;
-  }
 }