diff --git a/pom.xml b/pom.xml
index 1df6c124d..89e20eaf9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -147,12 +147,14 @@
4.7.1
test
-
+
+
com.h2database
h2
- 1.4.182
- test
+ 1.4.189
+ provided
diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbViewHistorySupport.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbViewHistorySupport.java
index 210027c66..9d9f0a7d6 100644
--- a/src/main/java/com/avaje/ebean/config/dbplatform/DbViewHistorySupport.java
+++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbViewHistorySupport.java
@@ -55,7 +55,7 @@ public abstract class DbViewHistorySupport implements DbHistorySupport {
public String getAsOfPredicate(String asOfTableAlias, String asOfSysPeriod) {
// (sys_period_start < ? and (sys_period_end is null or sys_period_end > ?));
- return "(" + asOfTableAlias + "." + asOfSysPeriod + "_start" + " < ? and (" + asOfTableAlias + "." + asOfSysPeriod + "_end" + " is null or " + asOfTableAlias + "." + asOfSysPeriod + "_end" + " > ?))";
+ return "(" + asOfTableAlias + "." + asOfSysPeriod + "_start" + " <= ? and (" + asOfTableAlias + "." + asOfSysPeriod + "_end" + " is null or " + asOfTableAlias + "." + asOfSysPeriod + "_end" + " > ?))";
}
/**
diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/H2HistorySupport.java b/src/main/java/com/avaje/ebean/config/dbplatform/H2HistorySupport.java
new file mode 100644
index 000000000..b8aa4a023
--- /dev/null
+++ b/src/main/java/com/avaje/ebean/config/dbplatform/H2HistorySupport.java
@@ -0,0 +1,8 @@
+package com.avaje.ebean.config.dbplatform;
+
+/**
+ * Runtime support for @History with H2.
+ */
+public class H2HistorySupport extends DbViewHistorySupport {
+
+}
diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/H2HistoryTrigger.java b/src/main/java/com/avaje/ebean/config/dbplatform/H2HistoryTrigger.java
new file mode 100644
index 000000000..2e091dead
--- /dev/null
+++ b/src/main/java/com/avaje/ebean/config/dbplatform/H2HistoryTrigger.java
@@ -0,0 +1,127 @@
+package com.avaje.ebean.config.dbplatform;
+
+import org.h2.api.Trigger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * H2 database trigger used to populate history tables to support the @History feature.
+ */
+public class H2HistoryTrigger implements Trigger {
+
+ private static final Logger logger = LoggerFactory.getLogger(H2HistoryTrigger.class);
+
+ /**
+ * Hardcoding the column and history table suffix for now. Not sure how to get that
+ * configuration into the trigger instance nicely as it is instantiated by H2.
+ */
+ private static final String SYS_PERIOD_START = "SYS_PERIOD_START";
+ private static final String SYS_PERIOD_END = "SYS_PERIOD_END";
+ private static final String HISTORY_SUFFIX = "_history";
+
+ /**
+ * SQL to insert into the history table.
+ */
+ private String insertHistorySql;
+
+ /**
+ * Position of SYS_PERIOD_START column in the Object[].
+ */
+ private int effectStartPosition;
+
+ /**
+ * Position of SYS_PERIOD_END column in the Object[].
+ */
+ private int effectEndPosition;
+
+ @Override
+ public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException {
+
+ // get the columns for the table
+ ResultSet rs = conn.getMetaData().getColumns(null, schemaName, tableName, null);
+
+ // build the insert into history table SQL
+ StringBuilder insertSql = new StringBuilder(150);
+ insertSql.append("insert into ").append(tableName).append(HISTORY_SUFFIX).append(" (");
+
+ int count = 0;
+ List columns = new ArrayList();
+ while (rs.next()) {
+ if (++count > 1) {
+ insertSql.append(",");
+ }
+ String columnName = rs.getString("COLUMN_NAME");
+ if (columnName.equalsIgnoreCase(SYS_PERIOD_START)) {
+ this.effectStartPosition = count - 1;
+ } else if (columnName.equalsIgnoreCase(SYS_PERIOD_END)) {
+ this.effectEndPosition = count - 1;
+ }
+ insertSql.append(columnName);
+ columns.add(columnName);
+ }
+ insertSql.append(") values (");
+ for (int i = 0; i < count; i++) {
+ if (i > 0) {
+ insertSql.append(",");
+ }
+ insertSql.append("?");
+ }
+ insertSql.append(");");
+
+ this.insertHistorySql = insertSql.toString();
+ logger.debug("History table insert sql: {}", insertHistorySql);
+ }
+
+ @Override
+ public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException {
+
+ if (oldRow != null) {
+ // a delete or update event
+ Timestamp now = new Timestamp(System.currentTimeMillis());
+ oldRow[effectEndPosition] = now;
+ if (newRow != null) {
+ // update event. Set the effective start timestamp to now.
+ newRow[effectStartPosition] = now;
+ }
+ if (logger.isDebugEnabled()) {
+ logger.debug("History insert: {}", Arrays.toString(oldRow));
+ }
+ insertIntoHistory(connection, oldRow);
+ }
+ }
+
+ /**
+ * Insert the data into the history table.
+ */
+ private void insertIntoHistory(Connection connection, Object[] oldRow) throws SQLException {
+
+ PreparedStatement stmt = connection.prepareStatement(insertHistorySql);
+ try {
+ for (int i = 0; i < oldRow.length; i++) {
+ stmt.setObject(i + 1, oldRow[i]);
+ }
+ stmt.executeUpdate();
+ } finally {
+ stmt.close();
+ }
+ }
+
+ @Override
+ public void close() throws SQLException {
+
+ }
+
+ @Override
+ public void remove() throws SQLException {
+
+ }
+}
diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/H2Platform.java b/src/main/java/com/avaje/ebean/config/dbplatform/H2Platform.java
index 521f8b55c..8e8189032 100644
--- a/src/main/java/com/avaje/ebean/config/dbplatform/H2Platform.java
+++ b/src/main/java/com/avaje/ebean/config/dbplatform/H2Platform.java
@@ -15,6 +15,7 @@ public class H2Platform extends DatabasePlatform {
this.name = "h2";
this.dbEncrypt = new H2DbEncrypt();
this.platformDdl = new H2Ddl(this.dbTypeMap, dbIdentity);
+ this.historySupport = new H2HistorySupport();
// only support getGeneratedKeys with non-batch JDBC
// so generally use SEQUENCE instead of IDENTITY for H2
diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/H2Ddl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/H2Ddl.java
index d274b7b66..cfeaf1a1f 100644
--- a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/H2Ddl.java
+++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/H2Ddl.java
@@ -10,6 +10,7 @@ public class H2Ddl extends PlatformDdl {
public H2Ddl(DbTypeMap platformTypes, DbIdentity dbIdentity) {
super(platformTypes, dbIdentity);
+ this.historyDdl = new H2HistoryDdl();
}
}
diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/H2HistoryDdl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/H2HistoryDdl.java
new file mode 100644
index 000000000..46b7759af
--- /dev/null
+++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/H2HistoryDdl.java
@@ -0,0 +1,73 @@
+package com.avaje.ebean.dbmigration.ddlgeneration.platform;
+
+import com.avaje.ebean.config.dbplatform.H2HistoryTrigger;
+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;
+
+/**
+ * H2 history support using DB triggers to maintain a history table.
+ */
+public class H2HistoryDdl extends DbTriggerBasedHistoryDdl {
+
+ private static final String TRIGGER_CLASS = H2HistoryTrigger.class.getName();
+
+ public H2HistoryDdl() {
+ }
+
+ @Override
+ protected void dropTriggers(DdlBuffer buffer, String baseTable) throws IOException {
+
+ buffer.append("drop trigger ").append(updateTriggerName(baseTable)).endOfStatement();
+ }
+
+ @Override
+ protected void createTriggers(DdlWrite writer, MTable table) throws IOException {
+
+ String baseTableName = table.getName();
+ String historyTableName = historyTableName(baseTableName);
+ List includedColumns = includedColumnNames(table);
+
+ DdlBuffer apply = writer.applyHistory();
+
+ addCreateTrigger(apply, updateTriggerName(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 includedColumns = includedColumnNames(table);
+
+ DdlBuffer apply = writer.applyHistory();
+
+ apply.append("-- Regenerated ").newLine();
+ apply.append("-- changes: ").append(update.description()).newLine();
+
+ dropTriggers(apply, baseTableName);
+ addCreateTrigger(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
+
+ // 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();
+ dropTriggers(rollback, baseTableName);
+ addCreateTrigger(rollback, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
+ }
+
+ private void addCreateTrigger(DdlBuffer apply, String triggerName, String baseTable, String historyTable, List includedColumns) throws IOException {
+
+ // Note that this does not take into account the historyTable name (excepts _history suffix) and
+ // does not take into account excluded columns (all columns included in history)
+ apply
+ .append("create trigger ").append(triggerName).append(" before update,delete on ").append(baseTable)
+ .append(" for each row call \"" + TRIGGER_CLASS + "\";").newLine();
+ }
+
+}
diff --git a/src/test/java/com/avaje/ebean/dbmigration/model/ModelContainerApplyTest.java b/src/test/java/com/avaje/ebean/dbmigration/model/ModelContainerApplyTest.java
index 0e35e847f..bec2f3760 100644
--- a/src/test/java/com/avaje/ebean/dbmigration/model/ModelContainerApplyTest.java
+++ b/src/test/java/com/avaje/ebean/dbmigration/model/ModelContainerApplyTest.java
@@ -35,7 +35,7 @@ public class ModelContainerApplyTest {
assertThat(foo.getComment()).isEqualTo("comment");
assertThat(foo.getTablespace()).isEqualTo("fooSpace");
assertThat(foo.getIndexTablespace()).isEqualTo("fooIndexSpace");
- assertThat(foo.isWithHistory()).isEqualTo(true);
+ assertThat(foo.isWithHistory()).isEqualTo(false);
assertThat(foo.getColumns()).containsKeys("col1", "col3", "added_to_foo");
}
}
\ No newline at end of file
diff --git a/src/test/java/com/avaje/tests/history/TestHistoryInsert.java b/src/test/java/com/avaje/tests/history/TestHistoryInsert.java
new file mode 100644
index 000000000..9fda60e7b
--- /dev/null
+++ b/src/test/java/com/avaje/tests/history/TestHistoryInsert.java
@@ -0,0 +1,83 @@
+package com.avaje.tests.history;
+
+import com.avaje.ebean.BaseTestCase;
+import com.avaje.ebean.Ebean;
+import com.avaje.ebean.SqlQuery;
+import com.avaje.ebean.SqlRow;
+import com.avaje.ebean.Version;
+import com.avaje.tests.model.converstation.User;
+import org.junit.Test;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestHistoryInsert extends BaseTestCase {
+
+ @Test
+ public void test() {
+
+ User user = new User();
+ user.setName("Jim");
+ user.setEmail("one@email.com");
+
+ Ebean.save(user);
+
+ Timestamp afterInsert = new Timestamp(System.currentTimeMillis());
+
+ List history = fetchHistory(user);
+ assertThat(history).isEmpty();
+
+ List> versions = Ebean.find(User.class).setId(user.getId()).findVersions();
+ assertThat(versions).hasSize(1);
+
+ user.setName("Jim v2");
+ Ebean.save(user);
+
+ history = fetchHistory(user);
+ assertThat(history).hasSize(1);
+ assertThat(history.get(0).getString("name")).isEqualTo("Jim");
+
+ versions = Ebean.find(User.class).setId(user.getId()).findVersions();
+ assertThat(versions).hasSize(2);
+ assertThat(versions.get(0).getDiff()).containsKeys("name", "version", "whenModified");
+
+ user.setName("Jim v3");
+ user.setEmail("three@email.com");
+ Ebean.save(user);
+
+ history = fetchHistory(user);
+ assertThat(history).hasSize(2);
+ assertThat(history.get(1).getString("name")).isEqualTo("Jim v2");
+ assertThat(history.get(1).getString("email")).isEqualTo("one@email.com");
+
+ versions = Ebean.find(User.class).setId(user.getId()).findVersions();
+ assertThat(versions).hasSize(3);
+ assertThat(versions.get(0).getDiff()).containsKeys("name", "email", "version", "whenModified");
+
+ Ebean.delete(user);
+
+ User earlyVersion = Ebean.find(User.class).setId(user.getId()).asOf(afterInsert).findUnique();
+ assertThat(earlyVersion.getName()).isEqualTo("Jim");
+ assertThat(earlyVersion.getEmail()).isEqualTo("one@email.com");
+
+
+ history = fetchHistory(user);
+ assertThat(history).hasSize(3);
+ assertThat(history.get(2).getString("name")).isEqualTo("Jim v3");
+ assertThat(history.get(2).getString("email")).isEqualTo("three@email.com");
+
+ versions = Ebean.find(User.class).setId(user.getId()).findVersions();
+ assertThat(versions).hasSize(3);
+ }
+
+ /**
+ * Use SqlQuery to query the history table directly.
+ */
+ private List fetchHistory(User user) {
+ SqlQuery sqlQuery = Ebean.createSqlQuery("select * from c_user_history where id = :id order by sys_period_start");
+ sqlQuery.setParameter("id", user.getId());
+ return sqlQuery.findList();
+ }
+}
diff --git a/src/test/java/com/avaje/tests/model/converstation/User.java b/src/test/java/com/avaje/tests/model/converstation/User.java
index 0b94cabeb..3e365901c 100644
--- a/src/test/java/com/avaje/tests/model/converstation/User.java
+++ b/src/test/java/com/avaje/tests/model/converstation/User.java
@@ -4,8 +4,10 @@ import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
+import com.avaje.ebean.annotation.History;
import com.avaje.tests.model.BaseModel;
+@History
@Entity
@Table(name="c_user")
public class User extends BaseModel {
diff --git a/src/test/resources/container/test-create-table.xml b/src/test/resources/container/test-create-table.xml
index 22ad62f4a..9c5bc2e30 100644
--- a/src/test/resources/container/test-create-table.xml
+++ b/src/test/resources/container/test-create-table.xml
@@ -4,7 +4,7 @@
-
+
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
index 3fadff078..0feee6d67 100644
--- a/src/test/resources/logback-test.xml
+++ b/src/test/resources/logback-test.xml
@@ -86,6 +86,6 @@
-
+
\ No newline at end of file