Add transaction support for Jdbc dialects

Transaction support is a no-op for Jdbc dialects.
In some rare cases it's however indispensable to have transaction
support, as in:

  beginTransaction();
  try {
    insertPatchSetAncestors();
    insertPatchSets();
    updateChange();
    commit();
  } catch (OrmException e) {
    rollback();
  }

I assume that I65e111bed was trying to achieve just that.  Make it work
also for Jdbc dialects and not only for NoSQL dialects.

Bug: issue 2034
Bug: issue 2246
Bug: issue 2383
Bug: issue 2702
Change-Id: I29ab37e1cbc78468cb3f8f6abfd1bf54359e6090
diff --git a/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java b/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
index 7e2a611..aaddfa3 100644
--- a/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
+++ b/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
@@ -39,6 +39,15 @@
   }
 
   @Override
+  public void beginTransaction(K key) throws OrmException {
+    try {
+      schema.getConnection().setAutoCommit(false);
+    } catch (SQLException e) {
+      throw convertError("beginTransaction", e);
+    }
+  }
+
+  @Override
   public final com.google.gwtorm.server.ResultSet<T> get(final Iterable<K> keys)
       throws OrmException {
     final Collection<K> keySet;
diff --git a/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java b/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
index 5c24e4e..c7c5b5a 100644
--- a/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
+++ b/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
@@ -49,6 +49,36 @@
   }
 
   @Override
+  public void commit() throws OrmException {
+    try {
+      conn.commit();
+    } catch (SQLException err) {
+      throw new OrmException("Cannot commit transaction", err);
+    } finally {
+      try {
+        conn.setAutoCommit(true);
+      } catch (SQLException err) {
+        throw new OrmException("Cannot set auto commit mode", err);
+      }
+    }
+  }
+
+  @Override
+  public void rollback() throws OrmException {
+    try {
+      conn.rollback();
+    } catch (SQLException err) {
+      throw new OrmException("Cannot rollback transaction", err);
+    } finally {
+      try {
+        conn.setAutoCommit(true);
+      } catch (SQLException err) {
+        throw new OrmException("Cannot set auto commit mode", err);
+      }
+    }
+  }
+
+  @Override
   public void updateSchema(final StatementExecutor e) throws OrmException {
     try {
       createSequences(e);
diff --git a/src/test/java/com/google/gwtorm/schema/sql/DialectH2Test.java b/src/test/java/com/google/gwtorm/schema/sql/DialectH2Test.java
index 68ca123..ce37fbe 100644
--- a/src/test/java/com/google/gwtorm/schema/sql/DialectH2Test.java
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectH2Test.java
@@ -35,7 +35,9 @@
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Properties;
 import java.util.Set;
 
@@ -64,6 +66,16 @@
 
   @After
   public void tearDown() {
+    // Database content must be flushed because
+    // tests assume that the database is empty
+    drop("SEQUENCE address_id");
+    drop("SEQUENCE cnt");
+
+    drop("TABLE addresses");
+    drop("TABLE foo");
+    drop("TABLE bar");
+    drop("TABLE people");
+
     if (executor != null) {
       executor.close();
     }
@@ -79,6 +91,13 @@
     db = null;
   }
 
+  private void drop(String drop) {
+    try {
+      execute("DROP " + drop);
+    } catch (OrmException e) {
+    }
+  }
+
   private void execute(final String sql) throws OrmException {
     executor.execute(sql);
   }
@@ -135,8 +154,6 @@
 
       execute("ALTER TABLE people ADD COLUMN fake_name VARCHAR(20)");
       execute("ALTER TABLE people DROP COLUMN registered");
-      execute("DROP TABLE addresses");
-      execute("DROP SEQUENCE address_id");
 
       Set<String> sequences, tables;
 
@@ -192,4 +209,30 @@
     assertTrue(s.contains("bar"));
     assertFalse(s.contains("for"));
   }
+
+  @Test
+  public void testRollbackTransaction() throws SQLException, OrmException {
+    PhoneBookDb schema = phoneBook.open();
+    schema.updateSchema(executor);
+    schema.people().beginTransaction(null);
+    ArrayList<Person> all = new ArrayList<>();
+    all.add(new Person(new Person.Key("Bob"), 18));
+    schema.people().insert(all);
+    schema.rollback();
+    List<Person> r = schema.people().olderThan(10).toList();
+    assertEquals(0, r.size());
+  }
+
+  @Test
+  public void testCommitTransaction() throws SQLException, OrmException {
+    PhoneBookDb schema = phoneBook.open();
+    schema.updateSchema(executor);
+    schema.people().beginTransaction(null);
+    ArrayList<Person> all = new ArrayList<>();
+    all.add(new Person(new Person.Key("Bob"), 18));
+    schema.people().insert(all);
+    schema.commit();
+    List<Person> r = schema.people().olderThan(10).toList();
+    assertEquals(1, r.size());
+  }
 }
diff --git a/src/test/java/com/google/gwtorm/schema/sql/DialectMaxDBTest.java b/src/test/java/com/google/gwtorm/schema/sql/DialectMaxDBTest.java
index bea6ae8..9bab614 100644
--- a/src/test/java/com/google/gwtorm/schema/sql/DialectMaxDBTest.java
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectMaxDBTest.java
@@ -40,6 +40,8 @@
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Properties;
 import java.util.Set;
 
@@ -79,14 +81,6 @@
         new Database<PhoneBookDb>(new SimpleDataSource(p), PhoneBookDb.class);
     phoneBook2 =
         new Database<PhoneBookDb2>(new SimpleDataSource(p), PhoneBookDb2.class);
-
-    drop("SEQUENCE address_id");
-    drop("SEQUENCE cnt");
-
-    drop("TABLE addresses");
-    drop("TABLE foo");
-    drop("TABLE bar");
-    drop("TABLE people");
   }
 
   private void drop(String drop) {
@@ -98,6 +92,16 @@
 
   @After
   public void tearDown() {
+    // Database content must be flushed because
+    // tests assume that the database is empty
+    drop("SEQUENCE address_id");
+    drop("SEQUENCE cnt");
+
+    drop("TABLE addresses");
+    drop("TABLE foo");
+    drop("TABLE bar");
+    drop("TABLE people");
+
     if (executor != null) {
       executor.close();
     }
@@ -326,4 +330,29 @@
     }
   }
 
+  @Test
+  public void testRollbackTransaction() throws SQLException, OrmException {
+    PhoneBookDb schema = phoneBook.open();
+    schema.updateSchema(executor);
+    schema.people().beginTransaction(null);
+    ArrayList<Person> all = new ArrayList<>();
+    all.add(new Person(new Person.Key("Bob"), 18));
+    schema.people().insert(all);
+    schema.rollback();
+    List<Person> r = schema.people().olderThan(10).toList();
+    assertEquals(0, r.size());
+  }
+
+  @Test
+  public void testCommitTransaction() throws SQLException, OrmException {
+    PhoneBookDb schema = phoneBook.open();
+    schema.updateSchema(executor);
+    schema.people().beginTransaction(null);
+    ArrayList<Person> all = new ArrayList<>();
+    all.add(new Person(new Person.Key("Bob"), 18));
+    schema.people().insert(all);
+    schema.commit();
+    List<Person> r = schema.people().olderThan(10).toList();
+    assertEquals(1, r.size());
+  }
 }
diff --git a/src/test/java/com/google/gwtorm/schema/sql/DialectMySQLTest.java b/src/test/java/com/google/gwtorm/schema/sql/DialectMySQLTest.java
index e20435f..b0c8db5 100644
--- a/src/test/java/com/google/gwtorm/schema/sql/DialectMySQLTest.java
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectMySQLTest.java
@@ -37,7 +37,9 @@
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Properties;
 import java.util.Set;
 
@@ -75,24 +77,19 @@
         new Database<PhoneBookDb>(new SimpleDataSource(p), PhoneBookDb.class);
     phoneBook2 =
         new Database<PhoneBookDb2>(new SimpleDataSource(p), PhoneBookDb2.class);
+  }
 
+  @After
+  public void tearDown() {
+    // Database content must be flushed because
+    // tests assume that the database is empty
     drop("TABLE address_id");
     drop("TABLE addresses");
     drop("TABLE cnt");
     drop("TABLE bar");
     drop("TABLE foo");
     drop("TABLE people");
-  }
 
-  private void drop(String drop) {
-    try {
-      execute("DROP " + drop);
-    } catch (OrmException e) {
-    }
-  }
-
-  @After
-  public void tearDown() {
     if (executor != null) {
       executor.close();
     }
@@ -108,6 +105,13 @@
     db = null;
   }
 
+  private void drop(String drop) {
+    try {
+      execute("DROP " + drop);
+    } catch (OrmException e) {
+    }
+  }
+
   private void execute(final String sql) throws OrmException {
     executor.execute(sql);
   }
@@ -220,4 +224,30 @@
     assertTrue(s.contains("bar"));
     assertFalse(s.contains("for"));
   }
+
+  @Test
+  public void testRollbackTransaction() throws SQLException, OrmException {
+    PhoneBookDb schema = phoneBook.open();
+    schema.updateSchema(executor);
+    schema.people().beginTransaction(null);
+    ArrayList<Person> all = new ArrayList<>();
+    all.add(new Person(new Person.Key("Bob"), 18));
+    schema.people().insert(all);
+    schema.rollback();
+    List<Person> r = schema.people().olderThan(10).toList();
+    assertEquals(0, r.size());
+  }
+
+  @Test
+  public void testCommitTransaction() throws SQLException, OrmException {
+    PhoneBookDb schema = phoneBook.open();
+    schema.updateSchema(executor);
+    schema.people().beginTransaction(null);
+    ArrayList<Person> all = new ArrayList<>();
+    all.add(new Person(new Person.Key("Bob"), 18));
+    schema.people().insert(all);
+    schema.commit();
+    List<Person> r = schema.people().olderThan(10).toList();
+    assertEquals(1, r.size());
+  }
 }
diff --git a/src/test/java/com/google/gwtorm/schema/sql/DialectOracleSQLTest.java b/src/test/java/com/google/gwtorm/schema/sql/DialectOracleSQLTest.java
index 43f513d..bb4d7fb 100644
--- a/src/test/java/com/google/gwtorm/schema/sql/DialectOracleSQLTest.java
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectOracleSQLTest.java
@@ -36,7 +36,9 @@
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Properties;
 import java.util.Set;
 
@@ -74,7 +76,12 @@
         new Database<PhoneBookDb>(new SimpleDataSource(p), PhoneBookDb.class);
     phoneBook2 =
         new Database<PhoneBookDb2>(new SimpleDataSource(p), PhoneBookDb2.class);
+  }
 
+  @After
+  public void tearDown() {
+    // Database content must be flushed because
+    // tests assume that the database is empty
     drop("SEQUENCE address_id");
     drop("SEQUENCE cnt");
 
@@ -82,17 +89,7 @@
     drop("TABLE foo");
     drop("TABLE bar");
     drop("TABLE people");
-  }
 
-  private void drop(String drop) {
-    try {
-      execute("DROP " + drop);
-    } catch (OrmException e) {
-    }
-  }
-
-  @After
-  public void tearDown() {
     if (executor != null) {
       executor.close();
     }
@@ -108,6 +105,13 @@
     db = null;
   }
 
+  private void drop(String drop) {
+    try {
+      execute("DROP " + drop);
+    } catch (OrmException e) {
+    }
+  }
+
   private void execute(final String sql) throws OrmException {
     executor.execute(sql);
   }
@@ -220,6 +224,31 @@
     s = dialect.listTables(db);
     assertTrue(s.contains("bar"));
     assertFalse(s.contains("for"));
+  }
 
+  @Test
+  public void testRollbackTransaction() throws SQLException, OrmException {
+    PhoneBookDb schema = phoneBook.open();
+    schema.updateSchema(executor);
+    schema.people().beginTransaction(null);
+    ArrayList<Person> all = new ArrayList<>();
+    all.add(new Person(new Person.Key("Bob"), 18));
+    schema.people().insert(all);
+    schema.rollback();
+    List<Person> r = schema.people().olderThan(10).toList();
+    assertEquals(0, r.size());
+  }
+
+  @Test
+  public void testCommitTransaction() throws SQLException, OrmException {
+    PhoneBookDb schema = phoneBook.open();
+    schema.updateSchema(executor);
+    schema.people().beginTransaction(null);
+    ArrayList<Person> all = new ArrayList<>();
+    all.add(new Person(new Person.Key("Bob"), 18));
+    schema.people().insert(all);
+    schema.commit();
+    List<Person> r = schema.people().olderThan(10).toList();
+    assertEquals(1, r.size());
   }
 }
diff --git a/src/test/java/com/google/gwtorm/schema/sql/DialectPostgreSQLTest.java b/src/test/java/com/google/gwtorm/schema/sql/DialectPostgreSQLTest.java
index ed20480..aae302f 100644
--- a/src/test/java/com/google/gwtorm/schema/sql/DialectPostgreSQLTest.java
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectPostgreSQLTest.java
@@ -37,7 +37,9 @@
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Properties;
 import java.util.Set;
 
@@ -73,7 +75,12 @@
         new Database<PhoneBookDb>(new SimpleDataSource(p), PhoneBookDb.class);
     phoneBook2 =
         new Database<PhoneBookDb2>(new SimpleDataSource(p), PhoneBookDb2.class);
+  }
 
+  @After
+  public void tearDown() {
+    // Database content must be flushed because
+    // tests assume that the database is empty
     drop("SEQUENCE address_id");
     drop("SEQUENCE cnt");
 
@@ -81,17 +88,7 @@
     drop("TABLE foo");
     drop("TABLE bar");
     drop("TABLE people");
-  }
 
-  private void drop(String drop) {
-    try {
-      execute("DROP " + drop);
-    } catch (OrmException e) {
-    }
-  }
-
-  @After
-  public void tearDown() {
     if (executor != null) {
       executor.close();
     }
@@ -107,6 +104,13 @@
     db = null;
   }
 
+  private void drop(String drop) {
+    try {
+      execute("DROP " + drop);
+    } catch (OrmException e) {
+    }
+  }
+
   private void execute(final String sql) throws OrmException {
     executor.execute(sql);
   }
@@ -219,4 +223,30 @@
     assertTrue(s.contains("bar"));
     assertFalse(s.contains("for"));
   }
+
+  @Test
+  public void testRollbackTransaction() throws SQLException, OrmException {
+    PhoneBookDb schema = phoneBook.open();
+    schema.updateSchema(executor);
+    schema.people().beginTransaction(null);
+    ArrayList<Person> all = new ArrayList<>();
+    all.add(new Person(new Person.Key("Bob"), 18));
+    schema.people().insert(all);
+    schema.rollback();
+    List<Person> r = schema.people().olderThan(10).toList();
+    assertEquals(0, r.size());
+  }
+
+  @Test
+  public void testCommitTransaction() throws SQLException, OrmException {
+    PhoneBookDb schema = phoneBook.open();
+    schema.updateSchema(executor);
+    schema.people().beginTransaction(null);
+    ArrayList<Person> all = new ArrayList<>();
+    all.add(new Person(new Person.Key("Bob"), 18));
+    schema.people().insert(all);
+    schema.commit();
+    List<Person> r = schema.people().olderThan(10).toList();
+    assertEquals(1, r.size());
+  }
 }