Merge branch 'stable-1.7'

* stable-1.7:
  gwtorm 1.7.3
  Only bind LIMIT variable in SQL when dialect supports it
  Start 1.7.3 development
  gwtorm 1.7.2
  Make rollback() and commit() no ops when no transaction is active
  Fix conditional unit test skipping
  Start 1.7.2

Conflicts:
	pom.xml

Change-Id: Iefbfa04cfdcc399bf083a2bea4d0e7dc8c3b3447
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 5002612..6dbcac5 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -1,15 +1,15 @@
 #Wed May 13 16:33:08 PDT 2009
 eclipse.preferences.version=1
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.compliance=1.7
 org.eclipse.jdt.core.compiler.debug.lineNumber=generate
 org.eclipse.jdt.core.compiler.debug.localVariable=generate
 org.eclipse.jdt.core.compiler.debug.sourceFile=generate
 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
 org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
-org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.compiler.source=1.7
 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
diff --git a/README_MAXDB b/README_MAXDB
new file mode 100644
index 0000000..7230e5b
--- /dev/null
+++ b/README_MAXDB
@@ -0,0 +1,23 @@
+To test DialectMaxDB a SAP MaxDB JDBC driver "sapdbc.jar" is needed. It is
+not available in a public maven repository. However, the driver can be found
+in your MaxDB installation at the following location:
+
+- on Windows 64bit at "C:\Program Files\sdb\MaxDB\runtime\jar\sapdbc.jar"
+- on Linux at "/opt/sdb/MaxDB/runtime/jar/sapdbc.jar"
+
+To execute tests on MaxDB, you firstly need to create a test user with an
+associated empty schema in your database. Then you can execute the tests
+using maven with the profile "maxdb". The following properties need to be set:
+
+maxdb.driver.jar=<path to maxdb jdbc driver>
+maxdb.url=<url of test database>
+maxdb.user=<user name>
+maxdb.password=<password of test user>
+
+So the complete command would be:
+
+mvn package -P maxdb
+  -Dmaxdb.driver.jar=<path to maxdb jdbc driver>
+  -Dmaxdb.url=<url of test database>
+  -Dmaxdb.user=<user name>
+  -Dmaxdb.password=<password of test user>
diff --git a/pom.xml b/pom.xml
index a6a31cb..603e8b9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,14 +18,19 @@
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
-  <groupId>gwtorm</groupId>
+  <groupId>com.google.gerrit</groupId>
   <artifactId>gwtorm</artifactId>
   <packaging>jar</packaging>
-  <version>1.7.3</version>
+  <version>1.13-SNAPSHOT</version>
   <name>gwtorm</name>
   <description>Tiny ORM</description>
   <url>https://gerrit.googlesource.com/gwtorm</url>
 
+  <scm>
+    <url>https://gerrit.googlesource.com/gwtorm</url>
+    <connection>https://gerrit.googlesource.com/gwtorm</connection>
+  </scm>
+
   <mailingLists>
     <mailingList>
       <name>repo-discuss mailing list</name>
@@ -265,6 +270,23 @@
       </dependencies>
     </profile>
     <profile>
+      <id>maxdb</id>
+      <build>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <version>2.17</version>
+          <configuration>
+            <additionalClasspathElements>
+              <additionalClasspathElement>${maxdb.driver.jar}</additionalClasspathElement>
+            </additionalClasspathElements>
+          </configuration>
+        </plugin>
+      </plugins>
+    </build>
+    </profile>
+    <profile>
       <id>skip-proprietary-databases</id>
       <activation>
         <activeByDefault>true</activeByDefault>
@@ -290,8 +312,8 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
         <configuration>
-          <source>1.6</source>
-          <target>1.6</target>
+          <source>1.7</source>
+          <target>1.7</target>
           <encoding>UTF-8</encoding>
         </configuration>
       </plugin>
@@ -305,6 +327,26 @@
           </execution>
         </executions>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <version>2.9.1</version>
+        <configuration>
+          <encoding>UTF-8</encoding>
+          <quiet>true</quiet>
+          <links>
+            <link>http://java.sun.com/j2se/1.7.0/docs/api</link>
+          </links>
+        </configuration>
+        <executions>
+          <execution>
+            <id>attach-javadocs</id>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
     <extensions>
       <extension>
@@ -319,14 +361,14 @@
     <dependency>
       <groupId>org.ow2.asm</groupId>
       <artifactId>asm</artifactId>
-      <version>4.0</version>
+      <version>4.1</version>
       <scope>compile</scope>
     </dependency>
 
     <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
-      <version>13.0.1</version>
+      <version>15.0</version>
       <scope>compile</scope>
     </dependency>
 
@@ -375,7 +417,7 @@
     <dependency>
       <groupId>com.google.protobuf</groupId>
       <artifactId>protobuf-java</artifactId>
-      <version>2.4.1</version>
+      <version>2.5.0</version>
       <scope>provided</scope>
     </dependency>
   </dependencies>
diff --git a/src/main/java/com/google/gwtorm/client/CompoundKey.java b/src/main/java/com/google/gwtorm/client/CompoundKey.java
index 8224cac..0087ccb 100644
--- a/src/main/java/com/google/gwtorm/client/CompoundKey.java
+++ b/src/main/java/com/google/gwtorm/client/CompoundKey.java
@@ -33,6 +33,7 @@
   /**
    * @return the parent key instance; null if this is a root level key.
    */
+  @Override
   public P getParentKey() {
     return null;
   }
@@ -92,6 +93,7 @@
     return r.toString();
   }
 
+  @Override
   public void fromString(final String in) {
     final String[] parts = in.split(",");
     int p = 0;
diff --git a/src/main/java/com/google/gwtorm/client/IntKey.java b/src/main/java/com/google/gwtorm/client/IntKey.java
index e7995a3..f64493b 100644
--- a/src/main/java/com/google/gwtorm/client/IntKey.java
+++ b/src/main/java/com/google/gwtorm/client/IntKey.java
@@ -37,6 +37,7 @@
   /**
    * @return the parent key instance; null if this is a root level key.
    */
+  @Override
   public P getParentKey() {
     return null;
   }
@@ -72,6 +73,7 @@
     return r.toString();
   }
 
+  @Override
   public void fromString(final String in) {
     set(Integer.parseInt(KeyUtil.parseFromString(getParentKey(), in)));
   }
diff --git a/src/main/java/com/google/gwtorm/client/Key.java b/src/main/java/com/google/gwtorm/client/Key.java
index a08929a..b5b3d3a 100644
--- a/src/main/java/com/google/gwtorm/client/Key.java
+++ b/src/main/java/com/google/gwtorm/client/Key.java
@@ -31,11 +31,14 @@
    */
   public P getParentKey();
 
+  @Override
   public int hashCode();
 
+  @Override
   public boolean equals(Object o);
 
   /** @return the key, encoded in a string format . */
+  @Override
   public String toString();
 
   /** Reset this key instance to represent the data in the supplied string. */
diff --git a/src/main/java/com/google/gwtorm/client/KeyUtil.java b/src/main/java/com/google/gwtorm/client/KeyUtil.java
index d9caeb5..c4b4551 100644
--- a/src/main/java/com/google/gwtorm/client/KeyUtil.java
+++ b/src/main/java/com/google/gwtorm/client/KeyUtil.java
@@ -90,6 +90,7 @@
     if (comma < 0 && parent != null) {
       throw new IllegalArgumentException("Not enough components: " + in);
     }
+    assert(parent != null);
     parent.fromString(in.substring(0, comma));
     return decode(in.substring(comma + 1));
   }
diff --git a/src/main/java/com/google/gwtorm/client/LongKey.java b/src/main/java/com/google/gwtorm/client/LongKey.java
index 0fc4ae6..986285e 100644
--- a/src/main/java/com/google/gwtorm/client/LongKey.java
+++ b/src/main/java/com/google/gwtorm/client/LongKey.java
@@ -37,6 +37,7 @@
   /**
    * @return the parent key instance; null if this is a root level key.
    */
+  @Override
   public P getParentKey() {
     return null;
   }
@@ -72,6 +73,7 @@
     return r.toString();
   }
 
+  @Override
   public void fromString(final String in) {
     set(Long.parseLong(KeyUtil.parseFromString(getParentKey(), in)));
   }
diff --git a/src/main/java/com/google/gwtorm/client/ShortKey.java b/src/main/java/com/google/gwtorm/client/ShortKey.java
index 59783f4..abda6d1 100644
--- a/src/main/java/com/google/gwtorm/client/ShortKey.java
+++ b/src/main/java/com/google/gwtorm/client/ShortKey.java
@@ -38,6 +38,7 @@
   /**
    * @return the parent key instance; null if this is a root level key.
    */
+  @Override
   public P getParentKey() {
     return null;
   }
@@ -73,6 +74,7 @@
     return r.toString();
   }
 
+  @Override
   public void fromString(final String in) {
     set(Short.parseShort(KeyUtil.parseFromString(getParentKey(), in)));
   }
diff --git a/src/main/java/com/google/gwtorm/client/StringKey.java b/src/main/java/com/google/gwtorm/client/StringKey.java
index 4c85d2a..a4dcf4c 100644
--- a/src/main/java/com/google/gwtorm/client/StringKey.java
+++ b/src/main/java/com/google/gwtorm/client/StringKey.java
@@ -38,6 +38,7 @@
   /**
    * @return the parent key instance; null if this is a root level key.
    */
+  @Override
   public P getParentKey() {
     return null;
   }
@@ -79,6 +80,7 @@
     return r.toString();
   }
 
+  @Override
   public void fromString(final String in) {
     set(KeyUtil.parseFromString(getParentKey(), in));
   }
diff --git a/src/main/java/com/google/gwtorm/jdbc/Database.java b/src/main/java/com/google/gwtorm/jdbc/Database.java
index 2c8d256..802bcec 100644
--- a/src/main/java/com/google/gwtorm/jdbc/Database.java
+++ b/src/main/java/com/google/gwtorm/jdbc/Database.java
@@ -117,6 +117,7 @@
    *         The JDBC exception detail should be examined to determine the root
    *         cause of the connection failure.
    */
+  @Override
   public T open() throws OrmException {
     return implFactory.open();
   }
diff --git a/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java b/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
index 758f727..188b833 100644
--- a/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
+++ b/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
@@ -82,6 +82,7 @@
     }
   }
 
+  @Override
   public void updateSchema(final StatementExecutor e) throws OrmException {
     try {
       createSequences(e);
@@ -175,6 +176,7 @@
     return null;
   }
 
+  @Override
   public void pruneSchema(final StatementExecutor e) throws OrmException {
     try {
       pruneSequences(e);
@@ -238,6 +240,7 @@
     return getDialect().nextLong(getConnection(), poolName);
   }
 
+  @Override
   public void close() {
     if (conn != null) {
       try {
diff --git a/src/main/java/com/google/gwtorm/jdbc/SimpleDataSource.java b/src/main/java/com/google/gwtorm/jdbc/SimpleDataSource.java
index 9bb96d2..a80a3a1 100644
--- a/src/main/java/com/google/gwtorm/jdbc/SimpleDataSource.java
+++ b/src/main/java/com/google/gwtorm/jdbc/SimpleDataSource.java
@@ -98,6 +98,7 @@
     return sqle;
   }
 
+  @Override
   public Connection getConnection() throws SQLException {
     if (driver != null) {
       return driver.connect(url, connectionInfo);
@@ -105,6 +106,7 @@
     return DriverManager.getConnection(url, connectionInfo);
   }
 
+  @Override
   public Connection getConnection(String user, String password)
       throws SQLException {
     if (driver != null) {
@@ -120,29 +122,36 @@
     return DriverManager.getConnection(url, user, password);
   }
 
+  @Override
   public PrintWriter getLogWriter() {
     return logWriter;
   }
 
+  @Override
   public void setLogWriter(final PrintWriter out) {
     logWriter = out;
   }
 
+  @Override
   public int getLoginTimeout() {
     return 0;
   }
 
+  @Override
   public void setLoginTimeout(int seconds) {
   }
 
+  @Override
   public boolean isWrapperFor(Class<?> iface) {
     return false;
   }
 
+  @Override
   public <T> T unwrap(Class<T> iface) throws SQLException {
     throw new SQLException(getClass().getName() + " wraps nothing");
   }
 
+  @Override
   public Logger getParentLogger() throws SQLFeatureNotSupportedException {
     throw new SQLFeatureNotSupportedException();
   }
diff --git a/src/main/java/com/google/gwtorm/schema/Util.java b/src/main/java/com/google/gwtorm/schema/Util.java
index 1ca5e5d..6a55c72 100644
--- a/src/main/java/com/google/gwtorm/schema/Util.java
+++ b/src/main/java/com/google/gwtorm/schema/Util.java
@@ -41,9 +41,16 @@
         r.append(c);
       }
     }
-    return r.toString();
+    String friendlyName = r.toString();
+    if (friendlyName.length() > 30) {
+      throw new IllegalArgumentException(String.format(
+          "Identifier '%s' for name '%s' is greater than 30 characters",
+          friendlyName, name));
+    }
+    return friendlyName;
   }
 
+
   public static String any(final String a, final String b) {
     if (a != null && a.length() > 0) {
       return a;
diff --git a/src/main/java/com/google/gwtorm/schema/sql/DialectMaxDB.java b/src/main/java/com/google/gwtorm/schema/sql/DialectMaxDB.java
new file mode 100644
index 0000000..b1cbc07
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/schema/sql/DialectMaxDB.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.schema.sql;
+
+import com.google.gwtorm.schema.ColumnModel;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Types;
+import java.util.HashSet;
+import java.util.Set;
+
+public class DialectMaxDB extends SqlDialect {
+
+  public DialectMaxDB() {
+    typeNames.put(Types.BIGINT, "FIXED (19,0)");
+    typeNames.put(Types.LONGVARCHAR, "LONG UNICODE");
+  }
+
+  @Override
+  public boolean canDetermineIndividualBatchUpdateCounts() {
+    return false;
+  }
+
+  @Override
+  public boolean canDetermineTotalBatchUpdateCount() {
+    return true;
+  }
+
+  @Override
+  public int executeBatch(PreparedStatement ps) throws SQLException {
+    ps.executeBatch();
+    return ps.getUpdateCount(); // total number of rows updated (on MaxDB)
+  }
+
+  @Override
+  public Set<String> listSequences(Connection conn) throws SQLException {
+    final Statement s = conn.createStatement();
+    try {
+      // lists sequences from schema associated with the current connection only
+      final ResultSet rs =
+          s.executeQuery("SELECT sequence_name FROM sequences");
+      try {
+        HashSet<String> sequences = new HashSet<String>();
+        while (rs.next()) {
+          sequences.add(rs.getString(1).toLowerCase());
+        }
+        return sequences;
+      } finally {
+        rs.close();
+      }
+    } finally {
+      s.close();
+    }
+  }
+
+  @Override
+  public void renameColumn(StatementExecutor e, String tableName,
+      String fromColumn, ColumnModel col) throws OrmException {
+    final StringBuilder s = new StringBuilder();
+    s.append("RENAME COLUMN ");
+    s.append(tableName).append(".").append(fromColumn);
+    s.append(" TO ");
+    s.append(col.getColumnName());
+    e.execute(s.toString());
+  }
+
+  @Override
+  public OrmException convertError(String op, String entity, SQLException err) {
+    int sqlstate = getSQLStateInt(err);
+    if (sqlstate == 23000) { // UNIQUE CONSTRAINT VIOLATION
+      int errorCode = err.getErrorCode();
+      if (errorCode == 200 || errorCode == -20) { // Duplicate Key
+        return new OrmDuplicateKeyException(entity, err);
+      }
+    }
+    return super.convertError(op, entity, err);
+  }
+
+  @Override
+  public String getNextSequenceValueSql(String seqname) {
+    return "SELECT " + seqname + ".nextval FROM dual";
+  }
+
+  @Override
+  public boolean handles(String url, Connection c) throws SQLException {
+    return url.startsWith("jdbc:sapdb:");
+  }
+
+  @Override
+  public void renameTable(StatementExecutor e, String from, String to)
+      throws OrmException {
+    final StringBuilder r = new StringBuilder();
+    r.append("RENAME TABLE ");
+    r.append(from);
+    r.append(" TO ");
+    r.append(to);
+    e.execute(r.toString());
+  }
+
+}
diff --git a/src/main/java/com/google/gwtorm/schema/sql/DialectOracle.java b/src/main/java/com/google/gwtorm/schema/sql/DialectOracle.java
index 63227be..827c656 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/DialectOracle.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/DialectOracle.java
@@ -19,6 +19,7 @@
 import com.google.gwtorm.server.StatementExecutor;
 
 import java.sql.Connection;
+import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
@@ -53,7 +54,7 @@
     try {
       ResultSet rs = s.executeQuery("SELECT table_name FROM user_tables");
       try {
-        HashSet<String> tables = new HashSet<String>();
+        Set<String> tables = new HashSet<String>();
         while (rs.next()) {
           tables.add(rs.getString(1).toLowerCase());
         }
@@ -67,12 +68,34 @@
   }
 
   @Override
+  public Set<String> listIndexes(final Connection db, String tableName)
+      throws SQLException {
+    PreparedStatement s = db.prepareStatement("SELECT distinct index_name"
+        + " FROM user_indexes WHERE table_name = ?");
+    try {
+      s.setString(1, tableName.toUpperCase());
+      ResultSet rs = s.executeQuery();
+      try {
+        Set<String> indexes = new HashSet<String>();
+        while (rs.next()) {
+          indexes.add(rs.getString(1).toLowerCase());
+        }
+        return indexes;
+      } finally {
+        rs.close();
+      }
+    } finally {
+      s.close();
+    }
+  }
+
+  @Override
   public Set<String> listSequences(Connection db) throws SQLException {
     Statement s = db.createStatement();
     try {
       ResultSet rs = s.executeQuery("SELECT sequence_name FROM user_sequences");
       try {
-        HashSet<String> sequences = new HashSet<String>();
+        Set<String> sequences = new HashSet<String>();
         while (rs.next()) {
           sequences.add(rs.getString(1).toLowerCase());
         }
diff --git a/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java b/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java
index ad1f16b..fdfe6c8 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java
@@ -44,6 +44,7 @@
     DIALECTS.add(new DialectPostgreSQL());
     DIALECTS.add(new DialectMySQL());
     DIALECTS.add(new DialectOracle());
+    DIALECTS.add(new DialectMaxDB());
   }
 
   public static void register(SqlDialect dialect) {
@@ -224,7 +225,7 @@
     final String[] types = new String[] {"TABLE"};
     final ResultSet rs = db.getMetaData().getTables(null, null, null, types);
     try {
-      HashSet<String> tables = new HashSet<String>();
+      Set<String> tables = new HashSet<String>();
       while (rs.next()) {
         tables.add(rs.getString("TABLE_NAME").toLowerCase());
       }
@@ -254,6 +255,35 @@
   }
 
   /**
+   * List all indexes for the given table name.
+   *
+   * @param db connection to the schema.
+   * @param tableName the table to list indexes from, in lowercase.
+   * @return set of declared indexes, in lowercase.
+   * @throws SQLException the indexes cannot be listed.
+   */
+  public Set<String> listIndexes(final Connection db, String tableName)
+      throws SQLException {
+    final DatabaseMetaData meta = db.getMetaData();
+    if (meta.storesUpperCaseIdentifiers()) {
+      tableName = tableName.toUpperCase();
+    } else if (meta.storesLowerCaseIdentifiers()) {
+      tableName = tableName.toLowerCase();
+    }
+
+    ResultSet rs = meta.getIndexInfo(null, null, tableName, false, true);
+    try {
+      Set<String> indexes = new HashSet<String>();
+      while (rs.next()) {
+        indexes.add(rs.getString("INDEX_NAME").toLowerCase());
+      }
+      return indexes;
+    } finally {
+      rs.close();
+    }
+  }
+
+  /**
    * List all sequences in the current database schema.
    *
    * @param db connection to the schema.
diff --git a/src/main/java/com/google/gwtorm/server/AbstractAccess.java b/src/main/java/com/google/gwtorm/server/AbstractAccess.java
index 8c6fd9f..f598630 100644
--- a/src/main/java/com/google/gwtorm/server/AbstractAccess.java
+++ b/src/main/java/com/google/gwtorm/server/AbstractAccess.java
@@ -32,6 +32,7 @@
     // Do nothing by default.
   }
 
+  @Override
   public CheckedFuture<E, OrmException> getAsync(K key) {
     try {
       return Futures.immediateCheckedFuture(get(key));
@@ -40,6 +41,7 @@
     }
   }
 
+  @Override
   public ResultSet<E> get(final Iterable<K> keys) throws OrmException {
     final ArrayList<E> r = new ArrayList<E>();
     for (final K key : keys) {
@@ -51,6 +53,7 @@
     return new ListResultSet<E>(r);
   }
 
+  @Override
   public Map<K, E> toMap(final Iterable<E> c) {
     try {
       final HashMap<K, E> r = new HashMap<K, E>();
diff --git a/src/main/java/com/google/gwtorm/server/AbstractResultSet.java b/src/main/java/com/google/gwtorm/server/AbstractResultSet.java
index c17270b..5eb2803 100644
--- a/src/main/java/com/google/gwtorm/server/AbstractResultSet.java
+++ b/src/main/java/com/google/gwtorm/server/AbstractResultSet.java
@@ -45,6 +45,7 @@
     };
   }
 
+  @Override
   public List<T> toList() {
     List<T> r = new ArrayList<T>();
     for (T obj : this) {
diff --git a/src/main/java/com/google/gwtorm/server/ListResultSet.java b/src/main/java/com/google/gwtorm/server/ListResultSet.java
index de6b224..642dd0c 100644
--- a/src/main/java/com/google/gwtorm/server/ListResultSet.java
+++ b/src/main/java/com/google/gwtorm/server/ListResultSet.java
@@ -26,10 +26,12 @@
     items = r;
   }
 
+  @Override
   public Iterator<T> iterator() {
     return toList().iterator();
   }
 
+  @Override
   public List<T> toList() {
     if (items == null) {
       throw new IllegalStateException("Results already obtained");
@@ -39,6 +41,7 @@
     return r;
   }
 
+  @Override
   public void close() {
     items = null;
   }
diff --git a/src/main/java/com/google/gwtorm/server/ResultSet.java b/src/main/java/com/google/gwtorm/server/ResultSet.java
index cf589c5..18f8c75 100644
--- a/src/main/java/com/google/gwtorm/server/ResultSet.java
+++ b/src/main/java/com/google/gwtorm/server/ResultSet.java
@@ -30,6 +30,7 @@
    * <code>hasNext()</code> returns false) {@link #close()} will be
    * automatically called.
    */
+  @Override
   Iterator<T> iterator();
 
   /**
diff --git a/src/test/java/com/google/gwtorm/data/Person.java b/src/test/java/com/google/gwtorm/data/Person.java
index 4984802..38644da 100644
--- a/src/test/java/com/google/gwtorm/data/Person.java
+++ b/src/test/java/com/google/gwtorm/data/Person.java
@@ -67,6 +67,10 @@
     return age;
   }
 
+  public void setAge(int age) {
+    this.age = age;
+  }
+
   public boolean isRegistered() {
     return registered;
   }
diff --git a/src/test/java/com/google/gwtorm/data/PersonAccess.java b/src/test/java/com/google/gwtorm/data/PersonAccess.java
index 091af8e..af36e7a 100644
--- a/src/test/java/com/google/gwtorm/data/PersonAccess.java
+++ b/src/test/java/com/google/gwtorm/data/PersonAccess.java
@@ -21,6 +21,7 @@
 import com.google.gwtorm.server.ResultSet;
 
 public interface PersonAccess extends Access<Person, Person.Key> {
+  @Override
   @PrimaryKey("name")
   Person get(Person.Key key) throws OrmException;
 
diff --git a/src/test/java/com/google/gwtorm/data/PersonAccess2.java b/src/test/java/com/google/gwtorm/data/PersonAccess2.java
index 46e12cd..a937ef9 100644
--- a/src/test/java/com/google/gwtorm/data/PersonAccess2.java
+++ b/src/test/java/com/google/gwtorm/data/PersonAccess2.java
@@ -19,6 +19,7 @@
 import com.google.gwtorm.server.PrimaryKey;
 
 public interface PersonAccess2 extends Access<Person2, Person.Key> {
+  @Override
   @PrimaryKey("name")
   Person2 get(Person.Key key) throws OrmException;
 }
diff --git a/src/test/java/com/google/gwtorm/jdbc/AbstractTestJdbcAccess.java b/src/test/java/com/google/gwtorm/jdbc/AbstractTestJdbcAccess.java
index c3893cd..3fad032 100644
--- a/src/test/java/com/google/gwtorm/jdbc/AbstractTestJdbcAccess.java
+++ b/src/test/java/com/google/gwtorm/jdbc/AbstractTestJdbcAccess.java
@@ -459,6 +459,7 @@
       super(s);
     }
 
+    @Override
     public String getRelationName() {
       return "Data";
     }
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 36f6533..c46f86b 100644
--- a/src/test/java/com/google/gwtorm/schema/sql/DialectH2Test.java
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectH2Test.java
@@ -129,6 +129,21 @@
   }
 
   @Test
+  public void testListIndexes() throws OrmException, SQLException {
+    assertTrue(dialect.listTables(db).isEmpty());
+
+    execute("CREATE SEQUENCE cnt");
+    execute("CREATE TABLE foo (cnt INT, bar INT, baz INT)");
+    execute("CREATE UNIQUE INDEX FOO_PRIMARY_IND ON foo(cnt)");
+    execute("CREATE INDEX FOO_SECOND_IND ON foo(bar, baz)");
+
+    Set<String> s = dialect.listIndexes(db, "foo");
+    assertEquals(2, s.size());
+    assertTrue(s.contains("foo_primary_ind"));
+    assertTrue(s.contains("foo_second_ind"));
+  }
+
+  @Test
   public void testUpgradeSchema() throws SQLException, OrmException {
     final PhoneBookDb p = phoneBook.open();
     try {
@@ -200,7 +215,7 @@
     PhoneBookDb schema = phoneBook.open();
     schema.updateSchema(executor);
     schema.people().beginTransaction(null);
-    ArrayList<Person> all = new ArrayList<Person>();
+    ArrayList<Person> all = new ArrayList<>();
     all.add(new Person(new Person.Key("Bob"), 18));
     schema.people().insert(all);
     schema.rollback();
@@ -226,7 +241,7 @@
     PhoneBookDb schema = phoneBook.open();
     schema.updateSchema(executor);
     schema.people().beginTransaction(null);
-    ArrayList<Person> all = new ArrayList<Person>();
+    ArrayList<Person> all = new ArrayList<>();
     all.add(new Person(new Person.Key("Bob"), 18));
     schema.people().insert(all);
     schema.commit();
diff --git a/src/test/java/com/google/gwtorm/schema/sql/DialectMaxDBTest.java b/src/test/java/com/google/gwtorm/schema/sql/DialectMaxDBTest.java
new file mode 100644
index 0000000..2eb8cf4
--- /dev/null
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectMaxDBTest.java
@@ -0,0 +1,362 @@
+// Copyright 2014 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.schema.sql;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeNoException;
+
+import com.google.gwtorm.data.Address;
+import com.google.gwtorm.data.Person;
+import com.google.gwtorm.data.PhoneBookDb;
+import com.google.gwtorm.data.PhoneBookDb2;
+import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.jdbc.JdbcExecutor;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.jdbc.SimpleDataSource;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+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;
+
+public class DialectMaxDBTest {
+  private static final String MAXDB_URL_KEY = "maxdb.url";
+  private static final String MAXDB_USER_KEY = "maxdb.user";
+  private static final String MAXDB_PASSWORD_KEY = "maxdb.password";
+  private static final String MAXDB_DRIVER = "com.sap.dbtech.jdbc.DriverSapDB";
+  private Connection db;
+  private JdbcExecutor executor;
+  private SqlDialect dialect;
+  private Database<PhoneBookDb> phoneBook;
+  private Database<PhoneBookDb2> phoneBook2;
+
+  @Before
+  public void setUp() throws Exception {
+    try {
+      Class.forName(MAXDB_DRIVER);
+    } catch (Exception e) {
+      assumeNoException(e);
+    }
+
+    final String url = System.getProperty(MAXDB_URL_KEY);
+    final String user = System.getProperty(MAXDB_USER_KEY);
+    final String pass = System.getProperty(MAXDB_PASSWORD_KEY);
+
+    db = DriverManager.getConnection(url, user, pass);
+    executor = new JdbcExecutor(db);
+    dialect = new DialectMaxDB().refine(db);
+
+    final Properties p = new Properties();
+    p.setProperty("driver", MAXDB_DRIVER);
+    p.setProperty("url", db.getMetaData().getURL());
+    p.setProperty("user", user);
+    p.setProperty("password", pass);
+    phoneBook =
+        new Database<PhoneBookDb>(new SimpleDataSource(p), PhoneBookDb.class);
+    phoneBook2 =
+        new Database<PhoneBookDb2>(new SimpleDataSource(p), PhoneBookDb2.class);
+  }
+
+  private void drop(String drop) {
+    try {
+      execute("DROP " + drop);
+    } catch (OrmException e) {
+    }
+  }
+
+  @After
+  public void tearDown() {
+    if (executor == null) {
+      return;
+    }
+
+    // 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();
+    }
+    executor = null;
+
+    if (db != null) {
+      try {
+        db.close();
+      } catch (SQLException e) {
+        throw new RuntimeException("Cannot close database", e);
+      }
+    }
+    db = null;
+  }
+
+  private void execute(final String sql) throws OrmException {
+    executor.execute(sql);
+  }
+
+  @Test
+  public void testListSequences() throws OrmException, SQLException {
+    assertTrue(dialect.listSequences(db).isEmpty());
+
+    execute("CREATE SEQUENCE cnt");
+    execute("CREATE TABLE foo (cnt INT)");
+
+    Set<String> s = dialect.listSequences(db);
+    assertEquals(1, s.size());
+    assertTrue(s.contains("cnt"));
+    assertFalse(s.contains("foo"));
+  }
+
+  @Test
+  public void testListTables() throws OrmException, SQLException {
+    assertTrue(dialect.listTables(db).isEmpty());
+
+    execute("CREATE SEQUENCE cnt");
+    execute("CREATE TABLE foo (cnt INT)");
+
+    Set<String> s = dialect.listTables(db);
+    assertEquals(1, s.size());
+    assertFalse(s.contains("cnt"));
+    assertTrue(s.contains("foo"));
+  }
+
+  @Test
+  public void testListIndexes() throws OrmException, SQLException {
+    assertTrue(dialect.listTables(db).isEmpty());
+
+    execute("CREATE SEQUENCE cnt");
+    execute("CREATE TABLE foo (cnt INT, bar INT, baz INT)");
+    execute("CREATE UNIQUE INDEX FOO_PRIMARY_IND ON foo(cnt)");
+    execute("CREATE INDEX FOO_SECOND_IND ON foo(bar, baz)");
+
+    Set<String> s = dialect.listIndexes(db, "foo");
+    assertEquals(2, s.size());
+    assertTrue(s.contains("foo_primary_ind"));
+    assertTrue(s.contains("foo_second_ind"));
+  }
+
+  @Test
+  public void testUpgradeSchema() throws SQLException, OrmException {
+    final PhoneBookDb p = phoneBook.open();
+    try {
+      p.updateSchema(executor);
+
+      execute("CREATE SEQUENCE cnt");
+      execute("CREATE TABLE foo (cnt INT)");
+
+      execute("ALTER TABLE people ADD fake_name VARCHAR(20)");
+      execute("ALTER TABLE people DROP COLUMN registered");
+      execute("DROP TABLE addresses");
+      execute("DROP SEQUENCE address_id");
+
+      Set<String> sequences, tables;
+
+      p.updateSchema(executor);
+      sequences = dialect.listSequences(db);
+      tables = dialect.listTables(db);
+      assertTrue(sequences.contains("cnt"));
+      assertTrue(tables.contains("foo"));
+
+      assertTrue(sequences.contains("address_id"));
+      assertTrue(tables.contains("addresses"));
+
+      p.pruneSchema(executor);
+      sequences = dialect.listSequences(db);
+      tables = dialect.listTables(db);
+      assertFalse(sequences.contains("cnt"));
+      assertFalse(tables.contains("foo"));
+
+      final Person.Key pk = new Person.Key("Bob");
+      final Person bob = new Person(pk, p.nextAddressId());
+      p.people().insert(asList(bob));
+
+      final Address addr =
+          new Address(new Address.Key(pk, "home"), "some place");
+      p.addresses().insert(asList(addr));
+    } finally {
+      p.close();
+    }
+
+    final PhoneBookDb2 p2 = phoneBook2.open();
+    try {
+      ((JdbcSchema) p2).renameField(executor, "people", "registered",
+          "isRegistered");
+    } finally {
+      p2.close();
+    }
+  }
+
+  @Test
+  public void testRenameTable() throws SQLException, OrmException {
+    assertTrue(dialect.listTables(db).isEmpty());
+    execute("CREATE TABLE foo (cnt INT)");
+    Set<String> s = dialect.listTables(db);
+    assertEquals(1, s.size());
+    assertTrue(s.contains("foo"));
+    final PhoneBookDb p = phoneBook.open();
+    try {
+      ((JdbcSchema) p).renameTable(executor, "foo", "bar");
+    } finally {
+      p.close();
+    }
+    s = dialect.listTables(db);
+    assertTrue(s.contains("bar"));
+    assertFalse(s.contains("for"));
+  }
+
+  @Test
+  public void testInsert() throws OrmException {
+    final PhoneBookDb p = phoneBook.open();
+    try {
+      p.updateSchema(executor);
+
+      final Person.Key pk = new Person.Key("Bob");
+      final Person bob = new Person(pk, p.nextAddressId());
+      p.people().insert(asList(bob));
+
+      try {
+        p.people().insert(asList(bob));
+        fail();
+      } catch (OrmDuplicateKeyException duprec) {
+        // expected
+      }
+    } finally {
+      p.close();
+    }
+  }
+
+  @Test
+  public void testConstraintViolationOnIndex() throws OrmException {
+    final PhoneBookDb p = phoneBook.open();
+    try {
+      p.updateSchema(executor);
+
+      execute("CREATE UNIQUE INDEX idx ON people (age)");
+      try {
+
+        final Person.Key pk = new Person.Key("Bob");
+        final Person bob = new Person(pk, p.nextAddressId());
+        bob.setAge(40);
+        p.people().insert(asList(bob));
+
+        final Person.Key joePk = new Person.Key("Joe");
+        Person joe = new Person(joePk, p.nextAddressId());
+        joe.setAge(40);
+        try {
+        p.people().insert(asList(joe));
+        fail();
+        } catch (OrmDuplicateKeyException duprec) {
+          fail();
+        } catch (OrmException noDuprec) {
+          // expeceted
+        }
+      } finally {
+        execute("DROP INDEX idx ON people");
+      }
+    } finally {
+      p.close();
+    }
+  }
+
+  @Test
+  public void testUpdate() throws OrmException {
+    final PhoneBookDb p = phoneBook.open();
+    try {
+      p.updateSchema(executor);
+
+      final Person.Key pk = new Person.Key("Bob");
+      Person bob = new Person(pk, p.nextAddressId());
+      bob.setAge(40);
+      p.people().insert(asList(bob));
+
+      bob.setAge(50);
+      p.people().update(asList(bob));
+
+      bob = p.people().get(pk);
+      assertEquals(50, bob.age());
+    } finally {
+      p.close();
+    }
+  }
+
+  @Test
+  public void testUpsert() throws OrmException {
+    final PhoneBookDb p = phoneBook.open();
+    try {
+      p.updateSchema(executor);
+
+      final Person.Key bobPk = new Person.Key("Bob");
+      Person bob = new Person(bobPk, p.nextAddressId());
+      bob.setAge(40);
+      p.people().insert(asList(bob));
+
+      final Person.Key joePk = new Person.Key("Joe");
+      Person joe = new Person(joePk, p.nextAddressId());
+      bob.setAge(50);
+      p.people().upsert(asList(bob, joe));
+
+      bob = p.people().get(bobPk);
+      assertEquals(50, bob.age());
+      assertNotNull(p.people().get(joePk));
+    } finally {
+      p.close();
+    }
+  }
+
+  @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 c47a092..e092e56 100644
--- a/src/test/java/com/google/gwtorm/schema/sql/DialectMySQLTest.java
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectMySQLTest.java
@@ -147,6 +147,20 @@
   }
 
   @Test
+  public void testListIndexes() throws OrmException, SQLException {
+    assertTrue(dialect.listTables(db).isEmpty());
+
+    execute("CREATE TABLE foo (cnt INT, bar INT, baz INT)");
+    execute("CREATE UNIQUE INDEX FOO_PRIMARY_IND ON foo(cnt)");
+    execute("CREATE INDEX FOO_SECOND_IND ON foo(bar, baz)");
+
+    Set<String> s = dialect.listIndexes(db, "foo");
+    assertEquals(2, s.size());
+    assertTrue(s.contains("foo_primary_ind"));
+    assertTrue(s.contains("foo_second_ind"));
+  }
+
+  @Test
   public void testUpgradeSchema() throws SQLException, OrmException {
     final PhoneBookDb p = phoneBook.open();
     try {
@@ -220,7 +234,7 @@
     PhoneBookDb schema = phoneBook.open();
     schema.updateSchema(executor);
     schema.people().beginTransaction(null);
-    ArrayList<Person> all = new ArrayList<Person>();
+    ArrayList<Person> all = new ArrayList<>();
     all.add(new Person(new Person.Key("Bob"), 18));
     schema.people().insert(all);
     schema.rollback();
@@ -246,7 +260,7 @@
     PhoneBookDb schema = phoneBook.open();
     schema.updateSchema(executor);
     schema.people().beginTransaction(null);
-    ArrayList<Person> all = new ArrayList<Person>();
+    ArrayList<Person> all = new ArrayList<>();
     all.add(new Person(new Person.Key("Bob"), 18));
     schema.people().insert(all);
     schema.commit();
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 3869dd5..f0ced86 100644
--- a/src/test/java/com/google/gwtorm/schema/sql/DialectOracleSQLTest.java
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectOracleSQLTest.java
@@ -147,6 +147,21 @@
   }
 
   @Test
+  public void testListIndexes() throws OrmException, SQLException {
+    assertTrue(dialect.listTables(db).isEmpty());
+
+    execute("CREATE SEQUENCE cnt");
+    execute("CREATE TABLE foo (cnt INT, bar INT, baz INT)");
+    execute("CREATE UNIQUE INDEX FOO_PRIMARY_IND ON foo(cnt)");
+    execute("CREATE INDEX FOO_SECOND_IND ON foo(bar, baz)");
+
+    Set<String> s = dialect.listIndexes(db, "foo");
+    assertEquals(2, s.size());
+    assertTrue(s.contains("foo_primary_ind"));
+    assertTrue(s.contains("foo_second_ind"));
+  }
+
+  @Test
   public void testUpgradeSchema() throws SQLException, OrmException {
     final PhoneBookDb p = phoneBook.open();
     try {
@@ -220,7 +235,7 @@
     PhoneBookDb schema = phoneBook.open();
     schema.updateSchema(executor);
     schema.people().beginTransaction(null);
-    ArrayList<Person> all = new ArrayList<Person>();
+    ArrayList<Person> all = new ArrayList<>();
     all.add(new Person(new Person.Key("Bob"), 18));
     schema.people().insert(all);
     schema.rollback();
@@ -246,7 +261,7 @@
     PhoneBookDb schema = phoneBook.open();
     schema.updateSchema(executor);
     schema.people().beginTransaction(null);
-    ArrayList<Person> all = new ArrayList<Person>();
+    ArrayList<Person> all = new ArrayList<>();
     all.add(new Person(new Person.Key("Bob"), 18));
     schema.people().insert(all);
     schema.commit();
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 e8612cc..ef0a067 100644
--- a/src/test/java/com/google/gwtorm/schema/sql/DialectPostgreSQLTest.java
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectPostgreSQLTest.java
@@ -146,6 +146,20 @@
   }
 
   @Test
+  public void testListIndexes() throws OrmException, SQLException {
+    assertTrue(dialect.listTables(db).isEmpty());
+
+    execute("CREATE TABLE foo (cnt INT, bar INT, baz INT)");
+    execute("CREATE UNIQUE INDEX FOO_PRIMARY_IND ON foo(cnt)");
+    execute("CREATE INDEX FOO_SECOND_IND ON foo(bar, baz)");
+
+    Set<String> s = dialect.listIndexes(db, "foo");
+    assertEquals(2, s.size());
+    assertTrue(s.contains("foo_primary_ind"));
+    assertTrue(s.contains("foo_second_ind"));
+  }
+
+  @Test
   public void testUpgradeSchema() throws SQLException, OrmException {
     final PhoneBookDb p = phoneBook.open();
     try {
@@ -219,7 +233,7 @@
     PhoneBookDb schema = phoneBook.open();
     schema.updateSchema(executor);
     schema.people().beginTransaction(null);
-    ArrayList<Person> all = new ArrayList<Person>();
+    ArrayList<Person> all = new ArrayList<>();
     all.add(new Person(new Person.Key("Bob"), 18));
     schema.people().insert(all);
     schema.rollback();
@@ -245,7 +259,7 @@
     PhoneBookDb schema = phoneBook.open();
     schema.updateSchema(executor);
     schema.people().beginTransaction(null);
-    ArrayList<Person> all = new ArrayList<Person>();
+    ArrayList<Person> all = new ArrayList<>();
     all.add(new Person(new Person.Key("Bob"), 18));
     schema.people().insert(all);
     schema.commit();