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; - } }