]> source.dussan.org Git - vaadin-framework.git/commitdiff
- Merged SQLContainer with Vaadin 6.7.
authorJohn Alhroos <john.ahlroos@itmill.com>
Wed, 10 Aug 2011 07:35:47 +0000 (07:35 +0000)
committerJohn Alhroos <john.ahlroos@itmill.com>
Wed, 10 Aug 2011 07:35:47 +0000 (07:35 +0000)
- Updated EasyMock to version 3.0 (SQLContainer requirement)

svn changeset:20252/svn branch:6.7

59 files changed:
src/com/vaadin/data/util/CacheFlushNotifier.java [new file with mode: 0644]
src/com/vaadin/data/util/CacheMap.java [new file with mode: 0644]
src/com/vaadin/data/util/ColumnProperty.java [new file with mode: 0644]
src/com/vaadin/data/util/OptimisticLockException.java [new file with mode: 0644]
src/com/vaadin/data/util/ReadOnlyRowId.java [new file with mode: 0644]
src/com/vaadin/data/util/Reference.java [new file with mode: 0644]
src/com/vaadin/data/util/RowId.java [new file with mode: 0644]
src/com/vaadin/data/util/RowItem.java [new file with mode: 0644]
src/com/vaadin/data/util/SQLContainer.java [new file with mode: 0644]
src/com/vaadin/data/util/SQLUtil.java [new file with mode: 0644]
src/com/vaadin/data/util/TemporaryRowId.java [new file with mode: 0644]
src/com/vaadin/data/util/connection/J2EEConnectionPool.java [new file with mode: 0644]
src/com/vaadin/data/util/connection/JDBCConnectionPool.java [new file with mode: 0644]
src/com/vaadin/data/util/connection/SimpleJDBCConnectionPool.java [new file with mode: 0644]
src/com/vaadin/data/util/filter/Between.java [new file with mode: 0644]
src/com/vaadin/data/util/filter/Like.java [new file with mode: 0644]
src/com/vaadin/data/util/query/FreeformQuery.java [new file with mode: 0644]
src/com/vaadin/data/util/query/FreeformQueryDelegate.java [new file with mode: 0644]
src/com/vaadin/data/util/query/FreeformStatementDelegate.java [new file with mode: 0644]
src/com/vaadin/data/util/query/OrderBy.java [new file with mode: 0644]
src/com/vaadin/data/util/query/QueryDelegate.java [new file with mode: 0644]
src/com/vaadin/data/util/query/TableQuery.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/DefaultSQLGenerator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/MSSQLGenerator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/OracleGenerator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/SQLGenerator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/StatementHelper.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/filter/AndTranslator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/filter/BetweenTranslator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/filter/CompareTranslator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/filter/FilterTranslator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/filter/IsNullTranslator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/filter/LikeTranslator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/filter/NotTranslator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/filter/OrTranslator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/filter/QueryBuilder.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/filter/SimpleStringTranslator.java [new file with mode: 0644]
src/com/vaadin/data/util/query/generator/filter/StringDecorator.java [new file with mode: 0644]
tests/src/com/vaadin/tests/containers/sqlcontainer/CheckboxUpdateProblem.java [new file with mode: 0644]
tests/src/com/vaadin/tests/containers/sqlcontainer/MassInsertMemoryLeakTestApp.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/AllTests.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/ColumnPropertyTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/DataGenerator.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/FreeformQueryUtil.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/ReadOnlyRowIdTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/RowIdTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/SQLContainerTableQueryTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/SQLContainerTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/TicketTests.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/UtilTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/connection/J2EEConnectionPoolTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/connection/MockInitialContextFactory.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/connection/SimpleJDBCConnectionPoolTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/filters/BetweenTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/filters/LikeTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/generator/SQLGeneratorsTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/query/FreeformQueryTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/query/QueryBuilderTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/sqlcontainer/query/TableQueryTest.java [new file with mode: 0644]

diff --git a/src/com/vaadin/data/util/CacheFlushNotifier.java b/src/com/vaadin/data/util/CacheFlushNotifier.java
new file mode 100644 (file)
index 0000000..c953785
--- /dev/null
@@ -0,0 +1,88 @@
+package com.vaadin.data.util;\r
+\r
+import java.lang.ref.ReferenceQueue;\r
+import java.lang.ref.WeakReference;\r
+import java.util.ArrayList;\r
+import java.util.List;\r
+\r
+import com.vaadin.data.util.query.FreeformQuery;\r
+import com.vaadin.data.util.query.QueryDelegate;\r
+import com.vaadin.data.util.query.TableQuery;\r
+\r
+/**\r
+ * CacheFlushNotifier is a simple static notification mechanism to inform other\r
+ * SQLContainers that the contents of their caches may have become stale.\r
+ */\r
+class CacheFlushNotifier {\r
+    /*\r
+     * SQLContainer instance reference list and dead reference queue. Used for\r
+     * the cache flush notification feature.\r
+     */\r
+    private static List<WeakReference<SQLContainer>> allInstances = new ArrayList<WeakReference<SQLContainer>>();\r
+    private static ReferenceQueue<SQLContainer> deadInstances = new ReferenceQueue<SQLContainer>();\r
+\r
+    /**\r
+     * Adds the given SQLContainer to the cache flush notification receiver list\r
+     * \r
+     * @param c\r
+     *            Container to add\r
+     */\r
+    public static void addInstance(SQLContainer c) {\r
+        removeDeadReferences();\r
+        if (c != null) {\r
+            allInstances.add(new WeakReference<SQLContainer>(c, deadInstances));\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Removes dead references from instance list\r
+     */\r
+    private static void removeDeadReferences() {\r
+        java.lang.ref.Reference<? extends SQLContainer> dead = deadInstances\r
+                .poll();\r
+        while (dead != null) {\r
+            allInstances.remove(dead);\r
+            dead = deadInstances.poll();\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Iterates through the instances and notifies containers which are\r
+     * connected to the same table or are using the same query string.\r
+     * \r
+     * @param c\r
+     *            SQLContainer that issued the cache flush notification\r
+     */\r
+    public static void notifyOfCacheFlush(SQLContainer c) {\r
+        removeDeadReferences();\r
+        for (WeakReference<SQLContainer> wr : allInstances) {\r
+            if (wr.get() != null) {\r
+                SQLContainer wrc = wr.get();\r
+                if (wrc == null) {\r
+                    continue;\r
+                }\r
+                /*\r
+                 * If the reference points to the container sending the\r
+                 * notification, do nothing.\r
+                 */\r
+                if (wrc.equals(c)) {\r
+                    continue;\r
+                }\r
+                /* Compare QueryDelegate types and tableName/queryString */\r
+                QueryDelegate wrQd = wrc.getQueryDelegate();\r
+                QueryDelegate qd = c.getQueryDelegate();\r
+                if (wrQd instanceof TableQuery\r
+                        && qd instanceof TableQuery\r
+                        && ((TableQuery) wrQd).getTableName().equals(\r
+                                ((TableQuery) qd).getTableName())) {\r
+                    wrc.refresh();\r
+                } else if (wrQd instanceof FreeformQuery\r
+                        && qd instanceof FreeformQuery\r
+                        && ((FreeformQuery) wrQd).getQueryString().equals(\r
+                                ((FreeformQuery) qd).getQueryString())) {\r
+                    wrc.refresh();\r
+                }\r
+            }\r
+        }\r
+    }\r
+}\r
diff --git a/src/com/vaadin/data/util/CacheMap.java b/src/com/vaadin/data/util/CacheMap.java
new file mode 100644 (file)
index 0000000..a7a17c6
--- /dev/null
@@ -0,0 +1,28 @@
+package com.vaadin.data.util;\r
+\r
+import java.util.LinkedHashMap;\r
+import java.util.Map;\r
+\r
+/**\r
+ * CacheMap extends LinkedHashMap, adding the possibility to adjust maximum\r
+ * number of items. In SQLContainer this is used for RowItem -cache. Cache size\r
+ * will be two times the page length parameter of the container.\r
+ */\r
+class CacheMap<K, V> extends LinkedHashMap<K, V> {\r
+    private static final long serialVersionUID = 679999766473555231L;\r
+    private int cacheLimit = SQLContainer.CACHE_RATIO\r
+            * SQLContainer.DEFAULT_PAGE_LENGTH;\r
+\r
+    @Override\r
+    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {\r
+        return size() > cacheLimit;\r
+    }\r
+\r
+    void setCacheLimit(int limit) {\r
+        cacheLimit = limit > 0 ? limit : SQLContainer.DEFAULT_PAGE_LENGTH;\r
+    }\r
+\r
+    int getCacheLimit() {\r
+        return cacheLimit;\r
+    }\r
+}
\ No newline at end of file
diff --git a/src/com/vaadin/data/util/ColumnProperty.java b/src/com/vaadin/data/util/ColumnProperty.java
new file mode 100644 (file)
index 0000000..0deb1a7
--- /dev/null
@@ -0,0 +1,241 @@
+package com.vaadin.data.util;
+
+import java.lang.reflect.Constructor;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+
+import com.vaadin.data.Property;
+
+/**
+ * ColumnProperty represents the value of one column in a RowItem. In addition
+ * to the value, ColumnProperty also contains some basic column attributes such
+ * as nullability status, read-only status and data type.
+ * 
+ * Note that depending on the QueryDelegate in use this does not necessarily map
+ * into an actual column in a database table.
+ */
+final public class ColumnProperty implements Property {
+    private static final long serialVersionUID = -3694463129581802457L;
+
+    private RowItem owner;
+
+    private String propertyId;
+
+    private boolean readOnly;
+    private boolean allowReadOnlyChange = true;
+    private boolean nullable = true;
+
+    private Object value;
+    private Object changedValue;
+    private Class<?> type;
+
+    private boolean modified;
+
+    private boolean versionColumn;
+
+    /**
+     * Prevent instantiation without required parameters.
+     */
+    @SuppressWarnings("unused")
+    private ColumnProperty() {
+    }
+
+    public ColumnProperty(String propertyId, boolean readOnly,
+            boolean allowReadOnlyChange, boolean nullable, Object value,
+            Class<?> type) {
+        if (propertyId == null) {
+            throw new IllegalArgumentException("Properties must be named.");
+        }
+        if (type == null) {
+            throw new IllegalArgumentException("Property type must be set.");
+        }
+        this.propertyId = propertyId;
+        this.type = type;
+        this.value = value;
+
+        this.allowReadOnlyChange = allowReadOnlyChange;
+        this.nullable = nullable;
+        this.readOnly = readOnly;
+    }
+
+    public Object getValue() {
+        if (isModified()) {
+            return changedValue;
+        }
+        return value;
+    }
+
+    public void setValue(Object newValue) throws ReadOnlyException,
+            ConversionException {
+        if (newValue == null && !nullable) {
+            throw new NotNullableException(
+                    "Null values are not allowed for this property.");
+        }
+        if (readOnly) {
+            throw new ReadOnlyException(
+                    "Cannot set value for read-only property.");
+        }
+
+        /* Check if this property is a date property. */
+        boolean isDateProperty = Time.class.equals(getType())
+                || Date.class.equals(getType())
+                || Timestamp.class.equals(getType());
+
+        if (newValue != null) {
+            /* Handle SQL dates, times and Timestamps given as java.util.Date */
+            if (isDateProperty) {
+                /*
+                 * Try to get the millisecond value from the new value of this
+                 * property. Possible type to convert from is java.util.Date.
+                 */
+                long millis = 0;
+                if (newValue instanceof java.util.Date) {
+                    millis = ((java.util.Date) newValue).getTime();
+                    /*
+                     * Create the new object based on the millisecond value,
+                     * according to the type of this property.
+                     */
+                    if (Time.class.equals(getType())) {
+                        newValue = new Time(millis);
+                    } else if (Date.class.equals(getType())) {
+                        newValue = new Date(millis);
+                    } else if (Timestamp.class.equals(getType())) {
+                        newValue = new Timestamp(millis);
+                    }
+                }
+            }
+
+            /*
+             * If the type is not correct, try to generate it through a possibly
+             * existing String constructor.
+             */
+            if (!getType().isAssignableFrom(newValue.getClass())) {
+                try {
+                    final Constructor<?> constr = getType().getConstructor(
+                            new Class[] { String.class });
+                    newValue = constr.newInstance(new Object[] { newValue
+                            .toString() });
+                } catch (Exception e) {
+                    throw new ConversionException(e);
+                }
+            }
+
+            /*
+             * If the value to be set is the same that has already been set, do
+             * not set it again.
+             */
+            if (newValue.equals(value)) {
+                return;
+            }
+        }
+
+        /* Set the new value and notify container of the change. */
+        changedValue = newValue;
+        owner.getContainer().itemChangeNotification(owner);
+        modified = true;
+    }
+
+    public Class<?> getType() {
+        return type;
+    }
+
+    public boolean isReadOnly() {
+        return readOnly;
+    }
+
+    public boolean isReadOnlyChangeAllowed() {
+        return allowReadOnlyChange;
+    }
+
+    public void setReadOnly(boolean newStatus) {
+        if (allowReadOnlyChange) {
+            readOnly = newStatus;
+        }
+    }
+
+    public String getPropertyId() {
+        return propertyId;
+    }
+
+    @Override
+    public String toString() {
+        Object val = getValue();
+        if (val == null) {
+            return null;
+        }
+        return val.toString();
+    }
+
+    public void setOwner(RowItem owner) {
+        if (owner == null) {
+            throw new IllegalArgumentException("Owner can not be set to null.");
+        }
+        if (this.owner != null) {
+            throw new IllegalStateException(
+                    "ColumnProperties can only be bound once.");
+        }
+        this.owner = owner;
+    }
+
+    public boolean isModified() {
+        return modified;
+    }
+
+    public boolean isVersionColumn() {
+        return versionColumn;
+    }
+
+    public void setVersionColumn(boolean versionColumn) {
+        this.versionColumn = versionColumn;
+    }
+
+    public boolean isNullable() {
+        return nullable;
+    }
+
+    /**
+     * An exception that signals that a <code>null</code> value was passed to
+     * the <code>setValue</code> method, but the value of this property can not
+     * be set to <code>null</code>.
+     */
+    @SuppressWarnings("serial")
+    public class NotNullableException extends RuntimeException {
+
+        /**
+         * Constructs a new <code>NotNullableException</code> without a detail
+         * message.
+         */
+        public NotNullableException() {
+        }
+
+        /**
+         * Constructs a new <code>NotNullableException</code> with the specified
+         * detail message.
+         * 
+         * @param msg
+         *            the detail message
+         */
+        public NotNullableException(String msg) {
+            super(msg);
+        }
+
+        /**
+         * Constructs a new <code>NotNullableException</code> from another
+         * exception.
+         * 
+         * @param cause
+         *            The cause of the failure
+         */
+        public NotNullableException(Throwable cause) {
+            super(cause);
+        }
+    }
+
+    public void commit() {
+        if (isModified()) {
+            modified = false;
+            value = changedValue;
+        }
+    }
+}
diff --git a/src/com/vaadin/data/util/OptimisticLockException.java b/src/com/vaadin/data/util/OptimisticLockException.java
new file mode 100644 (file)
index 0000000..a32994a
--- /dev/null
@@ -0,0 +1,33 @@
+package com.vaadin.data.util;
+
+/**
+ * An OptimisticLockException is thrown when trying to update or delete a row
+ * that has been changed since last read from the database.
+ * 
+ * OptimisticLockException is a runtime exception because optimistic locking is
+ * turned off by default, and as such will never be thrown in a default
+ * configuration. In order to turn on optimistic locking, you need to specify
+ * the version column in your TableQuery instance.
+ * 
+ * @see com.vaadin.addon.sqlcontainer.query.TableQuery#setVersionColumn(String)
+ * 
+ * @author Jonatan Kronqvist / Vaadin Ltd
+ */
+public class OptimisticLockException extends RuntimeException {
+
+    private final RowId rowId;
+
+    public OptimisticLockException(RowId rowId) {
+        super();
+        this.rowId = rowId;
+    }
+
+    public OptimisticLockException(String msg, RowId rowId) {
+        super(msg);
+        this.rowId = rowId;
+    }
+
+    public RowId getRowId() {
+        return rowId;
+    }
+}
diff --git a/src/com/vaadin/data/util/ReadOnlyRowId.java b/src/com/vaadin/data/util/ReadOnlyRowId.java
new file mode 100644 (file)
index 0000000..a3c559c
--- /dev/null
@@ -0,0 +1,28 @@
+package com.vaadin.data.util;
+
+public class ReadOnlyRowId extends RowId {
+    private static final long serialVersionUID = -2626764781642012467L;
+    private final Integer rowNum;
+
+    public ReadOnlyRowId(int rowNum) {
+        super();
+        this.rowNum = rowNum;
+    }
+
+    @Override
+    public int hashCode() {
+        return rowNum.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj instanceof ReadOnlyRowId)) {
+            return false;
+        }
+        return rowNum.equals(((ReadOnlyRowId) obj).rowNum);
+    }
+
+    public int getRowNum() {
+        return rowNum;
+    }
+}
diff --git a/src/com/vaadin/data/util/Reference.java b/src/com/vaadin/data/util/Reference.java
new file mode 100644 (file)
index 0000000..9e174b4
--- /dev/null
@@ -0,0 +1,53 @@
+package com.vaadin.data.util;\r
+\r
+import java.io.Serializable;\r
+\r
+/**\r
+ * The reference class represents a simple [usually foreign key] reference to\r
+ * another SQLContainer. Actual foreign key reference in the database is not\r
+ * required, but it is recommended to make sure that certain constraints are\r
+ * followed.\r
+ */\r
+@SuppressWarnings("serial")\r
+class Reference implements Serializable {\r
+\r
+    /**\r
+     * The SQLContainer that this reference points to.\r
+     */\r
+    private SQLContainer referencedContainer;\r
+\r
+    /**\r
+     * The column ID/name in the referencing SQLContainer that contains the key\r
+     * used for the reference.\r
+     */\r
+    private String referencingColumn;\r
+\r
+    /**\r
+     * The column ID/name in the referenced SQLContainer that contains the key\r
+     * used for the reference.\r
+     */\r
+    private String referencedColumn;\r
+\r
+    /**\r
+     * Constructs a new reference to be used within the SQLContainer to\r
+     * reference another SQLContainer.\r
+     */\r
+    Reference(SQLContainer referencedContainer, String referencingColumn,\r
+            String referencedColumn) {\r
+        this.referencedContainer = referencedContainer;\r
+        this.referencingColumn = referencingColumn;\r
+        this.referencedColumn = referencedColumn;\r
+    }\r
+\r
+    SQLContainer getReferencedContainer() {\r
+        return referencedContainer;\r
+    }\r
+\r
+    String getReferencingColumn() {\r
+        return referencingColumn;\r
+    }\r
+\r
+    String getReferencedColumn() {\r
+        return referencedColumn;\r
+    }\r
+}\r
diff --git a/src/com/vaadin/data/util/RowId.java b/src/com/vaadin/data/util/RowId.java
new file mode 100644 (file)
index 0000000..565161b
--- /dev/null
@@ -0,0 +1,78 @@
+package com.vaadin.data.util;\r
+\r
+import java.io.Serializable;\r
+\r
+/**\r
+ * RowId represents identifiers of a single database result set row.\r
+ * \r
+ * The data structure of a RowId is an Object array which contains the values of\r
+ * the primary key columns of the identified row. This allows easy equals()\r
+ * -comparison of RowItems.\r
+ */\r
+public class RowId implements Serializable {\r
+    private static final long serialVersionUID = -3161778404698901258L;\r
+    protected Object[] id;\r
+\r
+    /**\r
+     * Prevent instantiation without required parameters.\r
+     */\r
+    protected RowId() {\r
+    }\r
+\r
+    public RowId(Object[] id) {\r
+        if (id == null) {\r
+            throw new IllegalArgumentException("id parameter must not be null!");\r
+        }\r
+        this.id = id;\r
+    }\r
+\r
+    public Object[] getId() {\r
+        return id;\r
+    }\r
+\r
+    @Override\r
+    public int hashCode() {\r
+        int result = 31;\r
+        if (id != null) {\r
+            for (Object o : id) {\r
+                if (o != null) {\r
+                    result += o.hashCode();\r
+                }\r
+            }\r
+        }\r
+        return result;\r
+    }\r
+\r
+    @Override\r
+    public boolean equals(Object obj) {\r
+        if (obj == null || !(obj instanceof RowId)) {\r
+            return false;\r
+        }\r
+        Object[] compId = ((RowId) obj).getId();\r
+        if (id == null && compId == null) {\r
+            return true;\r
+        }\r
+        if (id.length != compId.length) {\r
+            return false;\r
+        }\r
+        for (int i = 0; i < id.length; i++) {\r
+            if ((id[i] == null && compId[i] != null)\r
+                    || (id[i] != null && !id[i].equals(compId[i]))) {\r
+                return false;\r
+            }\r
+        }\r
+        return true;\r
+    }\r
+\r
+    @Override\r
+    public String toString() {\r
+        StringBuffer s = new StringBuffer();\r
+        for (int i = 0; i < id.length; i++) {\r
+            s.append(id[i]);\r
+            if (i < id.length - 1) {\r
+                s.append("/");\r
+            }\r
+        }\r
+        return s.toString();\r
+    }\r
+}\r
diff --git a/src/com/vaadin/data/util/RowItem.java b/src/com/vaadin/data/util/RowItem.java
new file mode 100644 (file)
index 0000000..5fae278
--- /dev/null
@@ -0,0 +1,125 @@
+package com.vaadin.data.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * RowItem represents one row of a result set obtained from a QueryDelegate.
+ * 
+ * Note that depending on the QueryDelegate in use this does not necessarily map
+ * into an actual row in a database table.
+ */
+public final class RowItem implements Item {
+    private static final long serialVersionUID = -6228966439127951408L;
+    private SQLContainer container;
+    private RowId id;
+    private Collection<ColumnProperty> properties;
+
+    /**
+     * Prevent instantiation without required parameters.
+     */
+    @SuppressWarnings("unused")
+    private RowItem() {
+    }
+
+    public RowItem(SQLContainer container, RowId id,
+            Collection<ColumnProperty> properties) {
+        if (container == null) {
+            throw new IllegalArgumentException("Container cannot be null.");
+        }
+        if (id == null) {
+            throw new IllegalArgumentException("Row ID cannot be null.");
+        }
+        this.container = container;
+        this.properties = properties;
+        /* Set this RowItem as owner to the properties */
+        if (properties != null) {
+            for (ColumnProperty p : properties) {
+                p.setOwner(this);
+            }
+        }
+        this.id = id;
+    }
+
+    public Property getItemProperty(Object id) {
+        if (id instanceof String && id != null) {
+            for (ColumnProperty cp : properties) {
+                if (id.equals(cp.getPropertyId())) {
+                    return cp;
+                }
+            }
+        }
+        return null;
+    }
+
+    public Collection<?> getItemPropertyIds() {
+        Collection<String> ids = new ArrayList<String>(properties.size());
+        for (ColumnProperty cp : properties) {
+            ids.add(cp.getPropertyId());
+        }
+        return Collections.unmodifiableCollection(ids);
+    }
+
+    /**
+     * Adding properties is not supported. Properties are generated by
+     * SQLContainer.
+     */
+    public boolean addItemProperty(Object id, Property property)
+            throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Removing properties is not supported. Properties are generated by
+     * SQLContainer.
+     */
+    public boolean removeItemProperty(Object id)
+            throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
+    }
+
+    public RowId getId() {
+        return id;
+    }
+
+    public SQLContainer getContainer() {
+        return container;
+    }
+
+    public boolean isModified() {
+        if (properties != null) {
+            for (ColumnProperty p : properties) {
+                if (p.isModified()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer s = new StringBuffer();
+        s.append("ID:");
+        s.append(getId().toString());
+        for (Object propId : getItemPropertyIds()) {
+            s.append("|");
+            s.append(propId.toString());
+            s.append(":");
+            s.append(getItemProperty(propId).toString());
+        }
+        return s.toString();
+    }
+
+    public void commit() {
+        if (properties != null) {
+            for (ColumnProperty p : properties) {
+                p.commit();
+            }
+        }
+    }
+}
diff --git a/src/com/vaadin/data/util/SQLContainer.java b/src/com/vaadin/data/util/SQLContainer.java
new file mode 100644 (file)
index 0000000..9189727
--- /dev/null
@@ -0,0 +1,1587 @@
+package com.vaadin.data.util;
+
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.Date;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.filter.Compare.Equal;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+import com.vaadin.data.util.query.OrderBy;
+import com.vaadin.data.util.query.QueryDelegate;
+import com.vaadin.data.util.query.QueryDelegate.RowIdChangeListener;
+import com.vaadin.data.util.query.TableQuery;
+import com.vaadin.data.util.query.generator.MSSQLGenerator;
+import com.vaadin.data.util.query.generator.OracleGenerator;
+
+public class SQLContainer implements Container, Container.Filterable,
+        Container.Indexed, Container.Sortable, Container.ItemSetChangeNotifier {
+    private static final long serialVersionUID = -3863564310693712511L;
+
+    /** Query delegate */
+    private QueryDelegate delegate;
+    /** Auto commit mode, default = false */
+    private boolean autoCommit = false;
+
+    /** Page length = number of items contained in one page */
+    private int pageLength = DEFAULT_PAGE_LENGTH;
+    public static final int DEFAULT_PAGE_LENGTH = 100;
+
+    /** Number of items to cache = CACHE_RATIO x pageLength */
+    public static final int CACHE_RATIO = 2;
+
+    /** Item and index caches */
+    private final Map<Integer, RowId> itemIndexes = new HashMap<Integer, RowId>();
+    private final CacheMap<RowId, RowItem> cachedItems = new CacheMap<RowId, RowItem>();
+
+    /** Container properties = column names, data types and statuses */
+    private final List<String> propertyIds = new ArrayList<String>();
+    private final Map<String, Class<?>> propertyTypes = new HashMap<String, Class<?>>();
+    private final Map<String, Boolean> propertyReadOnly = new HashMap<String, Boolean>();
+    private final Map<String, Boolean> propertyNullable = new HashMap<String, Boolean>();
+
+    /** Filters (WHERE) and sorters (ORDER BY) */
+    private final List<Filter> filters = new ArrayList<Filter>();
+    private final List<OrderBy> sorters = new ArrayList<OrderBy>();
+
+    /**
+     * Total number of items available in the data source using the current
+     * query, filters and sorters.
+     */
+    private int size;
+
+    /**
+     * Size updating logic. Do not update size from data source if it has been
+     * updated in the last sizeValidMilliSeconds milliseconds.
+     */
+    private final int sizeValidMilliSeconds = 10000;
+    private boolean sizeDirty = true;
+    private Date sizeUpdated = new Date();
+
+    /** Starting row number of the currently fetched page */
+    private int currentOffset;
+
+    /** ItemSetChangeListeners */
+    private LinkedList<Container.ItemSetChangeListener> itemSetChangeListeners;
+
+    /** Temporary storage for modified items and items to be removed and added */
+    private final Map<RowId, RowItem> removedItems = new HashMap<RowId, RowItem>();
+    private final List<RowItem> addedItems = new ArrayList<RowItem>();
+    private final List<RowItem> modifiedItems = new ArrayList<RowItem>();
+
+    /** List of references to other SQLContainers */
+    private final Map<SQLContainer, Reference> references = new HashMap<SQLContainer, Reference>();
+
+    /** Cache flush notification system enabled. Disabled by default. */
+    private boolean notificationsEnabled;
+
+    /** Enable to output possible stack traces and diagnostic information */
+    private boolean debugMode;
+
+    /**
+     * Prevent instantiation without a QueryDelegate.
+     */
+    @SuppressWarnings("unused")
+    private SQLContainer() {
+    }
+
+    /**
+     * Creates and initializes SQLContainer using the given QueryDelegate
+     * 
+     * @param delegate
+     *            QueryDelegate implementation
+     * @throws SQLException
+     */
+    public SQLContainer(QueryDelegate delegate) throws SQLException {
+        if (delegate == null) {
+            throw new IllegalArgumentException(
+                    "QueryDelegate must not be null.");
+        }
+        this.delegate = delegate;
+        getPropertyIds();
+        cachedItems.setCacheLimit(CACHE_RATIO * getPageLength());
+    }
+
+    /**************************************/
+    /** Methods from interface Container **/
+    /**************************************/
+
+    /**
+     * Note! If auto commit mode is enabled, this method will still return the
+     * temporary row ID assigned for the item. Implement
+     * QueryDelegate.RowIdChangeListener to receive the actual Row ID value
+     * after the addition has been committed.
+     * 
+     * {@inheritDoc}
+     */
+    public Object addItem() throws UnsupportedOperationException {
+        Object emptyKey[] = new Object[delegate.getPrimaryKeyColumns().size()];
+        RowId itemId = new TemporaryRowId(emptyKey);
+        // Create new empty column properties for the row item.
+        List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>();
+        for (String propertyId : propertyIds) {
+            /* Default settings for new item properties. */
+            itemProperties
+                    .add(new ColumnProperty(propertyId, propertyReadOnly
+                            .get(propertyId),
+                            !propertyReadOnly.get(propertyId), propertyNullable
+                                    .get(propertyId), null, getType(propertyId)));
+        }
+        RowItem newRowItem = new RowItem(this, itemId, itemProperties);
+
+        if (autoCommit) {
+            /* Add and commit instantly */
+            try {
+                if (delegate instanceof TableQuery) {
+                    itemId = ((TableQuery) delegate)
+                            .storeRowImmediately(newRowItem);
+                } else {
+                    delegate.beginTransaction();
+                    delegate.storeRow(newRowItem);
+                    delegate.commit();
+                }
+                refresh();
+                if (notificationsEnabled) {
+                    CacheFlushNotifier.notifyOfCacheFlush(this);
+                }
+                debug(null, "Row added to DB...");
+                return itemId;
+            } catch (SQLException e) {
+                debug(e, null);
+                try {
+                    delegate.rollback();
+                } catch (SQLException ee) {
+                    debug(ee, null);
+                }
+                return null;
+            }
+        } else {
+            addedItems.add(newRowItem);
+            fireContentsChange();
+            return itemId;
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container#containsId(java.lang.Object)
+     */
+    public boolean containsId(Object itemId) {
+        if (itemId == null) {
+            return false;
+        }
+
+        if (cachedItems.containsKey(itemId)) {
+            return true;
+        } else {
+            for (RowItem item : addedItems) {
+                if (item.getId().equals(itemId)) {
+                    return itemPassesFilters(item);
+                }
+            }
+        }
+        if (removedItems.containsKey(itemId)) {
+            return false;
+        }
+
+        if (itemId instanceof ReadOnlyRowId) {
+            int rowNum = ((ReadOnlyRowId) itemId).getRowNum();
+            return rowNum >= 0 && rowNum < size;
+        }
+
+        if (!(itemId instanceof TemporaryRowId)) {
+            try {
+                return delegate.containsRowWithKey(((RowId) itemId).getId());
+            } catch (Exception e) {
+                /* Query failed, just return false. */
+                debug(e, null);
+            }
+        }
+        return false;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object,
+     * java.lang.Object)
+     */
+    public Property getContainerProperty(Object itemId, Object propertyId) {
+        Item item = getItem(itemId);
+        if (item == null) {
+            return null;
+        }
+        return item.getItemProperty(propertyId);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container#getContainerPropertyIds()
+     */
+    public Collection<?> getContainerPropertyIds() {
+        return Collections.unmodifiableCollection(propertyIds);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container#getItem(java.lang.Object)
+     */
+    public Item getItem(Object itemId) {
+        if (!cachedItems.containsKey(itemId)) {
+            int index = indexOfId(itemId);
+            if (index >= size) {
+                // The index is in the added items
+                int offset = index - size;
+                RowItem item = addedItems.get(offset);
+                if (itemPassesFilters(item)) {
+                    return item;
+                } else {
+                    return null;
+                }
+            } else {
+                // load the item into cache
+                updateOffsetAndCache(index);
+            }
+        }
+        return cachedItems.get(itemId);
+    }
+
+    /**
+     * Bypasses in-memory filtering to return items that are cached in memory.
+     * <em>NOTE</em>: This does not bypass database-level filtering.
+     * 
+     * @param itemId
+     *            the id of the item to retrieve.
+     * @return the item represented by itemId.
+     */
+    public Item getItemUnfiltered(Object itemId) {
+        if (!cachedItems.containsKey(itemId)) {
+            for (RowItem item : addedItems) {
+                if (item.getId().equals(itemId)) {
+                    return item;
+                }
+            }
+        }
+        return cachedItems.get(itemId);
+    }
+
+    /**
+     * NOTE! Do not use this method if in any way avoidable. This method doesn't
+     * (and cannot) use lazy loading, which means that all rows in the database
+     * will be loaded into memory.
+     * 
+     * {@inheritDoc}
+     */
+    public Collection<?> getItemIds() {
+        updateCount();
+        ArrayList<RowId> ids = new ArrayList<RowId>();
+        ResultSet rs = null;
+        try {
+            // Load ALL rows :(
+            delegate.beginTransaction();
+            rs = delegate.getResults(0, 0);
+            List<String> pKeys = delegate.getPrimaryKeyColumns();
+            while (rs.next()) {
+                RowId id = null;
+                if (pKeys.isEmpty()) {
+                    /* Create a read only itemId */
+                    id = new ReadOnlyRowId(rs.getRow());
+                } else {
+                    /* Generate itemId for the row based on primary key(s) */
+                    Object[] itemId = new Object[pKeys.size()];
+                    for (int i = 0; i < pKeys.size(); i++) {
+                        itemId[i] = rs.getObject(pKeys.get(i));
+                    }
+                    id = new RowId(itemId);
+                }
+                if (id != null && !removedItems.containsKey(id)) {
+                    ids.add(id);
+                }
+            }
+            rs.getStatement().close();
+            rs.close();
+            delegate.commit();
+        } catch (SQLException e) {
+            debug(e, null);
+            try {
+                delegate.rollback();
+            } catch (SQLException e1) {
+                debug(e1, null);
+            }
+            try {
+                rs.getStatement().close();
+                rs.close();
+            } catch (SQLException e1) {
+                debug(e1, null);
+            }
+            throw new RuntimeException("Failed to fetch item indexes.", e);
+        }
+        for (RowItem item : getFilteredAddedItems()) {
+            ids.add(item.getId());
+        }
+        return Collections.unmodifiableCollection(ids);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container#getType(java.lang.Object)
+     */
+    public Class<?> getType(Object propertyId) {
+        if (!propertyIds.contains(propertyId)) {
+            return null;
+        }
+        return propertyTypes.get(propertyId);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container#size()
+     */
+    public int size() {
+        updateCount();
+        return size + sizeOfAddedItems() - removedItems.size();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+     */
+    public boolean removeItem(Object itemId)
+            throws UnsupportedOperationException {
+        if (!containsId(itemId)) {
+            return false;
+        }
+        for (RowItem item : addedItems) {
+            if (item.getId().equals(itemId)) {
+                addedItems.remove(item);
+                fireContentsChange();
+                return true;
+            }
+        }
+
+        if (autoCommit) {
+            /* Remove and commit instantly. */
+            Item i = getItem(itemId);
+            if (i == null) {
+                return false;
+            }
+            try {
+                delegate.beginTransaction();
+                boolean success = delegate.removeRow((RowItem) i);
+                delegate.commit();
+                refresh();
+                if (notificationsEnabled) {
+                    CacheFlushNotifier.notifyOfCacheFlush(this);
+                }
+                if (success) {
+                    debug(null, "Row removed from DB...");
+                }
+                return success;
+            } catch (SQLException e) {
+                debug(e, null);
+                try {
+                    delegate.rollback();
+                } catch (SQLException ee) {
+                    /* Nothing can be done here */
+                    debug(ee, null);
+                }
+                return false;
+            }
+        } else {
+            removedItems.put((RowId) itemId, (RowItem) getItem(itemId));
+            cachedItems.remove(itemId);
+            refresh();
+            return true;
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container#removeAllItems()
+     */
+    public boolean removeAllItems() throws UnsupportedOperationException {
+        if (autoCommit) {
+            /* Remove and commit instantly. */
+            try {
+                delegate.beginTransaction();
+                boolean success = true;
+                for (Object id : getItemIds()) {
+                    if (!delegate.removeRow((RowItem) getItem(id))) {
+                        success = false;
+                    }
+                }
+                if (success) {
+                    delegate.commit();
+                    debug(null, "All rows removed from DB...");
+                    refresh();
+                    if (notificationsEnabled) {
+                        CacheFlushNotifier.notifyOfCacheFlush(this);
+                    }
+                } else {
+                    delegate.rollback();
+                }
+                return success;
+            } catch (SQLException e) {
+                debug(e, null);
+                try {
+                    delegate.rollback();
+                } catch (SQLException ee) {
+                    /* Nothing can be done here */
+                    debug(ee, null);
+                }
+                return false;
+            }
+        } else {
+            for (Object id : getItemIds()) {
+                removedItems.put((RowId) id, (RowItem) getItem(id));
+                cachedItems.remove(id);
+            }
+            refresh();
+            return true;
+        }
+    }
+
+    /*************************************************/
+    /** Methods from interface Container.Filterable **/
+    /*************************************************/
+
+    /**
+     * {@inheritDoc}
+     */
+    public void addContainerFilter(Filter filter)
+            throws UnsupportedFilterException {
+        // filter.setCaseSensitive(!ignoreCase);
+
+        filters.add(filter);
+        refresh();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void removeContainerFilter(Filter filter) {
+        filters.remove(filter);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void addContainerFilter(Object propertyId, String filterString,
+            boolean ignoreCase, boolean onlyMatchPrefix) {
+        if (propertyId == null || !propertyIds.contains(propertyId)) {
+            return;
+        }
+
+        /* Generate Filter -object */
+        String likeStr = onlyMatchPrefix ? filterString + "%" : "%"
+                + filterString + "%";
+        Like like = new Like(propertyId.toString(), likeStr);
+        like.setCaseSensitive(!ignoreCase);
+        filters.add(like);
+        refresh();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void removeContainerFilters(Object propertyId) {
+        ArrayList<Filter> toRemove = new ArrayList<Filter>();
+        for (Filter f : filters) {
+            if (f.appliesToProperty(propertyId)) {
+                toRemove.add(f);
+            }
+        }
+        filters.removeAll(toRemove);
+        refresh();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void removeAllContainerFilters() {
+        filters.clear();
+        refresh();
+    }
+
+    /**********************************************/
+    /** Methods from interface Container.Indexed **/
+    /**********************************************/
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Indexed#indexOfId(java.lang.Object)
+     */
+    public int indexOfId(Object itemId) {
+        // First check if the id is in the added items
+        for (int ix = 0; ix < addedItems.size(); ix++) {
+            RowItem item = addedItems.get(ix);
+            if (item.getId().equals(itemId)) {
+                if (itemPassesFilters(item)) {
+                    updateCount();
+                    return size + ix;
+                } else {
+                    return -1;
+                }
+            }
+        }
+
+        if (!containsId(itemId)) {
+            return -1;
+        }
+        if (cachedItems.isEmpty()) {
+            getPage();
+        }
+        int size = size();
+        boolean wrappedAround = false;
+        while (!wrappedAround) {
+            for (Integer i : itemIndexes.keySet()) {
+                if (itemIndexes.get(i).equals(itemId)) {
+                    return i;
+                }
+            }
+            // load in the next page.
+            int nextIndex = (currentOffset / (pageLength * CACHE_RATIO) + 1)
+                    * (pageLength * CACHE_RATIO);
+            if (nextIndex >= size) {
+                // Container wrapped around, start from index 0.
+                wrappedAround = true;
+                nextIndex = 0;
+            }
+            updateOffsetAndCache(nextIndex);
+        }
+        return -1;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Indexed#getIdByIndex(int)
+     */
+    public Object getIdByIndex(int index) {
+        if (index < 0 || index > size() - 1) {
+            return null;
+        }
+        if (index < size) {
+            if (itemIndexes.keySet().contains(index)) {
+                return itemIndexes.get(index);
+            }
+            updateOffsetAndCache(index);
+            return itemIndexes.get(index);
+        } else {
+            // The index is in the added items
+            int offset = index - size;
+            return addedItems.get(offset).getId();
+        }
+    }
+
+    /**********************************************/
+    /** Methods from interface Container.Ordered **/
+    /**********************************************/
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object)
+     */
+    public Object nextItemId(Object itemId) {
+        return getIdByIndex(indexOfId(itemId) + 1);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object)
+     */
+    public Object prevItemId(Object itemId) {
+        return getIdByIndex(indexOfId(itemId) - 1);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Ordered#firstItemId()
+     */
+    public Object firstItemId() {
+        updateCount();
+        if (size == 0) {
+            if (addedItems.isEmpty()) {
+                return null;
+            } else {
+                int ix = -1;
+                do {
+                    ix++;
+                } while (!itemPassesFilters(addedItems.get(ix))
+                        && ix < addedItems.size());
+                if (ix < addedItems.size()) {
+                    return addedItems.get(ix).getId();
+                }
+            }
+        }
+        if (!itemIndexes.containsKey(0)) {
+            updateOffsetAndCache(0);
+        }
+        return itemIndexes.get(0);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Ordered#lastItemId()
+     */
+    public Object lastItemId() {
+        if (addedItems.isEmpty()) {
+            int lastIx = size() - 1;
+            if (!itemIndexes.containsKey(lastIx)) {
+                updateOffsetAndCache(size - 1);
+            }
+            return itemIndexes.get(lastIx);
+        } else {
+            int ix = addedItems.size();
+            do {
+                ix--;
+            } while (!itemPassesFilters(addedItems.get(ix)) && ix >= 0);
+            if (ix >= 0) {
+                return addedItems.get(ix).getId();
+            } else {
+                return null;
+            }
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object)
+     */
+    public boolean isFirstId(Object itemId) {
+        return firstItemId().equals(itemId);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object)
+     */
+    public boolean isLastId(Object itemId) {
+        return lastItemId().equals(itemId);
+    }
+
+    /***********************************************/
+    /** Methods from interface Container.Sortable **/
+    /***********************************************/
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
+     * boolean[])
+     */
+    public void sort(Object[] propertyId, boolean[] ascending) {
+        sorters.clear();
+        if (propertyId == null || propertyId.length == 0) {
+            refresh();
+            return;
+        }
+        /* Generate OrderBy -objects */
+        boolean asc = true;
+        for (int i = 0; i < propertyId.length; i++) {
+            /* Check that the property id is valid */
+            if (propertyId[i] instanceof String
+                    && propertyIds.contains(propertyId[i])) {
+                try {
+                    asc = ascending[i];
+                } catch (Exception e) {
+                    debug(e, null);
+                }
+                sorters.add(new OrderBy((String) propertyId[i], asc));
+            }
+        }
+        refresh();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds()
+     */
+    public Collection<?> getSortableContainerPropertyIds() {
+        return getContainerPropertyIds();
+    }
+
+    /**************************************/
+    /** Methods specific to SQLContainer **/
+    /**************************************/
+
+    /**
+     * Refreshes the container - clears all caches and resets size and offset.
+     * Does NOT remove sorting or filtering rules!
+     */
+    public void refresh() {
+        sizeDirty = true;
+        currentOffset = 0;
+        cachedItems.clear();
+        itemIndexes.clear();
+        fireContentsChange();
+    }
+
+    /**
+     * Returns modify state of the container.
+     * 
+     * @return true if contents of this container have been modified
+     */
+    public boolean isModified() {
+        return !removedItems.isEmpty() || !addedItems.isEmpty()
+                || !modifiedItems.isEmpty();
+    }
+
+    /**
+     * Set auto commit mode enabled or disabled. Auto commit mode means that all
+     * changes made to items of this container will be immediately written to
+     * the underlying data source.
+     * 
+     * @param autoCommitEnabled
+     *            true to enable auto commit mode
+     */
+    public void setAutoCommit(boolean autoCommitEnabled) {
+        autoCommit = autoCommitEnabled;
+    }
+
+    /**
+     * Returns status of the auto commit mode.
+     * 
+     * @return true if auto commit mode is enabled
+     */
+    public boolean isAutoCommit() {
+        return autoCommit;
+    }
+
+    /**
+     * Returns the currently set page length.
+     * 
+     * @return current page length
+     */
+    public int getPageLength() {
+        return pageLength;
+    }
+
+    /**
+     * Sets the page length used in lazy fetching of items from the data source.
+     * Also resets the cache size to match the new page length.
+     * 
+     * As a side effect the container will be refreshed.
+     * 
+     * @param pageLength
+     *            new page length
+     */
+    public void setPageLength(int pageLength) {
+        setPageLengthInternal(pageLength);
+        refresh();
+    }
+
+    /**
+     * Sets the page length internally, without refreshing the container.
+     * 
+     * @param pageLength
+     *            the new page length
+     */
+    private void setPageLengthInternal(int pageLength) {
+        this.pageLength = pageLength > 0 ? pageLength : DEFAULT_PAGE_LENGTH;
+        cachedItems.setCacheLimit(CACHE_RATIO * getPageLength());
+    }
+
+    /**
+     * Adds the given OrderBy to this container and refreshes the container
+     * contents with the new sorting rules.
+     * 
+     * Note that orderBy.getColumn() must return a column name that exists in
+     * this container.
+     * 
+     * @param orderBy
+     *            OrderBy to be added to the container sorting rules
+     */
+    public void addOrderBy(OrderBy orderBy) {
+        if (orderBy == null) {
+            return;
+        }
+        if (!propertyIds.contains(orderBy.getColumn())) {
+            throw new IllegalArgumentException(
+                    "The column given for sorting does not exist in this container.");
+        }
+        sorters.add(orderBy);
+        refresh();
+    }
+
+    /**
+     * Commits all the changes, additions and removals made to the items of this
+     * container.
+     * 
+     * @throws UnsupportedOperationException
+     * @throws SQLException
+     */
+    public void commit() throws UnsupportedOperationException, SQLException {
+        try {
+            debug(null, "Commiting changes through delegate...");
+            delegate.beginTransaction();
+            /* Perform buffered deletions */
+            for (RowItem item : removedItems.values()) {
+                if (!delegate.removeRow(item)) {
+                    throw new SQLException("Removal failed for row with ID: "
+                            + item.getId());
+                }
+            }
+            /* Perform buffered modifications */
+            for (RowItem item : modifiedItems) {
+                if (delegate.storeRow(item) > 0) {
+                    /*
+                     * Also reset the modified state in the item in case it is
+                     * reused e.g. in a form.
+                     */
+                    item.commit();
+                } else {
+                    delegate.rollback();
+                    refresh();
+                    throw new ConcurrentModificationException(
+                            "Item with the ID '" + item.getId()
+                                    + "' has been externally modified.");
+                }
+            }
+            /* Perform buffered additions */
+            for (RowItem item : addedItems) {
+                delegate.storeRow(item);
+            }
+            delegate.commit();
+            removedItems.clear();
+            addedItems.clear();
+            modifiedItems.clear();
+            refresh();
+            if (notificationsEnabled) {
+                CacheFlushNotifier.notifyOfCacheFlush(this);
+            }
+        } catch (SQLException e) {
+            delegate.rollback();
+            throw e;
+        }
+    }
+
+    /**
+     * Rolls back all the changes, additions and removals made to the items of
+     * this container.
+     * 
+     * @throws UnsupportedOperationException
+     * @throws SQLException
+     */
+    public void rollback() throws UnsupportedOperationException, SQLException {
+        debug(null, "Rolling back changes...");
+        removedItems.clear();
+        addedItems.clear();
+        modifiedItems.clear();
+        refresh();
+    }
+
+    /**
+     * Notifies this container that a property in the given item has been
+     * modified. The change will be buffered or made instantaneously depending
+     * on auto commit mode.
+     * 
+     * @param changedItem
+     *            item that has a modified property
+     */
+    void itemChangeNotification(RowItem changedItem) {
+        if (autoCommit) {
+            try {
+                delegate.beginTransaction();
+                if (delegate.storeRow(changedItem) == 0) {
+                    delegate.rollback();
+                    refresh();
+                    throw new ConcurrentModificationException(
+                            "Item with the ID '" + changedItem.getId()
+                                    + "' has been externally modified.");
+                }
+                delegate.commit();
+                if (notificationsEnabled) {
+                    CacheFlushNotifier.notifyOfCacheFlush(this);
+                }
+                debug(null, "Row updated to DB...");
+            } catch (SQLException e) {
+                debug(e, null);
+                try {
+                    delegate.rollback();
+                } catch (SQLException ee) {
+                    /* Nothing can be done here */
+                    debug(e, null);
+                }
+                throw new RuntimeException(e);
+            }
+        } else {
+            if (!(changedItem.getId() instanceof TemporaryRowId)
+                    && !modifiedItems.contains(changedItem)) {
+                modifiedItems.add(changedItem);
+            }
+        }
+    }
+
+    /**
+     * Determines a new offset for updating the row cache. The offset is
+     * calculated from the given index, and will be fixed to match the start of
+     * a page, based on the value of pageLength.
+     * 
+     * @param index
+     *            Index of the item that was requested, but not found in cache
+     */
+    private void updateOffsetAndCache(int index) {
+        if (itemIndexes.containsKey(index)) {
+            return;
+        }
+        currentOffset = (index / (pageLength * CACHE_RATIO))
+                * (pageLength * CACHE_RATIO);
+        if (currentOffset < 0) {
+            currentOffset = 0;
+        }
+        getPage();
+    }
+
+    /**
+     * Fetches new count of rows from the data source, if needed.
+     */
+    private void updateCount() {
+        if (!sizeDirty
+                && new Date().getTime() < sizeUpdated.getTime()
+                        + sizeValidMilliSeconds) {
+            return;
+        }
+        try {
+            try {
+                delegate.setFilters(filters);
+            } catch (UnsupportedOperationException e) {
+                /* The query delegate doesn't support filtering. */
+                debug(e, null);
+            }
+            try {
+                delegate.setOrderBy(sorters);
+            } catch (UnsupportedOperationException e) {
+                /* The query delegate doesn't support filtering. */
+                debug(e, null);
+            }
+            int newSize = delegate.getCount();
+            if (newSize != size) {
+                size = newSize;
+                refresh();
+            }
+            sizeUpdated = new Date();
+            sizeDirty = false;
+            debug(null, "Updated row count. New count is: " + size);
+        } catch (SQLException e) {
+            throw new RuntimeException("Failed to update item set size.", e);
+        }
+    }
+
+    /**
+     * Fetches property id's (column names and their types) from the data
+     * source.
+     * 
+     * @throws SQLException
+     */
+    private void getPropertyIds() throws SQLException {
+        propertyIds.clear();
+        propertyTypes.clear();
+        delegate.setFilters(null);
+        delegate.setOrderBy(null);
+        ResultSet rs = null;
+        ResultSetMetaData rsmd = null;
+        try {
+            delegate.beginTransaction();
+            rs = delegate.getResults(0, 1);
+            boolean resultExists = rs.next();
+            rsmd = rs.getMetaData();
+            Class<?> type = null;
+            for (int i = 1; i <= rsmd.getColumnCount(); i++) {
+                if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) {
+                    continue;
+                }
+                String colName = rsmd.getColumnLabel(i);
+                /*
+                 * Make sure not to add the same colName twice. This can easily
+                 * happen if the SQL query joins many tables with an ID column.
+                 */
+                if (!propertyIds.contains(colName)) {
+                    propertyIds.add(colName);
+                }
+                /* Try to determine the column's JDBC class by all means. */
+                if (resultExists && rs.getObject(i) != null) {
+                    type = rs.getObject(i).getClass();
+                } else {
+                    try {
+                        type = Class.forName(rsmd.getColumnClassName(i));
+                    } catch (Exception e) {
+                        debug(e, null);
+                        /* On failure revert to Object and hope for the best. */
+                        type = Object.class;
+                    }
+                }
+                /*
+                 * Determine read only and nullability status of the column. A
+                 * column is read only if it is reported as either read only or
+                 * auto increment by the database, and also it is set as the
+                 * version column in a TableQuery delegate.
+                 */
+                boolean readOnly = rsmd.isAutoIncrement(i)
+                        || rsmd.isReadOnly(i);
+                if (delegate instanceof TableQuery
+                        && rsmd.getColumnLabel(i).equals(
+                                ((TableQuery) delegate).getVersionColumn())) {
+                    readOnly = true;
+                }
+                propertyReadOnly.put(colName, readOnly);
+                propertyNullable.put(colName,
+                        rsmd.isNullable(i) == ResultSetMetaData.columnNullable);
+                propertyTypes.put(colName, type);
+            }
+            rs.getStatement().close();
+            rs.close();
+            delegate.commit();
+            debug(null, "Property IDs fetched.");
+        } catch (SQLException e) {
+            debug(e, null);
+            try {
+                delegate.rollback();
+            } catch (SQLException e1) {
+                debug(e1, null);
+            }
+            try {
+                if (rs != null) {
+                    if (rs.getStatement() != null) {
+                        rs.getStatement().close();
+                    }
+                    rs.close();
+                }
+            } catch (SQLException e1) {
+                debug(e1, null);
+            }
+            throw e;
+        }
+    }
+
+    /**
+     * Fetches a page from the data source based on the values of pageLenght and
+     * currentOffset. Also updates the set of primary keys, used in
+     * identification of RowItems.
+     */
+    private void getPage() {
+        updateCount();
+        ResultSet rs = null;
+        ResultSetMetaData rsmd = null;
+        cachedItems.clear();
+        itemIndexes.clear();
+        try {
+            try {
+                delegate.setOrderBy(sorters);
+            } catch (UnsupportedOperationException e) {
+                /* The query delegate doesn't support sorting. */
+                /* No need to do anything. */
+                debug(e, null);
+            }
+            delegate.beginTransaction();
+            rs = delegate.getResults(currentOffset, pageLength * CACHE_RATIO);
+            rsmd = rs.getMetaData();
+            List<String> pKeys = delegate.getPrimaryKeyColumns();
+            // }
+            /* Create new items and column properties */
+            ColumnProperty cp = null;
+            int rowCount = currentOffset;
+            if (!delegate.implementationRespectsPagingLimits()) {
+                rowCount = currentOffset = 0;
+                setPageLengthInternal(size);
+            }
+            while (rs.next()) {
+                List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>();
+                /* Generate row itemId based on primary key(s) */
+                Object[] itemId = new Object[pKeys.size()];
+                for (int i = 0; i < pKeys.size(); i++) {
+                    itemId[i] = rs.getObject(pKeys.get(i));
+                }
+                RowId id = null;
+                if (pKeys.isEmpty()) {
+                    id = new ReadOnlyRowId(rs.getRow());
+                } else {
+                    id = new RowId(itemId);
+                }
+                List<String> propertiesToAdd = new ArrayList<String>(
+                        propertyIds);
+                if (!removedItems.containsKey(id)) {
+                    for (int i = 1; i <= rsmd.getColumnCount(); i++) {
+                        if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) {
+                            continue;
+                        }
+                        String colName = rsmd.getColumnLabel(i);
+                        Object value = rs.getObject(i);
+                        Class<?> type = value != null ? value.getClass()
+                                : Object.class;
+                        if (value == null) {
+                            for (String propName : propertyTypes.keySet()) {
+                                if (propName.equals(rsmd.getColumnLabel(i))) {
+                                    type = propertyTypes.get(propName);
+                                    break;
+                                }
+                            }
+                        }
+                        /*
+                         * In case there are more than one column with the same
+                         * name, add only the first one. This can easily happen
+                         * if you join many tables where each table has an ID
+                         * column.
+                         */
+                        if (propertiesToAdd.contains(colName)) {
+                            cp = new ColumnProperty(colName,
+                                    propertyReadOnly.get(colName),
+                                    !propertyReadOnly.get(colName),
+                                    propertyNullable.get(colName), value, type);
+                            itemProperties.add(cp);
+                            propertiesToAdd.remove(colName);
+                        }
+                    }
+                    /* Cache item */
+                    itemIndexes.put(rowCount, id);
+                    cachedItems.put(id, new RowItem(this, id, itemProperties));
+                    rowCount++;
+                }
+            }
+            rs.getStatement().close();
+            rs.close();
+            delegate.commit();
+            debug(null, "Fetched " + pageLength * CACHE_RATIO
+                    + " rows starting from " + currentOffset);
+        } catch (SQLException e) {
+            debug(e, null);
+            try {
+                delegate.rollback();
+            } catch (SQLException e1) {
+                debug(e1, null);
+            }
+            try {
+                if (rs != null) {
+                    if (rs.getStatement() != null) {
+                        rs.getStatement().close();
+                        rs.close();
+                    }
+                }
+            } catch (SQLException e1) {
+                debug(e1, null);
+            }
+            throw new RuntimeException("Failed to fetch page.", e);
+        }
+    }
+
+    private int sizeOfAddedItems() {
+        return getFilteredAddedItems().size();
+    }
+
+    private List<RowItem> getFilteredAddedItems() {
+        ArrayList<RowItem> filtered = new ArrayList<RowItem>(addedItems);
+        if (filters != null && !filters.isEmpty()) {
+            for (RowItem item : addedItems) {
+                if (!itemPassesFilters(item)) {
+                    filtered.remove(item);
+                }
+            }
+        }
+        return filtered;
+    }
+
+    private boolean itemPassesFilters(RowItem item) {
+        for (Filter filter : filters) {
+            if (!filter.passesFilter(item.getId(), item)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Checks is the given column identifier valid to be used with SQLContainer.
+     * Currently the only non-valid identifier is "rownum" when MSSQL or Oracle
+     * is used. This is due to the way the SELECT queries are constructed in
+     * order to implement paging in these databases.
+     * 
+     * @param identifier
+     *            Column identifier
+     * @return true if the identifier is valid
+     */
+    private boolean isColumnIdentifierValid(String identifier) {
+        if (identifier.equalsIgnoreCase("rownum")
+                && delegate instanceof TableQuery) {
+            TableQuery tq = (TableQuery) delegate;
+            if (tq.getSqlGenerator() instanceof MSSQLGenerator
+                    || tq.getSqlGenerator() instanceof OracleGenerator) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns the QueryDelegate set for this SQLContainer.
+     * 
+     * @return current querydelegate
+     */
+    protected QueryDelegate getQueryDelegate() {
+        return delegate;
+    }
+
+    /************************************/
+    /** UNSUPPORTED CONTAINER FEATURES **/
+    /************************************/
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object,
+     * java.lang.Class, java.lang.Object)
+     */
+    public boolean addContainerProperty(Object propertyId, Class<?> type,
+            Object defaultValue) throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object)
+     */
+    public boolean removeContainerProperty(Object propertyId)
+            throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container#addItem(java.lang.Object)
+     */
+    public Item addItem(Object itemId) throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object,
+     * java.lang.Object)
+     */
+    public Item addItemAfter(Object previousItemId, Object newItemId)
+            throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Indexed#addItemAt(int, java.lang.Object)
+     */
+    public Item addItemAt(int index, Object newItemId)
+            throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Indexed#addItemAt(int)
+     */
+    public Object addItemAt(int index) throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object)
+     */
+    public Object addItemAfter(Object previousItemId)
+            throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
+    }
+
+    /******************************************/
+    /** ITEMSETCHANGENOTIFIER IMPLEMENTATION **/
+    /******************************************/
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin
+     * .data.Container.ItemSetChangeListener)
+     */
+    public void addListener(Container.ItemSetChangeListener listener) {
+        if (itemSetChangeListeners == null) {
+            itemSetChangeListeners = new LinkedList<Container.ItemSetChangeListener>();
+        }
+        itemSetChangeListeners.add(listener);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin
+     * .data.Container.ItemSetChangeListener)
+     */
+    public void removeListener(Container.ItemSetChangeListener listener) {
+        if (itemSetChangeListeners != null) {
+            itemSetChangeListeners.remove(listener);
+        }
+    }
+
+    protected void fireContentsChange() {
+        if (itemSetChangeListeners != null) {
+            final Object[] l = itemSetChangeListeners.toArray();
+            final Container.ItemSetChangeEvent event = new SQLContainer.ItemSetChangeEvent(
+                    this);
+            for (int i = 0; i < l.length; i++) {
+                ((Container.ItemSetChangeListener) l[i])
+                        .containerItemSetChange(event);
+            }
+        }
+    }
+
+    /**
+     * Simple ItemSetChangeEvent implementation.
+     */
+    @SuppressWarnings("serial")
+    public class ItemSetChangeEvent extends EventObject implements
+            Container.ItemSetChangeEvent {
+
+        private ItemSetChangeEvent(SQLContainer source) {
+            super(source);
+        }
+
+        public Container getContainer() {
+            return (Container) getSource();
+        }
+    }
+
+    /**************************************************/
+    /** ROWIDCHANGELISTENER PASSING TO QUERYDELEGATE **/
+    /**************************************************/
+
+    /**
+     * Adds a RowIdChangeListener to the QueryDelegate
+     * 
+     * @param listener
+     */
+    public void addListener(RowIdChangeListener listener) {
+        if (delegate instanceof QueryDelegate.RowIdChangeNotifier) {
+            ((QueryDelegate.RowIdChangeNotifier) delegate)
+                    .addListener(listener);
+        }
+    }
+
+    /**
+     * Removes a RowIdChangeListener from the QueryDelegate
+     * 
+     * @param listener
+     */
+    public void removeListener(RowIdChangeListener listener) {
+        if (delegate instanceof QueryDelegate.RowIdChangeNotifier) {
+            ((QueryDelegate.RowIdChangeNotifier) delegate)
+                    .removeListener(listener);
+        }
+    }
+
+    public boolean isDebugMode() {
+        return debugMode;
+    }
+
+    public void setDebugMode(boolean debugMode) {
+        this.debugMode = debugMode;
+    }
+
+    /**
+     * Output a debug message or a stack trace of an exception
+     * 
+     * @param message
+     */
+    private void debug(Exception e, String message) {
+        if (debugMode) {
+            // TODO: Replace with the common Vaadin logging system once it is
+            // available.
+            if (message != null) {
+                System.err.println(message);
+            }
+            if (e != null) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Calling this will enable this SQLContainer to send and receive cache
+     * flush notifications for its lifetime.
+     */
+    public void enableCacheFlushNotifications() {
+        if (!notificationsEnabled) {
+            notificationsEnabled = true;
+            CacheFlushNotifier.addInstance(this);
+        }
+    }
+
+    /******************************************/
+    /** Referencing mechanism implementation **/
+    /******************************************/
+
+    /**
+     * Adds a new reference to the given SQLContainer. In addition to the
+     * container you must provide the column (property) names used for the
+     * reference in both this and the referenced SQLContainer.
+     * 
+     * Note that multiple references pointing to the same SQLContainer are not
+     * supported.
+     * 
+     * @param refdCont
+     *            Target SQLContainer of the new reference
+     * @param refingCol
+     *            Column (property) name in this container storing the (foreign
+     *            key) reference
+     * @param refdCol
+     *            Column (property) name in the referenced container storing the
+     *            referenced key
+     */
+    public void addReference(SQLContainer refdCont, String refingCol,
+            String refdCol) {
+        if (refdCont == null) {
+            throw new IllegalArgumentException(
+                    "Referenced SQLContainer can not be null.");
+        }
+        if (!getContainerPropertyIds().contains(refingCol)) {
+            throw new IllegalArgumentException(
+                    "Given referencing column name is invalid."
+                            + " Please ensure that this container"
+                            + " contains a property ID named: " + refingCol);
+        }
+        if (!refdCont.getContainerPropertyIds().contains(refdCol)) {
+            throw new IllegalArgumentException(
+                    "Given referenced column name is invalid."
+                            + " Please ensure that the referenced container"
+                            + " contains a property ID named: " + refdCol);
+        }
+        if (references.keySet().contains(refdCont)) {
+            throw new IllegalArgumentException(
+                    "An SQLContainer instance can only be referenced once.");
+        }
+        references.put(refdCont, new Reference(refdCont, refingCol, refdCol));
+    }
+
+    /**
+     * Removes the reference pointing to the given SQLContainer.
+     * 
+     * @param refdCont
+     *            Target SQLContainer of the reference
+     * @return true if successful, false if the reference did not exist
+     */
+    public boolean removeReference(SQLContainer refdCont) {
+        if (refdCont == null) {
+            throw new IllegalArgumentException(
+                    "Referenced SQLContainer can not be null.");
+        }
+        return references.remove(refdCont) == null ? false : true;
+    }
+
+    /**
+     * Sets the referenced item. The referencing column of the item in this
+     * container is updated accordingly.
+     * 
+     * @param itemId
+     *            Item Id of the reference source (from this container)
+     * @param refdItemId
+     *            Item Id of the reference target (from referenced container)
+     * @param refdCont
+     *            Target SQLContainer of the reference
+     * @return true if the referenced item was successfully set, false on
+     *         failure
+     */
+    public boolean setReferencedItem(Object itemId, Object refdItemId,
+            SQLContainer refdCont) {
+        if (refdCont == null) {
+            throw new IllegalArgumentException(
+                    "Referenced SQLContainer can not be null.");
+        }
+        Reference r = references.get(refdCont);
+        if (r == null) {
+            throw new IllegalArgumentException(
+                    "Reference to the given SQLContainer not defined.");
+        }
+        try {
+            getContainerProperty(itemId, r.getReferencingColumn()).setValue(
+                    refdCont.getContainerProperty(refdItemId,
+                            r.getReferencedColumn()));
+            return true;
+        } catch (Exception e) {
+            debug(e, "Setting referenced item failed.");
+            return false;
+        }
+    }
+
+    /**
+     * Fetches the Item Id of the referenced item from the target SQLContainer.
+     * 
+     * @param itemId
+     *            Item Id of the reference source (from this container)
+     * @param refdCont
+     *            Target SQLContainer of the reference
+     * @return Item Id of the referenced item, or null if not found
+     */
+    public Object getReferencedItemId(Object itemId, SQLContainer refdCont) {
+        if (refdCont == null) {
+            throw new IllegalArgumentException(
+                    "Referenced SQLContainer can not be null.");
+        }
+        Reference r = references.get(refdCont);
+        if (r == null) {
+            throw new IllegalArgumentException(
+                    "Reference to the given SQLContainer not defined.");
+        }
+        Object refKey = getContainerProperty(itemId, r.getReferencingColumn())
+                .getValue();
+
+        refdCont.removeAllContainerFilters();
+        refdCont.addContainerFilter(new Equal(r.getReferencedColumn(), refKey));
+        Object toReturn = refdCont.firstItemId();
+        refdCont.removeAllContainerFilters();
+        return toReturn;
+    }
+
+    /**
+     * Fetches the referenced item from the target SQLContainer.
+     * 
+     * @param itemId
+     *            Item Id of the reference source (from this container)
+     * @param refdCont
+     *            Target SQLContainer of the reference
+     * @return The referenced item, or null if not found
+     */
+    public Item getReferencedItem(Object itemId, SQLContainer refdCont) {
+        return refdCont.getItem(getReferencedItemId(itemId, refdCont));
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/vaadin/data/util/SQLUtil.java b/src/com/vaadin/data/util/SQLUtil.java
new file mode 100644 (file)
index 0000000..5426d8d
--- /dev/null
@@ -0,0 +1,31 @@
+package com.vaadin.data.util;
+
+public class SQLUtil {
+       /**
+     * Escapes different special characters in strings that are passed to SQL.
+     * Replaces the following:
+     * 
+     * <list> <li>' is replaced with ''</li> <li>\x00 is removed</li> <li>\ is
+     * replaced with \\</li> <li>" is replaced with \"</li> <li>
+     * \x1a is removed</li> </list>
+     * 
+     * Also note! The escaping done here may or may not be enough to prevent any
+     * and all SQL injections so it is recommended to check user input before
+     * giving it to the SQLContainer/TableQuery.
+     * 
+     * @param constant
+     * @return \\\'\'
+     */
+    public static String escapeSQL(String constant) {
+        if (constant == null) {
+            return null;
+        }
+        String fixedConstant = constant;
+        fixedConstant = fixedConstant.replaceAll("\\\\x00", "");
+        fixedConstant = fixedConstant.replaceAll("\\\\x1a", "");
+        fixedConstant = fixedConstant.replaceAll("'", "''");
+        fixedConstant = fixedConstant.replaceAll("\\\\", "\\\\\\\\");
+        fixedConstant = fixedConstant.replaceAll("\\\"", "\\\\\"");
+        return fixedConstant;
+    }
+}
diff --git a/src/com/vaadin/data/util/TemporaryRowId.java b/src/com/vaadin/data/util/TemporaryRowId.java
new file mode 100644 (file)
index 0000000..ec5ca7f
--- /dev/null
@@ -0,0 +1,29 @@
+package com.vaadin.data.util;
+
+public class TemporaryRowId extends RowId {
+    private static final long serialVersionUID = -641983830469018329L;
+
+    public TemporaryRowId(Object[] id) {
+        super(id);
+    }
+
+    @Override
+    public int hashCode() {
+        return id.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj instanceof TemporaryRowId)) {
+            return false;
+        }
+        Object[] compId = ((TemporaryRowId) obj).getId();
+        return id.equals(compId);
+    }
+
+    @Override
+    public String toString() {
+        return "Temporary row id";
+    }
+
+}
diff --git a/src/com/vaadin/data/util/connection/J2EEConnectionPool.java b/src/com/vaadin/data/util/connection/J2EEConnectionPool.java
new file mode 100644 (file)
index 0000000..789fcb9
--- /dev/null
@@ -0,0 +1,61 @@
+package com.vaadin.data.util.connection;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+
+public class J2EEConnectionPool implements JDBCConnectionPool {
+
+    private String dataSourceJndiName;
+
+    private DataSource dataSource = null;
+
+    public J2EEConnectionPool(DataSource dataSource) {
+        this.dataSource = dataSource;
+    }
+
+    public J2EEConnectionPool(String dataSourceJndiName) {
+        this.dataSourceJndiName = dataSourceJndiName;
+    }
+
+    public Connection reserveConnection() throws SQLException {
+        Connection conn = getDataSource().getConnection();
+        conn.setAutoCommit(false);
+
+        return conn;
+    }
+
+    private DataSource getDataSource() throws SQLException {
+        if (dataSource == null) {
+            dataSource = lookupDataSource();
+        }
+        return dataSource;
+    }
+
+    private DataSource lookupDataSource() throws SQLException {
+        try {
+            InitialContext ic = new InitialContext();
+            return (DataSource) ic.lookup(dataSourceJndiName);
+        } catch (NamingException e) {
+            throw new SQLException(
+                    "NamingException - Cannot connect to the database. Cause: "
+                            + e.getMessage());
+        }
+    }
+
+    public void releaseConnection(Connection conn) {
+        try {
+            conn.close();
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void destroy() {
+        dataSource = null;
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/vaadin/data/util/connection/JDBCConnectionPool.java b/src/com/vaadin/data/util/connection/JDBCConnectionPool.java
new file mode 100644 (file)
index 0000000..8f10d77
--- /dev/null
@@ -0,0 +1,38 @@
+package com.vaadin.data.util.connection;
+
+import java.io.Serializable;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+/**
+ * Interface for implementing connection pools to be used with SQLContainer.
+ */
+public interface JDBCConnectionPool extends Serializable {
+    /**
+     * Retrieves a connection.
+     * 
+     * @return a usable connection to the database
+     * @throws SQLException
+     */
+    public Connection reserveConnection() throws SQLException;
+
+    /**
+     * Releases a connection that was retrieved earlier.
+     * 
+     * Note that depending on implementation, the transaction possibly open in
+     * the connection may or may not be rolled back.
+     * 
+     * @param conn
+     *            Connection to be released
+     */
+    public void releaseConnection(Connection conn);
+
+    /**
+     * Destroys the connection pool: close() is called an all the connections in
+     * the pool, whether available or reserved.
+     * 
+     * This method was added to fix PostgreSQL -related issues with connections
+     * that were left hanging 'idle'.
+     */
+    public void destroy();
+}
diff --git a/src/com/vaadin/data/util/connection/SimpleJDBCConnectionPool.java b/src/com/vaadin/data/util/connection/SimpleJDBCConnectionPool.java
new file mode 100644 (file)
index 0000000..91da3f1
--- /dev/null
@@ -0,0 +1,162 @@
+package com.vaadin.data.util.connection;\r
+\r
+import java.io.IOException;\r
+import java.sql.Connection;\r
+import java.sql.DriverManager;\r
+import java.sql.SQLException;\r
+import java.sql.Statement;\r
+import java.util.HashSet;\r
+import java.util.Set;\r
+\r
+/**\r
+ * Simple implementation of the JDBCConnectionPool interface. Handles loading\r
+ * the JDBC driver, setting up the connections and ensuring they are still\r
+ * usable upon release.\r
+ */\r
+@SuppressWarnings("serial")\r
+public class SimpleJDBCConnectionPool implements JDBCConnectionPool {\r
+\r
+    private int initialConnections = 5;\r
+    private int maxConnections = 20;\r
+\r
+    private String driverName;\r
+    private String connectionUri;\r
+    private String userName;\r
+    private String password;\r
+\r
+    private transient Set<Connection> availableConnections;\r
+    private transient Set<Connection> reservedConnections;\r
+\r
+    private boolean initialized;\r
+\r
+    public SimpleJDBCConnectionPool(String driverName, String connectionUri,\r
+            String userName, String password) throws SQLException {\r
+        if (driverName == null) {\r
+            throw new IllegalArgumentException(\r
+                    "JDBC driver class name must be given.");\r
+        }\r
+        if (connectionUri == null) {\r
+            throw new IllegalArgumentException(\r
+                    "Database connection URI must be given.");\r
+        }\r
+        if (userName == null) {\r
+            throw new IllegalArgumentException(\r
+                    "Database username must be given.");\r
+        }\r
+        if (password == null) {\r
+            throw new IllegalArgumentException(\r
+                    "Database password must be given.");\r
+        }\r
+        this.driverName = driverName;\r
+        this.connectionUri = connectionUri;\r
+        this.userName = userName;\r
+        this.password = password;\r
+\r
+        /* Initialize JDBC driver */\r
+        try {\r
+            Class.forName(driverName).newInstance();\r
+        } catch (Exception ex) {\r
+            throw new RuntimeException("Specified JDBC Driver: " + driverName\r
+                    + " - initialization failed.", ex);\r
+        }\r
+    }\r
+\r
+    public SimpleJDBCConnectionPool(String driverName, String connectionUri,\r
+            String userName, String password, int initialConnections,\r
+            int maxConnections) throws SQLException {\r
+        this(driverName, connectionUri, userName, password);\r
+        this.initialConnections = initialConnections;\r
+        this.maxConnections = maxConnections;\r
+    }\r
+\r
+    private void initializeConnections() throws SQLException {\r
+        availableConnections = new HashSet<Connection>(initialConnections);\r
+        reservedConnections = new HashSet<Connection>(initialConnections);\r
+        for (int i = 0; i < initialConnections; i++) {\r
+            availableConnections.add(createConnection());\r
+        }\r
+        initialized = true;\r
+    }\r
+\r
+    public synchronized Connection reserveConnection() throws SQLException {\r
+        if (!initialized) {\r
+            initializeConnections();\r
+        }\r
+        if (availableConnections.isEmpty()) {\r
+            if (reservedConnections.size() < maxConnections) {\r
+                availableConnections.add(createConnection());\r
+            } else {\r
+                throw new SQLException("Connection limit has been reached.");\r
+            }\r
+        }\r
+\r
+        Connection c = availableConnections.iterator().next();\r
+        availableConnections.remove(c);\r
+        reservedConnections.add(c);\r
+\r
+        return c;\r
+    }\r
+\r
+    public synchronized void releaseConnection(Connection conn) {\r
+        if (conn == null || !initialized) {\r
+            return;\r
+        }\r
+        /* Try to roll back if necessary */\r
+        try {\r
+            if (!conn.getAutoCommit()) {\r
+                conn.rollback();\r
+            }\r
+        } catch (SQLException e) {\r
+            /* Roll back failed, close and discard connection */\r
+            try {\r
+                conn.close();\r
+            } catch (SQLException e1) {\r
+                /* Nothing needs to be done */\r
+            }\r
+            reservedConnections.remove(conn);\r
+            return;\r
+        }\r
+        reservedConnections.remove(conn);\r
+        availableConnections.add(conn);\r
+    }\r
+\r
+    private Connection createConnection() throws SQLException {\r
+        Connection c = DriverManager.getConnection(connectionUri, userName,\r
+                password);\r
+        c.setAutoCommit(false);\r
+        if (driverName.toLowerCase().contains("mysql")) {\r
+            try {\r
+                Statement s = c.createStatement();\r
+                s.execute("SET SESSION sql_mode = 'ANSI'");\r
+                s.close();\r
+            } catch (Exception e) {\r
+                // Failed to set ansi mode; continue\r
+            }\r
+        }\r
+        return c;\r
+    }\r
+\r
+    public void destroy() {\r
+        for (Connection c : availableConnections) {\r
+            try {\r
+                c.close();\r
+            } catch (SQLException e) {\r
+                // No need to do anything\r
+            }\r
+        }\r
+        for (Connection c : reservedConnections) {\r
+            try {\r
+                c.close();\r
+            } catch (SQLException e) {\r
+                // No need to do anything\r
+            }\r
+        }\r
+\r
+    }\r
+\r
+    private void writeObject(java.io.ObjectOutputStream out) throws IOException {\r
+        initialized = false;\r
+        out.defaultWriteObject();\r
+    }\r
+\r
+}\r
diff --git a/src/com/vaadin/data/util/filter/Between.java b/src/com/vaadin/data/util/filter/Between.java
new file mode 100644 (file)
index 0000000..a44995b
--- /dev/null
@@ -0,0 +1,69 @@
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+
+public class Between implements Filter {
+
+    private final Object propertyId;
+    private final Comparable startValue;
+    private final Comparable endValue;
+
+    public Between(Object propertyId, Comparable startValue, Comparable endValue) {
+        this.propertyId = propertyId;
+        this.startValue = startValue;
+        this.endValue = endValue;
+    }
+
+    public Object getPropertyId() {
+        return propertyId;
+    }
+
+    public Comparable<?> getStartValue() {
+        return startValue;
+    }
+
+    public Comparable<?> getEndValue() {
+        return endValue;
+    }
+
+    public boolean passesFilter(Object itemId, Item item)
+            throws UnsupportedOperationException {
+        Object value = item.getItemProperty(getPropertyId()).getValue();
+        if (value instanceof Comparable) {
+            Comparable cval = (Comparable) value;
+            return cval.compareTo(getStartValue()) >= 0
+                    && cval.compareTo(getEndValue()) <= 0;
+        }
+        return false;
+    }
+
+    public boolean appliesToProperty(Object propertyId) {
+        return getPropertyId() != null && getPropertyId().equals(propertyId);
+    }
+
+    @Override
+    public int hashCode() {
+        return getPropertyId().hashCode() + getStartValue().hashCode()
+                + getEndValue().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        // Only objects of the same class can be equal
+        if (!getClass().equals(obj.getClass())) {
+            return false;
+        }
+        final Between o = (Between) obj;
+
+        // Checks the properties one by one
+        boolean propertyIdEqual = (null != getPropertyId()) ? getPropertyId()
+                .equals(o.getPropertyId()) : null == o.getPropertyId();
+        boolean startValueEqual = (null != getStartValue()) ? getStartValue()
+                .equals(o.getStartValue()) : null == o.getStartValue();
+        boolean endValueEqual = (null != getEndValue()) ? getEndValue().equals(
+                o.getEndValue()) : null == o.getEndValue();
+        return propertyIdEqual && startValueEqual && endValueEqual;
+
+    }
+}
diff --git a/src/com/vaadin/data/util/filter/Like.java b/src/com/vaadin/data/util/filter/Like.java
new file mode 100644 (file)
index 0000000..a3cee49
--- /dev/null
@@ -0,0 +1,78 @@
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+
+public class Like implements Filter {
+    private final Object propertyId;
+    private final String value;
+    private boolean caseSensitive;
+
+    public Like(String propertyId, String value) {
+        this(propertyId, value, true);
+    }
+
+    public Like(String propertyId, String value, boolean caseSensitive) {
+        this.propertyId = propertyId;
+        this.value = value;
+        setCaseSensitive(caseSensitive);
+    }
+
+    public Object getPropertyId() {
+        return propertyId;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setCaseSensitive(boolean caseSensitive) {
+        this.caseSensitive = caseSensitive;
+    }
+
+    public boolean isCaseSensitive() {
+        return caseSensitive;
+    }
+
+    public boolean passesFilter(Object itemId, Item item)
+            throws UnsupportedOperationException {
+        if (!item.getItemProperty(getPropertyId()).getType()
+                .isAssignableFrom(String.class)) {
+            // We can only handle strings
+            return false;
+        }
+        String colValue = (String) item.getItemProperty(getPropertyId())
+                .getValue();
+
+        String pattern = getValue().replace("%", ".*");
+        if (isCaseSensitive()) {
+            return colValue.matches(pattern);
+        }
+        return colValue.toUpperCase().matches(pattern.toUpperCase());
+    }
+
+    public boolean appliesToProperty(Object propertyId) {
+        return getPropertyId() != null && getPropertyId().equals(propertyId);
+    }
+
+    @Override
+    public int hashCode() {
+        return getPropertyId().hashCode() + getValue().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        // Only objects of the same class can be equal
+        if (!getClass().equals(obj.getClass())) {
+            return false;
+        }
+        final Like o = (Like) obj;
+
+        // Checks the properties one by one
+        boolean propertyIdEqual = (null != getPropertyId()) ? getPropertyId()
+                .equals(o.getPropertyId()) : null == o.getPropertyId();
+        boolean valueEqual = (null != getValue()) ? getValue().equals(
+                o.getValue()) : null == o.getValue();
+        return propertyIdEqual && valueEqual;
+    }
+}
diff --git a/src/com/vaadin/data/util/query/FreeformQuery.java b/src/com/vaadin/data/util/query/FreeformQuery.java
new file mode 100644 (file)
index 0000000..2ac67c9
--- /dev/null
@@ -0,0 +1,450 @@
+package com.vaadin.data.util.query;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.RowItem;
+import com.vaadin.data.util.SQLContainer;
+import com.vaadin.data.util.connection.JDBCConnectionPool;
+import com.vaadin.data.util.query.generator.StatementHelper;
+import com.vaadin.data.util.query.generator.filter.QueryBuilder;
+
+@SuppressWarnings("serial")
+public class FreeformQuery implements QueryDelegate {
+
+    FreeformQueryDelegate delegate = null;
+    private String queryString;
+    private List<String> primaryKeyColumns;
+    private JDBCConnectionPool connectionPool;
+    private transient Connection activeConnection = null;
+
+    /**
+     * Prevent no-parameters instantiation of FreeformQuery
+     */
+    @SuppressWarnings("unused")
+    private FreeformQuery() {
+    }
+
+    /**
+     * Creates a new freeform query delegate to be used with the
+     * {@link SQLContainer}.
+     * 
+     * @param queryString
+     *            The actual query to perform.
+     * @param primaryKeyColumns
+     *            The primary key columns. Read-only mode is forced if this
+     *            parameter is null or empty.
+     * @param connectionPool
+     *            the JDBCConnectionPool to use to open connections to the SQL
+     *            database.
+     * @deprecated @see
+     *             {@link FreeformQuery#FreeformQuery(String, JDBCConnectionPool, String...)}
+     */
+    @Deprecated
+    public FreeformQuery(String queryString, List<String> primaryKeyColumns,
+            JDBCConnectionPool connectionPool) {
+        if (primaryKeyColumns == null) {
+            primaryKeyColumns = new ArrayList<String>();
+        }
+        if (primaryKeyColumns.contains("")) {
+            throw new IllegalArgumentException(
+                    "The primary key columns contain an empty string!");
+        } else if (queryString == null || "".equals(queryString)) {
+            throw new IllegalArgumentException(
+                    "The query string may not be empty or null!");
+        } else if (connectionPool == null) {
+            throw new IllegalArgumentException(
+                    "The connectionPool may not be null!");
+        }
+        this.queryString = queryString;
+        this.primaryKeyColumns = Collections
+                .unmodifiableList(primaryKeyColumns);
+        this.connectionPool = connectionPool;
+    }
+
+    /**
+     * Creates a new freeform query delegate to be used with the
+     * {@link SQLContainer}.
+     * 
+     * @param queryString
+     *            The actual query to perform.
+     * @param connectionPool
+     *            the JDBCConnectionPool to use to open connections to the SQL
+     *            database.
+     * @param primaryKeyColumns
+     *            The primary key columns. Read-only mode is forced if none are
+     *            provided. (optional)
+     */
+    public FreeformQuery(String queryString, JDBCConnectionPool connectionPool,
+            String... primaryKeyColumns) {
+        this(queryString, Arrays.asList(primaryKeyColumns), connectionPool);
+    }
+
+    /**
+     * This implementation of getCount() actually fetches all records from the
+     * database, which might be a performance issue. Override this method with a
+     * SELECT COUNT(*) ... query if this is too slow for your needs.
+     * 
+     * {@inheritDoc}
+     */
+    public int getCount() throws SQLException {
+        // First try the delegate
+        int count = countByDelegate();
+        if (count < 0) {
+            // Couldn't use the delegate, use the bad way.
+            Connection conn = getConnection();
+            Statement statement = conn.createStatement(
+                    ResultSet.TYPE_SCROLL_INSENSITIVE,
+                    ResultSet.CONCUR_READ_ONLY);
+
+            ResultSet rs = statement.executeQuery(queryString);
+            if (rs.last()) {
+                count = rs.getRow();
+            } else {
+                count = 0;
+            }
+            rs.close();
+            statement.close();
+            releaseConnection(conn);
+        }
+        return count;
+    }
+
+    @SuppressWarnings("deprecation")
+    private int countByDelegate() throws SQLException {
+        int count = -1;
+        if (delegate == null) {
+            return count;
+        }
+        /* First try using prepared statement */
+        if (delegate instanceof FreeformStatementDelegate) {
+            try {
+                StatementHelper sh = ((FreeformStatementDelegate) delegate)
+                        .getCountStatement();
+                Connection c = getConnection();
+                PreparedStatement pstmt = c.prepareStatement(sh
+                        .getQueryString());
+                sh.setParameterValuesToStatement(pstmt);
+                ResultSet rs = pstmt.executeQuery();
+                rs.next();
+                count = rs.getInt(1);
+                rs.close();
+                pstmt.clearParameters();
+                pstmt.close();
+                releaseConnection(c);
+                return count;
+            } catch (UnsupportedOperationException e) {
+                // Count statement generation not supported
+            }
+        }
+        /* Try using regular statement */
+        try {
+            String countQuery = delegate.getCountQuery();
+            if (countQuery != null) {
+                Connection conn = getConnection();
+                Statement statement = conn.createStatement();
+                ResultSet rs = statement.executeQuery(countQuery);
+                rs.next();
+                count = rs.getInt(1);
+                rs.close();
+                statement.close();
+                releaseConnection(conn);
+                return count;
+            }
+        } catch (UnsupportedOperationException e) {
+            // Count query generation not supported
+        }
+        return count;
+    }
+
+    private Connection getConnection() throws SQLException {
+        if (activeConnection != null) {
+            return activeConnection;
+        }
+        return connectionPool.reserveConnection();
+    }
+
+    /**
+     * Fetches the results for the query. This implementation always fetches the
+     * entire record set, ignoring the offset and pagelength parameters. In
+     * order to support lazy loading of records, you must supply a
+     * FreeformQueryDelegate that implements the
+     * FreeformQueryDelegate.getQueryString(int,int) method.
+     * 
+     * @throws SQLException
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.FreeformQueryDelegate#getQueryString(int,
+     *      int) {@inheritDoc}
+     */
+    @SuppressWarnings("deprecation")
+    public ResultSet getResults(int offset, int pagelength) throws SQLException {
+        if (activeConnection == null) {
+            throw new SQLException("No active transaction!");
+        }
+        String query = queryString;
+        if (delegate != null) {
+            /* First try using prepared statement */
+            if (delegate instanceof FreeformStatementDelegate) {
+                try {
+                    StatementHelper sh = ((FreeformStatementDelegate) delegate)
+                            .getQueryStatement(offset, pagelength);
+                    PreparedStatement pstmt = activeConnection
+                            .prepareStatement(sh.getQueryString());
+                    sh.setParameterValuesToStatement(pstmt);
+                    return pstmt.executeQuery();
+                } catch (UnsupportedOperationException e) {
+                    // Statement generation not supported, continue...
+                }
+            }
+            try {
+                query = delegate.getQueryString(offset, pagelength);
+            } catch (UnsupportedOperationException e) {
+                // This is fine, we'll just use the default queryString.
+            }
+        }
+        Statement statement = activeConnection.createStatement();
+        ResultSet rs = statement.executeQuery(query);
+        return rs;
+    }
+
+    @SuppressWarnings("deprecation")
+    public boolean implementationRespectsPagingLimits() {
+        if (delegate == null) {
+            return false;
+        }
+        /* First try using prepared statement */
+        if (delegate instanceof FreeformStatementDelegate) {
+            try {
+                StatementHelper sh = ((FreeformStatementDelegate) delegate)
+                        .getCountStatement();
+                if (sh != null && sh.getQueryString() != null
+                        && sh.getQueryString().length() > 0) {
+                    return true;
+                }
+            } catch (UnsupportedOperationException e) {
+                // Statement generation not supported, continue...
+            }
+        }
+        try {
+            String queryString = delegate.getQueryString(0, 50);
+            return queryString != null && queryString.length() > 0;
+        } catch (UnsupportedOperationException e) {
+            return false;
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setFilters(java.util
+     * .List)
+     */
+    public void setFilters(List<Filter> filters)
+            throws UnsupportedOperationException {
+        if (delegate != null) {
+            delegate.setFilters(filters);
+        } else if (filters != null) {
+            throw new UnsupportedOperationException(
+                    "FreeFormQueryDelegate not set!");
+        }
+    }
+
+    public void setOrderBy(List<OrderBy> orderBys)
+            throws UnsupportedOperationException {
+        if (delegate != null) {
+            delegate.setOrderBy(orderBys);
+        } else if (orderBys != null) {
+            throw new UnsupportedOperationException(
+                    "FreeFormQueryDelegate not set!");
+        }
+    }
+
+    public int storeRow(RowItem row) throws SQLException {
+        if (activeConnection == null) {
+            throw new IllegalStateException("No transaction is active!");
+        } else if (primaryKeyColumns.isEmpty()) {
+            throw new UnsupportedOperationException(
+                    "Cannot store items fetched with a read-only freeform query!");
+        }
+        if (delegate != null) {
+            return delegate.storeRow(activeConnection, row);
+        } else {
+            throw new UnsupportedOperationException(
+                    "FreeFormQueryDelegate not set!");
+        }
+    }
+
+    public boolean removeRow(RowItem row) throws SQLException {
+        if (activeConnection == null) {
+            throw new IllegalStateException("No transaction is active!");
+        } else if (primaryKeyColumns.isEmpty()) {
+            throw new UnsupportedOperationException(
+                    "Cannot remove items fetched with a read-only freeform query!");
+        }
+        if (delegate != null) {
+            return delegate.removeRow(activeConnection, row);
+        } else {
+            throw new UnsupportedOperationException(
+                    "FreeFormQueryDelegate not set!");
+        }
+    }
+
+    public synchronized void beginTransaction()
+            throws UnsupportedOperationException, SQLException {
+        if (activeConnection != null) {
+            throw new IllegalStateException("A transaction is already active!");
+        }
+        activeConnection = connectionPool.reserveConnection();
+        activeConnection.setAutoCommit(false);
+    }
+
+    public synchronized void commit() throws UnsupportedOperationException,
+            SQLException {
+        if (activeConnection == null) {
+            throw new SQLException("No active transaction");
+        }
+        if (!activeConnection.getAutoCommit()) {
+            activeConnection.commit();
+        }
+        connectionPool.releaseConnection(activeConnection);
+        activeConnection = null;
+    }
+
+    public synchronized void rollback() throws UnsupportedOperationException,
+            SQLException {
+        if (activeConnection == null) {
+            throw new SQLException("No active transaction");
+        }
+        activeConnection.rollback();
+        connectionPool.releaseConnection(activeConnection);
+        activeConnection = null;
+    }
+
+    public List<String> getPrimaryKeyColumns() {
+        return primaryKeyColumns;
+    }
+
+    public String getQueryString() {
+        return queryString;
+    }
+
+    public FreeformQueryDelegate getDelegate() {
+        return delegate;
+    }
+
+    public void setDelegate(FreeformQueryDelegate delegate) {
+        this.delegate = delegate;
+    }
+
+    /**
+     * This implementation of the containsRowWithKey method rewrites existing
+     * WHERE clauses in the query string. The logic is, however, not very
+     * complex and some times can do the Wrong Thing<sup>TM</sup>. For the
+     * situations where this logic is not enough, you can implement the
+     * getContainsRowQueryString method in FreeformQueryDelegate and this will
+     * be used instead of the logic.
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.FreeformQueryDelegate#getContainsRowQueryString(Object...)
+     * 
+     *      {@inheritDoc}
+     */
+    @SuppressWarnings("deprecation")
+    public boolean containsRowWithKey(Object... keys) throws SQLException {
+        String query = null;
+        boolean contains = false;
+        if (delegate != null) {
+            if (delegate instanceof FreeformStatementDelegate) {
+                try {
+                    StatementHelper sh = ((FreeformStatementDelegate) delegate)
+                            .getContainsRowQueryStatement(keys);
+                    Connection c = getConnection();
+                    PreparedStatement pstmt = c.prepareStatement(sh
+                            .getQueryString());
+                    sh.setParameterValuesToStatement(pstmt);
+                    ResultSet rs = pstmt.executeQuery();
+                    contains = rs.next();
+                    rs.close();
+                    pstmt.clearParameters();
+                    pstmt.close();
+                    releaseConnection(c);
+                    return contains;
+                } catch (UnsupportedOperationException e) {
+                    // Statement generation not supported, continue...
+                }
+            }
+            try {
+                query = delegate.getContainsRowQueryString(keys);
+            } catch (UnsupportedOperationException e) {
+                query = modifyWhereClause(keys);
+            }
+        } else {
+            query = modifyWhereClause(keys);
+        }
+        Connection conn = getConnection();
+        try {
+            Statement statement = conn.createStatement();
+            ResultSet rs = statement.executeQuery(query);
+            contains = rs.next();
+            rs.close();
+            statement.close();
+        } finally {
+            releaseConnection(conn);
+        }
+        return contains;
+    }
+
+    /**
+     * Releases the connection if it is not part of an active transaction.
+     * 
+     * @param conn
+     *            the connection to release
+     */
+    private void releaseConnection(Connection conn) {
+        if (conn != activeConnection) {
+            connectionPool.releaseConnection(conn);
+        }
+    }
+
+    private String modifyWhereClause(Object... keys) {
+        // Build the where rules for the provided keys
+        StringBuffer where = new StringBuffer();
+        for (int ix = 0; ix < primaryKeyColumns.size(); ix++) {
+            where.append(QueryBuilder.quote(primaryKeyColumns.get(ix)));
+            if (keys[ix] == null) {
+                where.append(" IS NULL");
+            } else {
+                where.append(" = '").append(keys[ix]).append("'");
+            }
+            if (ix < primaryKeyColumns.size() - 1) {
+                where.append(" AND ");
+            }
+        }
+        // Is there already a WHERE clause in the query string?
+        int index = queryString.toLowerCase().indexOf("where ");
+        if (index > -1) {
+            // Rewrite the where clause
+            return queryString.substring(0, index) + "WHERE " + where + " AND "
+                    + queryString.substring(index + 6);
+        }
+        // Append a where clause
+        return queryString + " WHERE " + where;
+    }
+
+    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+        try {
+            rollback();
+        } catch (SQLException ignored) {
+        }
+        out.defaultWriteObject();
+    }
+}
diff --git a/src/com/vaadin/data/util/query/FreeformQueryDelegate.java b/src/com/vaadin/data/util/query/FreeformQueryDelegate.java
new file mode 100644 (file)
index 0000000..c87acec
--- /dev/null
@@ -0,0 +1,115 @@
+package com.vaadin.data.util.query;
+
+import java.io.Serializable;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.RowItem;
+
+public interface FreeformQueryDelegate extends Serializable {
+    /**
+     * Should return the SQL query string to be performed. This method is
+     * responsible for gluing together the select query from the filters and the
+     * order by conditions if these are supported.
+     * 
+     * @param offset
+     *            the first record (row) to fetch.
+     * @param pagelength
+     *            the number of records (rows) to fetch. 0 means all records
+     *            starting from offset.
+     * @deprecated Implement {@link FreeformStatementDelegate} instead of
+     *             {@link FreeformQueryDelegate}
+     */
+    @Deprecated
+    public String getQueryString(int offset, int limit)
+            throws UnsupportedOperationException;
+
+    /**
+     * Generates and executes a query to determine the current row count from
+     * the DB. Row count will be fetched using filters that are currently set to
+     * the QueryDelegate.
+     * 
+     * @return row count
+     * @throws SQLException
+     * @deprecated Implement {@link FreeformStatementDelegate} instead of
+     *             {@link FreeformQueryDelegate}
+     */
+    @Deprecated
+    public String getCountQuery() throws UnsupportedOperationException;
+
+    /**
+     * Sets the filters to apply when performing the SQL query. These are
+     * translated into a WHERE clause. Default filtering mode will be used.
+     * 
+     * @param filters
+     *            The filters to apply.
+     * @throws UnsupportedOperationException
+     *             if the implementation doesn't support filtering.
+     */
+    public void setFilters(List<Filter> filters)
+            throws UnsupportedOperationException;
+
+    /**
+     * Sets the order in which to retrieve rows from the database. The result
+     * can be ordered by zero or more columns and each column can be in
+     * ascending or descending order. These are translated into an ORDER BY
+     * clause in the SQL query.
+     * 
+     * @param orderBys
+     *            A list of the OrderBy conditions.
+     * @throws UnsupportedOperationException
+     *             if the implementation doesn't support ordering.
+     */
+    public void setOrderBy(List<OrderBy> orderBys)
+            throws UnsupportedOperationException;
+
+    /**
+     * Stores a row in the database. The implementation of this interface
+     * decides how to identify whether to store a new row or update an existing
+     * one.
+     * 
+     * @param conn
+     *            the JDBC connection to use
+     * @param row
+     *            RowItem to be stored or updated.
+     * @throws UnsupportedOperationException
+     *             if the implementation is read only.
+     * @throws SQLException
+     */
+    public int storeRow(Connection conn, RowItem row)
+            throws UnsupportedOperationException, SQLException;
+
+    /**
+     * Removes the given RowItem from the database.
+     * 
+     * @param conn
+     *            the JDBC connection to use
+     * @param row
+     *            RowItem to be removed
+     * @return true on success
+     * @throws UnsupportedOperationException
+     * @throws SQLException
+     */
+    public boolean removeRow(Connection conn, RowItem row)
+            throws UnsupportedOperationException, SQLException;
+
+    /**
+     * Generates an SQL Query string that allows the user of the FreeformQuery
+     * class to customize the query string used by the
+     * FreeformQuery.containsRowWithKeys() method. This is useful for cases when
+     * the logic in the containsRowWithKeys method is not enough to support more
+     * complex free form queries.
+     * 
+     * @param keys
+     *            the values of the primary keys
+     * @throws UnsupportedOperationException
+     *             to use the default logic in FreeformQuery
+     * @deprecated Implement {@link FreeformStatementDelegate} instead of
+     *             {@link FreeformQueryDelegate}
+     */
+    @Deprecated
+    public String getContainsRowQueryString(Object... keys)
+            throws UnsupportedOperationException;
+}
diff --git a/src/com/vaadin/data/util/query/FreeformStatementDelegate.java b/src/com/vaadin/data/util/query/FreeformStatementDelegate.java
new file mode 100644 (file)
index 0000000..0de8677
--- /dev/null
@@ -0,0 +1,54 @@
+package com.vaadin.data.util.query;\r
+\r
+import com.vaadin.data.util.query.generator.StatementHelper;\r
+\r
+/**\r
+ * FreeformStatementDelegate is an extension to FreeformQueryDelegate that\r
+ * provides definitions for methods that produce StatementHelper objects instead\r
+ * of basic query strings. This allows the FreeformQuery query delegate to use\r
+ * PreparedStatements instead of regular Statement when accessing the database.\r
+ * \r
+ * Due to the injection protection and other benefits of prepared statements, it\r
+ * is advisable to implement this interface instead of the FreeformQueryDelegate\r
+ * whenever possible.\r
+ */\r
+public interface FreeformStatementDelegate extends FreeformQueryDelegate {\r
+    /**\r
+     * Should return a new instance of StatementHelper that contains the query\r
+     * string and parameter values required to create a PreparedStatement. This\r
+     * method is responsible for gluing together the select query from the\r
+     * filters and the order by conditions if these are supported.\r
+     * \r
+     * @param offset\r
+     *            the first record (row) to fetch.\r
+     * @param pagelength\r
+     *            the number of records (rows) to fetch. 0 means all records\r
+     *            starting from offset.\r
+     */\r
+    public StatementHelper getQueryStatement(int offset, int limit)\r
+            throws UnsupportedOperationException;\r
+\r
+    /**\r
+     * Should return a new instance of StatementHelper that contains the query\r
+     * string and parameter values required to create a PreparedStatement that\r
+     * will fetch the row count from the DB. Row count should be fetched using\r
+     * filters that are currently set to the QueryDelegate.\r
+     */\r
+    public StatementHelper getCountStatement()\r
+            throws UnsupportedOperationException;\r
+\r
+    /**\r
+     * Should return a new instance of StatementHelper that contains the query\r
+     * string and parameter values required to create a PreparedStatement used\r
+     * by the FreeformQuery.containsRowWithKeys() method. This is useful for\r
+     * cases when the default logic in said method is not enough to support more\r
+     * complex free form queries.\r
+     * \r
+     * @param keys\r
+     *            the values of the primary keys\r
+     * @throws UnsupportedOperationException\r
+     *             to use the default logic in FreeformQuery\r
+     */\r
+    public StatementHelper getContainsRowQueryStatement(Object... keys)\r
+            throws UnsupportedOperationException;\r
+}\r
diff --git a/src/com/vaadin/data/util/query/OrderBy.java b/src/com/vaadin/data/util/query/OrderBy.java
new file mode 100644 (file)
index 0000000..d83d322
--- /dev/null
@@ -0,0 +1,43 @@
+package com.vaadin.data.util.query;
+
+import java.io.Serializable;
+
+/**
+ * OrderBy represents a sorting rule to be applied to a query made by the
+ * SQLContainer's QueryDelegate.
+ * 
+ * The sorting rule is simple and contains only the affected column's name and
+ * the direction of the sort.
+ */
+public class OrderBy implements Serializable {
+    private String column;
+    private boolean isAscending;
+
+    /**
+     * Prevent instantiation without required parameters.
+     */
+    @SuppressWarnings("unused")
+    private OrderBy() {
+    }
+
+    public OrderBy(String column, boolean isAscending) {
+        setColumn(column);
+        setAscending(isAscending);
+    }
+
+    public void setColumn(String column) {
+        this.column = column;
+    }
+
+    public String getColumn() {
+        return column;
+    }
+
+    public void setAscending(boolean isAscending) {
+        this.isAscending = isAscending;
+    }
+
+    public boolean isAscending() {
+        return isAscending;
+    }
+}
diff --git a/src/com/vaadin/data/util/query/QueryDelegate.java b/src/com/vaadin/data/util/query/QueryDelegate.java
new file mode 100644 (file)
index 0000000..95ec89f
--- /dev/null
@@ -0,0 +1,208 @@
+package com.vaadin.data.util.query;
+
+import java.io.Serializable;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.RowId;
+import com.vaadin.data.util.RowItem;
+
+public interface QueryDelegate extends Serializable {
+    /**
+     * Generates and executes a query to determine the current row count from
+     * the DB. Row count will be fetched using filters that are currently set to
+     * the QueryDelegate.
+     * 
+     * @return row count
+     * @throws SQLException
+     */
+    public int getCount() throws SQLException;
+
+    /**
+     * Executes a paged SQL query and returns the ResultSet. The query is
+     * defined through implementations of this QueryDelegate interface.
+     * 
+     * @param offset
+     *            the first item of the page to load
+     * @param pagelength
+     *            the length of the page to load
+     * @return a ResultSet containing the rows of the page
+     * @throws SQLException
+     *             if the database access fails.
+     */
+    public ResultSet getResults(int offset, int pagelength) throws SQLException;
+
+    /**
+     * Allows the SQLContainer implementation to check whether the QueryDelegate
+     * implementation implements paging in the getResults method.
+     * 
+     * @see QueryDelegate#getResults(int, int)
+     * 
+     * @return true if the delegate implements paging
+     */
+    public boolean implementationRespectsPagingLimits();
+
+    /**
+     * Sets the filters to apply when performing the SQL query. These are
+     * translated into a WHERE clause. Default filtering mode will be used.
+     * 
+     * @param filters
+     *            The filters to apply.
+     * @throws UnsupportedOperationException
+     *             if the implementation doesn't support filtering.
+     */
+    public void setFilters(List<Filter> filters)
+            throws UnsupportedOperationException;
+
+    /**
+     * Sets the order in which to retrieve rows from the database. The result
+     * can be ordered by zero or more columns and each column can be in
+     * ascending or descending order. These are translated into an ORDER BY
+     * clause in the SQL query.
+     * 
+     * @param orderBys
+     *            A list of the OrderBy conditions.
+     * @throws UnsupportedOperationException
+     *             if the implementation doesn't support ordering.
+     */
+    public void setOrderBy(List<OrderBy> orderBys)
+            throws UnsupportedOperationException;
+
+    /**
+     * Stores a row in the database. The implementation of this interface
+     * decides how to identify whether to store a new row or update an existing
+     * one.
+     * 
+     * @param columnToValueMap
+     *            A map containing the values for all columns to be stored or
+     *            updated.
+     * @return the number of affected rows in the database table
+     * @throws UnsupportedOperationException
+     *             if the implementation is read only.
+     */
+    public int storeRow(RowItem row) throws UnsupportedOperationException,
+            SQLException;
+
+    /**
+     * Removes the given RowItem from the database.
+     * 
+     * @param row
+     *            RowItem to be removed
+     * @return true on success
+     * @throws UnsupportedOperationException
+     * @throws SQLException
+     */
+    public boolean removeRow(RowItem row) throws UnsupportedOperationException,
+            SQLException;
+
+    /**
+     * Starts a new database transaction. Used when storing multiple changes.
+     * 
+     * Note that if a transaction is already open, it will be rolled back when a
+     * new transaction is started.
+     * 
+     * @throws SQLException
+     *             if the database access fails.
+     */
+    public void beginTransaction() throws SQLException;
+
+    /**
+     * Commits a transaction. If a transaction is not open nothing should
+     * happen.
+     * 
+     * @throws SQLException
+     *             if the database access fails.
+     */
+    public void commit() throws SQLException;
+
+    /**
+     * Rolls a transaction back. If a transaction is not open nothing should
+     * happen.
+     * 
+     * @throws SQLException
+     *             if the database access fails.
+     */
+    public void rollback() throws SQLException;
+
+    /**
+     * Returns a list of primary key column names. The list is either fetched
+     * from the database (TableQuery) or given as an argument depending on
+     * implementation.
+     * 
+     * @return
+     */
+    public List<String> getPrimaryKeyColumns();
+
+    /**
+     * Performs a query to find out whether the SQL table contains a row with
+     * the given set of primary keys.
+     * 
+     * @param keys
+     *            the primary keys
+     * @return true if the SQL table contains a row with the provided keys
+     * @throws SQLException
+     */
+    public boolean containsRowWithKey(Object... keys) throws SQLException;
+
+    /************************/
+    /** ROWID CHANGE EVENT **/
+    /************************/
+
+    /**
+     * An <code>Event</code> object specifying the old and new RowId of an added
+     * item after the addition has been successfully committed.
+     */
+    public interface RowIdChangeEvent extends Serializable {
+        /**
+         * Gets the old (temporary) RowId of the added row that raised this
+         * event.
+         * 
+         * @return old RowId
+         */
+        public RowId getOldRowId();
+
+        /**
+         * Gets the new, possibly database assigned RowId of the added row that
+         * raised this event.
+         * 
+         * @return new RowId
+         */
+        public RowId getNewRowId();
+    }
+
+    /** RowId change listener interface. */
+    public interface RowIdChangeListener extends Serializable {
+        /**
+         * Lets the listener know that a RowId has been changed.
+         * 
+         * @param event
+         */
+        public void rowIdChange(QueryDelegate.RowIdChangeEvent event);
+    }
+
+    /**
+     * The interface for adding and removing <code>RowIdChangeEvent</code>
+     * listeners. By implementing this interface a class explicitly announces
+     * that it will generate a <code>RowIdChangeEvent</code> when it performs a
+     * database commit that may change the RowId.
+     */
+    public interface RowIdChangeNotifier extends Serializable {
+        /**
+         * Adds a RowIdChangeListener for the object.
+         * 
+         * @param listener
+         *            listener to be added
+         */
+        public void addListener(QueryDelegate.RowIdChangeListener listener);
+
+        /**
+         * Removes the specified RowIdChangeListener from the object.
+         * 
+         * @param listener
+         *            listener to be removed
+         */
+        public void removeListener(QueryDelegate.RowIdChangeListener listener);
+    }
+}
diff --git a/src/com/vaadin/data/util/query/TableQuery.java b/src/com/vaadin/data/util/query/TableQuery.java
new file mode 100644 (file)
index 0000000..c44a748
--- /dev/null
@@ -0,0 +1,707 @@
+package com.vaadin.data.util.query;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.ColumnProperty;
+import com.vaadin.data.util.OptimisticLockException;
+import com.vaadin.data.util.RowId;
+import com.vaadin.data.util.RowItem;
+import com.vaadin.data.util.SQLUtil;
+import com.vaadin.data.util.TemporaryRowId;
+import com.vaadin.data.util.connection.JDBCConnectionPool;
+import com.vaadin.data.util.filter.Compare.Equal;
+import com.vaadin.data.util.query.generator.DefaultSQLGenerator;
+import com.vaadin.data.util.query.generator.MSSQLGenerator;
+import com.vaadin.data.util.query.generator.SQLGenerator;
+import com.vaadin.data.util.query.generator.StatementHelper;
+
+@SuppressWarnings("serial")
+public class TableQuery implements QueryDelegate,
+        QueryDelegate.RowIdChangeNotifier {
+
+    /** Table name, primary key column name(s) and version column name */
+    private String tableName;
+    private List<String> primaryKeyColumns;
+    private String versionColumn;
+
+    /** Currently set Filters and OrderBys */
+    private List<Filter> filters;
+    private List<OrderBy> orderBys;
+
+    /** SQLGenerator instance to use for generating queries */
+    private SQLGenerator sqlGenerator;
+
+    /** Fields related to Connection and Transaction handling */
+    private JDBCConnectionPool connectionPool;
+    private transient Connection activeConnection;
+    private boolean transactionOpen;
+
+    /** Row ID change listeners */
+    private LinkedList<RowIdChangeListener> rowIdChangeListeners;
+    /** Row ID change events, stored until commit() is called */
+    private final List<RowIdChangeEvent> bufferedEvents = new ArrayList<RowIdChangeEvent>();
+
+    /** Set to true to output generated SQL Queries to System.out */
+    private boolean debug = false;
+
+    /** Prevent no-parameters instantiation of TableQuery */
+    @SuppressWarnings("unused")
+    private TableQuery() {
+    }
+
+    /**
+     * Creates a new TableQuery using the given connection pool, SQL generator
+     * and table name to fetch the data from. All parameters must be non-null.
+     * 
+     * @param tableName
+     *            Name of the database table to connect to
+     * @param connectionPool
+     *            Connection pool for accessing the database
+     * @param sqlGenerator
+     *            SQL query generator implementation
+     */
+    public TableQuery(String tableName, JDBCConnectionPool connectionPool,
+            SQLGenerator sqlGenerator) {
+        if (tableName == null || tableName.trim().length() < 1
+                || connectionPool == null || sqlGenerator == null) {
+            throw new IllegalArgumentException(
+                    "All parameters must be non-null and a table name must be given.");
+        }
+        this.tableName = tableName;
+        this.sqlGenerator = sqlGenerator;
+        this.connectionPool = connectionPool;
+        fetchMetaData();
+    }
+
+    /**
+     * Creates a new TableQuery using the given connection pool and table name
+     * to fetch the data from. All parameters must be non-null. The default SQL
+     * generator will be used for queries.
+     * 
+     * @param tableName
+     *            Name of the database table to connect to
+     * @param connectionPool
+     *            Connection pool for accessing the database
+     */
+    public TableQuery(String tableName, JDBCConnectionPool connectionPool) {
+        this(tableName, connectionPool, new DefaultSQLGenerator());
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getCount()
+     */
+    public int getCount() throws SQLException {
+        debug("Fetching count...");
+        StatementHelper sh = sqlGenerator.generateSelectQuery(tableName,
+                filters, null, 0, 0, "COUNT(*)");
+        boolean shouldCloseTransaction = false;
+        if (!transactionOpen) {
+            shouldCloseTransaction = true;
+            beginTransaction();
+        }
+        ResultSet r = executeQuery(sh);
+        r.next();
+        int count = r.getInt(1);
+        r.getStatement().close();
+        r.close();
+        if (shouldCloseTransaction) {
+            commit();
+        }
+        return count;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getResults(int,
+     * int)
+     */
+    public ResultSet getResults(int offset, int pagelength) throws SQLException {
+        StatementHelper sh;
+        /*
+         * If no ordering is explicitly set, results will be ordered by the
+         * first primary key column.
+         */
+        if (orderBys == null || orderBys.isEmpty()) {
+            List<OrderBy> ob = new ArrayList<OrderBy>();
+            ob.add(new OrderBy(primaryKeyColumns.get(0), true));
+            sh = sqlGenerator.generateSelectQuery(tableName, filters, ob,
+                    offset, pagelength, null);
+        } else {
+            sh = sqlGenerator.generateSelectQuery(tableName, filters, orderBys,
+                    offset, pagelength, null);
+        }
+        return executeQuery(sh);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#
+     * implementationRespectsPagingLimits()
+     */
+    public boolean implementationRespectsPagingLimits() {
+        return true;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * com.vaadin.addon.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin
+     * .addon.sqlcontainer.RowItem)
+     */
+    public int storeRow(RowItem row) throws UnsupportedOperationException,
+            SQLException {
+        if (row == null) {
+            throw new IllegalArgumentException("Row argument must be non-null.");
+        }
+        StatementHelper sh;
+        int result = 0;
+        if (row.getId() instanceof TemporaryRowId) {
+            setVersionColumnFlagInProperty(row);
+            sh = sqlGenerator.generateInsertQuery(tableName, row);
+            result = executeUpdateReturnKeys(sh, row);
+        } else {
+            setVersionColumnFlagInProperty(row);
+            sh = sqlGenerator.generateUpdateQuery(tableName, row);
+            result = executeUpdate(sh);
+        }
+        if (versionColumn != null && result == 0) {
+            throw new OptimisticLockException(
+                    "Someone else changed the row that was being updated.",
+                    row.getId());
+        }
+        return result;
+    }
+
+    private void setVersionColumnFlagInProperty(RowItem row) {
+        ColumnProperty versionProperty = (ColumnProperty) row
+                .getItemProperty(versionColumn);
+        if (versionProperty != null) {
+            versionProperty.setVersionColumn(true);
+        }
+    }
+
+    /**
+     * Inserts the given row in the database table immediately. Begins and
+     * commits the transaction needed. This method was added specifically to
+     * solve the problem of returning the final RowId immediately on the
+     * SQLContainer.addItem() call when auto commit mode is enabled in the
+     * SQLContainer.
+     * 
+     * @param row
+     *            RowItem to add to the database
+     * @return Final RowId of the added row
+     * @throws SQLException
+     */
+    public RowId storeRowImmediately(RowItem row) throws SQLException {
+        beginTransaction();
+        /* Set version column, if one is provided */
+        setVersionColumnFlagInProperty(row);
+        /* Generate query */
+        StatementHelper sh = sqlGenerator.generateInsertQuery(tableName, row);
+        PreparedStatement pstmt = activeConnection.prepareStatement(
+                sh.getQueryString(), primaryKeyColumns.toArray(new String[0]));
+        sh.setParameterValuesToStatement(pstmt);
+        debug("DB -> " + sh.getQueryString());
+        int result = pstmt.executeUpdate();
+        if (result > 0) {
+            /*
+             * If affected rows exist, we'll get the new RowId, commit the
+             * transaction and return the new RowId.
+             */
+            ResultSet generatedKeys = pstmt.getGeneratedKeys();
+            RowId newId = getNewRowId(row, generatedKeys);
+            generatedKeys.close();
+            pstmt.clearParameters();
+            pstmt.close();
+            commit();
+            return newId;
+        } else {
+            pstmt.clearParameters();
+            pstmt.close();
+            /* On failure return null */
+            return null;
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setFilters(java.util
+     * .List)
+     */
+    public void setFilters(List<Filter> filters)
+            throws UnsupportedOperationException {
+        if (filters == null) {
+            this.filters = null;
+            return;
+        }
+        this.filters = Collections.unmodifiableList(filters);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setOrderBy(java.util
+     * .List)
+     */
+    public void setOrderBy(List<OrderBy> orderBys)
+            throws UnsupportedOperationException {
+        if (orderBys == null) {
+            this.orderBys = null;
+            return;
+        }
+        this.orderBys = Collections.unmodifiableList(orderBys);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#beginTransaction()
+     */
+    public void beginTransaction() throws UnsupportedOperationException,
+            SQLException {
+        if (transactionOpen && activeConnection != null) {
+            throw new IllegalStateException();
+        }
+        debug("DB -> begin transaction");
+        activeConnection = connectionPool.reserveConnection();
+        activeConnection.setAutoCommit(false);
+        transactionOpen = true;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#commit()
+     */
+    public void commit() throws UnsupportedOperationException, SQLException {
+        if (transactionOpen && activeConnection != null) {
+            debug("DB -> commit");
+            activeConnection.commit();
+            connectionPool.releaseConnection(activeConnection);
+        } else {
+            throw new SQLException("No active transaction");
+        }
+        transactionOpen = false;
+
+        /* Handle firing row ID change events */
+        RowIdChangeEvent[] unFiredEvents = bufferedEvents
+                .toArray(new RowIdChangeEvent[] {});
+        bufferedEvents.clear();
+        if (rowIdChangeListeners != null && !rowIdChangeListeners.isEmpty()) {
+            for (RowIdChangeListener r : rowIdChangeListeners) {
+                for (RowIdChangeEvent e : unFiredEvents) {
+                    r.rowIdChange(e);
+                }
+            }
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#rollback()
+     */
+    public void rollback() throws UnsupportedOperationException, SQLException {
+        if (transactionOpen && activeConnection != null) {
+            debug("DB -> rollback");
+            activeConnection.rollback();
+            connectionPool.releaseConnection(activeConnection);
+        } else {
+            throw new SQLException("No active transaction");
+        }
+        transactionOpen = false;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * com.vaadin.addon.sqlcontainer.query.QueryDelegate#getPrimaryKeyColumns()
+     */
+    public List<String> getPrimaryKeyColumns() {
+        return Collections.unmodifiableList(primaryKeyColumns);
+    }
+
+    public String getVersionColumn() {
+        return versionColumn;
+    }
+
+    public void setVersionColumn(String column) {
+        versionColumn = column;
+    }
+
+    public String getTableName() {
+        return tableName;
+    }
+
+    public SQLGenerator getSqlGenerator() {
+        return sqlGenerator;
+    }
+
+    /**
+     * Executes the given query string using either the active connection if a
+     * transaction is already open, or a new connection from this query's
+     * connection pool.
+     * 
+     * @param sh
+     *            an instance of StatementHelper, containing the query string
+     *            and parameter values.
+     * @return ResultSet of the query
+     * @throws SQLException
+     */
+    private ResultSet executeQuery(StatementHelper sh) throws SQLException {
+        Connection c = null;
+        if (transactionOpen && activeConnection != null) {
+            c = activeConnection;
+        } else {
+            throw new SQLException("No active transaction!");
+        }
+        PreparedStatement pstmt = c.prepareStatement(sh.getQueryString());
+        sh.setParameterValuesToStatement(pstmt);
+        debug("DB -> " + sh.getQueryString());
+        return pstmt.executeQuery();
+    }
+
+    /**
+     * Executes the given update query string using either the active connection
+     * if a transaction is already open, or a new connection from this query's
+     * connection pool.
+     * 
+     * @param sh
+     *            an instance of StatementHelper, containing the query string
+     *            and parameter values.
+     * @return Number of affected rows
+     * @throws SQLException
+     */
+    private int executeUpdate(StatementHelper sh) throws SQLException {
+        Connection c = null;
+        PreparedStatement pstmt = null;
+        try {
+            if (transactionOpen && activeConnection != null) {
+                c = activeConnection;
+            } else {
+                c = connectionPool.reserveConnection();
+            }
+            pstmt = c.prepareStatement(sh.getQueryString());
+            sh.setParameterValuesToStatement(pstmt);
+            debug("DB -> " + sh.getQueryString());
+            int retval = pstmt.executeUpdate();
+            return retval;
+        } finally {
+            if (pstmt != null) {
+                pstmt.clearParameters();
+                pstmt.close();
+            }
+            if (!transactionOpen) {
+                connectionPool.releaseConnection(c);
+            }
+        }
+    }
+
+    /**
+     * Executes the given update query string using either the active connection
+     * if a transaction is already open, or a new connection from this query's
+     * connection pool.
+     * 
+     * Additionally adds a new RowIdChangeEvent to the event buffer.
+     * 
+     * @param sh
+     *            an instance of StatementHelper, containing the query string
+     *            and parameter values.
+     * @param row
+     *            the row item to update
+     * @return Number of affected rows
+     * @throws SQLException
+     */
+    private int executeUpdateReturnKeys(StatementHelper sh, RowItem row)
+            throws SQLException {
+        Connection c = null;
+        PreparedStatement pstmt = null;
+        ResultSet genKeys = null;
+        try {
+            if (transactionOpen && activeConnection != null) {
+                c = activeConnection;
+            } else {
+                c = connectionPool.reserveConnection();
+            }
+            pstmt = c.prepareStatement(sh.getQueryString(),
+                    primaryKeyColumns.toArray(new String[0]));
+            sh.setParameterValuesToStatement(pstmt);
+            debug("DB -> " + sh.getQueryString());
+            int result = pstmt.executeUpdate();
+            genKeys = pstmt.getGeneratedKeys();
+            RowId newId = getNewRowId(row, genKeys);
+            bufferedEvents.add(new RowIdChangeEvent(row.getId(), newId));
+            return result;
+        } finally {
+            if (genKeys != null) {
+                genKeys.close();
+            }
+            if (pstmt != null) {
+                pstmt.clearParameters();
+                pstmt.close();
+            }
+            if (!transactionOpen) {
+                connectionPool.releaseConnection(c);
+            }
+        }
+    }
+
+    /**
+     * Fetches name(s) of primary key column(s) from DB metadata.
+     * 
+     * Also tries to get the escape string to be used in search strings.
+     */
+    private void fetchMetaData() {
+        Connection c = null;
+        try {
+            c = connectionPool.reserveConnection();
+            DatabaseMetaData dbmd = c.getMetaData();
+            if (dbmd != null) {
+                tableName = SQLUtil.escapeSQL(tableName);
+                ResultSet tables = dbmd.getTables(null, null, tableName, null);
+                if (!tables.next()) {
+                    tables = dbmd.getTables(null, null,
+                            tableName.toUpperCase(), null);
+                    if (!tables.next()) {
+                        throw new IllegalArgumentException(
+                                "Table with the name \""
+                                        + tableName
+                                        + "\" was not found. Check your database contents.");
+                    } else {
+                        tableName = tableName.toUpperCase();
+                    }
+                }
+                tables.close();
+                ResultSet rs = dbmd.getPrimaryKeys(null, null, tableName);
+                List<String> names = new ArrayList<String>();
+                while (rs.next()) {
+                    names.add(rs.getString("COLUMN_NAME"));
+                }
+                rs.close();
+                if (!names.isEmpty()) {
+                    primaryKeyColumns = names;
+                }
+                if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) {
+                    throw new IllegalArgumentException(
+                            "Primary key constraints have not been defined for the table \""
+                                    + tableName
+                                    + "\". Use FreeFormQuery to access this table.");
+                }
+                for (String colName : primaryKeyColumns) {
+                    if (colName.equalsIgnoreCase("rownum")) {
+                        if (getSqlGenerator() instanceof MSSQLGenerator
+                                || getSqlGenerator() instanceof MSSQLGenerator) {
+                            throw new IllegalArgumentException(
+                                    "When using Oracle or MSSQL, a primary key column"
+                                            + " named \'rownum\' is not allowed!");
+                        }
+                    }
+                }
+            }
+        } catch (SQLException e) {
+            throw new RuntimeException(e);
+        } finally {
+            connectionPool.releaseConnection(c);
+        }
+    }
+
+    private RowId getNewRowId(RowItem row, ResultSet genKeys) {
+        try {
+            /* Fetch primary key values and generate a map out of them. */
+            Map<String, Object> values = new HashMap<String, Object>();
+            ResultSetMetaData rsmd = genKeys.getMetaData();
+            int colCount = rsmd.getColumnCount();
+            if (genKeys.next()) {
+                for (int i = 1; i <= colCount; i++) {
+                    values.put(rsmd.getColumnName(i), genKeys.getObject(i));
+                }
+            }
+            /* Generate new RowId */
+            List<Object> newRowId = new ArrayList<Object>();
+            if (values.size() == 1) {
+                if (primaryKeyColumns.size() == 1) {
+                    newRowId.add(values.get(values.keySet().iterator().next()));
+                } else {
+                    for (String s : primaryKeyColumns) {
+                        if (!((ColumnProperty) row.getItemProperty(s))
+                                .isReadOnlyChangeAllowed()) {
+                            newRowId.add(values.get(values.keySet().iterator()
+                                    .next()));
+                        } else {
+                            newRowId.add(values.get(s));
+                        }
+                    }
+                }
+            } else {
+                for (String s : primaryKeyColumns) {
+                    newRowId.add(values.get(s));
+                }
+            }
+            return new RowId(newRowId.toArray());
+        } catch (Exception e) {
+            debug("Failed to fetch key values on insert: " + e.getMessage());
+            return null;
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * com.vaadin.addon.sqlcontainer.query.QueryDelegate#removeRow(com.vaadin
+     * .addon.sqlcontainer.RowItem)
+     */
+    public boolean removeRow(RowItem row) throws UnsupportedOperationException,
+            SQLException {
+        debug("Removing row with id: " + row.getId().getId()[0].toString());
+        if (executeUpdate(sqlGenerator.generateDeleteQuery(getTableName(),
+                primaryKeyColumns, versionColumn, row)) == 1) {
+            return true;
+        }
+        if (versionColumn != null) {
+            throw new OptimisticLockException(
+                    "Someone else changed the row that was being deleted.",
+                    row.getId());
+        }
+        return false;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * com.vaadin.addon.sqlcontainer.query.QueryDelegate#containsRowWithKey(
+     * java.lang.Object[])
+     */
+    public boolean containsRowWithKey(Object... keys) throws SQLException {
+        ArrayList<Filter> filtersAndKeys = new ArrayList<Filter>();
+        if (filters != null) {
+            filtersAndKeys.addAll(filters);
+        }
+        int ix = 0;
+        for (String colName : primaryKeyColumns) {
+            filtersAndKeys.add(new Equal(colName, keys[ix]));
+            ix++;
+        }
+        StatementHelper sh = sqlGenerator.generateSelectQuery(tableName,
+                filtersAndKeys, orderBys, 0, 0, "*");
+
+        boolean shouldCloseTransaction = false;
+        if (!transactionOpen) {
+            shouldCloseTransaction = true;
+            beginTransaction();
+        }
+        ResultSet rs = null;
+        try {
+            rs = executeQuery(sh);
+            boolean contains = rs.next();
+            return contains;
+        } finally {
+            if (rs != null) {
+                if (rs.getStatement() != null) {
+                    rs.getStatement().close();
+                }
+                rs.close();
+            }
+            if (shouldCloseTransaction) {
+                commit();
+            }
+        }
+    }
+
+    /**
+     * Output a debug message
+     * 
+     * @param message
+     */
+    private void debug(String message) {
+        if (debug) {
+            System.out.println(message);
+        }
+    }
+
+    /**
+     * Enable or disable debug mode.
+     * 
+     * @param debug
+     */
+    public void setDebug(boolean debug) {
+        this.debug = debug;
+    }
+
+    /**
+     * Custom writeObject to call rollback() if object is serialized.
+     */
+    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+        try {
+            rollback();
+        } catch (SQLException ignored) {
+        }
+        out.defaultWriteObject();
+    }
+
+    /**
+     * Simple RowIdChangeEvent implementation.
+     */
+    public class RowIdChangeEvent extends EventObject implements
+            QueryDelegate.RowIdChangeEvent {
+        private final RowId oldId;
+        private final RowId newId;
+
+        private RowIdChangeEvent(RowId oldId, RowId newId) {
+            super(oldId);
+            this.oldId = oldId;
+            this.newId = newId;
+        }
+
+        public RowId getNewRowId() {
+            return newId;
+        }
+
+        public RowId getOldRowId() {
+            return oldId;
+        }
+    }
+
+    /**
+     * Adds RowIdChangeListener to this query
+     */
+    public void addListener(RowIdChangeListener listener) {
+        if (rowIdChangeListeners == null) {
+            rowIdChangeListeners = new LinkedList<QueryDelegate.RowIdChangeListener>();
+        }
+        rowIdChangeListeners.add(listener);
+    }
+
+    /**
+     * Removes the given RowIdChangeListener from this query
+     */
+    public void removeListener(RowIdChangeListener listener) {
+        if (rowIdChangeListeners != null) {
+            rowIdChangeListeners.remove(listener);
+        }
+    }
+}
diff --git a/src/com/vaadin/data/util/query/generator/DefaultSQLGenerator.java b/src/com/vaadin/data/util/query/generator/DefaultSQLGenerator.java
new file mode 100644 (file)
index 0000000..b904141
--- /dev/null
@@ -0,0 +1,308 @@
+package com.vaadin.data.util.query.generator;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.ColumnProperty;
+import com.vaadin.data.util.RowItem;
+import com.vaadin.data.util.SQLUtil;
+import com.vaadin.data.util.TemporaryRowId;
+import com.vaadin.data.util.query.OrderBy;
+import com.vaadin.data.util.query.generator.filter.QueryBuilder;
+import com.vaadin.data.util.query.generator.filter.StringDecorator;
+
+/**
+ * Generates generic SQL that is supported by HSQLDB, MySQL and PostgreSQL.
+ * 
+ * @author Jonatan Kronqvist / IT Mill Ltd
+ */
+@SuppressWarnings("serial")
+public class DefaultSQLGenerator implements SQLGenerator {
+
+    public DefaultSQLGenerator() {
+
+    }
+
+    /**
+     * Construct a DefaultSQLGenerator with the specified identifiers for start
+     * and end of quoted strings. The identifiers may be different depending on
+     * the database engine and it's settings.
+     * 
+     * @param quoteStart
+     *            the identifier (character) denoting the start of a quoted
+     *            string
+     * @param quoteEnd
+     *            the identifier (character) denoting the end of a quoted string
+     */
+    public DefaultSQLGenerator(String quoteStart, String quoteEnd) {
+        QueryBuilder.setStringDecorator(new StringDecorator(quoteStart,
+                quoteEnd));
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+     * generateSelectQuery(java.lang.String, java.util.List, java.util.List,
+     * int, int, java.lang.String)
+     */
+    public StatementHelper generateSelectQuery(String tableName,
+            List<Filter> filters, List<OrderBy> orderBys, int offset,
+            int pagelength, String toSelect) {
+        if (tableName == null || tableName.trim().equals("")) {
+            throw new IllegalArgumentException("Table name must be given.");
+        }
+        toSelect = toSelect == null ? "*" : toSelect;
+        StatementHelper sh = new StatementHelper();
+        StringBuffer query = new StringBuffer();
+        query.append("SELECT " + toSelect + " FROM ").append(
+                SQLUtil.escapeSQL(tableName));
+        if (filters != null) {
+            query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+        }
+        if (orderBys != null) {
+            for (OrderBy o : orderBys) {
+                generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+            }
+        }
+        if (pagelength != 0) {
+            generateLimits(query, offset, pagelength);
+        }
+        sh.setQueryString(query.toString());
+        return sh;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+     * generateUpdateQuery(java.lang.String,
+     * com.vaadin.addon.sqlcontainer.RowItem)
+     */
+    public StatementHelper generateUpdateQuery(String tableName, RowItem item) {
+        if (tableName == null || tableName.trim().equals("")) {
+            throw new IllegalArgumentException("Table name must be given.");
+        }
+        if (item == null) {
+            throw new IllegalArgumentException("Updated item must be given.");
+        }
+        StatementHelper sh = new StatementHelper();
+        StringBuffer query = new StringBuffer();
+        query.append("UPDATE ").append(tableName).append(" SET");
+
+        /* Generate column<->value and rowidentifiers map */
+        Map<String, Object> columnToValueMap = generateColumnToValueMap(item);
+        Map<String, Object> rowIdentifiers = generateRowIdentifiers(item);
+        /* Generate columns and values to update */
+        boolean first = true;
+        for (String column : columnToValueMap.keySet()) {
+            if (first) {
+                query.append(" " + QueryBuilder.quote(column) + " = ?");
+            } else {
+                query.append(", " + QueryBuilder.quote(column) + " = ?");
+            }
+            sh.addParameterValue(columnToValueMap.get(column), item
+                    .getItemProperty(column).getType());
+            first = false;
+        }
+        /* Generate identifiers for the row to be updated */
+        first = true;
+        for (String column : rowIdentifiers.keySet()) {
+            if (first) {
+                query.append(" WHERE " + QueryBuilder.quote(column) + " = ?");
+            } else {
+                query.append(" AND " + QueryBuilder.quote(column) + " = ?");
+            }
+            sh.addParameterValue(rowIdentifiers.get(column), item
+                    .getItemProperty(column).getType());
+            first = false;
+        }
+        sh.setQueryString(query.toString());
+        return sh;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+     * generateInsertQuery(java.lang.String,
+     * com.vaadin.addon.sqlcontainer.RowItem)
+     */
+    public StatementHelper generateInsertQuery(String tableName, RowItem item) {
+        if (tableName == null || tableName.trim().equals("")) {
+            throw new IllegalArgumentException("Table name must be given.");
+        }
+        if (item == null) {
+            throw new IllegalArgumentException("New item must be given.");
+        }
+        if (!(item.getId() instanceof TemporaryRowId)) {
+            throw new IllegalArgumentException(
+                    "Cannot generate an insert query for item already in database.");
+        }
+        StatementHelper sh = new StatementHelper();
+        StringBuffer query = new StringBuffer();
+        query.append("INSERT INTO ").append(tableName).append(" (");
+
+        /* Generate column<->value map */
+        Map<String, Object> columnToValueMap = generateColumnToValueMap(item);
+        /* Generate column names for insert query */
+        boolean first = true;
+        for (String column : columnToValueMap.keySet()) {
+            if (!first) {
+                query.append(", ");
+            }
+            query.append(QueryBuilder.quote(column));
+            first = false;
+        }
+
+        /* Generate values for insert query */
+        query.append(") VALUES (");
+        first = true;
+        for (String column : columnToValueMap.keySet()) {
+            if (!first) {
+                query.append(", ");
+            }
+            query.append("?");
+            sh.addParameterValue(columnToValueMap.get(column), item
+                    .getItemProperty(column).getType());
+            first = false;
+        }
+        query.append(")");
+        sh.setQueryString(query.toString());
+        return sh;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+     * generateDeleteQuery(java.lang.String,
+     * com.vaadin.addon.sqlcontainer.RowItem)
+     */
+    public StatementHelper generateDeleteQuery(String tableName,
+            List<String> primaryKeyColumns, String versionColumn, RowItem item) {
+        if (tableName == null || tableName.trim().equals("")) {
+            throw new IllegalArgumentException("Table name must be given.");
+        }
+        if (item == null) {
+            throw new IllegalArgumentException(
+                    "Item to be deleted must be given.");
+        }
+        if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "Valid keyColumnNames must be provided.");
+        }
+        StatementHelper sh = new StatementHelper();
+        StringBuffer query = new StringBuffer();
+        query.append("DELETE FROM ").append(tableName).append(" WHERE ");
+        int count = 1;
+        for (String keyColName : primaryKeyColumns) {
+            if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
+                    && keyColName.equalsIgnoreCase("rownum")) {
+                count++;
+                continue;
+            }
+            if (count > 1) {
+                query.append(" AND ");
+            }
+            if (item.getItemProperty(keyColName).getValue() != null) {
+                query.append(QueryBuilder.quote(keyColName) + " = ?");
+                sh.addParameterValue(item.getItemProperty(keyColName)
+                        .getValue(), item.getItemProperty(keyColName).getType());
+            }
+            count++;
+        }
+        if (versionColumn != null) {
+            query.append(String.format(" AND %s = ?",
+                    QueryBuilder.quote(versionColumn)));
+            sh.addParameterValue(
+                    item.getItemProperty(versionColumn).getValue(), item
+                            .getItemProperty(versionColumn).getType());
+        }
+
+        sh.setQueryString(query.toString());
+        return sh;
+    }
+
+    /**
+     * Generates sorting rules as an ORDER BY -clause
+     * 
+     * @param sb
+     *            StringBuffer to which the clause is appended.
+     * @param o
+     *            OrderBy object to be added into the sb.
+     * @param firstOrderBy
+     *            If true, this is the first OrderBy.
+     * @return
+     */
+    protected StringBuffer generateOrderBy(StringBuffer sb, OrderBy o,
+            boolean firstOrderBy) {
+        if (firstOrderBy) {
+            sb.append(" ORDER BY ");
+        } else {
+            sb.append(", ");
+        }
+        sb.append(QueryBuilder.quote(o.getColumn()));
+        if (o.isAscending()) {
+            sb.append(" ASC");
+        } else {
+            sb.append(" DESC");
+        }
+        return sb;
+    }
+
+    /**
+     * Generates the LIMIT and OFFSET clause.
+     * 
+     * @param sb
+     *            StringBuffer to which the clause is appended.
+     * @param offset
+     *            Value for offset.
+     * @param pagelength
+     *            Value for pagelength.
+     * @return StringBuffer with LIMIT and OFFSET clause added.
+     */
+    protected StringBuffer generateLimits(StringBuffer sb, int offset,
+            int pagelength) {
+        sb.append(" LIMIT ").append(pagelength).append(" OFFSET ")
+                .append(offset);
+        return sb;
+    }
+
+    protected Map<String, Object> generateColumnToValueMap(RowItem item) {
+        Map<String, Object> columnToValueMap = new HashMap<String, Object>();
+        for (Object id : item.getItemPropertyIds()) {
+            ColumnProperty cp = (ColumnProperty) item.getItemProperty(id);
+            /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */
+            if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
+                    && cp.getPropertyId().equalsIgnoreCase("rownum")) {
+                continue;
+            }
+            Object value = cp.getValue() == null ? null : cp.getValue();
+            /* Only include properties whose read-only status can be altered */
+            if (cp.isReadOnlyChangeAllowed() && !cp.isVersionColumn()) {
+                columnToValueMap.put(cp.getPropertyId(), value);
+            }
+        }
+        return columnToValueMap;
+    }
+
+    protected Map<String, Object> generateRowIdentifiers(RowItem item) {
+        Map<String, Object> rowIdentifiers = new HashMap<String, Object>();
+        for (Object id : item.getItemPropertyIds()) {
+            ColumnProperty cp = (ColumnProperty) item.getItemProperty(id);
+            /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */
+            if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
+                    && cp.getPropertyId().equalsIgnoreCase("rownum")) {
+                continue;
+            }
+            Object value = cp.getValue() == null ? null : cp.getValue();
+            if (!cp.isReadOnlyChangeAllowed() || cp.isVersionColumn()) {
+                rowIdentifiers.put(cp.getPropertyId(), value);
+            }
+        }
+        return rowIdentifiers;
+    }
+}
\ No newline at end of file
diff --git a/src/com/vaadin/data/util/query/generator/MSSQLGenerator.java b/src/com/vaadin/data/util/query/generator/MSSQLGenerator.java
new file mode 100644 (file)
index 0000000..da98e67
--- /dev/null
@@ -0,0 +1,101 @@
+package com.vaadin.data.util.query.generator;
+
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.query.OrderBy;
+import com.vaadin.data.util.query.generator.filter.QueryBuilder;
+
+@SuppressWarnings("serial")
+public class MSSQLGenerator extends DefaultSQLGenerator {
+
+    public MSSQLGenerator() {
+
+    }
+
+    /**
+     * Construct a MSSQLGenerator with the specified identifiers for start and
+     * end of quoted strings. The identifiers may be different depending on the
+     * database engine and it's settings.
+     * 
+     * @param quoteStart
+     *            the identifier (character) denoting the start of a quoted
+     *            string
+     * @param quoteEnd
+     *            the identifier (character) denoting the end of a quoted string
+     */
+    public MSSQLGenerator(String quoteStart, String quoteEnd) {
+        super(quoteStart, quoteEnd);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator#
+     * generateSelectQuery(java.lang.String, java.util.List,
+     * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int,
+     * int, java.lang.String)
+     */
+    @Override
+    public StatementHelper generateSelectQuery(String tableName,
+            List<Filter> filters, List<OrderBy> orderBys, int offset,
+            int pagelength, String toSelect) {
+        if (tableName == null || tableName.trim().equals("")) {
+            throw new IllegalArgumentException("Table name must be given.");
+        }
+        /* Adjust offset and page length parameters to match "row numbers" */
+        offset = pagelength > 1 ? ++offset : offset;
+        pagelength = pagelength > 1 ? --pagelength : pagelength;
+        toSelect = toSelect == null ? "*" : toSelect;
+        StatementHelper sh = new StatementHelper();
+        StringBuffer query = new StringBuffer();
+
+        /* Row count request is handled here */
+        if ("COUNT(*)".equalsIgnoreCase(toSelect)) {
+            query.append(String.format(
+                    "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s",
+                    QueryBuilder.quote("rowcount"), tableName));
+            if (filters != null && !filters.isEmpty()) {
+                query.append(QueryBuilder.getWhereStringForFilters(
+                        filters, sh));
+            }
+            query.append(") AS t");
+            sh.setQueryString(query.toString());
+            return sh;
+        }
+
+        /* SELECT without row number constraints */
+        if (offset == 0 && pagelength == 0) {
+            query.append("SELECT ").append(toSelect).append(" FROM ")
+                    .append(tableName);
+            if (filters != null) {
+                query.append(QueryBuilder.getWhereStringForFilters(
+                        filters, sh));
+            }
+            if (orderBys != null) {
+                for (OrderBy o : orderBys) {
+                    generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+                }
+            }
+            sh.setQueryString(query.toString());
+            return sh;
+        }
+
+        /* Remaining SELECT cases are handled here */
+        query.append("SELECT * FROM (SELECT row_number() OVER (");
+        if (orderBys != null) {
+            for (OrderBy o : orderBys) {
+                generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+            }
+        }
+        query.append(") AS rownum, " + toSelect + " FROM ").append(tableName);
+        if (filters != null) {
+            query.append(QueryBuilder.getWhereStringForFilters(
+                    filters, sh));
+        }
+        query.append(") AS a WHERE a.rownum BETWEEN ").append(offset)
+                .append(" AND ").append(Integer.toString(offset + pagelength));
+        sh.setQueryString(query.toString());
+        return sh;
+    }
+}
\ No newline at end of file
diff --git a/src/com/vaadin/data/util/query/generator/OracleGenerator.java b/src/com/vaadin/data/util/query/generator/OracleGenerator.java
new file mode 100644 (file)
index 0000000..519d182
--- /dev/null
@@ -0,0 +1,99 @@
+package com.vaadin.data.util.query.generator;
+
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.query.OrderBy;
+import com.vaadin.data.util.query.generator.filter.QueryBuilder;
+
+@SuppressWarnings("serial")
+public class OracleGenerator extends DefaultSQLGenerator {
+
+    public OracleGenerator() {
+
+    }
+
+    /**
+     * Construct an OracleSQLGenerator with the specified identifiers for start
+     * and end of quoted strings. The identifiers may be different depending on
+     * the database engine and it's settings.
+     * 
+     * @param quoteStart
+     *            the identifier (character) denoting the start of a quoted
+     *            string
+     * @param quoteEnd
+     *            the identifier (character) denoting the end of a quoted string
+     */
+    public OracleGenerator(String quoteStart, String quoteEnd) {
+        super(quoteStart, quoteEnd);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator#
+     * generateSelectQuery(java.lang.String, java.util.List,
+     * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int,
+     * int, java.lang.String)
+     */
+    @Override
+    public StatementHelper generateSelectQuery(String tableName,
+            List<Filter> filters, List<OrderBy> orderBys, int offset,
+            int pagelength, String toSelect) {
+        if (tableName == null || tableName.trim().equals("")) {
+            throw new IllegalArgumentException("Table name must be given.");
+        }
+        /* Adjust offset and page length parameters to match "row numbers" */
+        offset = pagelength > 1 ? ++offset : offset;
+        pagelength = pagelength > 1 ? --pagelength : pagelength;
+        toSelect = toSelect == null ? "*" : toSelect;
+        StatementHelper sh = new StatementHelper();
+        StringBuffer query = new StringBuffer();
+
+        /* Row count request is handled here */
+        if ("COUNT(*)".equalsIgnoreCase(toSelect)) {
+            query.append(String.format(
+                    "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s",
+                    QueryBuilder.quote("rowcount"), tableName));
+            if (filters != null && !filters.isEmpty()) {
+                query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+            }
+            query.append(")");
+            sh.setQueryString(query.toString());
+            return sh;
+        }
+
+        /* SELECT without row number constraints */
+        if (offset == 0 && pagelength == 0) {
+            query.append("SELECT ").append(toSelect).append(" FROM ")
+                    .append(tableName);
+            if (filters != null) {
+                query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+            }
+            if (orderBys != null) {
+                for (OrderBy o : orderBys) {
+                    generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+                }
+            }
+            sh.setQueryString(query.toString());
+            return sh;
+        }
+
+        /* Remaining SELECT cases are handled here */
+        query.append(String
+                .format("SELECT * FROM (SELECT x.*, ROWNUM AS %s FROM (SELECT %s FROM %s",
+                        QueryBuilder.quote("rownum"), toSelect, tableName));
+        if (filters != null) {
+            query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+        }
+        if (orderBys != null) {
+            for (OrderBy o : orderBys) {
+                generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+            }
+        }
+        query.append(String.format(") x) WHERE %s BETWEEN %d AND %d",
+                QueryBuilder.quote("rownum"), offset, offset + pagelength));
+        sh.setQueryString(query.toString());
+        return sh;
+    }
+}
\ No newline at end of file
diff --git a/src/com/vaadin/data/util/query/generator/SQLGenerator.java b/src/com/vaadin/data/util/query/generator/SQLGenerator.java
new file mode 100644 (file)
index 0000000..dfdecae
--- /dev/null
@@ -0,0 +1,85 @@
+package com.vaadin.data.util.query.generator;
+
+import java.io.Serializable;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.RowItem;
+import com.vaadin.data.util.query.OrderBy;
+
+/**
+ * The SQLGenerator interface is meant to be implemented for each different SQL
+ * syntax that is to be supported. By default there are implementations for
+ * HSQLDB, MySQL, PostgreSQL, MSSQL and Oracle syntaxes.
+ * 
+ * @author Jonatan Kronqvist / IT Mill Ltd
+ */
+public interface SQLGenerator extends Serializable {
+    /**
+     * Generates a SELECT query with the provided parameters. Uses default
+     * filtering mode (INCLUSIVE).
+     * 
+     * @param tableName
+     *            Name of the table queried
+     * @param filters
+     *            The filters, converted into a WHERE clause
+     * @param orderBys
+     *            The the ordering conditions, converted into an ORDER BY clause
+     * @param offset
+     *            The offset of the first row to be included
+     * @param pagelength
+     *            The number of rows to be returned when the query executes
+     * @param toSelect
+     *            String containing what to select, e.g. "*", "COUNT(*)"
+     * @return StatementHelper instance containing the query string for a
+     *         PreparedStatement and the values required for the parameters
+     */
+    public StatementHelper generateSelectQuery(String tableName,
+            List<Filter> filters, List<OrderBy> orderBys, int offset,
+            int pagelength, String toSelect);
+
+    /**
+     * Generates an UPDATE query with the provided parameters.
+     * 
+     * @param tableName
+     *            Name of the table queried
+     * @param item
+     *            RowItem containing the updated values update.
+     * @return StatementHelper instance containing the query string for a
+     *         PreparedStatement and the values required for the parameters
+     */
+    public StatementHelper generateUpdateQuery(String tableName, RowItem item);
+
+    /**
+     * Generates an INSERT query for inserting a new row with the provided
+     * values.
+     * 
+     * @param tableName
+     *            Name of the table queried
+     * @param item
+     *            New RowItem to be inserted into the database.
+     * @return StatementHelper instance containing the query string for a
+     *         PreparedStatement and the values required for the parameters
+     */
+    public StatementHelper generateInsertQuery(String tableName, RowItem item);
+
+    /**
+     * Generates a DELETE query for deleting data related to the given RowItem
+     * from the database.
+     * 
+     * @param tableName
+     *            Name of the table queried
+     * @param primaryKeyColumns
+     *            the names of the columns holding the primary key. Usually just
+     *            one column, but might be several.
+     * @param versionColumn
+     *            the column containing the version number of the row, null if
+     *            versioning (optimistic locking) not enabled.
+     * @param item
+     *            Item to be deleted from the database
+     * @return StatementHelper instance containing the query string for a
+     *         PreparedStatement and the values required for the parameters
+     */
+    public StatementHelper generateDeleteQuery(String tableName,
+            List<String> primaryKeyColumns, String versionColumn, RowItem item);
+}
diff --git a/src/com/vaadin/data/util/query/generator/StatementHelper.java b/src/com/vaadin/data/util/query/generator/StatementHelper.java
new file mode 100644 (file)
index 0000000..b230f58
--- /dev/null
@@ -0,0 +1,131 @@
+package com.vaadin.data.util.query.generator;\r
+\r
+import java.math.BigDecimal;\r
+import java.sql.Date;\r
+import java.sql.PreparedStatement;\r
+import java.sql.SQLException;\r
+import java.sql.Time;\r
+import java.sql.Timestamp;\r
+import java.sql.Types;\r
+import java.util.ArrayList;\r
+import java.util.HashMap;\r
+import java.util.List;\r
+import java.util.Map;\r
+\r
+/**\r
+ * StatementHelper is a simple helper class that assists TableQuery and the\r
+ * query generators in filling a PreparedStatement. The actual statement is\r
+ * generated by the query generator methods, but the resulting statement and all\r
+ * the parameter values are stored in an instance of StatementHelper.\r
+ * \r
+ * This class will also fill the values with correct setters into the\r
+ * PreparedStatement on request.\r
+ */\r
+public class StatementHelper {\r
+\r
+    private String queryString;\r
+\r
+    private List<Object> parameters = new ArrayList<Object>();\r
+    private Map<Integer, Class<?>> dataTypes = new HashMap<Integer, Class<?>>();\r
+\r
+    public StatementHelper() {\r
+    }\r
+\r
+    public void setQueryString(String queryString) {\r
+        this.queryString = queryString;\r
+    }\r
+\r
+    public String getQueryString() {\r
+        return queryString;\r
+    }\r
+\r
+    public void addParameterValue(Object parameter) {\r
+        if (parameter != null) {\r
+            parameters.add(parameter);\r
+            dataTypes.put(parameters.size() - 1, parameter.getClass());\r
+        }\r
+    }\r
+\r
+    public void addParameterValue(Object parameter, Class<?> type) {\r
+        parameters.add(parameter);\r
+        dataTypes.put(parameters.size() - 1, type);\r
+    }\r
+\r
+    public void setParameterValuesToStatement(PreparedStatement pstmt)\r
+            throws SQLException {\r
+        for (int i = 0; i < parameters.size(); i++) {\r
+            if (parameters.get(i) == null) {\r
+                handleNullValue(i, pstmt);\r
+            } else {\r
+                pstmt.setObject(i + 1, parameters.get(i));\r
+            }\r
+        }\r
+\r
+        /*\r
+         * The following list contains the data types supported by\r
+         * PreparedStatement but not supported by SQLContainer:\r
+         * \r
+         * [The list is provided as PreparedStatement method signatures]\r
+         * \r
+         * setNCharacterStream(int parameterIndex, Reader value)\r
+         * \r
+         * setNClob(int parameterIndex, NClob value)\r
+         * \r
+         * setNString(int parameterIndex, String value)\r
+         * \r
+         * setRef(int parameterIndex, Ref x)\r
+         * \r
+         * setRowId(int parameterIndex, RowId x)\r
+         * \r
+         * setSQLXML(int parameterIndex, SQLXML xmlObject)\r
+         * \r
+         * setBytes(int parameterIndex, byte[] x)\r
+         * \r
+         * setCharacterStream(int parameterIndex, Reader reader)\r
+         * \r
+         * setClob(int parameterIndex, Clob x)\r
+         * \r
+         * setURL(int parameterIndex, URL x)\r
+         * \r
+         * setArray(int parameterIndex, Array x)\r
+         * \r
+         * setAsciiStream(int parameterIndex, InputStream x)\r
+         * \r
+         * setBinaryStream(int parameterIndex, InputStream x)\r
+         * \r
+         * setBlob(int parameterIndex, Blob x)\r
+         */\r
+    }\r
+\r
+    private void handleNullValue(int i, PreparedStatement pstmt)\r
+            throws SQLException {\r
+        if (BigDecimal.class.equals(dataTypes.get(i))) {\r
+            pstmt.setBigDecimal(i + 1, null);\r
+        } else if (Boolean.class.equals(dataTypes.get(i))) {\r
+            pstmt.setNull(i + 1, Types.BOOLEAN);\r
+        } else if (Byte.class.equals(dataTypes.get(i))) {\r
+            pstmt.setNull(i + 1, Types.SMALLINT);\r
+        } else if (Date.class.equals(dataTypes.get(i))) {\r
+            pstmt.setDate(i + 1, null);\r
+        } else if (Double.class.equals(dataTypes.get(i))) {\r
+            pstmt.setNull(i + 1, Types.DOUBLE);\r
+        } else if (Float.class.equals(dataTypes.get(i))) {\r
+            pstmt.setNull(i + 1, Types.FLOAT);\r
+        } else if (Integer.class.equals(dataTypes.get(i))) {\r
+            pstmt.setNull(i + 1, Types.INTEGER);\r
+        } else if (Long.class.equals(dataTypes.get(i))) {\r
+            pstmt.setNull(i + 1, Types.BIGINT);\r
+        } else if (Short.class.equals(dataTypes.get(i))) {\r
+            pstmt.setNull(i + 1, Types.SMALLINT);\r
+        } else if (String.class.equals(dataTypes.get(i))) {\r
+            pstmt.setString(i + 1, null);\r
+        } else if (Time.class.equals(dataTypes.get(i))) {\r
+            pstmt.setTime(i + 1, null);\r
+        } else if (Timestamp.class.equals(dataTypes.get(i))) {\r
+            pstmt.setTimestamp(i + 1, null);\r
+        } else {\r
+            throw new SQLException("Data type not supported by SQLContainer: "\r
+                    + parameters.get(i).getClass().toString());\r
+        }\r
+    }\r
+}\r
diff --git a/src/com/vaadin/data/util/query/generator/filter/AndTranslator.java b/src/com/vaadin/data/util/query/generator/filter/AndTranslator.java
new file mode 100644 (file)
index 0000000..19f1ce5
--- /dev/null
@@ -0,0 +1,18 @@
+package com.vaadin.data.util.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.And;
+import com.vaadin.data.util.query.generator.StatementHelper;
+
+public class AndTranslator implements FilterTranslator {
+
+    public boolean translatesFilter(Filter filter) {
+        return filter instanceof And;
+    }
+
+    public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+        return QueryBuilder.group(QueryBuilder
+                .getJoinedFilterString(((And) filter).getFilters(), "AND", sh));
+    }
+
+}
diff --git a/src/com/vaadin/data/util/query/generator/filter/BetweenTranslator.java b/src/com/vaadin/data/util/query/generator/filter/BetweenTranslator.java
new file mode 100644 (file)
index 0000000..0492741
--- /dev/null
@@ -0,0 +1,21 @@
+package com.vaadin.data.util.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Between;
+import com.vaadin.data.util.query.generator.StatementHelper;
+
+public class BetweenTranslator implements FilterTranslator {
+
+    public boolean translatesFilter(Filter filter) {
+        return filter instanceof Between;
+    }
+
+    public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+        Between between = (Between) filter;
+        sh.addParameterValue(between.getStartValue());
+        sh.addParameterValue(between.getEndValue());
+        return QueryBuilder.quote(between.getPropertyId())
+                + " BETWEEN ? AND ?";
+    }
+
+}
diff --git a/src/com/vaadin/data/util/query/generator/filter/CompareTranslator.java b/src/com/vaadin/data/util/query/generator/filter/CompareTranslator.java
new file mode 100644 (file)
index 0000000..c35baf0
--- /dev/null
@@ -0,0 +1,33 @@
+package com.vaadin.data.util.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Compare;
+import com.vaadin.data.util.query.generator.StatementHelper;
+
+public class CompareTranslator implements FilterTranslator {
+
+    public boolean translatesFilter(Filter filter) {
+        return filter instanceof Compare;
+    }
+
+    public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+        Compare compare = (Compare) filter;
+        sh.addParameterValue(compare.getValue());
+        String prop = QueryBuilder.quote(compare.getPropertyId());
+        switch (compare.getOperation()) {
+        case EQUAL:
+            return prop + " = ?";
+        case GREATER:
+            return prop + " > ?";
+        case GREATER_OR_EQUAL:
+            return prop + " >= ?";
+        case LESS:
+            return prop + " < ?";
+        case LESS_OR_EQUAL:
+            return prop + " <= ?";
+        default:
+            return "";
+        }
+    }
+
+}
diff --git a/src/com/vaadin/data/util/query/generator/filter/FilterTranslator.java b/src/com/vaadin/data/util/query/generator/filter/FilterTranslator.java
new file mode 100644 (file)
index 0000000..75ac488
--- /dev/null
@@ -0,0 +1,11 @@
+package com.vaadin.data.util.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.query.generator.StatementHelper;
+
+public interface FilterTranslator {
+    public boolean translatesFilter(Filter filter);
+
+    public String getWhereStringForFilter(Filter filter, StatementHelper sh);
+
+}
diff --git a/src/com/vaadin/data/util/query/generator/filter/IsNullTranslator.java b/src/com/vaadin/data/util/query/generator/filter/IsNullTranslator.java
new file mode 100644 (file)
index 0000000..fc44d65
--- /dev/null
@@ -0,0 +1,17 @@
+package com.vaadin.data.util.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.IsNull;
+import com.vaadin.data.util.query.generator.StatementHelper;
+
+public class IsNullTranslator implements FilterTranslator {
+
+    public boolean translatesFilter(Filter filter) {
+        return filter instanceof IsNull;
+    }
+
+    public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+        IsNull in = (IsNull) filter;
+        return QueryBuilder.quote(in.getPropertyId()) + " IS NULL";
+    }
+}
diff --git a/src/com/vaadin/data/util/query/generator/filter/LikeTranslator.java b/src/com/vaadin/data/util/query/generator/filter/LikeTranslator.java
new file mode 100644 (file)
index 0000000..e4d1d72
--- /dev/null
@@ -0,0 +1,27 @@
+package com.vaadin.data.util.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.query.generator.StatementHelper;
+
+public class LikeTranslator implements FilterTranslator {
+
+    public boolean translatesFilter(Filter filter) {
+        return filter instanceof Like;
+    }
+
+    public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+        Like like = (Like) filter;
+        if (like.isCaseSensitive()) {
+            sh.addParameterValue(like.getValue());
+            return QueryBuilder.quote(like.getPropertyId())
+                    + " LIKE ?";
+        } else {
+            sh.addParameterValue(like.getValue().toUpperCase());
+            return "UPPER("
+                    + QueryBuilder.quote(like.getPropertyId())
+                    + ") LIKE ?";
+        }
+    }
+
+}
diff --git a/src/com/vaadin/data/util/query/generator/filter/NotTranslator.java b/src/com/vaadin/data/util/query/generator/filter/NotTranslator.java
new file mode 100644 (file)
index 0000000..1cb7321
--- /dev/null
@@ -0,0 +1,26 @@
+package com.vaadin.data.util.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.IsNull;
+import com.vaadin.data.util.filter.Not;
+import com.vaadin.data.util.query.generator.StatementHelper;
+
+public class NotTranslator implements FilterTranslator {
+
+    public boolean translatesFilter(Filter filter) {
+        return filter instanceof Not;
+    }
+
+    public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+        Not not = (Not) filter;
+        if (not.getFilter() instanceof IsNull) {
+            IsNull in = (IsNull) not.getFilter();
+            return QueryBuilder.quote(in.getPropertyId())
+                    + " IS NOT NULL";
+        }
+        return "NOT "
+                + QueryBuilder.getWhereStringForFilter(
+                        not.getFilter(), sh);
+    }
+
+}
diff --git a/src/com/vaadin/data/util/query/generator/filter/OrTranslator.java b/src/com/vaadin/data/util/query/generator/filter/OrTranslator.java
new file mode 100644 (file)
index 0000000..1ba2db6
--- /dev/null
@@ -0,0 +1,18 @@
+package com.vaadin.data.util.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Or;
+import com.vaadin.data.util.query.generator.StatementHelper;
+
+public class OrTranslator implements FilterTranslator {
+
+    public boolean translatesFilter(Filter filter) {
+        return filter instanceof Or;
+    }
+
+    public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+        return QueryBuilder.group(QueryBuilder
+                .getJoinedFilterString(((Or) filter).getFilters(), "OR", sh));
+    }
+
+}
diff --git a/src/com/vaadin/data/util/query/generator/filter/QueryBuilder.java b/src/com/vaadin/data/util/query/generator/filter/QueryBuilder.java
new file mode 100644 (file)
index 0000000..75405d8
--- /dev/null
@@ -0,0 +1,94 @@
+package com.vaadin.data.util.query.generator.filter;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.query.generator.StatementHelper;
+
+public class QueryBuilder {
+
+    private static ArrayList<FilterTranslator> filterTranslators = new ArrayList<FilterTranslator>();
+    private static StringDecorator stringDecorator = new StringDecorator("\"",
+            "\"");
+
+    static {
+        /* Register all default filter translators */
+        addFilterTranslator(new AndTranslator());
+        addFilterTranslator(new OrTranslator());
+        addFilterTranslator(new LikeTranslator());
+        addFilterTranslator(new BetweenTranslator());
+        addFilterTranslator(new CompareTranslator());
+        addFilterTranslator(new NotTranslator());
+        addFilterTranslator(new IsNullTranslator());
+        addFilterTranslator(new SimpleStringTranslator());
+    }
+
+    public synchronized static void addFilterTranslator(
+            FilterTranslator translator) {
+        filterTranslators.add(translator);
+    }
+
+    /**
+     * Allows specification of a custom ColumnQuoter instance that handles
+     * quoting of column names for the current DB dialect.
+     * 
+     * @param decorator
+     *            the ColumnQuoter instance to use.
+     */
+    public static void setStringDecorator(StringDecorator decorator) {
+        stringDecorator = decorator;
+    }
+
+    public static String quote(Object str) {
+        return stringDecorator.quote(str);
+    }
+
+    public static String group(String str) {
+        return stringDecorator.group(str);
+    }
+
+    /**
+     * Constructs and returns a string representing the filter that can be used
+     * in a WHERE clause.
+     * 
+     * @param filter
+     *            the filter to translate
+     * @param sh
+     *            the statement helper to update with the value(s) of the filter
+     * @return a string representing the filter.
+     */
+    public synchronized static String getWhereStringForFilter(Filter filter,
+            StatementHelper sh) {
+        for (FilterTranslator ft : filterTranslators) {
+            if (ft.translatesFilter(filter)) {
+                return ft.getWhereStringForFilter(filter, sh);
+            }
+        }
+        return "";
+    }
+
+    public static String getJoinedFilterString(Collection<Filter> filters,
+            String joinString, StatementHelper sh) {
+        StringBuilder result = new StringBuilder();
+        for (Filter f : filters) {
+            result.append(getWhereStringForFilter(f, sh));
+            result.append(" ").append(joinString).append(" ");
+        }
+        // Remove the last instance of joinString
+        result.delete(result.length() - joinString.length() - 2,
+                result.length());
+        return result.toString();
+    }
+
+    public static String getWhereStringForFilters(List<Filter> filters,
+            StatementHelper sh) {
+        if (filters == null || filters.isEmpty()) {
+            return "";
+        }
+        StringBuilder where = new StringBuilder(" WHERE ");
+        where.append(getJoinedFilterString(filters, "AND", sh));
+        return where.toString();
+    }
+}
diff --git a/src/com/vaadin/data/util/query/generator/filter/SimpleStringTranslator.java b/src/com/vaadin/data/util/query/generator/filter/SimpleStringTranslator.java
new file mode 100644 (file)
index 0000000..6be678c
--- /dev/null
@@ -0,0 +1,25 @@
+package com.vaadin.data.util.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.data.util.query.generator.StatementHelper;
+
+public class SimpleStringTranslator implements FilterTranslator {
+
+    public boolean translatesFilter(Filter filter) {
+        return filter instanceof SimpleStringFilter;
+    }
+
+    public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+        SimpleStringFilter ssf = (SimpleStringFilter) filter;
+        // Create a Like filter based on the SimpleStringFilter and execute the
+        // LikeTranslator
+        String likeStr = ssf.isOnlyMatchPrefix() ? ssf.getFilterString() + "%"
+                : "%" + ssf.getFilterString() + "%";
+        Like like = new Like(ssf.getPropertyId().toString(), likeStr);
+        like.setCaseSensitive(!ssf.isIgnoreCase());
+        return new LikeTranslator().getWhereStringForFilter(like, sh);
+    }
+
+}
diff --git a/src/com/vaadin/data/util/query/generator/filter/StringDecorator.java b/src/com/vaadin/data/util/query/generator/filter/StringDecorator.java
new file mode 100644 (file)
index 0000000..0fdd9a1
--- /dev/null
@@ -0,0 +1,53 @@
+package com.vaadin.data.util.query.generator.filter;
+
+/**
+ * The StringDecorator knows how to produce a quoted string using the specified
+ * quote start and quote end characters. It also handles grouping of a string
+ * (surrounding it in parenthesis).
+ * 
+ * Extend this class if you need to support special characters for grouping
+ * (parenthesis).
+ * 
+ * @author Vaadin Ltd
+ */
+public class StringDecorator {
+
+    private final String quoteStart;
+    private final String quoteEnd;
+
+    /**
+     * Constructs a StringDecorator that uses the quoteStart and quoteEnd
+     * characters to create quoted strings.
+     * 
+     * @param quoteStart
+     *            the character denoting the start of a quote.
+     * @param quoteEnd
+     *            the character denoting the end of a quote.
+     */
+    public StringDecorator(String quoteStart, String quoteEnd) {
+        this.quoteStart = quoteStart;
+        this.quoteEnd = quoteEnd;
+    }
+
+    /**
+     * Surround a string with quote characters.
+     * 
+     * @param str
+     *            the string to quote
+     * @return the quoted string
+     */
+    public String quote(Object str) {
+        return quoteStart + str + quoteEnd;
+    }
+
+    /**
+     * Groups a string by surrounding it in parenthesis
+     * 
+     * @param str
+     *            the string to group
+     * @return the grouped string
+     */
+    public String group(String str) {
+        return "(" + str + ")";
+    }
+}
diff --git a/tests/src/com/vaadin/tests/containers/sqlcontainer/CheckboxUpdateProblem.java b/tests/src/com/vaadin/tests/containers/sqlcontainer/CheckboxUpdateProblem.java
new file mode 100644 (file)
index 0000000..2a609bc
--- /dev/null
@@ -0,0 +1,191 @@
+package com.vaadin.tests.containers.sqlcontainer;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import com.vaadin.Application;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.Property.ValueChangeEvent;
+import com.vaadin.data.util.SQLContainer;
+import com.vaadin.data.util.connection.JDBCConnectionPool;
+import com.vaadin.data.util.connection.SimpleJDBCConnectionPool;
+import com.vaadin.data.util.query.TableQuery;
+import com.vaadin.tests.server.container.sqlcontainer.AllTests;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.Form;
+import com.vaadin.ui.HorizontalSplitPanel;
+import com.vaadin.ui.Table;
+import com.vaadin.ui.Window;
+
+public class CheckboxUpdateProblem extends Application implements
+        Property.ValueChangeListener {
+    private final DatabaseHelper databaseHelper = new DatabaseHelper();
+    private Table testList;
+    private final HorizontalSplitPanel horizontalSplit = new HorizontalSplitPanel();
+
+    private TestForm testForm = new TestForm();
+
+    @Override
+    public void init() {
+        setMainWindow(new Window("Test window"));
+        horizontalSplit.setSizeFull();
+        testList = new Table();
+
+        horizontalSplit.setFirstComponent(testList);
+        testList.setSizeFull();
+        testList.setContainerDataSource(databaseHelper.getTestContainer());
+        testList.setSelectable(true);
+        testList.setImmediate(true);
+        testList.addListener(this);
+
+        databaseHelper.getTestContainer().addListener(
+                new ItemSetChangeListener() {
+                    public void containerItemSetChange(ItemSetChangeEvent event) {
+                        Object selected = testList.getValue();
+                        if (selected != null) {
+                            testForm.setItemDataSource(testList
+                                    .getItem(selected));
+                        }
+                    }
+                });
+
+        testForm = new TestForm();
+        testForm.setItemDataSource(null);
+
+        horizontalSplit.setSecondComponent(testForm);
+
+        getMainWindow().setContent(horizontalSplit);
+    }
+
+    public void valueChange(ValueChangeEvent event) {
+
+        Property property = event.getProperty();
+        if (property == testList) {
+            Item item = testList.getItem(testList.getValue());
+
+            if (item != testForm.getItemDataSource()) {
+                testForm.setItemDataSource(item);
+            }
+        }
+
+    }
+
+    private class TestForm extends Form implements Button.ClickListener {
+
+        private final Button save;
+
+        private TestForm() {
+            setSizeFull();
+            setWriteThrough(false);
+            setInvalidCommitted(false);
+
+            save = new Button("Save", (ClickListener) this);
+            getFooter().addComponent(save);
+            getFooter().setVisible(false);
+        }
+
+        public void buttonClick(ClickEvent event) {
+            if (event.getSource() == save) {
+                super.commit();
+
+                try {
+                    databaseHelper.getTestContainer().commit();
+                    getMainWindow().showNotification("Saved");
+                } catch (SQLException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+
+        @Override
+        public void setItemDataSource(Item newDataSource) {
+            super.setItemDataSource(newDataSource);
+
+            if (newDataSource != null) {
+                getFooter().setVisible(true);
+            } else {
+                getFooter().setVisible(false);
+            }
+        }
+
+    }
+
+    private class DatabaseHelper {
+
+        private JDBCConnectionPool connectionPool = null;
+        private SQLContainer testContainer = null;
+        private static final String TABLENAME = "testtable";
+
+        public DatabaseHelper() {
+            initConnectionPool();
+            initDatabase();
+            initContainers();
+        }
+
+        private void initDatabase() {
+            try {
+                Connection conn = connectionPool.reserveConnection();
+                Statement statement = conn.createStatement();
+                try {
+                    statement.execute("drop table " + TABLENAME);
+                } catch (SQLException e) {
+                    // Will fail if table doesn't exist, which is OK.
+                    conn.rollback();
+                }
+                switch (AllTests.db) {
+                case MYSQL:
+                    statement
+                            .execute("create table "
+                                    + TABLENAME
+                                    + " (id integer auto_increment not null, field1 varchar(100), field2 boolean, primary key(id))");
+                    break;
+                case POSTGRESQL:
+                    statement
+                            .execute("create table "
+                                    + TABLENAME
+                                    + " (\"id\" serial primary key, \"field1\" varchar(100), \"field2\" boolean)");
+                    break;
+                }
+                statement.executeUpdate("insert into " + TABLENAME
+                        + " values(default, 'Kalle', 'true')");
+                statement.close();
+                conn.commit();
+                connectionPool.releaseConnection(conn);
+            } catch (SQLException e) {
+                e.printStackTrace();
+            }
+        }
+
+        private void initContainers() {
+            try {
+                TableQuery q1 = new TableQuery(TABLENAME, connectionPool);
+                q1.setVersionColumn("id");
+                testContainer = new SQLContainer(q1);
+                testContainer.setDebugMode(true);
+            } catch (SQLException e) {
+                e.printStackTrace();
+            }
+        }
+
+        private void initConnectionPool() {
+            try {
+                connectionPool = new SimpleJDBCConnectionPool(
+                        AllTests.dbDriver, AllTests.dbURL, AllTests.dbUser,
+                        AllTests.dbPwd, 2, 5);
+            } catch (SQLException e) {
+                e.printStackTrace();
+            }
+        }
+
+        public SQLContainer getTestContainer() {
+            return testContainer;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/tests/src/com/vaadin/tests/containers/sqlcontainer/MassInsertMemoryLeakTestApp.java b/tests/src/com/vaadin/tests/containers/sqlcontainer/MassInsertMemoryLeakTestApp.java
new file mode 100644 (file)
index 0000000..6ead5b1
--- /dev/null
@@ -0,0 +1,134 @@
+package com.vaadin.tests.containers.sqlcontainer;
+
+import java.sql.SQLException;
+
+import com.vaadin.Application;
+import com.vaadin.data.util.SQLContainer;
+import com.vaadin.data.util.connection.JDBCConnectionPool;
+import com.vaadin.data.util.connection.SimpleJDBCConnectionPool;
+import com.vaadin.data.util.query.TableQuery;
+import com.vaadin.ui.Alignment;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.ComponentContainer;
+import com.vaadin.ui.ProgressIndicator;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.ui.Window;
+
+// author table in testdb (MySQL) is set out as follows
+// +-------------+-------------+------+-----+---------+----------------+
+// | Field       | Type        | Null | Key | Default | Extra          |
+// +-------------+-------------+------+-----+---------+----------------+
+// | id          | int(11)     | NO   | PRI | NULL    | auto_increment |
+// | last_name   | varchar(40) | NO   |     | NULL    |                |
+// | first_names | varchar(80) | NO   |     | NULL    |                |
+// +-------------+-------------+------+-----+---------+----------------+
+
+@SuppressWarnings("serial")
+public class MassInsertMemoryLeakTestApp extends Application {
+
+    ProgressIndicator proggress = new ProgressIndicator();
+    Button process = new Button("Mass insert");
+
+    @Override
+    public void init() {
+        setMainWindow(new Window("SQLContainer Test", buildLayout()));
+
+        process.addListener(new Button.ClickListener() {
+            public void buttonClick(ClickEvent event) {
+                MassInsert mi = new MassInsert();
+                mi.start();
+            }
+        });
+    }
+
+    private class MassInsert extends Thread {
+
+        @Override
+        public synchronized void start() {
+            proggress.setVisible(true);
+            proggress.setValue(new Float(0));
+            proggress.setPollingInterval(100);
+            process.setEnabled(false);
+            proggress.setCaption("");
+            super.start();
+        }
+
+        @Override
+        public void run() {
+            JDBCConnectionPool pool = getConnectionPool();
+            if (pool != null) {
+                try {
+                    int cents = 100;
+                    for (int cent = 0; cent < cents; cent++) {
+                        TableQuery q = new TableQuery("AUTHOR", pool);
+                        q.setVersionColumn("ID");
+                        SQLContainer c = new SQLContainer(q);
+                        for (int i = 0; i < 100; i++) {
+                            Object id = c.addItem();
+                            c.getContainerProperty(id, "FIRST_NAMES").setValue(
+                                    getRandonName());
+                            c.getContainerProperty(id, "LAST_NAME").setValue(
+                                    getRandonName());
+                        }
+                        c.commit();
+                        synchronized (MassInsertMemoryLeakTestApp.this) {
+                            proggress
+                                    .setValue(new Float((1.0f * cent) / cents));
+                            proggress.setCaption("" + 100 * cent
+                                    + " rows inserted");
+                        }
+                    }
+                } catch (SQLException e) {
+                    getMainWindow().showNotification(
+                            "SQLException while processing",
+                            e.getLocalizedMessage());
+                    e.printStackTrace();
+                }
+            }
+            synchronized (MassInsertMemoryLeakTestApp.this) {
+                proggress.setVisible(false);
+                proggress.setPollingInterval(0);
+                process.setEnabled(true);
+            }
+        }
+    }
+
+    private ComponentContainer buildLayout() {
+        VerticalLayout lo = new VerticalLayout();
+        lo.setSizeFull();
+        lo.addComponent(proggress);
+        lo.addComponent(process);
+        lo.setComponentAlignment(proggress, Alignment.BOTTOM_CENTER);
+        lo.setComponentAlignment(process, Alignment.TOP_CENTER);
+        lo.setSpacing(true);
+        proggress.setIndeterminate(false);
+        proggress.setVisible(false);
+        return lo;
+    }
+
+    private String getRandonName() {
+        final String[] tokens = new String[] { "sa", "len", "da", "vid", "ma",
+                "ry", "an", "na", "jo", "bri", "son", "mat", "e", "ric", "ge",
+                "eu", "han", "har", "ri", "ja", "lo" };
+        StringBuffer sb = new StringBuffer();
+        int len = (int) (Math.random() * 3 + 2);
+        while (len-- > 0) {
+            sb.append(tokens[(int) (Math.random() * tokens.length)]);
+        }
+        return Character.toUpperCase(sb.charAt(0)) + sb.toString().substring(1);
+    }
+
+    private JDBCConnectionPool getConnectionPool() {
+        SimpleJDBCConnectionPool pool = null;
+        try {
+            pool = new SimpleJDBCConnectionPool("com.mysql.jdbc.Driver",
+                    "jdbc:mysql://localhost:3306/sqlcontainer", "sqlcontainer",
+                    "sqlcontainer");
+        } catch (SQLException e) {
+            getMainWindow().showNotification("Error connecting to database");
+        }
+        return pool;
+    }
+
+}
\ No newline at end of file
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/AllTests.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/AllTests.java
new file mode 100644 (file)
index 0000000..709295d
--- /dev/null
@@ -0,0 +1,146 @@
+package com.vaadin.tests.server.container.sqlcontainer;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+import com.vaadin.data.util.query.generator.DefaultSQLGenerator;
+import com.vaadin.data.util.query.generator.MSSQLGenerator;
+import com.vaadin.data.util.query.generator.OracleGenerator;
+import com.vaadin.data.util.query.generator.SQLGenerator;
+import com.vaadin.tests.server.container.sqlcontainer.connection.J2EEConnectionPoolTest;
+import com.vaadin.tests.server.container.sqlcontainer.connection.SimpleJDBCConnectionPoolTest;
+import com.vaadin.tests.server.container.sqlcontainer.filters.BetweenTest;
+import com.vaadin.tests.server.container.sqlcontainer.filters.LikeTest;
+import com.vaadin.tests.server.container.sqlcontainer.generator.SQLGeneratorsTest;
+import com.vaadin.tests.server.container.sqlcontainer.query.FreeformQueryTest;
+import com.vaadin.tests.server.container.sqlcontainer.query.QueryBuilderTest;
+import com.vaadin.tests.server.container.sqlcontainer.query.TableQueryTest;
+
+@RunWith(Suite.class)
+@SuiteClasses({ SimpleJDBCConnectionPoolTest.class,
+        J2EEConnectionPoolTest.class, LikeTest.class,
+        QueryBuilderTest.class, FreeformQueryTest.class,
+        RowIdTest.class, SQLContainerTest.class,
+        SQLContainerTableQueryTest.class, ColumnPropertyTest.class,
+        TableQueryTest.class, SQLGeneratorsTest.class, UtilTest.class,
+        TicketTests.class, BetweenTest.class, ReadOnlyRowIdTest.class})
+public class AllTests {
+    /* Set the DB used for testing here! */
+    public enum DB {
+        HSQLDB, MYSQL, POSTGRESQL, MSSQL, ORACLE;
+    }
+
+    /* 0 = HSQLDB, 1 = MYSQL, 2 = POSTGRESQL, 3 = MSSQL, 4 = ORACLE */
+    public static final DB db = DB.HSQLDB;
+
+    /* Auto-increment column offset (HSQLDB = 0, MYSQL = 1, POSTGRES = 1) */
+    public static int offset;
+    /* Garbage table creation query (=three queries for oracle) */
+    public static String createGarbage;
+    public static String createGarbageSecond;
+    public static String createGarbageThird;
+    /* DB Drivers, urls, usernames and passwords */
+    public static String dbDriver;
+    public static String dbURL;
+    public static String dbUser;
+    public static String dbPwd;
+    /* People -test table creation statement(s) */
+    public static String peopleFirst;
+    public static String peopleSecond;
+    public static String peopleThird;
+    /* Versioned -test table createion statement(s) */
+    public static String[] versionStatements;
+    /* SQL Generator used during the testing */
+    public static SQLGenerator sqlGen;
+
+    /* Set DB-specific settings based on selected DB */
+    static {
+        sqlGen = new DefaultSQLGenerator();
+        switch (db) {
+        case HSQLDB:
+            offset = 0;
+            createGarbage = "create table garbage (id integer generated always as identity, type varchar(32), PRIMARY KEY(id))";
+            dbDriver = "org.hsqldb.jdbc.JDBCDriver";
+            dbURL = "jdbc:hsqldb:mem:sqlcontainer";
+            dbUser = "SA";
+            dbPwd = "";
+            peopleFirst = "create table people (id integer generated always as identity, name varchar(32), AGE INTEGER)";
+            peopleSecond = "alter table people add primary key (id)";
+            versionStatements = new String[] {
+                    "create table versioned (id integer generated always as identity, text varchar(255), version tinyint default 0)",
+                    "alter table versioned add primary key (id)" };
+            break;
+        case MYSQL:
+            offset = 1;
+            createGarbage = "create table GARBAGE (ID integer auto_increment, type varchar(32), PRIMARY KEY(ID))";
+            dbDriver = "com.mysql.jdbc.Driver";
+            dbURL = "jdbc:mysql:///sqlcontainer";
+            dbUser = "sqlcontainer";
+            dbPwd = "sqlcontainer";
+            peopleFirst = "create table PEOPLE (ID integer auto_increment not null, NAME varchar(32), AGE INTEGER, primary key(ID))";
+            peopleSecond = null;
+            versionStatements = new String[] {
+                    "create table VERSIONED (ID integer auto_increment not null, TEXT varchar(255), VERSION tinyint default 0, primary key(ID))",
+                    "CREATE TRIGGER upd_version BEFORE UPDATE ON VERSIONED"
+                            + " FOR EACH ROW SET NEW.VERSION = @VERSION+1" };
+            break;
+        case POSTGRESQL:
+            offset = 1;
+            createGarbage = "create table GARBAGE (\"ID\" serial PRIMARY KEY, \"TYPE\" varchar(32))";
+            dbDriver = "org.postgresql.Driver";
+            dbURL = "jdbc:postgresql://localhost:5432/test";
+            dbUser = "postgres";
+            dbPwd = "postgres";
+            peopleFirst = "create table PEOPLE (\"ID\" serial primary key, \"NAME\" VARCHAR(32), \"AGE\" INTEGER)";
+            peopleSecond = null;
+            versionStatements = new String[] {
+                    "create table VERSIONED (\"ID\" serial primary key, \"TEXT\" VARCHAR(255), \"VERSION\" INTEGER DEFAULT 0)",
+                    "CREATE OR REPLACE FUNCTION zz_row_version() RETURNS TRIGGER AS $$"
+                            + "BEGIN"
+                            + "   IF TG_OP = 'UPDATE'"
+                            + "       AND NEW.\"VERSION\" = old.\"VERSION\""
+                            + "       AND ROW(NEW.*) IS DISTINCT FROM ROW (old.*)"
+                            + "   THEN"
+                            + "       NEW.\"VERSION\" := NEW.\"VERSION\" + 1;"
+                            + "   END IF;" + "   RETURN NEW;" + "END;"
+                            + "$$ LANGUAGE plpgsql;",
+                    "CREATE TRIGGER \"mytable_modify_dt_tr\" BEFORE UPDATE"
+                            + "   ON VERSIONED FOR EACH ROW"
+                            + "   EXECUTE PROCEDURE \"public\".\"zz_row_version\"();" };
+            break;
+        case MSSQL:
+            offset = 1;
+            createGarbage = "create table GARBAGE (\"ID\" int identity(1,1) primary key, \"TYPE\" varchar(32))";
+            dbDriver = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
+            dbURL = "jdbc:sqlserver://localhost:1433;databaseName=tempdb;";
+            dbUser = "sa";
+            dbPwd = "sa";
+            peopleFirst = "create table PEOPLE (\"ID\" int identity(1,1) primary key, \"NAME\" VARCHAR(32), \"AGE\" INTEGER)";
+            peopleSecond = null;
+            versionStatements = new String[] { "create table VERSIONED (\"ID\" int identity(1,1) primary key, \"TEXT\" VARCHAR(255), \"VERSION\" rowversion not null)" };
+            sqlGen = new MSSQLGenerator();
+            break;
+        case ORACLE:
+            offset = 1;
+            createGarbage = "create table GARBAGE (\"ID\" integer primary key, \"TYPE\" varchar2(32))";
+            createGarbageSecond = "create sequence garbage_seq start with 1 increment by 1 nomaxvalue";
+            createGarbageThird = "create trigger garbage_trigger before insert on GARBAGE for each row begin select garbage_seq.nextval into :new.ID from dual; end;";
+            dbDriver = "oracle.jdbc.OracleDriver";
+            dbURL = "jdbc:oracle:thin:test/test@localhost:1521:XE";
+            dbUser = "test";
+            dbPwd = "test";
+            peopleFirst = "create table PEOPLE (\"ID\" integer primary key, \"NAME\" VARCHAR2(32), \"AGE\" INTEGER)";
+            peopleSecond = "create sequence people_seq start with 1 increment by 1 nomaxvalue";
+            peopleThird = "create trigger people_trigger before insert on PEOPLE for each row begin select people_seq.nextval into :new.ID from dual; end;";
+            versionStatements = new String[] {
+                    "create table VERSIONED (\"ID\" integer primary key, \"TEXT\" VARCHAR(255), \"VERSION\" INTEGER DEFAULT 0)",
+                    "create sequence versioned_seq start with 1 increment by 1 nomaxvalue",
+                    "create trigger versioned_trigger before insert on VERSIONED for each row begin select versioned_seq.nextval into :new.ID from dual; end;",
+                    "create sequence versioned_version start with 1 increment by 1 nomaxvalue",
+                    "create trigger versioned_version_trigger before insert or update on VERSIONED for each row begin select versioned_version.nextval into :new.VERSION from dual; end;" };
+            sqlGen = new OracleGenerator();
+            break;
+        }
+    }
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/ColumnPropertyTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/ColumnPropertyTest.java
new file mode 100644 (file)
index 0000000..d4331fa
--- /dev/null
@@ -0,0 +1,177 @@
+package com.vaadin.tests.server.container.sqlcontainer;
+
+import java.util.Arrays;
+
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.Property.ReadOnlyException;
+import com.vaadin.data.util.ColumnProperty;
+import com.vaadin.data.util.ColumnProperty.NotNullableException;
+import com.vaadin.data.util.RowId;
+import com.vaadin.data.util.RowItem;
+import com.vaadin.data.util.SQLContainer;
+
+public class ColumnPropertyTest {
+
+    @Test
+    public void constructor_legalParameters_shouldSucceed() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, true, true,
+                "Ville", String.class);
+        Assert.assertNotNull(cp);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructor_missingPropertyId_shouldFail() {
+        new ColumnProperty(null, false, true, true, "Ville", String.class);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructor_missingType_shouldFail() {
+        new ColumnProperty("NAME", false, true, true, "Ville", null);
+    }
+
+    @Test
+    public void getValue_defaultValue_returnsVille() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, true, true,
+                "Ville", String.class);
+        Assert.assertEquals("Ville", cp.getValue());
+    }
+
+    /*-
+     * TODO Removed test since currently the Vaadin test package structure
+     * does not allow testing protected methods. When it has been fixed
+     * then re-enable test.
+    @Test
+    public void setValue_readWriteNullable_returnsKalle() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, true, true,
+                "Ville", String.class);
+        SQLContainer container = EasyMock.createMock(SQLContainer.class);
+        RowItem owner = new RowItem(container, new RowId(new Object[] { 1 }),
+                Arrays.asList(cp));
+        container.itemChangeNotification(owner);
+        EasyMock.replay(container);
+        cp.setValue("Kalle");
+        Assert.assertEquals("Kalle", cp.getValue());
+        EasyMock.verify(container);
+    }
+    */
+
+    @Test(expected = ReadOnlyException.class)
+    public void setValue_readOnlyNullable_shouldFail() {
+        ColumnProperty cp = new ColumnProperty("NAME", true, true, true,
+                "Ville", String.class);
+        SQLContainer container = EasyMock.createMock(SQLContainer.class);
+        new RowItem(container, new RowId(new Object[] { 1 }), Arrays.asList(cp));
+        EasyMock.replay(container);
+        cp.setValue("Kalle");
+        EasyMock.verify(container);
+    }
+
+    /*-
+     * TODO Removed test since currently the Vaadin test package structure
+     * does not allow testing protected methods. When it has been fixed
+     * then re-enable test.
+    @Test
+    public void setValue_readWriteNullable_nullShouldWork() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, true, true,
+                "Ville", String.class);
+        SQLContainer container = EasyMock.createMock(SQLContainer.class);
+        RowItem owner = new RowItem(container, new RowId(new Object[] { 1 }),
+                Arrays.asList(cp));
+        container.itemChangeNotification(owner);
+        EasyMock.replay(container);
+        cp.setValue(null);
+        Assert.assertNull(cp.getValue());
+        EasyMock.verify(container);
+    }
+    
+
+    @Test(expected = NotNullableException.class)
+    public void setValue_readWriteNotNullable_nullShouldFail() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, true, false,
+                "Ville", String.class);
+        SQLContainer container = EasyMock.createMock(SQLContainer.class);
+        RowItem owner = new RowItem(container, new RowId(new Object[] { 1 }),
+                Arrays.asList(cp));
+        container.itemChangeNotification(owner);
+        EasyMock.replay(container);
+        cp.setValue(null);
+        Assert.assertNotNull(cp.getValue());
+        EasyMock.verify(container);
+    }
+    */
+
+    @Test
+    public void getType_normal_returnsStringClass() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, true, true,
+                "Ville", String.class);
+        Assert.assertSame(String.class, cp.getType());
+    }
+
+    @Test
+    public void isReadOnly_readWriteNullable_returnsTrue() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, true, true,
+                "Ville", String.class);
+        Assert.assertFalse(cp.isReadOnly());
+    }
+
+    @Test
+    public void isReadOnly_readOnlyNullable_returnsTrue() {
+        ColumnProperty cp = new ColumnProperty("NAME", true, true, true,
+                "Ville", String.class);
+        Assert.assertTrue(cp.isReadOnly());
+    }
+
+    @Test
+    public void setReadOnly_readOnlyChangeAllowed_shouldSucceed() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, true, true,
+                "Ville", String.class);
+        cp.setReadOnly(true);
+        Assert.assertTrue(cp.isReadOnly());
+    }
+
+    @Test
+    public void setReadOnly_readOnlyChangeDisallowed_shouldFail() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, false, true,
+                "Ville", String.class);
+        cp.setReadOnly(true);
+        Assert.assertFalse(cp.isReadOnly());
+    }
+
+    @Test
+    public void getPropertyId_normal_returnsNAME() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, false, true,
+                "Ville", String.class);
+        Assert.assertEquals("NAME", cp.getPropertyId());
+    }
+
+    /*-
+     * TODO Removed test since currently the Vaadin test package structure
+     * does not allow testing protected methods. When it has been fixed
+     * then re-enable test.
+    @Test
+    public void isModified_valueModified_returnsTrue() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, true, true,
+                "Ville", String.class);
+        SQLContainer container = EasyMock.createMock(SQLContainer.class);
+        RowItem owner = new RowItem(container, new RowId(new Object[] { 1 }),
+                Arrays.asList(cp));
+        container.itemChangeNotification(owner);
+        EasyMock.replay(container);
+        cp.setValue("Kalle");
+        Assert.assertEquals("Kalle", cp.getValue());
+        Assert.assertTrue(cp.isModified());
+        EasyMock.verify(container);
+    }
+    */
+
+    @Test
+    public void isModified_valueNotModified_returnsFalse() {
+        ColumnProperty cp = new ColumnProperty("NAME", false, false, true,
+                "Ville", String.class);
+        Assert.assertFalse(cp.isModified());
+    }
+
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/DataGenerator.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/DataGenerator.java
new file mode 100644 (file)
index 0000000..4029eb8
--- /dev/null
@@ -0,0 +1,132 @@
+package com.vaadin.tests.server.container.sqlcontainer;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.junit.Assert;
+
+import com.vaadin.data.util.connection.JDBCConnectionPool;
+import com.vaadin.tests.server.container.sqlcontainer.AllTests.DB;
+
+public class DataGenerator {
+
+    public static void addPeopleToDatabase(JDBCConnectionPool connectionPool)
+            throws SQLException {
+        Connection conn = connectionPool.reserveConnection();
+        Statement statement = conn.createStatement();
+        try {
+            statement.execute("drop table PEOPLE");
+            if (AllTests.db == DB.ORACLE) {
+                statement.execute("drop sequence people_seq");
+            }
+        } catch (SQLException e) {
+            // Will fail if table doesn't exist, which is OK.
+            conn.rollback();
+        }
+        statement.execute(AllTests.peopleFirst);
+        if (AllTests.peopleSecond != null) {
+            statement.execute(AllTests.peopleSecond);
+        }
+        if (AllTests.db == DB.ORACLE) {
+            statement.execute(AllTests.peopleThird);
+        }
+        if (AllTests.db == DB.MSSQL) {
+            statement.executeUpdate("insert into people values('Ville', '23')");
+            statement.executeUpdate("insert into people values('Kalle', '7')");
+            statement.executeUpdate("insert into people values('Pelle', '18')");
+            statement.executeUpdate("insert into people values('Börje', '64')");
+        } else {
+            statement
+                    .executeUpdate("insert into people values(default, 'Ville', '23')");
+            statement
+                    .executeUpdate("insert into people values(default, 'Kalle', '7')");
+            statement
+                    .executeUpdate("insert into people values(default, 'Pelle', '18')");
+            statement
+                    .executeUpdate("insert into people values(default, 'Börje', '64')");
+        }
+        statement.close();
+        statement = conn.createStatement();
+        ResultSet rs = statement.executeQuery("select * from PEOPLE");
+        Assert.assertTrue(rs.next());
+        statement.close();
+        conn.commit();
+        connectionPool.releaseConnection(conn);
+    }
+
+    public static void addFiveThousandPeople(JDBCConnectionPool connectionPool)
+            throws SQLException {
+        Connection conn = connectionPool.reserveConnection();
+        Statement statement = conn.createStatement();
+        for (int i = 4; i < 5000; i++) {
+            if (AllTests.db == DB.MSSQL) {
+                statement.executeUpdate("insert into people values('Person "
+                        + i + "', '" + i % 99 + "')");
+            } else {
+                statement
+                        .executeUpdate("insert into people values(default, 'Person "
+                                + i + "', '" + i % 99 + "')");
+            }
+        }
+        statement.close();
+        conn.commit();
+        connectionPool.releaseConnection(conn);
+    }
+
+    public static void addVersionedData(JDBCConnectionPool connectionPool)
+            throws SQLException {
+        Connection conn = connectionPool.reserveConnection();
+        Statement statement = conn.createStatement();
+        try {
+            statement.execute("DROP TABLE VERSIONED");
+            if (AllTests.db == DB.ORACLE) {
+                statement.execute("drop sequence versioned_seq");
+                statement.execute("drop sequence versioned_version");
+            }
+        } catch (SQLException e) {
+            // Will fail if table doesn't exist, which is OK.
+            conn.rollback();
+        }
+        for (String stmtString : AllTests.versionStatements) {
+            statement.execute(stmtString);
+        }
+        if (AllTests.db == DB.MSSQL) {
+            statement
+                    .executeUpdate("insert into VERSIONED values('Junk', default)");
+        } else {
+            statement
+                    .executeUpdate("insert into VERSIONED values(default, 'Junk', default)");
+        }
+        statement.close();
+        statement = conn.createStatement();
+        ResultSet rs = statement.executeQuery("select * from VERSIONED");
+        Assert.assertTrue(rs.next());
+        statement.close();
+        conn.commit();
+        connectionPool.releaseConnection(conn);
+    }
+
+    public static void createGarbage(JDBCConnectionPool connectionPool)
+            throws SQLException {
+        Connection conn = connectionPool.reserveConnection();
+        Statement statement = conn.createStatement();
+        try {
+            statement.execute("drop table GARBAGE");
+            if (AllTests.db == DB.ORACLE) {
+                statement.execute("drop sequence garbage_seq");
+            }
+        } catch (SQLException e) {
+            // Will fail if table doesn't exist, which is OK.
+            conn.rollback();
+        }
+        statement.execute(AllTests.createGarbage);
+        if (AllTests.db == DB.ORACLE) {
+            statement.execute(AllTests.createGarbageSecond);
+            statement.execute(AllTests.createGarbageThird);
+        }
+        conn.commit();
+        connectionPool.releaseConnection(conn);
+    }
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/FreeformQueryUtil.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/FreeformQueryUtil.java
new file mode 100644 (file)
index 0000000..a0170ba
--- /dev/null
@@ -0,0 +1,66 @@
+package com.vaadin.tests.server.container.sqlcontainer;
+
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.query.generator.StatementHelper;
+import com.vaadin.data.util.query.generator.filter.QueryBuilder;
+import com.vaadin.tests.server.container.sqlcontainer.AllTests.DB;
+
+public class FreeformQueryUtil {
+
+    public static StatementHelper getQueryWithFilters(List<Filter> filters,
+            int offset, int limit) {
+        StatementHelper sh = new StatementHelper();
+        if (AllTests.db == DB.MSSQL) {
+            if (limit > 1) {
+                offset++;
+                limit--;
+            }
+            StringBuilder query = new StringBuilder();
+            query.append("SELECT * FROM (SELECT row_number() OVER (");
+            query.append("ORDER BY \"ID\" ASC");
+            query.append(") AS rownum, * FROM \"PEOPLE\"");
+
+            if (!filters.isEmpty()) {
+                query.append(QueryBuilder.getWhereStringForFilters(
+                        filters, sh));
+            }
+            query.append(") AS a WHERE a.rownum BETWEEN ").append(offset)
+                    .append(" AND ").append(Integer.toString(offset + limit));
+            sh.setQueryString(query.toString());
+            return sh;
+        } else if (AllTests.db == DB.ORACLE) {
+            if (limit > 1) {
+                offset++;
+                limit--;
+            }
+            StringBuilder query = new StringBuilder();
+            query.append("SELECT * FROM (SELECT x.*, ROWNUM AS "
+                    + "\"rownum\" FROM (SELECT * FROM \"PEOPLE\"");
+            if (!filters.isEmpty()) {
+                query.append(QueryBuilder.getWhereStringForFilters(
+                        filters, sh));
+            }
+            query.append(") x) WHERE \"rownum\" BETWEEN ? AND ?");
+            sh.addParameterValue(offset);
+            sh.addParameterValue(offset + limit);
+            sh.setQueryString(query.toString());
+            return sh;
+        } else {
+            StringBuilder query = new StringBuilder("SELECT * FROM people");
+            if (!filters.isEmpty()) {
+                query.append(QueryBuilder.getWhereStringForFilters(
+                        filters, sh));
+            }
+            if (limit != 0 || offset != 0) {
+                query.append(" LIMIT ? OFFSET ?");
+                sh.addParameterValue(limit);
+                sh.addParameterValue(offset);
+            }
+            sh.setQueryString(query.toString());
+            return sh;
+        }
+    }
+
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/ReadOnlyRowIdTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/ReadOnlyRowIdTest.java
new file mode 100644 (file)
index 0000000..aa4e144
--- /dev/null
@@ -0,0 +1,50 @@
+package com.vaadin.tests.server.container.sqlcontainer;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+
+import com.vaadin.data.util.ReadOnlyRowId;
+
+public class ReadOnlyRowIdTest {
+
+    @Test
+    public void getRowNum_shouldReturnRowNumGivenInConstructor() {
+        int rowNum = 1337;
+        ReadOnlyRowId rid = new ReadOnlyRowId(rowNum);
+        Assert.assertEquals(rowNum, rid.getRowNum());
+    }
+
+    @Test
+    public void hashCode_shouldBeEqualToHashCodeOfRowNum() {
+        int rowNum = 1337;
+        ReadOnlyRowId rid = new ReadOnlyRowId(rowNum);
+        Assert.assertEquals(Integer.valueOf(rowNum).hashCode(), rid.hashCode());
+    }
+
+    @Test
+    public void equals_compareWithNull_shouldBeFalse() {
+        ReadOnlyRowId rid = new ReadOnlyRowId(1337);
+        Assert.assertFalse(rid.equals(null));
+    }
+
+    @Test
+    public void equals_compareWithSameInstance_shouldBeTrue() {
+        ReadOnlyRowId rid = new ReadOnlyRowId(1337);
+        ReadOnlyRowId rid2 = rid;
+        Assert.assertTrue(rid.equals(rid2));
+    }
+
+    @Test
+    public void equals_compareWithOtherType_shouldBeFalse() {
+        ReadOnlyRowId rid = new ReadOnlyRowId(1337);
+        Assert.assertFalse(rid.equals(new Object()));
+    }
+
+    @Test
+    public void equals_compareWithOtherRowId_shouldBeFalse() {
+        ReadOnlyRowId rid = new ReadOnlyRowId(1337);
+        ReadOnlyRowId rid2 = new ReadOnlyRowId(42);
+        Assert.assertFalse(rid.equals(rid2));
+    }
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/RowIdTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/RowIdTest.java
new file mode 100644 (file)
index 0000000..e619f20
--- /dev/null
@@ -0,0 +1,55 @@
+package com.vaadin.tests.server.container.sqlcontainer;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.util.RowId;
+
+public class RowIdTest {
+
+    @Test
+    public void constructor_withArrayOfPrimaryKeyColumns_shouldSucceed() {
+        RowId id = new RowId(new Object[] { "id", "name" });
+        Assert.assertArrayEquals(new Object[] { "id", "name" }, id.getId());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructor_withNullParameter_shouldFail() {
+        new RowId(null);
+    }
+
+    @Test
+    public void hashCode_samePrimaryKeys_sameResult() {
+        RowId id = new RowId(new Object[] { "id", "name" });
+        RowId id2 = new RowId(new Object[] { "id", "name" });
+        Assert.assertEquals(id.hashCode(), id2.hashCode());
+    }
+
+    @Test
+    public void hashCode_differentPrimaryKeys_differentResult() {
+        RowId id = new RowId(new Object[] { "id", "name" });
+        RowId id2 = new RowId(new Object[] { "id" });
+        Assert.assertFalse(id.hashCode() == id2.hashCode());
+    }
+
+    @Test
+    public void equals_samePrimaryKeys_returnsTrue() {
+        RowId id = new RowId(new Object[] { "id", "name" });
+        RowId id2 = new RowId(new Object[] { "id", "name" });
+        Assert.assertEquals(id, id2);
+    }
+
+    @Test
+    public void equals_differentPrimaryKeys_returnsFalse() {
+        RowId id = new RowId(new Object[] { "id", "name" });
+        RowId id2 = new RowId(new Object[] { "id" });
+        Assert.assertFalse(id.equals(id2.hashCode()));
+    }
+
+    @Test
+    public void equals_differentDataType_returnsFalse() {
+        RowId id = new RowId(new Object[] { "id", "name" });
+        Assert.assertFalse(id.equals("Tudiluu"));
+        Assert.assertFalse(id.equals(new Integer(1337)));
+    }
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/SQLContainerTableQueryTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/SQLContainerTableQueryTest.java
new file mode 100644 (file)
index 0000000..9c42906
--- /dev/null
@@ -0,0 +1,1501 @@
+package com.vaadin.tests.server.container.sqlcontainer;\r
+\r
+import java.math.BigDecimal;\r
+import java.sql.Connection;\r
+import java.sql.SQLException;\r
+import java.sql.Statement;\r
+import java.util.ArrayList;\r
+import java.util.Collection;\r
+import java.util.List;\r
+\r
+import org.easymock.EasyMock;\r
+import org.junit.After;\r
+import org.junit.Assert;\r
+import org.junit.Before;\r
+import org.junit.Test;\r
+\r
+import com.vaadin.data.Container.ItemSetChangeEvent;\r
+import com.vaadin.data.Container.ItemSetChangeListener;\r
+import com.vaadin.data.Item;\r
+import com.vaadin.data.util.RowId;\r
+import com.vaadin.data.util.RowItem;\r
+import com.vaadin.data.util.SQLContainer;\r
+import com.vaadin.data.util.TemporaryRowId;\r
+import com.vaadin.data.util.connection.JDBCConnectionPool;\r
+import com.vaadin.data.util.connection.SimpleJDBCConnectionPool;\r
+import com.vaadin.data.util.filter.Like;\r
+import com.vaadin.data.util.query.OrderBy;\r
+import com.vaadin.data.util.query.TableQuery;\r
+import com.vaadin.terminal.gwt.client.Util;\r
+import com.vaadin.tests.server.container.sqlcontainer.AllTests.DB;\r
+\r
+public class SQLContainerTableQueryTest {\r
+\r
+    private static final int offset = AllTests.offset;\r
+    private static final String createGarbage = AllTests.createGarbage;\r
+    private JDBCConnectionPool connectionPool;\r
+\r
+    @Before\r
+    public void setUp() throws SQLException {\r
+\r
+        try {\r
+            connectionPool = new SimpleJDBCConnectionPool(AllTests.dbDriver,\r
+                    AllTests.dbURL, AllTests.dbUser, AllTests.dbPwd, 2, 2);\r
+        } catch (SQLException e) {\r
+            e.printStackTrace();\r
+            Assert.fail(e.getMessage());\r
+        }\r
+\r
+        DataGenerator.addPeopleToDatabase(connectionPool);\r
+    }\r
+\r
+    @After\r
+    public void tearDown() {\r
+        if (connectionPool != null) {\r
+            connectionPool.destroy();\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void constructor_withTableQuery_shouldSucceed() throws SQLException {\r
+        new SQLContainer(new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen));\r
+    }\r
+\r
+    @Test\r
+    public void containsId_withTableQueryAndExistingId_returnsTrue()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertTrue(container.containsId(new RowId(\r
+                new Object[] { 1 + offset })));\r
+    }\r
+\r
+    @Test\r
+    public void containsId_withTableQueryAndNonexistingId_returnsFalse()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertFalse(container.containsId(new RowId(\r
+                new Object[] { 1337 + offset })));\r
+    }\r
+\r
+    @Test\r
+    public void getContainerProperty_tableExistingItemIdAndPropertyId_returnsProperty()\r
+            throws SQLException {\r
+        TableQuery t = new TableQuery("people", connectionPool, AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(t);\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertEquals(\r
+                    "Ville",\r
+                    container\r
+                            .getContainerProperty(\r
+                                    new RowId(new Object[] { new BigDecimal(\r
+                                            0 + offset) }), "NAME").getValue());\r
+        } else {\r
+            Assert.assertEquals(\r
+                    "Ville",\r
+                    container.getContainerProperty(\r
+                            new RowId(new Object[] { 0 + offset }), "NAME")\r
+                            .getValue());\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void getContainerProperty_tableExistingItemIdAndNonexistingPropertyId_returnsNull()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertNull(container.getContainerProperty(new RowId(\r
+                new Object[] { 1 + offset }), "asdf"));\r
+    }\r
+\r
+    @Test\r
+    public void getContainerProperty_tableNonexistingItemId_returnsNull()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertNull(container.getContainerProperty(new RowId(\r
+                new Object[] { 1337 + offset }), "NAME"));\r
+    }\r
+\r
+    @Test\r
+    public void getContainerPropertyIds_table_returnsIDAndNAME()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Collection<?> propertyIds = container.getContainerPropertyIds();\r
+        Assert.assertEquals(3, propertyIds.size());\r
+        Assert.assertArrayEquals(new String[] { "ID", "NAME", "AGE" },\r
+                propertyIds.toArray());\r
+    }\r
+\r
+    @Test\r
+    public void getItem_tableExistingItemId_returnsItem() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Item item;\r
+        if (AllTests.db == DB.ORACLE) {\r
+            item = container.getItem(new RowId(new Object[] { new BigDecimal(\r
+                    0 + offset) }));\r
+        } else {\r
+            item = container.getItem(new RowId(new Object[] { 0 + offset }));\r
+        }\r
+        Assert.assertNotNull(item);\r
+        Assert.assertEquals("Ville", item.getItemProperty("NAME").getValue());\r
+    }\r
+\r
+    @Test\r
+    public void getItem_table5000RowsWithParameter1337_returnsItemWithId1337()\r
+            throws SQLException {\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+\r
+        Item item;\r
+        if (AllTests.db == DB.ORACLE) {\r
+            item = container.getItem(new RowId(new Object[] { new BigDecimal(\r
+                    1337 + offset) }));\r
+            Assert.assertNotNull(item);\r
+            Assert.assertEquals(new BigDecimal(1337 + offset), item\r
+                    .getItemProperty("ID").getValue());\r
+        } else {\r
+            item = container.getItem(new RowId(new Object[] { 1337 + offset }));\r
+            Assert.assertNotNull(item);\r
+            Assert.assertEquals(1337 + offset, item.getItemProperty("ID")\r
+                    .getValue());\r
+        }\r
+        Assert.assertEquals("Person 1337", item.getItemProperty("NAME")\r
+                .getValue());\r
+    }\r
+\r
+    @Test\r
+    public void getItemIds_table_returnsItemIdsWithKeys0through3()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Collection<?> itemIds = container.getItemIds();\r
+        Assert.assertEquals(4, itemIds.size());\r
+        RowId zero = new RowId(new Object[] { 0 + offset });\r
+        RowId one = new RowId(new Object[] { 1 + offset });\r
+        RowId two = new RowId(new Object[] { 2 + offset });\r
+        RowId three = new RowId(new Object[] { 3 + offset });\r
+        if (AllTests.db == DB.ORACLE) {\r
+            String[] correct = new String[] { "1", "2", "3", "4" };\r
+            List<String> oracle = new ArrayList<String>();\r
+            for (Object o : itemIds) {\r
+                oracle.add(o.toString());\r
+            }\r
+            Assert.assertArrayEquals(correct, oracle.toArray());\r
+        } else {\r
+            Assert.assertArrayEquals(new Object[] { zero, one, two, three },\r
+                    itemIds.toArray());\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void getType_tableNAMEPropertyId_returnsString() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertEquals(String.class, container.getType("NAME"));\r
+    }\r
+\r
+    @Test\r
+    public void getType_tableIDPropertyId_returnsInteger() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertEquals(BigDecimal.class, container.getType("ID"));\r
+        } else {\r
+            Assert.assertEquals(Integer.class, container.getType("ID"));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void getType_tableNonexistingPropertyId_returnsNull()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertNull(container.getType("asdf"));\r
+    }\r
+\r
+    @Test\r
+    public void size_table_returnsFour() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertEquals(4, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void size_tableOneAddedItem_returnsFive() throws SQLException {\r
+        Connection conn = connectionPool.reserveConnection();\r
+        Statement statement = conn.createStatement();\r
+        if (AllTests.db == DB.MSSQL) {\r
+            statement.executeUpdate("insert into people values('Bengt', 30)");\r
+        } else {\r
+            statement\r
+                    .executeUpdate("insert into people values(default, 'Bengt', 30)");\r
+        }\r
+        statement.close();\r
+        conn.commit();\r
+        connectionPool.releaseConnection(conn);\r
+\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertEquals(5, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void indexOfId_tableWithParameterThree_returnsThree()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertEquals(3, container.indexOfId(new RowId(\r
+                    new Object[] { new BigDecimal(3 + offset) })));\r
+        } else {\r
+            Assert.assertEquals(3,\r
+                    container.indexOfId(new RowId(new Object[] { 3 + offset })));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void indexOfId_table5000RowsWithParameter1337_returns1337()\r
+            throws SQLException {\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+        TableQuery q = new TableQuery("people", connectionPool, AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(q);\r
+        if (AllTests.db == DB.ORACLE) {\r
+            container.getItem(new RowId(new Object[] { new BigDecimal(\r
+                    1337 + offset) }));\r
+            Assert.assertEquals(1337, container.indexOfId(new RowId(\r
+                    new Object[] { new BigDecimal(1337 + offset) })));\r
+        } else {\r
+            container.getItem(new RowId(new Object[] { 1337 + offset }));\r
+            Assert.assertEquals(1337, container.indexOfId(new RowId(\r
+                    new Object[] { 1337 + offset })));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void getIdByIndex_table5000rowsIndex1337_returnsRowId1337()\r
+            throws SQLException {\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object itemId = container.getIdByIndex(1337);\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertEquals(\r
+                    new RowId(new Object[] { 1337 + offset }).toString(),\r
+                    itemId.toString());\r
+        } else {\r
+            Assert.assertEquals(new RowId(new Object[] { 1337 + offset }),\r
+                    itemId);\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void getIdByIndex_tableWithPaging5000rowsIndex1337_returnsRowId1337()\r
+            throws SQLException {\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        Object itemId = container.getIdByIndex(1337);\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertEquals(\r
+                    new RowId(new Object[] { 1337 + offset }).toString(),\r
+                    itemId.toString());\r
+        } else {\r
+            Assert.assertEquals(new RowId(new Object[] { 1337 + offset }),\r
+                    itemId);\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void nextItemId_tableCurrentItem1337_returnsItem1338()\r
+            throws SQLException {\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object itemId = container.getIdByIndex(1337);\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertEquals(\r
+                    new RowId(new Object[] { 1338 + offset }).toString(),\r
+                    container.nextItemId(itemId).toString());\r
+        } else {\r
+            Assert.assertEquals(new RowId(new Object[] { 1338 + offset }),\r
+                    container.nextItemId(itemId));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void prevItemId_tableCurrentItem1337_returns1336()\r
+            throws SQLException {\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object itemId = container.getIdByIndex(1337);\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertEquals(\r
+                    new RowId(new Object[] { 1336 + offset }).toString(),\r
+                    container.prevItemId(itemId).toString());\r
+        } else {\r
+            Assert.assertEquals(new RowId(new Object[] { 1336 + offset }),\r
+                    container.prevItemId(itemId));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void firstItemId_table_returnsItemId0() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertEquals(\r
+                    new RowId(new Object[] { 0 + offset }).toString(),\r
+                    container.firstItemId().toString());\r
+        } else {\r
+            Assert.assertEquals(new RowId(new Object[] { 0 + offset }),\r
+                    container.firstItemId());\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void lastItemId_table5000Rows_returnsItemId4999()\r
+            throws SQLException {\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertEquals(\r
+                    new RowId(new Object[] { 4999 + offset }).toString(),\r
+                    container.lastItemId().toString());\r
+        } else {\r
+            Assert.assertEquals(new RowId(new Object[] { 4999 + offset }),\r
+                    container.lastItemId());\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void isFirstId_tableActualFirstId_returnsTrue() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertTrue(container.isFirstId(new RowId(\r
+                    new Object[] { new BigDecimal(0 + offset) })));\r
+        } else {\r
+            Assert.assertTrue(container.isFirstId(new RowId(\r
+                    new Object[] { 0 + offset })));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void isFirstId_tableSecondId_returnsFalse() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertFalse(container.isFirstId(new RowId(\r
+                    new Object[] { new BigDecimal(1 + offset) })));\r
+        } else {\r
+            Assert.assertFalse(container.isFirstId(new RowId(\r
+                    new Object[] { 1 + offset })));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void isLastId_tableSecondId_returnsFalse() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertFalse(container.isLastId(new RowId(\r
+                    new Object[] { new BigDecimal(1 + offset) })));\r
+        } else {\r
+            Assert.assertFalse(container.isLastId(new RowId(\r
+                    new Object[] { 1 + offset })));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void isLastId_tableLastId_returnsTrue() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertTrue(container.isLastId(new RowId(\r
+                    new Object[] { new BigDecimal(3 + offset) })));\r
+        } else {\r
+            Assert.assertTrue(container.isLastId(new RowId(\r
+                    new Object[] { 3 + offset })));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void isLastId_table5000RowsLastId_returnsTrue() throws SQLException {\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        if (AllTests.db == DB.ORACLE) {\r
+            Assert.assertTrue(container.isLastId(new RowId(\r
+                    new Object[] { new BigDecimal(4999 + offset) })));\r
+        } else {\r
+            Assert.assertTrue(container.isLastId(new RowId(\r
+                    new Object[] { 4999 + offset })));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void allIdsFound_table5000RowsLastId_shouldSucceed()\r
+            throws SQLException {\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        for (int i = 0; i < 5000; i++) {\r
+            Assert.assertTrue(container.containsId(container.getIdByIndex(i)));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void allIdsFound_table5000RowsLastId_autoCommit_shouldSucceed()\r
+            throws SQLException {\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.setAutoCommit(true);\r
+        for (int i = 0; i < 5000; i++) {\r
+            Assert.assertTrue(container.containsId(container.getIdByIndex(i)));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void refresh_table_sizeShouldUpdate() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertEquals(4, container.size());\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+        container.refresh();\r
+        Assert.assertEquals(5000, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void refresh_tableWithoutCallingRefresh_sizeShouldNotUpdate()\r
+            throws SQLException {\r
+        // Yeah, this is a weird one. We're testing that the size doesn't update\r
+        // after adding lots of items unless we call refresh inbetween. This to\r
+        // make sure that the refresh method actually refreshes stuff and isn't\r
+        // a NOP.\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertEquals(4, container.size());\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+        Assert.assertEquals(4, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void setAutoCommit_table_shouldSucceed() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.setAutoCommit(true);\r
+        Assert.assertTrue(container.isAutoCommit());\r
+        container.setAutoCommit(false);\r
+        Assert.assertFalse(container.isAutoCommit());\r
+    }\r
+\r
+    @Test\r
+    public void getPageLength_table_returnsDefault100() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertEquals(100, container.getPageLength());\r
+    }\r
+\r
+    @Test\r
+    public void setPageLength_table_shouldSucceed() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.setPageLength(20);\r
+        Assert.assertEquals(20, container.getPageLength());\r
+        container.setPageLength(200);\r
+        Assert.assertEquals(200, container.getPageLength());\r
+    }\r
+\r
+    @Test(expected = UnsupportedOperationException.class)\r
+    public void addContainerProperty_normal_isUnsupported() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.addContainerProperty("asdf", String.class, "");\r
+    }\r
+\r
+    @Test(expected = UnsupportedOperationException.class)\r
+    public void removeContainerProperty_normal_isUnsupported()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.removeContainerProperty("asdf");\r
+    }\r
+\r
+    @Test(expected = UnsupportedOperationException.class)\r
+    public void addItemObject_normal_isUnsupported() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.addItem("asdf");\r
+    }\r
+\r
+    @Test(expected = UnsupportedOperationException.class)\r
+    public void addItemAfterObjectObject_normal_isUnsupported()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.addItemAfter("asdf", "foo");\r
+    }\r
+\r
+    @Test(expected = UnsupportedOperationException.class)\r
+    public void addItemAtIntObject_normal_isUnsupported() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.addItemAt(2, "asdf");\r
+    }\r
+\r
+    @Test(expected = UnsupportedOperationException.class)\r
+    public void addItemAtInt_normal_isUnsupported() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.addItemAt(2);\r
+    }\r
+\r
+    @Test(expected = UnsupportedOperationException.class)\r
+    public void addItemAfterObject_normal_isUnsupported() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.addItemAfter("asdf");\r
+    }\r
+\r
+    @Test\r
+    public void addItem_tableAddOneNewItem_returnsItemId() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object itemId = container.addItem();\r
+        Assert.assertNotNull(itemId);\r
+    }\r
+\r
+    @Test\r
+    public void addItem_tableAddOneNewItem_autoCommit_returnsFinalItemId()\r
+            throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        container.setAutoCommit(true);\r
+        Object itemId = container.addItem();\r
+        Assert.assertNotNull(itemId);\r
+        Assert.assertTrue(itemId instanceof RowId);\r
+        Assert.assertFalse(itemId instanceof TemporaryRowId);\r
+    }\r
+\r
+    @Test\r
+    public void addItem_tableAddOneNewItem_autoCommit_sizeIsIncreased()\r
+            throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        container.setAutoCommit(true);\r
+        int originalSize = container.size();\r
+        container.addItem();\r
+        Assert.assertEquals(originalSize + 1, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void addItem_tableAddOneNewItem_shouldChangeSize()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        int size = container.size();\r
+        container.addItem();\r
+        Assert.assertEquals(size + 1, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void addItem_tableAddTwoNewItems_shouldChangeSize()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        int size = container.size();\r
+        Object id1 = container.addItem();\r
+        Object id2 = container.addItem();\r
+        Assert.assertEquals(size + 2, container.size());\r
+        Assert.assertNotSame(id1, id2);\r
+        Assert.assertFalse(id1.equals(id2));\r
+    }\r
+\r
+    @Test\r
+    public void nextItemId_tableNewlyAddedItem_returnsNewlyAdded()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object lastId = container.lastItemId();\r
+        Object id = container.addItem();\r
+        Assert.assertEquals(id, container.nextItemId(lastId));\r
+    }\r
+\r
+    @Test\r
+    public void lastItemId_tableNewlyAddedItem_returnsNewlyAdded()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object lastId = container.lastItemId();\r
+        Object id = container.addItem();\r
+        Assert.assertEquals(id, container.lastItemId());\r
+        Assert.assertNotSame(lastId, container.lastItemId());\r
+    }\r
+\r
+    @Test\r
+    public void indexOfId_tableNewlyAddedItem_returnsFour() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Assert.assertEquals(4, container.indexOfId(id));\r
+    }\r
+\r
+    @Test\r
+    public void getItem_tableNewlyAddedItem_returnsNewlyAdded()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Assert.assertNotNull(container.getItem(id));\r
+    }\r
+\r
+    @Test\r
+    public void getItemIds_tableNewlyAddedItem_containsNewlyAdded()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Assert.assertTrue(container.getItemIds().contains(id));\r
+    }\r
+\r
+    @Test\r
+    public void getContainerProperty_tableNewlyAddedItem_returnsPropertyOfNewlyAddedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Item item = container.getItem(id);\r
+        item.getItemProperty("NAME").setValue("asdf");\r
+        Assert.assertEquals("asdf", container.getContainerProperty(id, "NAME")\r
+                .getValue());\r
+    }\r
+\r
+    @Test\r
+    public void containsId_tableNewlyAddedItem_returnsTrue()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Assert.assertTrue(container.containsId(id));\r
+    }\r
+\r
+    @Test\r
+    public void prevItemId_tableTwoNewlyAddedItems_returnsFirstAddedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id1 = container.addItem();\r
+        Object id2 = container.addItem();\r
+        Assert.assertEquals(id1, container.prevItemId(id2));\r
+    }\r
+\r
+    @Test\r
+    public void firstItemId_tableEmptyResultSet_returnsFirstAddedItem()\r
+            throws SQLException {\r
+        DataGenerator.createGarbage(connectionPool);\r
+        SQLContainer container = new SQLContainer(new TableQuery("garbage",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Assert.assertSame(id, container.firstItemId());\r
+    }\r
+\r
+    @Test\r
+    public void isFirstId_tableEmptyResultSet_returnsFirstAddedItem()\r
+            throws SQLException {\r
+        DataGenerator.createGarbage(connectionPool);\r
+        SQLContainer container = new SQLContainer(new TableQuery("garbage",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Assert.assertTrue(container.isFirstId(id));\r
+    }\r
+\r
+    @Test\r
+    public void isLastId_tableOneItemAdded_returnsTrueForAddedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Assert.assertTrue(container.isLastId(id));\r
+    }\r
+\r
+    @Test\r
+    public void isLastId_tableTwoItemsAdded_returnsTrueForLastAddedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.addItem();\r
+        Object id2 = container.addItem();\r
+        Assert.assertTrue(container.isLastId(id2));\r
+    }\r
+\r
+    @Test\r
+    public void getIdByIndex_tableOneItemAddedLastIndexInContainer_returnsAddedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Assert.assertEquals(id, container.getIdByIndex(container.size() - 1));\r
+    }\r
+\r
+    @Test\r
+    public void removeItem_tableNoAddedItems_removesItemFromContainer()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        int size = container.size();\r
+        Object id = container.firstItemId();\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertNotSame(id, container.firstItemId());\r
+        Assert.assertEquals(size - 1, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void containsId_tableRemovedItem_returnsFalse() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.firstItemId();\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertFalse(container.containsId(id));\r
+    }\r
+\r
+    @Test\r
+    public void removeItem_tableOneAddedItem_removesTheAddedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        int size = container.size();\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertFalse(container.containsId(id));\r
+        Assert.assertEquals(size - 1, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void getItem_tableItemRemoved_returnsNull() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.firstItemId();\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertNull(container.getItem(id));\r
+    }\r
+\r
+    @Test\r
+    public void getItem_tableAddedItemRemoved_returnsNull() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Assert.assertNotNull(container.getItem(id));\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertNull(container.getItem(id));\r
+    }\r
+\r
+    @Test\r
+    public void getItemIds_tableItemRemoved_shouldNotContainRemovedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.firstItemId();\r
+        Assert.assertTrue(container.getItemIds().contains(id));\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertFalse(container.getItemIds().contains(id));\r
+    }\r
+\r
+    @Test\r
+    public void getItemIds_tableAddedItemRemoved_shouldNotContainRemovedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Assert.assertTrue(container.getItemIds().contains(id));\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertFalse(container.getItemIds().contains(id));\r
+    }\r
+\r
+    @Test\r
+    public void containsId_tableItemRemoved_returnsFalse() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.firstItemId();\r
+        Assert.assertTrue(container.containsId(id));\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertFalse(container.containsId(id));\r
+    }\r
+\r
+    @Test\r
+    public void containsId_tableAddedItemRemoved_returnsFalse()\r
+            throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        Object id = container.addItem();\r
+        Assert.assertTrue(container.containsId(id));\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertFalse(container.containsId(id));\r
+    }\r
+\r
+    @Test\r
+    public void nextItemId_tableItemRemoved_skipsRemovedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object first = container.getIdByIndex(0);\r
+        Object second = container.getIdByIndex(1);\r
+        Object third = container.getIdByIndex(2);\r
+        Assert.assertTrue(container.removeItem(second));\r
+        Assert.assertEquals(third, container.nextItemId(first));\r
+    }\r
+\r
+    @Test\r
+    public void nextItemId_tableAddedItemRemoved_skipsRemovedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object first = container.lastItemId();\r
+        Object second = container.addItem();\r
+        Object third = container.addItem();\r
+        Assert.assertTrue(container.removeItem(second));\r
+        Assert.assertEquals(third, container.nextItemId(first));\r
+    }\r
+\r
+    @Test\r
+    public void prevItemId_tableItemRemoved_skipsRemovedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object first = container.getIdByIndex(0);\r
+        Object second = container.getIdByIndex(1);\r
+        Object third = container.getIdByIndex(2);\r
+        Assert.assertTrue(container.removeItem(second));\r
+        Assert.assertEquals(first, container.prevItemId(third));\r
+    }\r
+\r
+    @Test\r
+    public void prevItemId_tableAddedItemRemoved_skipsRemovedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object first = container.lastItemId();\r
+        Object second = container.addItem();\r
+        Object third = container.addItem();\r
+        Assert.assertTrue(container.removeItem(second));\r
+        Assert.assertEquals(first, container.prevItemId(third));\r
+    }\r
+\r
+    @Test\r
+    public void firstItemId_tableFirstItemRemoved_resultChanges()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object first = container.firstItemId();\r
+        Assert.assertTrue(container.removeItem(first));\r
+        Assert.assertNotSame(first, container.firstItemId());\r
+    }\r
+\r
+    @Test\r
+    public void firstItemId_tableNewlyAddedFirstItemRemoved_resultChanges()\r
+            throws SQLException {\r
+        DataGenerator.createGarbage(connectionPool);\r
+        SQLContainer container = new SQLContainer(new TableQuery("garbage",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object first = container.addItem();\r
+        Object second = container.addItem();\r
+        Assert.assertSame(first, container.firstItemId());\r
+        Assert.assertTrue(container.removeItem(first));\r
+        Assert.assertSame(second, container.firstItemId());\r
+    }\r
+\r
+    @Test\r
+    public void lastItemId_tableLastItemRemoved_resultChanges()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object last = container.lastItemId();\r
+        Assert.assertTrue(container.removeItem(last));\r
+        Assert.assertNotSame(last, container.lastItemId());\r
+    }\r
+\r
+    @Test\r
+    public void lastItemId_tableAddedLastItemRemoved_resultChanges()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object last = container.addItem();\r
+        Assert.assertSame(last, container.lastItemId());\r
+        Assert.assertTrue(container.removeItem(last));\r
+        Assert.assertNotSame(last, container.lastItemId());\r
+    }\r
+\r
+    @Test\r
+    public void isFirstId_tableFirstItemRemoved_returnsFalse()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object first = container.firstItemId();\r
+        Assert.assertTrue(container.removeItem(first));\r
+        Assert.assertFalse(container.isFirstId(first));\r
+    }\r
+\r
+    @Test\r
+    public void isFirstId_tableAddedFirstItemRemoved_returnsFalse()\r
+            throws SQLException {\r
+        DataGenerator.createGarbage(connectionPool);\r
+        SQLContainer container = new SQLContainer(new TableQuery("garbage",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object first = container.addItem();\r
+        container.addItem();\r
+        Assert.assertSame(first, container.firstItemId());\r
+        Assert.assertTrue(container.removeItem(first));\r
+        Assert.assertFalse(container.isFirstId(first));\r
+    }\r
+\r
+    @Test\r
+    public void isLastId_tableLastItemRemoved_returnsFalse()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object last = container.lastItemId();\r
+        Assert.assertTrue(container.removeItem(last));\r
+        Assert.assertFalse(container.isLastId(last));\r
+    }\r
+\r
+    @Test\r
+    public void isLastId_tableAddedLastItemRemoved_returnsFalse()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object last = container.addItem();\r
+        Assert.assertSame(last, container.lastItemId());\r
+        Assert.assertTrue(container.removeItem(last));\r
+        Assert.assertFalse(container.isLastId(last));\r
+    }\r
+\r
+    @Test\r
+    public void indexOfId_tableItemRemoved_returnsNegOne() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.getIdByIndex(2);\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertEquals(-1, container.indexOfId(id));\r
+    }\r
+\r
+    @Test\r
+    public void indexOfId_tableAddedItemRemoved_returnsNegOne()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        Assert.assertTrue(container.indexOfId(id) != -1);\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertEquals(-1, container.indexOfId(id));\r
+    }\r
+\r
+    @Test\r
+    public void getIdByIndex_tableItemRemoved_resultChanges()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.getIdByIndex(2);\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertNotSame(id, container.getIdByIndex(2));\r
+    }\r
+\r
+    @Test\r
+    public void getIdByIndex_tableAddedItemRemoved_resultChanges()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object id = container.addItem();\r
+        container.addItem();\r
+        int index = container.indexOfId(id);\r
+        Assert.assertTrue(container.removeItem(id));\r
+        Assert.assertNotSame(id, container.getIdByIndex(index));\r
+    }\r
+\r
+    @Test\r
+    public void removeAllItems_table_shouldSucceed() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertTrue(container.removeAllItems());\r
+        Assert.assertEquals(0, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void removeAllItems_tableAddedItems_shouldSucceed()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.addItem();\r
+        container.addItem();\r
+        Assert.assertTrue(container.removeAllItems());\r
+        Assert.assertEquals(0, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void commit_tableAddedItem_shouldBeWrittenToDB() throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        Object id = container.addItem();\r
+        container.getContainerProperty(id, "NAME").setValue("New Name");\r
+        Assert.assertTrue(id instanceof TemporaryRowId);\r
+        Assert.assertSame(id, container.lastItemId());\r
+        container.commit();\r
+        Assert.assertFalse(container.lastItemId() instanceof TemporaryRowId);\r
+        Assert.assertEquals("New Name",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+    }\r
+\r
+    @Test\r
+    public void commit_tableTwoAddedItems_shouldBeWrittenToDB()\r
+            throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        Object id = container.addItem();\r
+        Object id2 = container.addItem();\r
+        container.getContainerProperty(id, "NAME").setValue("Herbert");\r
+        container.getContainerProperty(id2, "NAME").setValue("Larry");\r
+        Assert.assertTrue(id2 instanceof TemporaryRowId);\r
+        Assert.assertSame(id2, container.lastItemId());\r
+        container.commit();\r
+        Object nextToLast = container.getIdByIndex(container.size() - 2);\r
+        Assert.assertFalse(nextToLast instanceof TemporaryRowId);\r
+        Assert.assertEquals("Herbert",\r
+                container.getContainerProperty(nextToLast, "NAME").getValue());\r
+        Assert.assertFalse(container.lastItemId() instanceof TemporaryRowId);\r
+        Assert.assertEquals("Larry",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+    }\r
+\r
+    @Test\r
+    public void commit_tableRemovedItem_shouldBeRemovedFromDB()\r
+            throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        Object last = container.lastItemId();\r
+        container.removeItem(last);\r
+        container.commit();\r
+        Assert.assertFalse(last.equals(container.lastItemId()));\r
+    }\r
+\r
+    @Test\r
+    public void commit_tableLastItemUpdated_shouldUpdateRowInDB()\r
+            throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        Object last = container.lastItemId();\r
+        container.getContainerProperty(last, "NAME").setValue("Donald");\r
+        container.commit();\r
+        Assert.assertEquals("Donald",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+    }\r
+\r
+    @Test\r
+    public void rollback_tableItemAdded_discardsAddedItem() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        int size = container.size();\r
+        Object id = container.addItem();\r
+        container.getContainerProperty(id, "NAME").setValue("foo");\r
+        Assert.assertEquals(size + 1, container.size());\r
+        container.rollback();\r
+        Assert.assertEquals(size, container.size());\r
+        Assert.assertFalse("foo".equals(container.getContainerProperty(\r
+                container.lastItemId(), "NAME").getValue()));\r
+    }\r
+\r
+    @Test\r
+    public void rollback_tableItemRemoved_restoresRemovedItem()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        int size = container.size();\r
+        Object last = container.lastItemId();\r
+        container.removeItem(last);\r
+        Assert.assertEquals(size - 1, container.size());\r
+        container.rollback();\r
+        Assert.assertEquals(size, container.size());\r
+        Assert.assertEquals(last, container.lastItemId());\r
+    }\r
+\r
+    @Test\r
+    public void rollback_tableItemChanged_discardsChanges() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Object last = container.lastItemId();\r
+        container.getContainerProperty(last, "NAME").setValue("foo");\r
+        container.rollback();\r
+        Assert.assertFalse("foo".equals(container.getContainerProperty(\r
+                container.lastItemId(), "NAME").getValue()));\r
+    }\r
+\r
+    /*-\r
+     * TODO Removed test since currently the Vaadin test package structure\r
+     * does not allow testing protected methods. When it has been fixed\r
+     * then re-enable test.\r
+    @Test\r
+    public void itemChangeNotification_table_isModifiedReturnsTrue()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertFalse(container.isModified());\r
+        RowItem last = (RowItem) container.getItem(container.lastItemId());\r
+        container.itemChangeNotification(last);\r
+        Assert.assertTrue(container.isModified());\r
+        Util.shakeBodyElement()\r
+    }\r
+    -*/\r
+\r
+    @Test\r
+    public void itemSetChangeListeners_table_shouldFire() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        ItemSetChangeListener listener = EasyMock\r
+                .createMock(ItemSetChangeListener.class);\r
+        listener.containerItemSetChange(EasyMock.isA(ItemSetChangeEvent.class));\r
+        EasyMock.replay(listener);\r
+\r
+        container.addListener(listener);\r
+        container.addItem();\r
+\r
+        EasyMock.verify(listener);\r
+    }\r
+\r
+    @Test\r
+    public void itemSetChangeListeners_tableItemRemoved_shouldFire()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        ItemSetChangeListener listener = EasyMock\r
+                .createMock(ItemSetChangeListener.class);\r
+        listener.containerItemSetChange(EasyMock.isA(ItemSetChangeEvent.class));\r
+        EasyMock.expectLastCall().anyTimes();\r
+        EasyMock.replay(listener);\r
+\r
+        container.addListener(listener);\r
+        container.removeItem(container.lastItemId());\r
+\r
+        EasyMock.verify(listener);\r
+    }\r
+\r
+    @Test\r
+    public void removeListener_table_shouldNotFire() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        ItemSetChangeListener listener = EasyMock\r
+                .createMock(ItemSetChangeListener.class);\r
+        EasyMock.replay(listener);\r
+\r
+        container.addListener(listener);\r
+        container.removeListener(listener);\r
+        container.addItem();\r
+\r
+        EasyMock.verify(listener);\r
+    }\r
+\r
+    @Test\r
+    public void isModified_tableRemovedItem_returnsTrue() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertFalse(container.isModified());\r
+        container.removeItem(container.lastItemId());\r
+        Assert.assertTrue(container.isModified());\r
+    }\r
+\r
+    @Test\r
+    public void isModified_tableAddedItem_returnsTrue() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertFalse(container.isModified());\r
+        container.addItem();\r
+        Assert.assertTrue(container.isModified());\r
+    }\r
+\r
+    @Test\r
+    public void isModified_tableChangedItem_returnsTrue() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Assert.assertFalse(container.isModified());\r
+        container.getContainerProperty(container.lastItemId(), "NAME")\r
+                .setValue("foo");\r
+        Assert.assertTrue(container.isModified());\r
+    }\r
+\r
+    @Test\r
+    public void getSortableContainerPropertyIds_table_returnsAllPropertyIds()\r
+            throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        Collection<?> sortableIds = container.getSortableContainerPropertyIds();\r
+        Assert.assertTrue(sortableIds.contains("ID"));\r
+        Assert.assertTrue(sortableIds.contains("NAME"));\r
+        Assert.assertTrue(sortableIds.contains("AGE"));\r
+        Assert.assertEquals(3, sortableIds.size());\r
+        if (AllTests.db == DB.MSSQL || AllTests.db == DB.ORACLE) {\r
+            Assert.assertFalse(sortableIds.contains("rownum"));\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void addOrderBy_table_shouldReorderResults() throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        // Ville, Kalle, Pelle, Börje\r
+        Assert.assertEquals("Ville",\r
+                container.getContainerProperty(container.firstItemId(), "NAME")\r
+                        .getValue());\r
+        Assert.assertEquals("Börje",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+\r
+        container.addOrderBy(new OrderBy("NAME", true));\r
+        // Börje, Kalle, Pelle, Ville\r
+        Assert.assertEquals("Börje",\r
+                container.getContainerProperty(container.firstItemId(), "NAME")\r
+                        .getValue());\r
+        Assert.assertEquals("Ville",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+    }\r
+\r
+    @Test(expected = IllegalArgumentException.class)\r
+    public void addOrderBy_tableIllegalColumn_shouldFail() throws SQLException {\r
+        SQLContainer container = new SQLContainer(new TableQuery("people",\r
+                connectionPool, AllTests.sqlGen));\r
+        container.addOrderBy(new OrderBy("asdf", true));\r
+    }\r
+\r
+    @Test\r
+    public void sort_table_sortsByName() throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        // Ville, Kalle, Pelle, Börje\r
+        Assert.assertEquals("Ville",\r
+                container.getContainerProperty(container.firstItemId(), "NAME")\r
+                        .getValue());\r
+        Assert.assertEquals("Börje",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+\r
+        container.sort(new Object[] { "NAME" }, new boolean[] { true });\r
+\r
+        // Börje, Kalle, Pelle, Ville\r
+        Assert.assertEquals("Börje",\r
+                container.getContainerProperty(container.firstItemId(), "NAME")\r
+                        .getValue());\r
+        Assert.assertEquals("Ville",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+    }\r
+\r
+    @Test\r
+    public void addFilter_table_filtersResults() throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        // Ville, Kalle, Pelle, Börje\r
+        Assert.assertEquals(4, container.size());\r
+        Assert.assertEquals("Börje",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+\r
+        container.addContainerFilter(new Like("NAME", "%lle"));\r
+        // Ville, Kalle, Pelle\r
+        Assert.assertEquals(3, container.size());\r
+        Assert.assertEquals("Pelle",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+    }\r
+\r
+    @Test\r
+    public void addContainerFilter_filtersResults() throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        // Ville, Kalle, Pelle, Börje\r
+        Assert.assertEquals(4, container.size());\r
+\r
+        container.addContainerFilter("NAME", "Vi", false, false);\r
+\r
+        // Ville\r
+        Assert.assertEquals(1, container.size());\r
+        Assert.assertEquals("Ville",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+    }\r
+\r
+    @Test\r
+    public void addContainerFilter_ignoreCase_filtersResults()\r
+            throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        // Ville, Kalle, Pelle, Börje\r
+        Assert.assertEquals(4, container.size());\r
+\r
+        container.addContainerFilter("NAME", "vi", true, false);\r
+\r
+        // Ville\r
+        Assert.assertEquals(1, container.size());\r
+        Assert.assertEquals("Ville",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+    }\r
+\r
+    @Test\r
+    public void removeAllContainerFilters_table_noFiltering()\r
+            throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        // Ville, Kalle, Pelle, Börje\r
+        Assert.assertEquals(4, container.size());\r
+\r
+        container.addContainerFilter("NAME", "Vi", false, false);\r
+\r
+        // Ville\r
+        Assert.assertEquals(1, container.size());\r
+        Assert.assertEquals("Ville",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+\r
+        container.removeAllContainerFilters();\r
+\r
+        Assert.assertEquals(4, container.size());\r
+        Assert.assertEquals("Börje",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+    }\r
+\r
+    @Test\r
+    public void removeContainerFilters_table_noFiltering() throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        // Ville, Kalle, Pelle, Börje\r
+        Assert.assertEquals(4, container.size());\r
+\r
+        container.addContainerFilter("NAME", "Vi", false, false);\r
+\r
+        // Ville\r
+        Assert.assertEquals(1, container.size());\r
+        Assert.assertEquals("Ville",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+\r
+        container.removeContainerFilters("NAME");\r
+\r
+        Assert.assertEquals(4, container.size());\r
+        Assert.assertEquals("Börje",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+    }\r
+\r
+    @Test\r
+    public void addFilter_tableBufferedItems_alsoFiltersBufferedItems()\r
+            throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        // Ville, Kalle, Pelle, Börje\r
+        Assert.assertEquals(4, container.size());\r
+        Assert.assertEquals("Börje",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+\r
+        Object id1 = container.addItem();\r
+        container.getContainerProperty(id1, "NAME").setValue("Palle");\r
+        Object id2 = container.addItem();\r
+        container.getContainerProperty(id2, "NAME").setValue("Bengt");\r
+\r
+        container.addContainerFilter(new Like("NAME", "%lle"));\r
+\r
+        // Ville, Kalle, Pelle, Palle\r
+        Assert.assertEquals(4, container.size());\r
+        Assert.assertEquals(\r
+                "Ville",\r
+                container.getContainerProperty(container.getIdByIndex(0),\r
+                        "NAME").getValue());\r
+        Assert.assertEquals(\r
+                "Kalle",\r
+                container.getContainerProperty(container.getIdByIndex(1),\r
+                        "NAME").getValue());\r
+        Assert.assertEquals(\r
+                "Pelle",\r
+                container.getContainerProperty(container.getIdByIndex(2),\r
+                        "NAME").getValue());\r
+        Assert.assertEquals(\r
+                "Palle",\r
+                container.getContainerProperty(container.getIdByIndex(3),\r
+                        "NAME").getValue());\r
+\r
+        Assert.assertNull(container.getIdByIndex(4));\r
+        Assert.assertNull(container.nextItemId(container.getIdByIndex(3)));\r
+\r
+        Assert.assertFalse(container.containsId(id2));\r
+        Assert.assertFalse(container.getItemIds().contains(id2));\r
+\r
+        Assert.assertNull(container.getItem(id2));\r
+        Assert.assertEquals(-1, container.indexOfId(id2));\r
+\r
+        Assert.assertNotSame(id2, container.lastItemId());\r
+        Assert.assertSame(id1, container.lastItemId());\r
+    }\r
+\r
+    @Test\r
+    public void sort_tableBufferedItems_sortsBufferedItemsLastInOrderAdded()\r
+            throws SQLException {\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+        // Ville, Kalle, Pelle, Börje\r
+        Assert.assertEquals("Ville",\r
+                container.getContainerProperty(container.firstItemId(), "NAME")\r
+                        .getValue());\r
+        Assert.assertEquals("Börje",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+\r
+        Object id1 = container.addItem();\r
+        container.getContainerProperty(id1, "NAME").setValue("Wilbert");\r
+        Object id2 = container.addItem();\r
+        container.getContainerProperty(id2, "NAME").setValue("Albert");\r
+\r
+        container.sort(new Object[] { "NAME" }, new boolean[] { true });\r
+\r
+        // Börje, Kalle, Pelle, Ville, Wilbert, Albert\r
+        Assert.assertEquals("Börje",\r
+                container.getContainerProperty(container.firstItemId(), "NAME")\r
+                        .getValue());\r
+        Assert.assertEquals(\r
+                "Wilbert",\r
+                container.getContainerProperty(\r
+                        container.getIdByIndex(container.size() - 2), "NAME")\r
+                        .getValue());\r
+        Assert.assertEquals("Albert",\r
+                container.getContainerProperty(container.lastItemId(), "NAME")\r
+                        .getValue());\r
+    }\r
+\r
+}\r
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/SQLContainerTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/SQLContainerTest.java
new file mode 100644 (file)
index 0000000..fbc3220
--- /dev/null
@@ -0,0 +1,2400 @@
+package com.vaadin.tests.server.container.sqlcontainer;
+
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.RowId;
+import com.vaadin.data.util.RowItem;
+import com.vaadin.data.util.SQLContainer;
+import com.vaadin.data.util.TemporaryRowId;
+import com.vaadin.data.util.connection.JDBCConnectionPool;
+import com.vaadin.data.util.connection.SimpleJDBCConnectionPool;
+import com.vaadin.data.util.filter.Compare.Equal;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.query.FreeformQuery;
+import com.vaadin.data.util.query.FreeformQueryDelegate;
+import com.vaadin.data.util.query.FreeformStatementDelegate;
+import com.vaadin.data.util.query.OrderBy;
+import com.vaadin.data.util.query.generator.MSSQLGenerator;
+import com.vaadin.data.util.query.generator.OracleGenerator;
+import com.vaadin.data.util.query.generator.SQLGenerator;
+import com.vaadin.data.util.query.generator.StatementHelper;
+import com.vaadin.data.util.query.generator.filter.QueryBuilder;
+import com.vaadin.tests.server.container.sqlcontainer.AllTests.DB;
+
+public class SQLContainerTest {
+    private static final int offset = AllTests.offset;
+    private JDBCConnectionPool connectionPool;
+
+    @Before
+    public void setUp() throws SQLException {
+
+        try {
+            connectionPool = new SimpleJDBCConnectionPool(AllTests.dbDriver,
+                    AllTests.dbURL, AllTests.dbUser, AllTests.dbPwd, 2, 2);
+        } catch (SQLException e) {
+            e.printStackTrace();
+            Assert.fail(e.getMessage());
+        }
+
+        DataGenerator.addPeopleToDatabase(connectionPool);
+    }
+
+    @After
+    public void tearDown() {
+        if (connectionPool != null) {
+            connectionPool.destroy();
+        }
+    }
+
+    @Test
+    public void constructor_withFreeformQuery_shouldSucceed()
+            throws SQLException {
+        new SQLContainer(new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool));
+    }
+
+    @Test(expected = SQLException.class)
+    public void constructor_withIllegalFreeformQuery_shouldFail()
+            throws SQLException {
+        SQLContainer c = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM asdf", Arrays.asList("ID"), connectionPool));
+        c.getItem(c.firstItemId());
+    }
+
+    @Test
+    public void containsId_withFreeformQueryAndExistingId_returnsTrue()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertTrue(container.containsId(new RowId(new Object[] { 1 })));
+    }
+
+    @Test
+    public void containsId_withFreeformQueryAndNonexistingId_returnsFalse()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertFalse(container
+                .containsId(new RowId(new Object[] { 1337 })));
+    }
+
+    @Test
+    public void getContainerProperty_freeformExistingItemIdAndPropertyId_returnsProperty()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertEquals(
+                    "Ville",
+                    container
+                            .getContainerProperty(
+                                    new RowId(new Object[] { new BigDecimal(
+                                            0 + offset) }), "NAME").getValue());
+        } else {
+            Assert.assertEquals(
+                    "Ville",
+                    container.getContainerProperty(
+                            new RowId(new Object[] { 0 + offset }), "NAME")
+                            .getValue());
+        }
+    }
+
+    @Test
+    public void getContainerProperty_freeformExistingItemIdAndNonexistingPropertyId_returnsNull()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertNull(container.getContainerProperty(new RowId(
+                new Object[] { 1 + offset }), "asdf"));
+    }
+
+    @Test
+    public void getContainerProperty_freeformNonexistingItemId_returnsNull()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertNull(container.getContainerProperty(new RowId(
+                new Object[] { 1337 + offset }), "NAME"));
+    }
+
+    @Test
+    public void getContainerPropertyIds_freeform_returnsIDAndNAME()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Collection<?> propertyIds = container.getContainerPropertyIds();
+        Assert.assertEquals(3, propertyIds.size());
+        Assert.assertArrayEquals(new String[] { "ID", "NAME", "AGE" },
+                propertyIds.toArray());
+    }
+
+    @Test
+    public void getItem_freeformExistingItemId_returnsItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Item item;
+        if (AllTests.db == DB.ORACLE) {
+            item = container.getItem(new RowId(new Object[] { new BigDecimal(
+                    0 + offset) }));
+        } else {
+            item = container.getItem(new RowId(new Object[] { 0 + offset }));
+        }
+        Assert.assertNotNull(item);
+        Assert.assertEquals("Ville", item.getItemProperty("NAME").getValue());
+    }
+
+    @Test
+    public void getItem_freeform5000RowsWithParameter1337_returnsItemWithId1337()
+            throws SQLException {
+        DataGenerator.addFiveThousandPeople(connectionPool);
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Item item;
+        if (AllTests.db == DB.ORACLE) {
+            item = container.getItem(new RowId(new Object[] { new BigDecimal(
+                    1337 + offset) }));
+            Assert.assertNotNull(item);
+            Assert.assertEquals(new BigDecimal(1337 + offset), item
+                    .getItemProperty("ID").getValue());
+        } else {
+            item = container.getItem(new RowId(new Object[] { 1337 + offset }));
+            Assert.assertNotNull(item);
+            Assert.assertEquals(1337 + offset, item.getItemProperty("ID")
+                    .getValue());
+        }
+        Assert.assertEquals("Person 1337", item.getItemProperty("NAME")
+                .getValue());
+    }
+
+    @Test
+    public void getItemIds_freeform_returnsItemIdsWithKeys0through3()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Collection<?> itemIds = container.getItemIds();
+        Assert.assertEquals(4, itemIds.size());
+        RowId zero = new RowId(new Object[] { 0 + offset });
+        RowId one = new RowId(new Object[] { 1 + offset });
+        RowId two = new RowId(new Object[] { 2 + offset });
+        RowId three = new RowId(new Object[] { 3 + offset });
+        if (AllTests.db == DB.ORACLE) {
+            String[] correct = new String[] { "1", "2", "3", "4" };
+            List<String> oracle = new ArrayList<String>();
+            for (Object o : itemIds) {
+                oracle.add(o.toString());
+            }
+            Assert.assertArrayEquals(correct, oracle.toArray());
+        } else {
+            Assert.assertArrayEquals(new Object[] { zero, one, two, three },
+                    itemIds.toArray());
+        }
+    }
+
+    @Test
+    public void getType_freeformNAMEPropertyId_returnsString()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertEquals(String.class, container.getType("NAME"));
+    }
+
+    @Test
+    public void getType_freeformIDPropertyId_returnsInteger()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertEquals(BigDecimal.class, container.getType("ID"));
+        } else {
+            Assert.assertEquals(Integer.class, container.getType("ID"));
+        }
+    }
+
+    @Test
+    public void getType_freeformNonexistingPropertyId_returnsNull()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertNull(container.getType("asdf"));
+    }
+
+    @Test
+    public void size_freeform_returnsFour() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertEquals(4, container.size());
+    }
+
+    @Test
+    public void size_freeformOneAddedItem_returnsFive() throws SQLException {
+        Connection conn = connectionPool.reserveConnection();
+        Statement statement = conn.createStatement();
+        if (AllTests.db == DB.MSSQL) {
+            statement.executeUpdate("insert into people values('Bengt', '42')");
+        } else {
+            statement
+                    .executeUpdate("insert into people values(default, 'Bengt', '42')");
+        }
+        statement.close();
+        conn.commit();
+        connectionPool.releaseConnection(conn);
+
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertEquals(5, container.size());
+    }
+
+    @Test
+    public void indexOfId_freeformWithParameterThree_returnsThree()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertEquals(3, container.indexOfId(new RowId(
+                    new Object[] { new BigDecimal(3 + offset) })));
+        } else {
+            Assert.assertEquals(3,
+                    container.indexOfId(new RowId(new Object[] { 3 + offset })));
+        }
+    }
+
+    @Test
+    public void indexOfId_freeform5000RowsWithParameter1337_returns1337()
+            throws SQLException {
+        DataGenerator.addFiveThousandPeople(connectionPool);
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people ORDER BY \"ID\" ASC",
+                Arrays.asList("ID"), connectionPool));
+        if (AllTests.db == DB.ORACLE) {
+            container.getItem(new RowId(new Object[] { new BigDecimal(
+                    1337 + offset) }));
+            Assert.assertEquals(1337, container.indexOfId(new RowId(
+                    new Object[] { new BigDecimal(1337 + offset) })));
+        } else {
+            container.getItem(new RowId(new Object[] { 1337 + offset }));
+            Assert.assertEquals(1337, container.indexOfId(new RowId(
+                    new Object[] { 1337 + offset })));
+        }
+    }
+
+    @Test
+    public void getIdByIndex_freeform5000rowsIndex1337_returnsRowId1337()
+            throws SQLException {
+        DataGenerator.addFiveThousandPeople(connectionPool);
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people ORDER BY \"ID\" ASC",
+                Arrays.asList("ID"), connectionPool));
+        Object itemId = container.getIdByIndex(1337);
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertEquals(new RowId(new Object[] { new BigDecimal(
+                    1337 + offset) }), itemId);
+        } else {
+            Assert.assertEquals(new RowId(new Object[] { 1337 + offset }),
+                    itemId);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void getIdByIndex_freeformWithPaging5000rowsIndex1337_returnsRowId1337()
+            throws SQLException {
+        DataGenerator.addFiveThousandPeople(connectionPool);
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(
+                delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<String>() {
+                    public String answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        if (AllTests.db == DB.MSSQL) {
+                            int start = offset + 1;
+                            int end = offset + limit + 1;
+                            String q = "SELECT * FROM (SELECT row_number() OVER"
+                                    + " ( ORDER BY \"ID\" ASC) AS rownum, * FROM people)"
+                                    + " AS a WHERE a.rownum BETWEEN "
+                                    + start
+                                    + " AND " + end;
+                            return q;
+                        } else if (AllTests.db == DB.ORACLE) {
+                            int start = offset + 1;
+                            int end = offset + limit + 1;
+                            String q = "SELECT * FROM (SELECT x.*, ROWNUM AS r FROM"
+                                    + " (SELECT * FROM people ORDER BY \"ID\" ASC) x) "
+                                    + " WHERE r BETWEEN "
+                                    + start
+                                    + " AND "
+                                    + end;
+                            return q;
+                        } else {
+                            return "SELECT * FROM people LIMIT " + limit
+                                    + " OFFSET " + offset;
+                        }
+                    }
+                }).anyTimes();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        EasyMock.expect(delegate.getCountQuery())
+                .andThrow(new UnsupportedOperationException()).anyTimes();
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+        SQLContainer container = new SQLContainer(query);
+        Object itemId = container.getIdByIndex(1337);
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertEquals(
+                    new RowId(new Object[] { 1337 + offset }).toString(),
+                    itemId.toString());
+        } else {
+            Assert.assertEquals(new RowId(new Object[] { 1337 + offset }),
+                    itemId);
+        }
+    }
+
+    @Test
+    public void nextItemId_freeformCurrentItem1337_returnsItem1338()
+            throws SQLException {
+        DataGenerator.addFiveThousandPeople(connectionPool);
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people ORDER BY \"ID\" ASC",
+                Arrays.asList("ID"), connectionPool));
+        Object itemId = container.getIdByIndex(1337);
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertEquals(
+                    new RowId(new Object[] { 1338 + offset }).toString(),
+                    container.nextItemId(itemId).toString());
+        } else {
+            Assert.assertEquals(new RowId(new Object[] { 1338 + offset }),
+                    container.nextItemId(itemId));
+        }
+    }
+
+    @Test
+    public void prevItemId_freeformCurrentItem1337_returns1336()
+            throws SQLException {
+        DataGenerator.addFiveThousandPeople(connectionPool);
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people ORDER BY \"ID\" ASC",
+                Arrays.asList("ID"), connectionPool));
+        Object itemId = container.getIdByIndex(1337);
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertEquals(
+                    new RowId(new Object[] { 1336 + offset }).toString(),
+                    container.prevItemId(itemId).toString());
+        } else {
+            Assert.assertEquals(new RowId(new Object[] { 1336 + offset }),
+                    container.prevItemId(itemId));
+        }
+    }
+
+    @Test
+    public void firstItemId_freeform_returnsItemId0() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertEquals(
+                    new RowId(new Object[] { 0 + offset }).toString(),
+                    container.firstItemId().toString());
+        } else {
+            Assert.assertEquals(new RowId(new Object[] { 0 + offset }),
+                    container.firstItemId());
+        }
+    }
+
+    @Test
+    public void lastItemId_freeform5000Rows_returnsItemId4999()
+            throws SQLException {
+        DataGenerator.addFiveThousandPeople(connectionPool);
+
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people ORDER BY \"ID\" ASC",
+                Arrays.asList("ID"), connectionPool));
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertEquals(
+                    new RowId(new Object[] { 4999 + offset }).toString(),
+                    container.lastItemId().toString());
+        } else {
+            Assert.assertEquals(new RowId(new Object[] { 4999 + offset }),
+                    container.lastItemId());
+        }
+    }
+
+    @Test
+    public void isFirstId_freeformActualFirstId_returnsTrue()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertTrue(container.isFirstId(new RowId(
+                    new Object[] { new BigDecimal(0 + offset) })));
+        } else {
+            Assert.assertTrue(container.isFirstId(new RowId(
+                    new Object[] { 0 + offset })));
+        }
+    }
+
+    @Test
+    public void isFirstId_freeformSecondId_returnsFalse() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertFalse(container.isFirstId(new RowId(
+                    new Object[] { new BigDecimal(1 + offset) })));
+        } else {
+            Assert.assertFalse(container.isFirstId(new RowId(
+                    new Object[] { 1 + offset })));
+        }
+    }
+
+    @Test
+    public void isLastId_freeformSecondId_returnsFalse() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertFalse(container.isLastId(new RowId(
+                    new Object[] { new BigDecimal(1 + offset) })));
+        } else {
+            Assert.assertFalse(container.isLastId(new RowId(
+                    new Object[] { 1 + offset })));
+        }
+    }
+
+    @Test
+    public void isLastId_freeformLastId_returnsTrue() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertTrue(container.isLastId(new RowId(
+                    new Object[] { new BigDecimal(3 + offset) })));
+        } else {
+            Assert.assertTrue(container.isLastId(new RowId(
+                    new Object[] { 3 + offset })));
+        }
+    }
+
+    @Test
+    public void isLastId_freeform5000RowsLastId_returnsTrue()
+            throws SQLException {
+        DataGenerator.addFiveThousandPeople(connectionPool);
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people ORDER BY \"ID\" ASC",
+                Arrays.asList("ID"), connectionPool));
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertTrue(container.isLastId(new RowId(
+                    new Object[] { new BigDecimal(4999 + offset) })));
+        } else {
+            Assert.assertTrue(container.isLastId(new RowId(
+                    new Object[] { 4999 + offset })));
+        }
+    }
+
+    @Test
+    public void refresh_freeform_sizeShouldUpdate() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertEquals(4, container.size());
+        DataGenerator.addFiveThousandPeople(connectionPool);
+        container.refresh();
+        Assert.assertEquals(5000, container.size());
+    }
+
+    @Test
+    public void refresh_freeformWithoutCallingRefresh_sizeShouldNotUpdate()
+            throws SQLException {
+        // Yeah, this is a weird one. We're testing that the size doesn't update
+        // after adding lots of items unless we call refresh inbetween. This to
+        // make sure that the refresh method actually refreshes stuff and isn't
+        // a NOP.
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertEquals(4, container.size());
+        DataGenerator.addFiveThousandPeople(connectionPool);
+        Assert.assertEquals(4, container.size());
+    }
+
+    @Test
+    public void setAutoCommit_freeform_shouldSucceed() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.setAutoCommit(true);
+        Assert.assertTrue(container.isAutoCommit());
+        container.setAutoCommit(false);
+        Assert.assertFalse(container.isAutoCommit());
+    }
+
+    @Test
+    public void getPageLength_freeform_returnsDefault100() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertEquals(100, container.getPageLength());
+    }
+
+    @Test
+    public void setPageLength_freeform_shouldSucceed() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.setPageLength(20);
+        Assert.assertEquals(20, container.getPageLength());
+        container.setPageLength(200);
+        Assert.assertEquals(200, container.getPageLength());
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void addContainerProperty_normal_isUnsupported() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.addContainerProperty("asdf", String.class, "");
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void removeContainerProperty_normal_isUnsupported()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.removeContainerProperty("asdf");
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void addItemObject_normal_isUnsupported() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.addItem("asdf");
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void addItemAfterObjectObject_normal_isUnsupported()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.addItemAfter("asdf", "foo");
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void addItemAtIntObject_normal_isUnsupported() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.addItemAt(2, "asdf");
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void addItemAtInt_normal_isUnsupported() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.addItemAt(2);
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void addItemAfterObject_normal_isUnsupported() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.addItemAfter("asdf");
+    }
+
+    @Test
+    public void addItem_freeformAddOneNewItem_returnsItemId()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object itemId = container.addItem();
+        Assert.assertNotNull(itemId);
+    }
+
+    @Test
+    public void addItem_freeformAddOneNewItem_shouldChangeSize()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        int size = container.size();
+        container.addItem();
+        Assert.assertEquals(size + 1, container.size());
+    }
+
+    @Test
+    public void addItem_freeformAddTwoNewItems_shouldChangeSize()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        int size = container.size();
+        Object id1 = container.addItem();
+        Object id2 = container.addItem();
+        Assert.assertEquals(size + 2, container.size());
+        Assert.assertNotSame(id1, id2);
+        Assert.assertFalse(id1.equals(id2));
+    }
+
+    @Test
+    public void nextItemId_freeformNewlyAddedItem_returnsNewlyAdded()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object lastId = container.lastItemId();
+        Object id = container.addItem();
+        Assert.assertEquals(id, container.nextItemId(lastId));
+    }
+
+    @Test
+    public void lastItemId_freeformNewlyAddedItem_returnsNewlyAdded()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object lastId = container.lastItemId();
+        Object id = container.addItem();
+        Assert.assertEquals(id, container.lastItemId());
+        Assert.assertNotSame(lastId, container.lastItemId());
+    }
+
+    @Test
+    public void indexOfId_freeformNewlyAddedItem_returnsFour()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertEquals(4, container.indexOfId(id));
+    }
+
+    @Test
+    public void getItem_freeformNewlyAddedItem_returnsNewlyAdded()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertNotNull(container.getItem(id));
+    }
+
+    @Test
+    public void getItem_freeformNewlyAddedItemAndFiltered_returnsNull()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.addContainerFilter(new Equal("NAME", "asdf"));
+        Object id = container.addItem();
+        Assert.assertNull(container.getItem(id));
+    }
+
+    @Test
+    public void getItemUnfiltered_freeformNewlyAddedItemAndFiltered_returnsNewlyAdded()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.addContainerFilter(new Equal("NAME", "asdf"));
+        Object id = container.addItem();
+        Assert.assertNotNull(container.getItemUnfiltered(id));
+    }
+
+    @Test
+    public void getItemIds_freeformNewlyAddedItem_containsNewlyAdded()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertTrue(container.getItemIds().contains(id));
+    }
+
+    @Test
+    public void getContainerProperty_freeformNewlyAddedItem_returnsPropertyOfNewlyAddedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Item item = container.getItem(id);
+        item.getItemProperty("NAME").setValue("asdf");
+        Assert.assertEquals("asdf", container.getContainerProperty(id, "NAME")
+                .getValue());
+    }
+
+    @Test
+    public void containsId_freeformNewlyAddedItem_returnsTrue()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertTrue(container.containsId(id));
+    }
+
+    @Test
+    public void prevItemId_freeformTwoNewlyAddedItems_returnsFirstAddedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id1 = container.addItem();
+        Object id2 = container.addItem();
+        Assert.assertEquals(id1, container.prevItemId(id2));
+    }
+
+    @Test
+    public void firstItemId_freeformEmptyResultSet_returnsFirstAddedItem()
+            throws SQLException {
+        DataGenerator.createGarbage(connectionPool);
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM GARBAGE", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertSame(id, container.firstItemId());
+    }
+
+    @Test
+    public void isFirstId_freeformEmptyResultSet_returnsFirstAddedItem()
+            throws SQLException {
+        DataGenerator.createGarbage(connectionPool);
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM GARBAGE", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertTrue(container.isFirstId(id));
+    }
+
+    @Test
+    public void isLastId_freeformOneItemAdded_returnsTrueForAddedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertTrue(container.isLastId(id));
+    }
+
+    @Test
+    public void isLastId_freeformTwoItemsAdded_returnsTrueForLastAddedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.addItem();
+        Object id2 = container.addItem();
+        Assert.assertTrue(container.isLastId(id2));
+    }
+
+    @Test
+    public void getIdByIndex_freeformOneItemAddedLastIndexInContainer_returnsAddedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertEquals(id, container.getIdByIndex(container.size() - 1));
+    }
+
+    @Test
+    public void removeItem_freeformNoAddedItems_removesItemFromContainer()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        int size = container.size();
+        Object id = container.firstItemId();
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertNotSame(id, container.firstItemId());
+        Assert.assertEquals(size - 1, container.size());
+    }
+
+    @Test
+    public void containsId_freeformRemovedItem_returnsFalse()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.firstItemId();
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertFalse(container.containsId(id));
+    }
+
+    @Test
+    public void removeItem_freeformOneAddedItem_removesTheAddedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        int size = container.size();
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertFalse(container.containsId(id));
+        Assert.assertEquals(size - 1, container.size());
+    }
+
+    @Test
+    public void getItem_freeformItemRemoved_returnsNull() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.firstItemId();
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertNull(container.getItem(id));
+    }
+
+    @Test
+    public void getItem_freeformAddedItemRemoved_returnsNull()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertNotNull(container.getItem(id));
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertNull(container.getItem(id));
+    }
+
+    @Test
+    public void getItemIds_freeformItemRemoved_shouldNotContainRemovedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.firstItemId();
+        Assert.assertTrue(container.getItemIds().contains(id));
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertFalse(container.getItemIds().contains(id));
+    }
+
+    @Test
+    public void getItemIds_freeformAddedItemRemoved_shouldNotContainRemovedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertTrue(container.getItemIds().contains(id));
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertFalse(container.getItemIds().contains(id));
+    }
+
+    @Test
+    public void containsId_freeformItemRemoved_returnsFalse()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.firstItemId();
+        Assert.assertTrue(container.containsId(id));
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertFalse(container.containsId(id));
+    }
+
+    @Test
+    public void containsId_freeformAddedItemRemoved_returnsFalse()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertTrue(container.containsId(id));
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertFalse(container.containsId(id));
+    }
+
+    @Test
+    public void nextItemId_freeformItemRemoved_skipsRemovedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object first = container.getIdByIndex(0);
+        Object second = container.getIdByIndex(1);
+        Object third = container.getIdByIndex(2);
+        Assert.assertTrue(container.removeItem(second));
+        Assert.assertEquals(third, container.nextItemId(first));
+    }
+
+    @Test
+    public void nextItemId_freeformAddedItemRemoved_skipsRemovedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object first = container.lastItemId();
+        Object second = container.addItem();
+        Object third = container.addItem();
+        Assert.assertTrue(container.removeItem(second));
+        Assert.assertEquals(third, container.nextItemId(first));
+    }
+
+    @Test
+    public void prevItemId_freeformItemRemoved_skipsRemovedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object first = container.getIdByIndex(0);
+        Object second = container.getIdByIndex(1);
+        Object third = container.getIdByIndex(2);
+        Assert.assertTrue(container.removeItem(second));
+        Assert.assertEquals(first, container.prevItemId(third));
+    }
+
+    @Test
+    public void prevItemId_freeformAddedItemRemoved_skipsRemovedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object first = container.lastItemId();
+        Object second = container.addItem();
+        Object third = container.addItem();
+        Assert.assertTrue(container.removeItem(second));
+        Assert.assertEquals(first, container.prevItemId(third));
+    }
+
+    @Test
+    public void firstItemId_freeformFirstItemRemoved_resultChanges()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object first = container.firstItemId();
+        Assert.assertTrue(container.removeItem(first));
+        Assert.assertNotSame(first, container.firstItemId());
+    }
+
+    @Test
+    public void firstItemId_freeformNewlyAddedFirstItemRemoved_resultChanges()
+            throws SQLException {
+        DataGenerator.createGarbage(connectionPool);
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM GARBAGE", Arrays.asList("ID"), connectionPool));
+        Object first = container.addItem();
+        Object second = container.addItem();
+        Assert.assertSame(first, container.firstItemId());
+        Assert.assertTrue(container.removeItem(first));
+        Assert.assertSame(second, container.firstItemId());
+    }
+
+    @Test
+    public void lastItemId_freeformLastItemRemoved_resultChanges()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object last = container.lastItemId();
+        Assert.assertTrue(container.removeItem(last));
+        Assert.assertNotSame(last, container.lastItemId());
+    }
+
+    @Test
+    public void lastItemId_freeformAddedLastItemRemoved_resultChanges()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object last = container.addItem();
+        Assert.assertSame(last, container.lastItemId());
+        Assert.assertTrue(container.removeItem(last));
+        Assert.assertNotSame(last, container.lastItemId());
+    }
+
+    @Test
+    public void isFirstId_freeformFirstItemRemoved_returnsFalse()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object first = container.firstItemId();
+        Assert.assertTrue(container.removeItem(first));
+        Assert.assertFalse(container.isFirstId(first));
+    }
+
+    @Test
+    public void isFirstId_freeformAddedFirstItemRemoved_returnsFalse()
+            throws SQLException {
+        DataGenerator.createGarbage(connectionPool);
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM GARBAGE", Arrays.asList("ID"), connectionPool));
+        Object first = container.addItem();
+        container.addItem();
+        Assert.assertSame(first, container.firstItemId());
+        Assert.assertTrue(container.removeItem(first));
+        Assert.assertFalse(container.isFirstId(first));
+    }
+
+    @Test
+    public void isLastId_freeformLastItemRemoved_returnsFalse()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object last = container.lastItemId();
+        Assert.assertTrue(container.removeItem(last));
+        Assert.assertFalse(container.isLastId(last));
+    }
+
+    @Test
+    public void isLastId_freeformAddedLastItemRemoved_returnsFalse()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object last = container.addItem();
+        Assert.assertSame(last, container.lastItemId());
+        Assert.assertTrue(container.removeItem(last));
+        Assert.assertFalse(container.isLastId(last));
+    }
+
+    @Test
+    public void indexOfId_freeformItemRemoved_returnsNegOne()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.getIdByIndex(2);
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertEquals(-1, container.indexOfId(id));
+    }
+
+    @Test
+    public void indexOfId_freeformAddedItemRemoved_returnsNegOne()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        Assert.assertTrue(container.indexOfId(id) != -1);
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertEquals(-1, container.indexOfId(id));
+    }
+
+    @Test
+    public void getIdByIndex_freeformItemRemoved_resultChanges()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.getIdByIndex(2);
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertNotSame(id, container.getIdByIndex(2));
+    }
+
+    @Test
+    public void getIdByIndex_freeformAddedItemRemoved_resultChanges()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object id = container.addItem();
+        container.addItem();
+        int index = container.indexOfId(id);
+        Assert.assertTrue(container.removeItem(id));
+        Assert.assertNotSame(id, container.getIdByIndex(index));
+    }
+
+    @Test
+    public void removeAllItems_freeform_shouldSucceed() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertTrue(container.removeAllItems());
+        Assert.assertEquals(0, container.size());
+    }
+
+    @Test
+    public void removeAllItems_freeformAddedItems_shouldSucceed()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.addItem();
+        container.addItem();
+        Assert.assertTrue(container.removeAllItems());
+        Assert.assertEquals(0, container.size());
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void commit_freeformAddedItem_shouldBeWrittenToDB()
+            throws SQLException {
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(
+                delegate.storeRow(EasyMock.isA(Connection.class),
+                        EasyMock.isA(RowItem.class)))
+                .andAnswer(new IAnswer<Integer>() {
+                    public Integer answer() throws Throwable {
+                        Connection conn = (Connection) EasyMock
+                                .getCurrentArguments()[0];
+                        RowItem item = (RowItem) EasyMock.getCurrentArguments()[1];
+                        Statement statement = conn.createStatement();
+                        if (AllTests.db == DB.MSSQL) {
+                            statement
+                                    .executeUpdate("insert into people values('"
+                                            + item.getItemProperty("NAME")
+                                                    .getValue()
+                                            + "', '"
+                                            + item.getItemProperty("AGE")
+                                                    .getValue() + "')");
+                        } else {
+                            statement
+                                    .executeUpdate("insert into people values(default, '"
+                                            + item.getItemProperty("NAME")
+                                                    .getValue()
+                                            + "', '"
+                                            + item.getItemProperty("AGE")
+                                                    .getValue() + "')");
+                        }
+                        statement.close();
+                        conn.commit();
+                        connectionPool.releaseConnection(conn);
+                        return 1;
+                    }
+                }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<String>() {
+                    public String answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        if (AllTests.db == DB.MSSQL) {
+                            int start = offset + 1;
+                            int end = offset + limit + 1;
+                            String q = "SELECT * FROM (SELECT row_number() OVER"
+                                    + " ( ORDER BY \"ID\" ASC) AS rownum, * FROM people)"
+                                    + " AS a WHERE a.rownum BETWEEN "
+                                    + start
+                                    + " AND " + end;
+                            return q;
+                        } else if (AllTests.db == DB.ORACLE) {
+                            int start = offset + 1;
+                            int end = offset + limit + 1;
+                            String q = "SELECT * FROM (SELECT x.*, ROWNUM AS r FROM"
+                                    + " (SELECT * FROM people ORDER BY \"ID\" ASC) x) "
+                                    + " WHERE r BETWEEN "
+                                    + start
+                                    + " AND "
+                                    + end;
+                            return q;
+                        } else {
+                            return "SELECT * FROM people LIMIT " + limit
+                                    + " OFFSET " + offset;
+                        }
+                    }
+                }).anyTimes();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        EasyMock.expect(delegate.getCountQuery())
+                .andThrow(new UnsupportedOperationException()).anyTimes();
+
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.setDelegate(delegate);
+        EasyMock.replay(delegate);
+        SQLContainer container = new SQLContainer(query);
+        Object id = container.addItem();
+        container.getContainerProperty(id, "NAME").setValue("New Name");
+        container.getContainerProperty(id, "AGE").setValue(30);
+        Assert.assertTrue(id instanceof TemporaryRowId);
+        Assert.assertSame(id, container.lastItemId());
+        container.commit();
+        Assert.assertFalse(container.lastItemId() instanceof TemporaryRowId);
+        Assert.assertEquals("New Name",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+        EasyMock.verify(delegate);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void commit_freeformTwoAddedItems_shouldBeWrittenToDB()
+            throws SQLException {
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(
+                delegate.storeRow(EasyMock.isA(Connection.class),
+                        EasyMock.isA(RowItem.class)))
+                .andAnswer(new IAnswer<Integer>() {
+                    public Integer answer() throws Throwable {
+                        Connection conn = (Connection) EasyMock
+                                .getCurrentArguments()[0];
+                        RowItem item = (RowItem) EasyMock.getCurrentArguments()[1];
+                        Statement statement = conn.createStatement();
+                        if (AllTests.db == DB.MSSQL) {
+                            statement
+                                    .executeUpdate("insert into people values('"
+                                            + item.getItemProperty("NAME")
+                                                    .getValue()
+                                            + "', '"
+                                            + item.getItemProperty("AGE")
+                                                    .getValue() + "')");
+                        } else {
+                            statement
+                                    .executeUpdate("insert into people values(default, '"
+                                            + item.getItemProperty("NAME")
+                                                    .getValue()
+                                            + "', '"
+                                            + item.getItemProperty("AGE")
+                                                    .getValue() + "')");
+                        }
+                        statement.close();
+                        conn.commit();
+                        connectionPool.releaseConnection(conn);
+                        return 1;
+                    }
+                }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<String>() {
+                    public String answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        if (AllTests.db == DB.MSSQL) {
+                            int start = offset + 1;
+                            int end = offset + limit + 1;
+                            String q = "SELECT * FROM (SELECT row_number() OVER"
+                                    + " ( ORDER BY \"ID\" ASC) AS rownum, * FROM people)"
+                                    + " AS a WHERE a.rownum BETWEEN "
+                                    + start
+                                    + " AND " + end;
+                            return q;
+                        } else if (AllTests.db == DB.ORACLE) {
+                            int start = offset + 1;
+                            int end = offset + limit + 1;
+                            String q = "SELECT * FROM (SELECT x.*, ROWNUM AS r FROM"
+                                    + " (SELECT * FROM people ORDER BY \"ID\" ASC) x) "
+                                    + " WHERE r BETWEEN "
+                                    + start
+                                    + " AND "
+                                    + end;
+                            return q;
+                        } else {
+                            return "SELECT * FROM people LIMIT " + limit
+                                    + " OFFSET " + offset;
+                        }
+                    }
+                }).anyTimes();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        EasyMock.expect(delegate.getCountQuery())
+                .andThrow(new UnsupportedOperationException()).anyTimes();
+
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.setDelegate(delegate);
+        EasyMock.replay(delegate);
+        SQLContainer container = new SQLContainer(query);
+        Object id = container.addItem();
+        Object id2 = container.addItem();
+        container.getContainerProperty(id, "NAME").setValue("Herbert");
+        container.getContainerProperty(id, "AGE").setValue(30);
+        container.getContainerProperty(id2, "NAME").setValue("Larry");
+        container.getContainerProperty(id2, "AGE").setValue(50);
+        Assert.assertTrue(id2 instanceof TemporaryRowId);
+        Assert.assertSame(id2, container.lastItemId());
+        container.commit();
+        Object nextToLast = container.getIdByIndex(container.size() - 2);
+        Assert.assertFalse(nextToLast instanceof TemporaryRowId);
+        Assert.assertEquals("Herbert",
+                container.getContainerProperty(nextToLast, "NAME").getValue());
+        Assert.assertFalse(container.lastItemId() instanceof TemporaryRowId);
+        Assert.assertEquals("Larry",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+        EasyMock.verify(delegate);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void commit_freeformRemovedItem_shouldBeRemovedFromDB()
+            throws SQLException {
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(
+                delegate.removeRow(EasyMock.isA(Connection.class),
+                        EasyMock.isA(RowItem.class)))
+                .andAnswer(new IAnswer<Boolean>() {
+                    public Boolean answer() throws Throwable {
+                        Connection conn = (Connection) EasyMock
+                                .getCurrentArguments()[0];
+                        RowItem item = (RowItem) EasyMock.getCurrentArguments()[1];
+                        Statement statement = conn.createStatement();
+                        statement
+                                .executeUpdate("DELETE FROM people WHERE \"ID\"="
+                                        + item.getItemProperty("ID"));
+                        statement.close();
+                        return true;
+                    }
+                }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<String>() {
+                    public String answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        if (AllTests.db == DB.MSSQL) {
+                            int start = offset + 1;
+                            int end = offset + limit + 1;
+                            String q = "SELECT * FROM (SELECT row_number() OVER"
+                                    + " ( ORDER BY \"ID\" ASC) AS rownum, * FROM people)"
+                                    + " AS a WHERE a.rownum BETWEEN "
+                                    + start
+                                    + " AND " + end;
+                            return q;
+                        } else if (AllTests.db == DB.ORACLE) {
+                            int start = offset + 1;
+                            int end = offset + limit + 1;
+                            String q = "SELECT * FROM (SELECT x.*, ROWNUM AS r FROM"
+                                    + " (SELECT * FROM people ORDER BY \"ID\" ASC) x) "
+                                    + " WHERE r BETWEEN "
+                                    + start
+                                    + " AND "
+                                    + end;
+                            return q;
+                        } else {
+                            return "SELECT * FROM people LIMIT " + limit
+                                    + " OFFSET " + offset;
+                        }
+                    }
+                }).anyTimes();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        EasyMock.expect(delegate.getCountQuery())
+                .andThrow(new UnsupportedOperationException()).anyTimes();
+
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.setDelegate(delegate);
+        EasyMock.replay(delegate);
+        SQLContainer container = new SQLContainer(query);
+        Object last = container.lastItemId();
+        container.removeItem(last);
+        container.commit();
+        Assert.assertFalse(last.equals(container.lastItemId()));
+        EasyMock.verify(delegate);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void commit_freeformLastItemUpdated_shouldUpdateRowInDB()
+            throws SQLException {
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(
+                delegate.storeRow(EasyMock.isA(Connection.class),
+                        EasyMock.isA(RowItem.class)))
+                .andAnswer(new IAnswer<Integer>() {
+                    public Integer answer() throws Throwable {
+                        Connection conn = (Connection) EasyMock
+                                .getCurrentArguments()[0];
+                        RowItem item = (RowItem) EasyMock.getCurrentArguments()[1];
+                        Statement statement = conn.createStatement();
+                        statement.executeUpdate("UPDATE people SET \"NAME\"='"
+                                + item.getItemProperty("NAME").getValue()
+                                + "' WHERE \"ID\"="
+                                + item.getItemProperty("ID").getValue());
+                        statement.close();
+                        conn.commit();
+                        connectionPool.releaseConnection(conn);
+                        return 1;
+                    }
+                }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<String>() {
+                    public String answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        if (AllTests.db == DB.MSSQL) {
+                            int start = offset + 1;
+                            int end = offset + limit + 1;
+                            String q = "SELECT * FROM (SELECT row_number() OVER"
+                                    + " ( ORDER BY \"ID\" ASC) AS rownum, * FROM people)"
+                                    + " AS a WHERE a.rownum BETWEEN "
+                                    + start
+                                    + " AND " + end;
+                            return q;
+                        } else if (AllTests.db == DB.ORACLE) {
+                            int start = offset + 1;
+                            int end = offset + limit + 1;
+                            String q = "SELECT * FROM (SELECT x.*, ROWNUM AS r FROM"
+                                    + " (SELECT * FROM people ORDER BY \"ID\" ASC) x) "
+                                    + " WHERE r BETWEEN "
+                                    + start
+                                    + " AND "
+                                    + end;
+                            return q;
+                        } else {
+                            return "SELECT * FROM people LIMIT " + limit
+                                    + " OFFSET " + offset;
+                        }
+                    }
+                }).anyTimes();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        EasyMock.expect(delegate.getCountQuery())
+                .andThrow(new UnsupportedOperationException()).anyTimes();
+
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.setDelegate(delegate);
+        EasyMock.replay(delegate);
+        SQLContainer container = new SQLContainer(query);
+        Object last = container.lastItemId();
+        container.getContainerProperty(last, "NAME").setValue("Donald");
+        container.commit();
+        Assert.assertEquals("Donald",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+        EasyMock.verify(delegate);
+    }
+
+    @Test
+    public void rollback_freeformItemAdded_discardsAddedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        int size = container.size();
+        Object id = container.addItem();
+        container.getContainerProperty(id, "NAME").setValue("foo");
+        Assert.assertEquals(size + 1, container.size());
+        container.rollback();
+        Assert.assertEquals(size, container.size());
+        Assert.assertFalse("foo".equals(container.getContainerProperty(
+                container.lastItemId(), "NAME").getValue()));
+    }
+
+    @Test
+    public void rollback_freeformItemRemoved_restoresRemovedItem()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        int size = container.size();
+        Object last = container.lastItemId();
+        container.removeItem(last);
+        Assert.assertEquals(size - 1, container.size());
+        container.rollback();
+        Assert.assertEquals(size, container.size());
+        Assert.assertEquals(last, container.lastItemId());
+    }
+
+    @Test
+    public void rollback_freeformItemChanged_discardsChanges()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Object last = container.lastItemId();
+        container.getContainerProperty(last, "NAME").setValue("foo");
+        container.rollback();
+        Assert.assertFalse("foo".equals(container.getContainerProperty(
+                container.lastItemId(), "NAME").getValue()));
+    }
+
+    /*-
+     * TODO Removed test since currently the Vaadin test package structure
+     * does not allow testing protected methods. When it has been fixed
+     * then re-enable test.
+    @Test
+    public void itemChangeNotification_freeform_isModifiedReturnsTrue()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertFalse(container.isModified());
+        RowItem last = (RowItem) container.getItem(container.lastItemId());
+        container.itemChangeNotification(last);
+        Assert.assertTrue(container.isModified());
+    }
+    */
+
+    @Test
+    public void itemSetChangeListeners_freeform_shouldFire()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        ItemSetChangeListener listener = EasyMock
+                .createMock(ItemSetChangeListener.class);
+        listener.containerItemSetChange(EasyMock.isA(ItemSetChangeEvent.class));
+        EasyMock.replay(listener);
+
+        container.addListener(listener);
+        container.addItem();
+
+        EasyMock.verify(listener);
+    }
+
+    @Test
+    public void itemSetChangeListeners_freeformItemRemoved_shouldFire()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        ItemSetChangeListener listener = EasyMock
+                .createMock(ItemSetChangeListener.class);
+        listener.containerItemSetChange(EasyMock.isA(ItemSetChangeEvent.class));
+        EasyMock.expectLastCall().anyTimes();
+        EasyMock.replay(listener);
+
+        container.addListener(listener);
+        container.removeItem(container.lastItemId());
+
+        EasyMock.verify(listener);
+    }
+
+    @Test
+    public void removeListener_freeform_shouldNotFire() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        ItemSetChangeListener listener = EasyMock
+                .createMock(ItemSetChangeListener.class);
+        EasyMock.replay(listener);
+
+        container.addListener(listener);
+        container.removeListener(listener);
+        container.addItem();
+
+        EasyMock.verify(listener);
+    }
+
+    @Test
+    public void isModified_freeformRemovedItem_returnsTrue()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertFalse(container.isModified());
+        container.removeItem(container.lastItemId());
+        Assert.assertTrue(container.isModified());
+    }
+
+    @Test
+    public void isModified_freeformAddedItem_returnsTrue() throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertFalse(container.isModified());
+        container.addItem();
+        Assert.assertTrue(container.isModified());
+    }
+
+    @Test
+    public void isModified_freeformChangedItem_returnsTrue()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Assert.assertFalse(container.isModified());
+        container.getContainerProperty(container.lastItemId(), "NAME")
+                .setValue("foo");
+        Assert.assertTrue(container.isModified());
+    }
+
+    @Test
+    public void getSortableContainerPropertyIds_freeform_returnsAllPropertyIds()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Collection<?> sortableIds = container.getSortableContainerPropertyIds();
+        Assert.assertTrue(sortableIds.contains("ID"));
+        Assert.assertTrue(sortableIds.contains("NAME"));
+        Assert.assertTrue(sortableIds.contains("AGE"));
+        Assert.assertEquals(3, sortableIds.size());
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void addOrderBy_freeform_shouldReorderResults() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        final ArrayList<OrderBy> orderBys = new ArrayList<OrderBy>();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+            public Object answer() throws Throwable {
+                List<OrderBy> orders = (List<OrderBy>) EasyMock
+                        .getCurrentArguments()[0];
+                orderBys.clear();
+                orderBys.addAll(orders);
+                return null;
+            }
+        }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<String>() {
+                    public String answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        if (AllTests.db == DB.MSSQL) {
+                            SQLGenerator gen = new MSSQLGenerator();
+                            if (orderBys == null || orderBys.isEmpty()) {
+                                List<OrderBy> ob = new ArrayList<OrderBy>();
+                                ob.add(new OrderBy("ID", true));
+                                return gen.generateSelectQuery("people", null,
+                                        ob, offset, limit, null)
+                                        .getQueryString();
+                            } else {
+                                return gen.generateSelectQuery("people", null,
+                                        orderBys, offset, limit, null)
+                                        .getQueryString();
+                            }
+                        } else if (AllTests.db == DB.ORACLE) {
+                            SQLGenerator gen = new OracleGenerator();
+                            if (orderBys == null || orderBys.isEmpty()) {
+                                List<OrderBy> ob = new ArrayList<OrderBy>();
+                                ob.add(new OrderBy("ID", true));
+                                return gen.generateSelectQuery("people", null,
+                                        ob, offset, limit, null)
+                                        .getQueryString();
+                            } else {
+                                return gen.generateSelectQuery("people", null,
+                                        orderBys, offset, limit, null)
+                                        .getQueryString();
+                            }
+                        } else {
+                            StringBuffer query = new StringBuffer(
+                                    "SELECT * FROM people");
+                            if (!orderBys.isEmpty()) {
+                                query.append(" ORDER BY ");
+                                for (OrderBy orderBy : orderBys) {
+                                    query.append("\"" + orderBy.getColumn()
+                                            + "\"");
+                                    if (orderBy.isAscending()) {
+                                        query.append(" ASC");
+                                    } else {
+                                        query.append(" DESC");
+                                    }
+                                }
+                            }
+                            query.append(" LIMIT ").append(limit)
+                                    .append(" OFFSET ").append(offset);
+                            return query.toString();
+                        }
+                    }
+                }).anyTimes();
+        EasyMock.expect(delegate.getCountQuery())
+                .andThrow(new UnsupportedOperationException()).anyTimes();
+
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+        SQLContainer container = new SQLContainer(query);
+        // Ville, Kalle, Pelle, Börje
+        Assert.assertEquals("Ville",
+                container.getContainerProperty(container.firstItemId(), "NAME")
+                        .getValue());
+        Assert.assertEquals("Börje",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        container.addOrderBy(new OrderBy("NAME", true));
+        // Börje, Kalle, Pelle, Ville
+        Assert.assertEquals("Börje",
+                container.getContainerProperty(container.firstItemId(), "NAME")
+                        .getValue());
+        Assert.assertEquals("Ville",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        EasyMock.verify(delegate);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void addOrderBy_freeformIllegalColumn_shouldFail()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        container.addOrderBy(new OrderBy("asdf", true));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void sort_freeform_sortsByName() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        final ArrayList<OrderBy> orderBys = new ArrayList<OrderBy>();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+            public Object answer() throws Throwable {
+                List<OrderBy> orders = (List<OrderBy>) EasyMock
+                        .getCurrentArguments()[0];
+                orderBys.clear();
+                orderBys.addAll(orders);
+                return null;
+            }
+        }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<String>() {
+                    public String answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        if (AllTests.db == DB.MSSQL) {
+                            SQLGenerator gen = new MSSQLGenerator();
+                            if (orderBys == null || orderBys.isEmpty()) {
+                                List<OrderBy> ob = new ArrayList<OrderBy>();
+                                ob.add(new OrderBy("ID", true));
+                                return gen.generateSelectQuery("people", null,
+                                        ob, offset, limit, null)
+                                        .getQueryString();
+                            } else {
+                                return gen.generateSelectQuery("people", null,
+                                        orderBys, offset, limit, null)
+                                        .getQueryString();
+                            }
+                        } else if (AllTests.db == DB.ORACLE) {
+                            SQLGenerator gen = new OracleGenerator();
+                            if (orderBys == null || orderBys.isEmpty()) {
+                                List<OrderBy> ob = new ArrayList<OrderBy>();
+                                ob.add(new OrderBy("ID", true));
+                                return gen.generateSelectQuery("people", null,
+                                        ob, offset, limit, null)
+                                        .getQueryString();
+                            } else {
+                                return gen.generateSelectQuery("people", null,
+                                        orderBys, offset, limit, null)
+                                        .getQueryString();
+                            }
+                        } else {
+                            StringBuffer query = new StringBuffer(
+                                    "SELECT * FROM people");
+                            if (!orderBys.isEmpty()) {
+                                query.append(" ORDER BY ");
+                                for (OrderBy orderBy : orderBys) {
+                                    query.append("\"" + orderBy.getColumn()
+                                            + "\"");
+                                    if (orderBy.isAscending()) {
+                                        query.append(" ASC");
+                                    } else {
+                                        query.append(" DESC");
+                                    }
+                                }
+                            }
+                            query.append(" LIMIT ").append(limit)
+                                    .append(" OFFSET ").append(offset);
+                            return query.toString();
+                        }
+                    }
+                }).anyTimes();
+        EasyMock.expect(delegate.getCountQuery())
+                .andThrow(new UnsupportedOperationException()).anyTimes();
+        EasyMock.replay(delegate);
+
+        query.setDelegate(delegate);
+        SQLContainer container = new SQLContainer(query);
+        // Ville, Kalle, Pelle, Börje
+        Assert.assertEquals("Ville",
+                container.getContainerProperty(container.firstItemId(), "NAME")
+                        .getValue());
+        Assert.assertEquals("Börje",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        container.sort(new Object[] { "NAME" }, new boolean[] { true });
+
+        // Börje, Kalle, Pelle, Ville
+        Assert.assertEquals("Börje",
+                container.getContainerProperty(container.firstItemId(), "NAME")
+                        .getValue());
+        Assert.assertEquals("Ville",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        EasyMock.verify(delegate);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void addFilter_freeform_filtersResults() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformStatementDelegate delegate = EasyMock
+                .createMock(FreeformStatementDelegate.class);
+        final ArrayList<Filter> filters = new ArrayList<Filter>();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+            public Object answer() throws Throwable {
+                List<Filter> orders = (List<Filter>) EasyMock
+                        .getCurrentArguments()[0];
+                filters.clear();
+                filters.addAll(orders);
+                return null;
+            }
+        }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryStatement(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    public StatementHelper answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        return FreeformQueryUtil.getQueryWithFilters(filters,
+                                offset, limit);
+                    }
+                }).anyTimes();
+        EasyMock.expect(delegate.getCountStatement())
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    @SuppressWarnings("deprecation")
+                    public StatementHelper answer() throws Throwable {
+                        StatementHelper sh = new StatementHelper();
+                        StringBuffer query = new StringBuffer(
+                                "SELECT COUNT(*) FROM people");
+                        if (!filters.isEmpty()) {
+                            query.append(QueryBuilder
+                                    .getWhereStringForFilters(filters, sh));
+                        }
+                        sh.setQueryString(query.toString());
+                        return sh;
+                    }
+                }).anyTimes();
+
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+        SQLContainer container = new SQLContainer(query);
+        // Ville, Kalle, Pelle, Börje
+        Assert.assertEquals(4, container.size());
+        Assert.assertEquals("Börje",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        container.addContainerFilter(new Like("NAME", "%lle"));
+        // Ville, Kalle, Pelle
+        Assert.assertEquals(3, container.size());
+        Assert.assertEquals("Pelle",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        EasyMock.verify(delegate);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void addContainerFilter_filtersResults() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformStatementDelegate delegate = EasyMock
+                .createMock(FreeformStatementDelegate.class);
+        final ArrayList<Filter> filters = new ArrayList<Filter>();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+            public Object answer() throws Throwable {
+                List<Filter> orders = (List<Filter>) EasyMock
+                        .getCurrentArguments()[0];
+                filters.clear();
+                filters.addAll(orders);
+                return null;
+            }
+        }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryStatement(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    public StatementHelper answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        return FreeformQueryUtil.getQueryWithFilters(filters,
+                                offset, limit);
+                    }
+                }).anyTimes();
+        EasyMock.expect(delegate.getCountStatement())
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    @SuppressWarnings("deprecation")
+                    public StatementHelper answer() throws Throwable {
+                        StatementHelper sh = new StatementHelper();
+                        StringBuffer query = new StringBuffer(
+                                "SELECT COUNT(*) FROM people");
+                        if (!filters.isEmpty()) {
+                            query.append(QueryBuilder
+                                    .getWhereStringForFilters(filters, sh));
+                        }
+                        sh.setQueryString(query.toString());
+                        return sh;
+                    }
+                }).anyTimes();
+
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+        SQLContainer container = new SQLContainer(query);
+        // Ville, Kalle, Pelle, Börje
+        Assert.assertEquals(4, container.size());
+
+        container.addContainerFilter("NAME", "Vi", false, false);
+
+        // Ville
+        Assert.assertEquals(1, container.size());
+        Assert.assertEquals("Ville",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        EasyMock.verify(delegate);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void addContainerFilter_ignoreCase_filtersResults()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformStatementDelegate delegate = EasyMock
+                .createMock(FreeformStatementDelegate.class);
+        final ArrayList<Filter> filters = new ArrayList<Filter>();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+            public Object answer() throws Throwable {
+                List<Filter> orders = (List<Filter>) EasyMock
+                        .getCurrentArguments()[0];
+                filters.clear();
+                filters.addAll(orders);
+                return null;
+            }
+        }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryStatement(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    public StatementHelper answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        return FreeformQueryUtil.getQueryWithFilters(filters,
+                                offset, limit);
+                    }
+                }).anyTimes();
+        EasyMock.expect(delegate.getCountStatement())
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    public StatementHelper answer() throws Throwable {
+                        StatementHelper sh = new StatementHelper();
+                        StringBuffer query = new StringBuffer(
+                                "SELECT COUNT(*) FROM people");
+                        if (!filters.isEmpty()) {
+                            query.append(QueryBuilder
+                                    .getWhereStringForFilters(filters, sh));
+                        }
+                        sh.setQueryString(query.toString());
+                        return sh;
+                    }
+                }).anyTimes();
+
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+        SQLContainer container = new SQLContainer(query);
+        // Ville, Kalle, Pelle, Börje
+        Assert.assertEquals(4, container.size());
+
+        // FIXME LIKE %asdf% doesn't match a string that begins with asdf
+        container.addContainerFilter("NAME", "vi", true, true);
+
+        // Ville
+        Assert.assertEquals(1, container.size());
+        Assert.assertEquals("Ville",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        EasyMock.verify(delegate);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void removeAllContainerFilters_freeform_noFiltering()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformStatementDelegate delegate = EasyMock
+                .createMock(FreeformStatementDelegate.class);
+        final ArrayList<Filter> filters = new ArrayList<Filter>();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+            public Object answer() throws Throwable {
+                List<Filter> orders = (List<Filter>) EasyMock
+                        .getCurrentArguments()[0];
+                filters.clear();
+                filters.addAll(orders);
+                return null;
+            }
+        }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryStatement(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    public StatementHelper answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        return FreeformQueryUtil.getQueryWithFilters(filters,
+                                offset, limit);
+                    }
+                }).anyTimes();
+        EasyMock.expect(delegate.getCountStatement())
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    @SuppressWarnings("deprecation")
+                    public StatementHelper answer() throws Throwable {
+                        StatementHelper sh = new StatementHelper();
+                        StringBuffer query = new StringBuffer(
+                                "SELECT COUNT(*) FROM people");
+                        if (!filters.isEmpty()) {
+                            query.append(QueryBuilder
+                                    .getWhereStringForFilters(filters, sh));
+                        }
+                        sh.setQueryString(query.toString());
+                        return sh;
+                    }
+                }).anyTimes();
+
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+        SQLContainer container = new SQLContainer(query);
+        // Ville, Kalle, Pelle, Börje
+        Assert.assertEquals(4, container.size());
+
+        container.addContainerFilter("NAME", "Vi", false, false);
+
+        // Ville
+        Assert.assertEquals(1, container.size());
+        Assert.assertEquals("Ville",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        container.removeAllContainerFilters();
+
+        Assert.assertEquals(4, container.size());
+        Assert.assertEquals("Börje",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        EasyMock.verify(delegate);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void removeContainerFilters_freeform_noFiltering()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformStatementDelegate delegate = EasyMock
+                .createMock(FreeformStatementDelegate.class);
+        final ArrayList<Filter> filters = new ArrayList<Filter>();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+            public Object answer() throws Throwable {
+                List<Filter> orders = (List<Filter>) EasyMock
+                        .getCurrentArguments()[0];
+                filters.clear();
+                filters.addAll(orders);
+                return null;
+            }
+        }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryStatement(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    public StatementHelper answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        return FreeformQueryUtil.getQueryWithFilters(filters,
+                                offset, limit);
+                    }
+                }).anyTimes();
+        EasyMock.expect(delegate.getCountStatement())
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    @SuppressWarnings("deprecation")
+                    public StatementHelper answer() throws Throwable {
+                        StatementHelper sh = new StatementHelper();
+                        StringBuffer query = new StringBuffer(
+                                "SELECT COUNT(*) FROM people");
+                        if (!filters.isEmpty()) {
+                            query.append(QueryBuilder
+                                    .getWhereStringForFilters(filters, sh));
+                        }
+                        sh.setQueryString(query.toString());
+                        return sh;
+                    }
+                }).anyTimes();
+
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+        SQLContainer container = new SQLContainer(query);
+        // Ville, Kalle, Pelle, Börje
+        Assert.assertEquals(4, container.size());
+
+        container.addContainerFilter("NAME", "Vi", false, true);
+
+        // Ville
+        Assert.assertEquals(1, container.size());
+        Assert.assertEquals("Ville",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        container.removeContainerFilters("NAME");
+
+        Assert.assertEquals(4, container.size());
+        Assert.assertEquals("Börje",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        EasyMock.verify(delegate);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void addFilter_freeformBufferedItems_alsoFiltersBufferedItems()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformStatementDelegate delegate = EasyMock
+                .createMock(FreeformStatementDelegate.class);
+        final ArrayList<Filter> filters = new ArrayList<Filter>();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+            public Object answer() throws Throwable {
+                List<Filter> orders = (List<Filter>) EasyMock
+                        .getCurrentArguments()[0];
+                filters.clear();
+                filters.addAll(orders);
+                return null;
+            }
+        }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryStatement(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    public StatementHelper answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        return FreeformQueryUtil.getQueryWithFilters(filters,
+                                offset, limit);
+                    }
+                }).anyTimes();
+        EasyMock.expect(delegate.getCountStatement())
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    @SuppressWarnings("deprecation")
+                    public StatementHelper answer() throws Throwable {
+                        StatementHelper sh = new StatementHelper();
+                        StringBuffer query = new StringBuffer(
+                                "SELECT COUNT(*) FROM people");
+                        if (!filters.isEmpty()) {
+                            query.append(QueryBuilder
+                                    .getWhereStringForFilters(filters, sh));
+                        }
+                        sh.setQueryString(query.toString());
+                        return sh;
+                    }
+                }).anyTimes();
+
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+        SQLContainer container = new SQLContainer(query);
+        // Ville, Kalle, Pelle, Börje
+        Assert.assertEquals(4, container.size());
+        Assert.assertEquals("Börje",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        Object id1 = container.addItem();
+        container.getContainerProperty(id1, "NAME").setValue("Palle");
+        Object id2 = container.addItem();
+        container.getContainerProperty(id2, "NAME").setValue("Bengt");
+
+        container.addContainerFilter(new Like("NAME", "%lle"));
+
+        // Ville, Kalle, Pelle, Palle
+        Assert.assertEquals(4, container.size());
+        Assert.assertEquals(
+                "Ville",
+                container.getContainerProperty(container.getIdByIndex(0),
+                        "NAME").getValue());
+        Assert.assertEquals(
+                "Kalle",
+                container.getContainerProperty(container.getIdByIndex(1),
+                        "NAME").getValue());
+        Assert.assertEquals(
+                "Pelle",
+                container.getContainerProperty(container.getIdByIndex(2),
+                        "NAME").getValue());
+        Assert.assertEquals(
+                "Palle",
+                container.getContainerProperty(container.getIdByIndex(3),
+                        "NAME").getValue());
+
+        Assert.assertNull(container.getIdByIndex(4));
+        Assert.assertNull(container.nextItemId(container.getIdByIndex(3)));
+
+        Assert.assertFalse(container.containsId(id2));
+        Assert.assertFalse(container.getItemIds().contains(id2));
+
+        Assert.assertNull(container.getItem(id2));
+        Assert.assertEquals(-1, container.indexOfId(id2));
+
+        Assert.assertNotSame(id2, container.lastItemId());
+        Assert.assertSame(id1, container.lastItemId());
+
+        EasyMock.verify(delegate);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void sort_freeformBufferedItems_sortsBufferedItemsLastInOrderAdded()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        final ArrayList<OrderBy> orderBys = new ArrayList<OrderBy>();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+            public Object answer() throws Throwable {
+                List<OrderBy> orders = (List<OrderBy>) EasyMock
+                        .getCurrentArguments()[0];
+                orderBys.clear();
+                orderBys.addAll(orders);
+                return null;
+            }
+        }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<String>() {
+                    public String answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        if (AllTests.db == DB.MSSQL) {
+                            SQLGenerator gen = new MSSQLGenerator();
+                            if (orderBys == null || orderBys.isEmpty()) {
+                                List<OrderBy> ob = new ArrayList<OrderBy>();
+                                ob.add(new OrderBy("ID", true));
+                                return gen.generateSelectQuery("people", null,
+                                        ob, offset, limit, null)
+                                        .getQueryString();
+                            } else {
+                                return gen.generateSelectQuery("people", null,
+                                        orderBys, offset, limit, null)
+                                        .getQueryString();
+                            }
+                        } else if (AllTests.db == DB.ORACLE) {
+                            SQLGenerator gen = new OracleGenerator();
+                            if (orderBys == null || orderBys.isEmpty()) {
+                                List<OrderBy> ob = new ArrayList<OrderBy>();
+                                ob.add(new OrderBy("ID", true));
+                                return gen.generateSelectQuery("people", null,
+                                        ob, offset, limit, null)
+                                        .getQueryString();
+                            } else {
+                                return gen.generateSelectQuery("people", null,
+                                        orderBys, offset, limit, null)
+                                        .getQueryString();
+                            }
+                        } else {
+                            StringBuffer query = new StringBuffer(
+                                    "SELECT * FROM people");
+                            if (!orderBys.isEmpty()) {
+                                query.append(" ORDER BY ");
+                                for (OrderBy orderBy : orderBys) {
+                                    query.append("\"" + orderBy.getColumn()
+                                            + "\"");
+                                    if (orderBy.isAscending()) {
+                                        query.append(" ASC");
+                                    } else {
+                                        query.append(" DESC");
+                                    }
+                                }
+                            }
+                            query.append(" LIMIT ").append(limit)
+                                    .append(" OFFSET ").append(offset);
+                            return query.toString();
+                        }
+                    }
+                }).anyTimes();
+        EasyMock.expect(delegate.getCountQuery())
+                .andThrow(new UnsupportedOperationException()).anyTimes();
+        EasyMock.replay(delegate);
+
+        query.setDelegate(delegate);
+        SQLContainer container = new SQLContainer(query);
+        // Ville, Kalle, Pelle, Börje
+        Assert.assertEquals("Ville",
+                container.getContainerProperty(container.firstItemId(), "NAME")
+                        .getValue());
+        Assert.assertEquals("Börje",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        Object id1 = container.addItem();
+        container.getContainerProperty(id1, "NAME").setValue("Wilbert");
+        Object id2 = container.addItem();
+        container.getContainerProperty(id2, "NAME").setValue("Albert");
+
+        container.sort(new Object[] { "NAME" }, new boolean[] { true });
+
+        // Börje, Kalle, Pelle, Ville, Wilbert, Albert
+        Assert.assertEquals("Börje",
+                container.getContainerProperty(container.firstItemId(), "NAME")
+                        .getValue());
+        Assert.assertEquals(
+                "Wilbert",
+                container.getContainerProperty(
+                        container.getIdByIndex(container.size() - 2), "NAME")
+                        .getValue());
+        Assert.assertEquals("Albert",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        EasyMock.verify(delegate);
+    }
+
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/TicketTests.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/TicketTests.java
new file mode 100644 (file)
index 0000000..513bdf3
--- /dev/null
@@ -0,0 +1,157 @@
+package com.vaadin.tests.server.container.sqlcontainer;
+
+import java.math.BigDecimal;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.SQLContainer;
+import com.vaadin.data.util.connection.SimpleJDBCConnectionPool;
+import com.vaadin.data.util.filter.Compare.Equal;
+import com.vaadin.data.util.query.FreeformQuery;
+import com.vaadin.data.util.query.FreeformStatementDelegate;
+import com.vaadin.data.util.query.TableQuery;
+import com.vaadin.data.util.query.generator.StatementHelper;
+import com.vaadin.data.util.query.generator.filter.QueryBuilder;
+import com.vaadin.tests.server.container.sqlcontainer.AllTests.DB;
+import com.vaadin.ui.Table;
+import com.vaadin.ui.Window;
+
+public class TicketTests {
+
+    private SimpleJDBCConnectionPool connectionPool;
+
+    @Before
+    public void setUp() throws SQLException {
+        connectionPool = new SimpleJDBCConnectionPool(AllTests.dbDriver,
+                AllTests.dbURL, AllTests.dbUser, AllTests.dbPwd, 2, 2);
+        DataGenerator.addPeopleToDatabase(connectionPool);
+    }
+
+    @Test
+    public void ticket5867_throwsIllegalState_transactionAlreadyActive()
+            throws SQLException {
+        SQLContainer container = new SQLContainer(new FreeformQuery(
+                "SELECT * FROM people", Arrays.asList("ID"), connectionPool));
+        Table table = new Table();
+        Window w = new Window();
+        w.addComponent(table);
+        table.setContainerDataSource(container);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void ticket6136_freeform_ageIs18() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformStatementDelegate delegate = EasyMock
+                .createMock(FreeformStatementDelegate.class);
+        final ArrayList<Filter> filters = new ArrayList<Filter>();
+        delegate.setFilters(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setOrderBy(null);
+        EasyMock.expectLastCall().anyTimes();
+        delegate.setFilters(EasyMock.isA(List.class));
+        EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+            public Object answer() throws Throwable {
+                List<Filter> orders = (List<Filter>) EasyMock
+                        .getCurrentArguments()[0];
+                filters.clear();
+                filters.addAll(orders);
+                return null;
+            }
+        }).anyTimes();
+        EasyMock.expect(
+                delegate.getQueryStatement(EasyMock.anyInt(), EasyMock.anyInt()))
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    public StatementHelper answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        int offset = (Integer) (args[0]);
+                        int limit = (Integer) (args[1]);
+                        return FreeformQueryUtil.getQueryWithFilters(filters,
+                                offset, limit);
+                    }
+                }).anyTimes();
+        EasyMock.expect(delegate.getCountStatement())
+                .andAnswer(new IAnswer<StatementHelper>() {
+                    @SuppressWarnings("deprecation")
+                    public StatementHelper answer() throws Throwable {
+                        StatementHelper sh = new StatementHelper();
+                        StringBuffer query = new StringBuffer(
+                                "SELECT COUNT(*) FROM people");
+                        if (!filters.isEmpty()) {
+                            query.append(QueryBuilder.getWhereStringForFilters(
+                                    filters, sh));
+                        }
+                        sh.setQueryString(query.toString());
+                        return sh;
+                    }
+                }).anyTimes();
+
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+        SQLContainer container = new SQLContainer(query);
+        // Ville, Kalle, Pelle, Börje
+        Assert.assertEquals(4, container.size());
+        Assert.assertEquals("Börje",
+                container.getContainerProperty(container.lastItemId(), "NAME")
+                        .getValue());
+
+        container.addContainerFilter(new Equal("AGE", 18));
+        // Pelle
+        Assert.assertEquals(1, container.size());
+        Assert.assertEquals("Pelle",
+                container.getContainerProperty(container.firstItemId(), "NAME")
+                        .getValue());
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertEquals(new BigDecimal(18), container
+                    .getContainerProperty(container.firstItemId(), "AGE")
+                    .getValue());
+        } else {
+            Assert.assertEquals(
+                    18,
+                    container.getContainerProperty(container.firstItemId(),
+                            "AGE").getValue());
+        }
+
+        EasyMock.verify(delegate);
+    }
+
+    @Test
+    public void ticket6136_table_ageIs18() throws SQLException {
+        TableQuery query = new TableQuery("people", connectionPool,
+                AllTests.sqlGen);
+        SQLContainer container = new SQLContainer(query);
+        // Ville, Kalle, Pelle, Börje
+        Assert.assertEquals(4, container.size());
+
+        container.addContainerFilter(new Equal("AGE", 18));
+
+        // Pelle
+        Assert.assertEquals(1, container.size());
+        Assert.assertEquals("Pelle",
+                container.getContainerProperty(container.firstItemId(), "NAME")
+                        .getValue());
+        if (AllTests.db == DB.ORACLE) {
+            Assert.assertEquals(new BigDecimal(18), container
+                    .getContainerProperty(container.firstItemId(), "AGE")
+                    .getValue());
+        } else {
+            Assert.assertEquals(
+                    18,
+                    container.getContainerProperty(container.firstItemId(),
+                            "AGE").getValue());
+        }
+    }
+
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/UtilTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/UtilTest.java
new file mode 100644 (file)
index 0000000..b998396
--- /dev/null
@@ -0,0 +1,52 @@
+package com.vaadin.tests.server.container.sqlcontainer;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+
+import com.vaadin.data.util.SQLUtil;
+
+public class UtilTest {
+
+    @Test
+    public void escapeSQL_noQuotes_returnsSameString() {
+        Assert.assertEquals("asdf", SQLUtil.escapeSQL("asdf"));
+    }
+
+    @Test
+    public void escapeSQL_singleQuotes_returnsEscapedString() {
+        Assert.assertEquals("O''Brien", SQLUtil.escapeSQL("O'Brien"));
+    }
+
+    @Test
+    public void escapeSQL_severalQuotes_returnsEscapedString() {
+        Assert.assertEquals("asdf''ghjk''qwerty",
+                       SQLUtil.escapeSQL("asdf'ghjk'qwerty"));
+    }
+
+    @Test
+    public void escapeSQL_doubleQuotes_returnsEscapedString() {
+        Assert.assertEquals("asdf\\\"foo", SQLUtil.escapeSQL("asdf\"foo"));
+    }
+
+    @Test
+    public void escapeSQL_multipleDoubleQuotes_returnsEscapedString() {
+        Assert.assertEquals("asdf\\\"foo\\\"bar",
+                       SQLUtil.escapeSQL("asdf\"foo\"bar"));
+    }
+
+    @Test
+    public void escapeSQL_backslashes_returnsEscapedString() {
+        Assert.assertEquals("foo\\\\nbar\\\\r", SQLUtil.escapeSQL("foo\\nbar\\r"));
+    }
+
+    @Test
+    public void escapeSQL_x00_removesX00() {
+        Assert.assertEquals("foobar", SQLUtil.escapeSQL("foo\\x00bar"));
+    }
+
+    @Test
+    public void escapeSQL_x1a_removesX1a() {
+        Assert.assertEquals("foobar", SQLUtil.escapeSQL("foo\\x1abar"));
+    }
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/connection/J2EEConnectionPoolTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/connection/J2EEConnectionPoolTest.java
new file mode 100644 (file)
index 0000000..7f9f5a0
--- /dev/null
@@ -0,0 +1,101 @@
+package com.vaadin.tests.server.container.sqlcontainer.connection;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+
+import junit.framework.Assert;
+
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import com.vaadin.data.util.connection.J2EEConnectionPool;
+
+public class J2EEConnectionPoolTest {
+
+    @Test
+    public void reserveConnection_dataSourceSpecified_shouldReturnValidConnection()
+            throws SQLException {
+        Connection connection = EasyMock.createMock(Connection.class);
+        connection.setAutoCommit(false);
+        EasyMock.expectLastCall();
+        DataSource ds = EasyMock.createMock(DataSource.class);
+        ds.getConnection();
+        EasyMock.expectLastCall().andReturn(connection);
+        EasyMock.replay(connection, ds);
+
+        J2EEConnectionPool pool = new J2EEConnectionPool(ds);
+        Connection c = pool.reserveConnection();
+        Assert.assertEquals(connection, c);
+        EasyMock.verify(connection, ds);
+    }
+
+    @Test
+    public void releaseConnection_shouldCloseConnection() throws SQLException {
+        Connection connection = EasyMock.createMock(Connection.class);
+        connection.setAutoCommit(false);
+        EasyMock.expectLastCall();
+        connection.close();
+        EasyMock.expectLastCall();
+        DataSource ds = EasyMock.createMock(DataSource.class);
+        ds.getConnection();
+        EasyMock.expectLastCall().andReturn(connection);
+        EasyMock.replay(connection, ds);
+
+        J2EEConnectionPool pool = new J2EEConnectionPool(ds);
+        Connection c = pool.reserveConnection();
+        Assert.assertEquals(connection, c);
+        pool.releaseConnection(c);
+        EasyMock.verify(connection, ds);
+    }
+
+    @Test
+    public void reserveConnection_dataSourceLookedUp_shouldReturnValidConnection()
+            throws SQLException, NamingException {
+        Connection connection = EasyMock.createMock(Connection.class);
+        connection.setAutoCommit(false);
+        EasyMock.expectLastCall();
+        connection.close();
+        EasyMock.expectLastCall();
+
+        DataSource ds = EasyMock.createMock(DataSource.class);
+        ds.getConnection();
+        EasyMock.expectLastCall().andReturn(connection);
+
+        System.setProperty("java.naming.factory.initial",
+                "com.vaadin.tests.server.container.sqlcontainer.connection.MockInitialContextFactory");
+        Context context = EasyMock.createMock(Context.class);
+        context.lookup("testDataSource");
+        EasyMock.expectLastCall().andReturn(ds);
+        MockInitialContextFactory.setMockContext(context);
+
+        EasyMock.replay(context, connection, ds);
+
+        J2EEConnectionPool pool = new J2EEConnectionPool("testDataSource");
+        Connection c = pool.reserveConnection();
+        Assert.assertEquals(connection, c);
+        pool.releaseConnection(c);
+        EasyMock.verify(context, connection, ds);
+    }
+
+    @Test(expected = SQLException.class)
+    public void reserveConnection_nonExistantDataSourceLookedUp_shouldFail()
+            throws SQLException, NamingException {
+        System.setProperty("java.naming.factory.initial",
+                "com.vaadin.addon.sqlcontainer.connection.MockInitialContextFactory");
+        Context context = EasyMock.createMock(Context.class);
+        context.lookup("foo");
+        EasyMock.expectLastCall().andThrow(new NamingException("fail"));
+        MockInitialContextFactory.setMockContext(context);
+
+        EasyMock.replay(context);
+
+        J2EEConnectionPool pool = new J2EEConnectionPool("foo");
+        Connection c = pool.reserveConnection();
+        EasyMock.verify(context);
+    }
+
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/connection/MockInitialContextFactory.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/connection/MockInitialContextFactory.java
new file mode 100644 (file)
index 0000000..0d3d266
--- /dev/null
@@ -0,0 +1,24 @@
+package com.vaadin.tests.server.container.sqlcontainer.connection;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.spi.InitialContextFactory;
+
+/**
+ * Provides a JNDI initial context factory for the MockContext.
+ */
+public class MockInitialContextFactory implements InitialContextFactory {
+    private static Context mockCtx = null;
+
+    public static void setMockContext(Context ctx) {
+        mockCtx = ctx;
+    }
+
+    public Context getInitialContext(java.util.Hashtable<?, ?> environment)
+            throws NamingException {
+        if (mockCtx == null) {
+            throw new IllegalStateException("mock context was not set.");
+        }
+        return mockCtx;
+    }
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/connection/SimpleJDBCConnectionPoolTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/connection/SimpleJDBCConnectionPoolTest.java
new file mode 100644 (file)
index 0000000..018d893
--- /dev/null
@@ -0,0 +1,172 @@
+package com.vaadin.tests.server.container.sqlcontainer.connection;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import junit.framework.Assert;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.connection.JDBCConnectionPool;
+import com.vaadin.data.util.connection.SimpleJDBCConnectionPool;
+import com.vaadin.tests.server.container.sqlcontainer.AllTests;
+
+public class SimpleJDBCConnectionPoolTest {
+    private JDBCConnectionPool connectionPool;
+
+    @Before
+    public void setUp() throws SQLException {
+        connectionPool = new SimpleJDBCConnectionPool(AllTests.dbDriver,
+                AllTests.dbURL, AllTests.dbUser, AllTests.dbPwd, 2, 2);
+    }
+
+    @Test
+    public void reserveConnection_reserveNewConnection_returnsConnection()
+            throws SQLException {
+        Connection conn = connectionPool.reserveConnection();
+        Assert.assertNotNull(conn);
+    }
+
+    @Test
+    public void releaseConnection_releaseUnused_shouldNotThrowException()
+            throws SQLException {
+        Connection conn = connectionPool.reserveConnection();
+        connectionPool.releaseConnection(conn);
+        Assert.assertFalse(conn.isClosed());
+    }
+
+    @Test(expected = SQLException.class)
+    public void reserveConnection_noConnectionsLeft_shouldFail()
+            throws SQLException {
+        try {
+            connectionPool.reserveConnection();
+            connectionPool.reserveConnection();
+        } catch (SQLException e) {
+            e.printStackTrace();
+            Assert.fail("Exception before all connections used! "
+                    + e.getMessage());
+        }
+
+        connectionPool.reserveConnection();
+        Assert.fail("Reserving connection didn't fail even though no connections are available!");
+    }
+
+    @Test
+    public void reserveConnection_oneConnectionLeft_returnsConnection()
+            throws SQLException {
+        try {
+            connectionPool.reserveConnection();
+        } catch (SQLException e) {
+            e.printStackTrace();
+            Assert.fail("Exception before all connections used! "
+                    + e.getMessage());
+        }
+
+        Connection conn = connectionPool.reserveConnection();
+        Assert.assertNotNull(conn);
+    }
+
+    @Test
+    public void reserveConnection_oneConnectionJustReleased_returnsConnection()
+            throws SQLException {
+        Connection conn2 = null;
+        try {
+            connectionPool.reserveConnection();
+            conn2 = connectionPool.reserveConnection();
+        } catch (SQLException e) {
+            e.printStackTrace();
+            Assert.fail("Exception before all connections used! "
+                    + e.getMessage());
+        }
+
+        connectionPool.releaseConnection(conn2);
+
+        connectionPool.reserveConnection();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void construct_allParametersNull_shouldFail() throws SQLException {
+        SimpleJDBCConnectionPool cp = new SimpleJDBCConnectionPool(null, null,
+                null, null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void construct_onlyDriverNameGiven_shouldFail() throws SQLException {
+        SimpleJDBCConnectionPool cp = new SimpleJDBCConnectionPool(
+                AllTests.dbDriver, null, null, null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void construct_onlyDriverNameAndUrlGiven_shouldFail()
+            throws SQLException {
+        SimpleJDBCConnectionPool cp = new SimpleJDBCConnectionPool(
+                AllTests.dbDriver, AllTests.dbURL, null, null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void construct_onlyDriverNameAndUrlAndUserGiven_shouldFail()
+            throws SQLException {
+        SimpleJDBCConnectionPool cp = new SimpleJDBCConnectionPool(
+                AllTests.dbDriver, AllTests.dbURL, AllTests.dbUser, null);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void construct_nonExistingDriver_shouldFail() throws SQLException {
+        SimpleJDBCConnectionPool cp = new SimpleJDBCConnectionPool("foo",
+                AllTests.dbURL, AllTests.dbUser, AllTests.dbPwd);
+    }
+
+    @Test
+    public void reserveConnection_newConnectionOpened_shouldSucceed()
+            throws SQLException {
+        connectionPool = new SimpleJDBCConnectionPool(AllTests.dbDriver,
+                AllTests.dbURL, AllTests.dbUser, AllTests.dbPwd, 0, 2);
+        Connection c = connectionPool.reserveConnection();
+        Assert.assertNotNull(c);
+    }
+
+    @Test
+    public void releaseConnection_nullConnection_shouldDoNothing() {
+        connectionPool.releaseConnection(null);
+    }
+
+    @Test
+    public void releaseConnection_failingRollback_shouldCallClose()
+            throws SQLException {
+        Connection c = EasyMock.createMock(Connection.class);
+        c.getAutoCommit();
+        EasyMock.expectLastCall().andReturn(false);
+        c.rollback();
+        EasyMock.expectLastCall().andThrow(new SQLException("Rollback failed"));
+        c.close();
+        EasyMock.expectLastCall().atLeastOnce();
+        EasyMock.replay(c);
+        // make sure the connection pool is initialized
+        connectionPool.reserveConnection();
+        connectionPool.releaseConnection(c);
+        EasyMock.verify(c);
+    }
+
+    @Test
+    public void destroy_shouldCloseAllConnections() throws SQLException {
+        Connection c1 = connectionPool.reserveConnection();
+        Connection c2 = connectionPool.reserveConnection();
+        connectionPool.destroy();
+        Assert.assertTrue(c1.isClosed());
+        Assert.assertTrue(c2.isClosed());
+    }
+
+    @Test
+    public void destroy_shouldCloseAllConnections2() throws SQLException {
+        Connection c1 = connectionPool.reserveConnection();
+        Connection c2 = connectionPool.reserveConnection();
+        connectionPool.releaseConnection(c1);
+        connectionPool.releaseConnection(c2);
+        connectionPool.destroy();
+        Assert.assertTrue(c1.isClosed());
+        Assert.assertTrue(c2.isClosed());
+    }
+
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/filters/BetweenTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/filters/BetweenTest.java
new file mode 100644 (file)
index 0000000..f86ca6d
--- /dev/null
@@ -0,0 +1,122 @@
+package com.vaadin.tests.server.container.sqlcontainer.filters;
+
+import junit.framework.Assert;
+
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.filter.Between;
+
+public class BetweenTest {
+
+    private Item itemWithPropertyValue(Object propertyId, Object value) {
+        Property property = EasyMock.createMock(Property.class);
+        property.getValue();
+        EasyMock.expectLastCall().andReturn(value).anyTimes();
+        EasyMock.replay(property);
+
+        Item item = EasyMock.createMock(Item.class);
+        item.getItemProperty(propertyId);
+        EasyMock.expectLastCall().andReturn(property).anyTimes();
+        EasyMock.replay(item);
+        return item;
+    }
+
+    @Test
+    public void passesFilter_valueIsInRange_shouldBeTrue() {
+        Item item = itemWithPropertyValue("foo", 15);
+        Between between = new Between("foo", 1, 30);
+        Assert.assertTrue(between.passesFilter("foo", item));
+    }
+
+    @Test
+    public void passesFilter_valueIsOutOfRange_shouldBeFalse() {
+        Item item = itemWithPropertyValue("foo", 15);
+        Between between = new Between("foo", 0, 2);
+        Assert.assertFalse(between.passesFilter("foo", item));
+    }
+
+    @Test
+    public void passesFilter_valueNotComparable_shouldBeFalse() {
+        Item item = itemWithPropertyValue("foo", new Object());
+        Between between = new Between("foo", 0, 2);
+        Assert.assertFalse(between.passesFilter("foo", item));
+    }
+
+    @Test
+    public void appliesToProperty_differentProperties_shoudlBeFalse() {
+        Between between = new Between("foo", 0, 2);
+        Assert.assertFalse(between.appliesToProperty("bar"));
+    }
+
+    @Test
+    public void appliesToProperty_sameProperties_shouldBeTrue() {
+        Between between = new Between("foo", 0, 2);
+        Assert.assertTrue(between.appliesToProperty("foo"));
+    }
+
+    @Test
+    public void hashCode_equalInstances_shouldBeEqual() {
+        Between b1 = new Between("foo", 0, 2);
+        Between b2 = new Between("foo", 0, 2);
+        Assert.assertEquals(b1.hashCode(), b2.hashCode());
+    }
+
+    @Test
+    public void equals_differentObjects_shouldBeFalse() {
+        Between b1 = new Between("foo", 0, 2);
+        Object obj = new Object();
+        Assert.assertFalse(b1.equals(obj));
+    }
+
+    @Test
+    public void equals_sameInstance_shouldBeTrue() {
+        Between b1 = new Between("foo", 0, 2);
+        Between b2 = b1;
+        Assert.assertTrue(b1.equals(b2));
+    }
+
+    @Test
+    public void equals_equalInstances_shouldBeTrue() {
+        Between b1 = new Between("foo", 0, 2);
+        Between b2 = new Between("foo", 0, 2);
+        Assert.assertTrue(b1.equals(b2));
+    }
+
+    @Test
+    public void equals_equalInstances2_shouldBeTrue() {
+        Between b1 = new Between(null, null, null);
+        Between b2 = new Between(null, null, null);
+        Assert.assertTrue(b1.equals(b2));
+    }
+
+    @Test
+    public void equals_secondValueDiffers_shouldBeFalse() {
+        Between b1 = new Between("foo", 0, 1);
+        Between b2 = new Between("foo", 0, 2);
+        Assert.assertFalse(b1.equals(b2));
+    }
+
+    @Test
+    public void equals_firstAndSecondValueDiffers_shouldBeFalse() {
+        Between b1 = new Between("foo", 0, null);
+        Between b2 = new Between("foo", 1, 2);
+        Assert.assertFalse(b1.equals(b2));
+    }
+
+    @Test
+    public void equals_propertyAndFirstAndSecondValueDiffers_shouldBeFalse() {
+        Between b1 = new Between("foo", null, 1);
+        Between b2 = new Between("bar", 1, 2);
+        Assert.assertFalse(b1.equals(b2));
+    }
+
+    @Test
+    public void equals_propertiesDiffer_shouldBeFalse() {
+        Between b1 = new Between(null, 0, 1);
+        Between b2 = new Between("bar", 0, 1);
+        Assert.assertFalse(b1.equals(b2));
+    }
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/filters/LikeTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/filters/LikeTest.java
new file mode 100644 (file)
index 0000000..07dd499
--- /dev/null
@@ -0,0 +1,229 @@
+package com.vaadin.tests.server.container.sqlcontainer.filters;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.ObjectProperty;
+import com.vaadin.data.util.PropertysetItem;
+import com.vaadin.data.util.filter.Like;
+
+public class LikeTest {
+
+    @Test
+    public void passesFilter_valueIsNotStringType_shouldFail() {
+        Like like = new Like("test", "%foo%");
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<Integer>(5));
+
+        Assert.assertFalse(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void passesFilter_containsLikeQueryOnStringContainingValue_shouldSucceed() {
+        Like like = new Like("test", "%foo%");
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<String>("asdfooghij"));
+
+        Assert.assertTrue(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void passesFilter_containsLikeQueryOnStringContainingValueCaseInsensitive_shouldSucceed() {
+        Like like = new Like("test", "%foo%");
+        like.setCaseSensitive(false);
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<String>("asdfOOghij"));
+
+        Assert.assertTrue(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void passesFilter_containsLikeQueryOnStringContainingValueConstructedCaseInsensitive_shouldSucceed() {
+        Like like = new Like("test", "%foo%", false);
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<String>("asdfOOghij"));
+
+        Assert.assertTrue(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void passesFilter_containsLikeQueryOnStringNotContainingValue_shouldFail() {
+        Like like = new Like("test", "%foo%");
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<String>("asdbarghij"));
+
+        Assert.assertFalse(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void passesFilter_containsLikeQueryOnStringExactlyEqualToValue_shouldSucceed() {
+        Like like = new Like("test", "%foo%");
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<String>("foo"));
+
+        Assert.assertTrue(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void passesFilter_containsLikeQueryOnStringEqualToValueMinusOneCharAtTheEnd_shouldFail() {
+        Like like = new Like("test", "%foo%");
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<String>("fo"));
+
+        Assert.assertFalse(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void passesFilter_beginsWithLikeQueryOnStringBeginningWithValue_shouldSucceed() {
+        Like like = new Like("test", "foo%");
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<String>("foobar"));
+
+        Assert.assertTrue(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void passesFilter_beginsWithLikeQueryOnStringNotBeginningWithValue_shouldFail() {
+        Like like = new Like("test", "foo%");
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<String>("barfoo"));
+
+        Assert.assertFalse(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void passesFilter_endsWithLikeQueryOnStringEndingWithValue_shouldSucceed() {
+        Like like = new Like("test", "%foo");
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<String>("barfoo"));
+
+        Assert.assertTrue(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void passesFilter_endsWithLikeQueryOnStringNotEndingWithValue_shouldFail() {
+        Like like = new Like("test", "%foo");
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<String>("foobar"));
+
+        Assert.assertFalse(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void passesFilter_startsWithAndEndsWithOnMatchingValue_shouldSucceed() {
+        Like like = new Like("test", "foo%bar");
+
+        Item item = new PropertysetItem();
+        item.addItemProperty("test", new ObjectProperty<String>("fooASDFbar"));
+
+        Assert.assertTrue(like.passesFilter("id", item));
+    }
+
+    @Test
+    public void appliesToProperty_valueIsProperty_shouldBeTrue() {
+        Like like = new Like("test", "%foo");
+        Assert.assertTrue(like.appliesToProperty("test"));
+    }
+
+    @Test
+    public void appliesToProperty_valueIsNotProperty_shouldBeFalse() {
+        Like like = new Like("test", "%foo");
+        Assert.assertFalse(like.appliesToProperty("bar"));
+    }
+
+    @Test
+    public void equals_sameInstances_shouldBeTrue() {
+        Like like1 = new Like("test", "%foo");
+        Like like2 = like1;
+        Assert.assertTrue(like1.equals(like2));
+    }
+
+    @Test
+    public void equals_twoEqualInstances_shouldBeTrue() {
+        Like like1 = new Like("test", "foo");
+        Like like2 = new Like("test", "foo");
+        Assert.assertTrue(like1.equals(like2));
+    }
+
+    @Test
+    public void equals_differentValues_shouldBeFalse() {
+        Like like1 = new Like("test", "foo");
+        Like like2 = new Like("test", "bar");
+        Assert.assertFalse(like1.equals(like2));
+    }
+
+    @Test
+    public void equals_differentProperties_shouldBeFalse() {
+        Like like1 = new Like("foo", "test");
+        Like like2 = new Like("bar", "test");
+        Assert.assertFalse(like1.equals(like2));
+    }
+
+    @Test
+    public void equals_differentPropertiesAndValues_shouldBeFalse() {
+        Like like1 = new Like("foo", "bar");
+        Like like2 = new Like("baz", "zomg");
+        Assert.assertFalse(like1.equals(like2));
+    }
+
+    @Test
+    public void equals_differentClasses_shouldBeFalse() {
+        Like like1 = new Like("foo", "bar");
+        Object obj = new Object();
+        Assert.assertFalse(like1.equals(obj));
+    }
+
+    @Test
+    public void equals_bothHaveNullProperties_shouldBeTrue() {
+        Like like1 = new Like(null, "foo");
+        Like like2 = new Like(null, "foo");
+        Assert.assertTrue(like1.equals(like2));
+    }
+
+    @Test
+    public void equals_bothHaveNullValues_shouldBeTrue() {
+        Like like1 = new Like("foo", null);
+        Like like2 = new Like("foo", null);
+        Assert.assertTrue(like1.equals(like2));
+    }
+
+    @Test
+    public void equals_onePropertyIsNull_shouldBeFalse() {
+        Like like1 = new Like(null, "bar");
+        Like like2 = new Like("foo", "baz");
+        Assert.assertFalse(like1.equals(like2));
+    }
+
+    @Test
+    public void equals_oneValueIsNull_shouldBeFalse() {
+        Like like1 = new Like("foo", null);
+        Like like2 = new Like("baz", "bar");
+        Assert.assertFalse(like1.equals(like2));
+    }
+
+    @Test
+    public void hashCode_equalInstances_shouldBeEqual() {
+        Like like1 = new Like("test", "foo");
+        Like like2 = new Like("test", "foo");
+        Assert.assertEquals(like1.hashCode(), like2.hashCode());
+    }
+
+    @Test
+    public void hashCode_differentPropertiesAndValues_shouldNotEqual() {
+        Like like1 = new Like("foo", "bar");
+        Like like2 = new Like("baz", "zomg");
+        Assert.assertTrue(like1.hashCode() != like2.hashCode());
+    }
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/generator/SQLGeneratorsTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/generator/SQLGeneratorsTest.java
new file mode 100644 (file)
index 0000000..3c16ebe
--- /dev/null
@@ -0,0 +1,241 @@
+package com.vaadin.tests.server.container.sqlcontainer.generator;\r
+\r
+import java.sql.SQLException;\r
+import java.util.ArrayList;\r
+import java.util.Arrays;\r
+import java.util.List;\r
+\r
+import org.junit.After;\r
+import org.junit.Assert;\r
+import org.junit.Before;\r
+import org.junit.Test;\r
+\r
+import com.vaadin.data.Container.Filter;\r
+import com.vaadin.data.util.RowItem;\r
+import com.vaadin.data.util.SQLContainer;\r
+import com.vaadin.data.util.connection.JDBCConnectionPool;\r
+import com.vaadin.data.util.connection.SimpleJDBCConnectionPool;\r
+import com.vaadin.data.util.filter.Like;\r
+import com.vaadin.data.util.filter.Or;\r
+import com.vaadin.data.util.query.OrderBy;\r
+import com.vaadin.data.util.query.TableQuery;\r
+import com.vaadin.data.util.query.generator.DefaultSQLGenerator;\r
+import com.vaadin.data.util.query.generator.MSSQLGenerator;\r
+import com.vaadin.data.util.query.generator.OracleGenerator;\r
+import com.vaadin.data.util.query.generator.SQLGenerator;\r
+import com.vaadin.data.util.query.generator.StatementHelper;\r
+import com.vaadin.tests.server.container.sqlcontainer.AllTests;\r
+import com.vaadin.tests.server.container.sqlcontainer.DataGenerator;\r
+\r
+public class SQLGeneratorsTest {\r
+    private JDBCConnectionPool connectionPool;\r
+\r
+    @Before\r
+    public void setUp() throws SQLException {\r
+\r
+        try {\r
+            connectionPool = new SimpleJDBCConnectionPool(AllTests.dbDriver,\r
+                    AllTests.dbURL, AllTests.dbUser, AllTests.dbPwd, 2, 2);\r
+        } catch (SQLException e) {\r
+            e.printStackTrace();\r
+            Assert.fail(e.getMessage());\r
+        }\r
+\r
+        DataGenerator.addPeopleToDatabase(connectionPool);\r
+    }\r
+\r
+    @After\r
+    public void tearDown() {\r
+        if (connectionPool != null) {\r
+            connectionPool.destroy();\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void generateSelectQuery_basicQuery_shouldSucceed() {\r
+        SQLGenerator sg = new DefaultSQLGenerator();\r
+        StatementHelper sh = sg.generateSelectQuery("TABLE", null, null, 0, 0,\r
+                null);\r
+        Assert.assertEquals(sh.getQueryString(), "SELECT * FROM TABLE");\r
+    }\r
+\r
+    @Test\r
+    public void generateSelectQuery_pagingAndColumnsSet_shouldSucceed() {\r
+        SQLGenerator sg = new DefaultSQLGenerator();\r
+        StatementHelper sh = sg.generateSelectQuery("TABLE", null, null, 4, 8,\r
+                "COL1, COL2, COL3");\r
+        Assert.assertEquals(sh.getQueryString(),\r
+                "SELECT COL1, COL2, COL3 FROM TABLE LIMIT 8 OFFSET 4");\r
+    }\r
+\r
+    /**\r
+     * Note: Only tests one kind of filter and ordering.\r
+     */\r
+    @Test\r
+    public void generateSelectQuery_filtersAndOrderingSet_shouldSucceed() {\r
+        SQLGenerator sg = new DefaultSQLGenerator();\r
+        List<com.vaadin.data.Container.Filter> f = new ArrayList<Filter>();\r
+        f.add(new Like("name", "%lle"));\r
+        List<OrderBy> ob = Arrays.asList(new OrderBy("name", true));\r
+        StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 0, 0, null);\r
+        Assert.assertEquals(sh.getQueryString(),\r
+                "SELECT * FROM TABLE WHERE \"name\" LIKE ? ORDER BY \"name\" ASC");\r
+    }\r
+\r
+    @Test\r
+    public void generateSelectQuery_filtersAndOrderingSet_exclusiveFilteringMode_shouldSucceed() {\r
+        SQLGenerator sg = new DefaultSQLGenerator();\r
+        List<Filter> f = new ArrayList<Filter>();\r
+        f.add(new Or(new Like("name", "%lle"), new Like("name", "vi%")));\r
+        List<OrderBy> ob = Arrays.asList(new OrderBy("name", true));\r
+        StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 0, 0, null);\r
+        // TODO\r
+        Assert.assertEquals(sh.getQueryString(),\r
+                "SELECT * FROM TABLE WHERE (\"name\" LIKE ? "\r
+                        + "OR \"name\" LIKE ?) ORDER BY \"name\" ASC");\r
+    }\r
+\r
+    @Test\r
+    public void generateDeleteQuery_basicQuery_shouldSucceed()\r
+            throws SQLException {\r
+        /*\r
+         * No need to run this for Oracle/MSSQL generators since the\r
+         * DefaultSQLGenerator method would be called anyway.\r
+         */\r
+        if (AllTests.sqlGen instanceof MSSQLGenerator\r
+                || AllTests.sqlGen instanceof OracleGenerator) {\r
+            return;\r
+        }\r
+        SQLGenerator sg = AllTests.sqlGen;\r
+        TableQuery query = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(query);\r
+\r
+        StatementHelper sh = sg.generateDeleteQuery(\r
+                "people",\r
+                query.getPrimaryKeyColumns(),\r
+                null,\r
+                (RowItem) container.getItem(container.getItemIds().iterator()\r
+                        .next()));\r
+        Assert.assertEquals("DELETE FROM people WHERE \"ID\" = ?",\r
+                sh.getQueryString());\r
+    }\r
+\r
+    @Test\r
+    public void generateUpdateQuery_basicQuery_shouldSucceed()\r
+            throws SQLException {\r
+        /*\r
+         * No need to run this for Oracle/MSSQL generators since the\r
+         * DefaultSQLGenerator method would be called anyway.\r
+         */\r
+        if (AllTests.sqlGen instanceof MSSQLGenerator\r
+                || AllTests.sqlGen instanceof OracleGenerator) {\r
+            return;\r
+        }\r
+        SQLGenerator sg = new DefaultSQLGenerator();\r
+        TableQuery query = new TableQuery("people", connectionPool);\r
+        SQLContainer container = new SQLContainer(query);\r
+\r
+        RowItem ri = (RowItem) container.getItem(container.getItemIds()\r
+                .iterator().next());\r
+        ri.getItemProperty("NAME").setValue("Viljami");\r
+\r
+        StatementHelper sh = sg.generateUpdateQuery("people", ri);\r
+        Assert.assertTrue("UPDATE people SET \"NAME\" = ?, \"AGE\" = ? WHERE \"ID\" = ?"\r
+                .equals(sh.getQueryString())\r
+                || "UPDATE people SET \"AGE\" = ?, \"NAME\" = ? WHERE \"ID\" = ?"\r
+                        .equals(sh.getQueryString()));\r
+    }\r
+\r
+    @Test\r
+    public void generateInsertQuery_basicQuery_shouldSucceed()\r
+            throws SQLException {\r
+        /*\r
+         * No need to run this for Oracle/MSSQL generators since the\r
+         * DefaultSQLGenerator method would be called anyway.\r
+         */\r
+        if (AllTests.sqlGen instanceof MSSQLGenerator\r
+                || AllTests.sqlGen instanceof OracleGenerator) {\r
+            return;\r
+        }\r
+        SQLGenerator sg = new DefaultSQLGenerator();\r
+        TableQuery query = new TableQuery("people", connectionPool);\r
+        SQLContainer container = new SQLContainer(query);\r
+\r
+        RowItem ri = (RowItem) container.getItem(container.addItem());\r
+        ri.getItemProperty("NAME").setValue("Viljami");\r
+\r
+        StatementHelper sh = sg.generateInsertQuery("people", ri);\r
+\r
+        Assert.assertTrue("INSERT INTO people (\"NAME\", \"AGE\") VALUES (?, ?)"\r
+                .equals(sh.getQueryString())\r
+                || "INSERT INTO people (\"AGE\", \"NAME\") VALUES (?, ?)"\r
+                        .equals(sh.getQueryString()));\r
+    }\r
+\r
+    @Test\r
+    public void generateComplexSelectQuery_forOracle_shouldSucceed()\r
+            throws SQLException {\r
+        SQLGenerator sg = new OracleGenerator();\r
+        List<Filter> f = new ArrayList<Filter>();\r
+        f.add(new Like("name", "%lle"));\r
+        List<OrderBy> ob = Arrays.asList(new OrderBy("name", true));\r
+        StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 4, 8,\r
+                "NAME, ID");\r
+        Assert.assertEquals(\r
+                "SELECT * FROM (SELECT x.*, ROWNUM AS \"rownum\" FROM"\r
+                        + " (SELECT NAME, ID FROM TABLE WHERE \"name\" LIKE ?"\r
+                        + " ORDER BY \"name\" ASC) x) WHERE \"rownum\" BETWEEN 5 AND 12",\r
+                sh.getQueryString());\r
+    }\r
+\r
+    @Test\r
+    public void generateComplexSelectQuery_forMSSQL_shouldSucceed()\r
+            throws SQLException {\r
+        SQLGenerator sg = new MSSQLGenerator();\r
+        List<Filter> f = new ArrayList<Filter>();\r
+        f.add(new Like("name", "%lle"));\r
+        List<OrderBy> ob = Arrays.asList(new OrderBy("name", true));\r
+        StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 4, 8,\r
+                "NAME, ID");\r
+        Assert.assertEquals(sh.getQueryString(),\r
+                "SELECT * FROM (SELECT row_number() OVER "\r
+                        + "( ORDER BY \"name\" ASC) AS rownum, NAME, ID "\r
+                        + "FROM TABLE WHERE \"name\" LIKE ?) "\r
+                        + "AS a WHERE a.rownum BETWEEN 5 AND 12");\r
+    }\r
+\r
+    @Test\r
+    public void generateComplexSelectQuery_forOracle_exclusiveFilteringMode_shouldSucceed()\r
+            throws SQLException {\r
+        SQLGenerator sg = new OracleGenerator();\r
+        List<Filter> f = new ArrayList<Filter>();\r
+        f.add(new Or(new Like("name", "%lle"), new Like("name", "vi%")));\r
+        List<OrderBy> ob = Arrays.asList(new OrderBy("name", true));\r
+        StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 4, 8,\r
+                "NAME, ID");\r
+        Assert.assertEquals(\r
+                sh.getQueryString(),\r
+                "SELECT * FROM (SELECT x.*, ROWNUM AS \"rownum\" FROM"\r
+                        + " (SELECT NAME, ID FROM TABLE WHERE (\"name\" LIKE ?"\r
+                        + " OR \"name\" LIKE ?) "\r
+                        + "ORDER BY \"name\" ASC) x) WHERE \"rownum\" BETWEEN 5 AND 12");\r
+    }\r
+\r
+    @Test\r
+    public void generateComplexSelectQuery_forMSSQL_exclusiveFilteringMode_shouldSucceed()\r
+            throws SQLException {\r
+        SQLGenerator sg = new MSSQLGenerator();\r
+        List<Filter> f = new ArrayList<Filter>();\r
+        f.add(new Or(new Like("name", "%lle"), new Like("name", "vi%")));\r
+        List<OrderBy> ob = Arrays.asList(new OrderBy("name", true));\r
+        StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 4, 8,\r
+                "NAME, ID");\r
+        Assert.assertEquals(sh.getQueryString(),\r
+                "SELECT * FROM (SELECT row_number() OVER "\r
+                        + "( ORDER BY \"name\" ASC) AS rownum, NAME, ID "\r
+                        + "FROM TABLE WHERE (\"name\" LIKE ? "\r
+                        + "OR \"name\" LIKE ?)) "\r
+                        + "AS a WHERE a.rownum BETWEEN 5 AND 12");\r
+    }\r
+}\r
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/query/FreeformQueryTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/query/FreeformQueryTest.java
new file mode 100644 (file)
index 0000000..743d8bd
--- /dev/null
@@ -0,0 +1,897 @@
+package com.vaadin.tests.server.container.sqlcontainer.query;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.RowId;
+import com.vaadin.data.util.RowItem;
+import com.vaadin.data.util.SQLContainer;
+import com.vaadin.data.util.connection.JDBCConnectionPool;
+import com.vaadin.data.util.connection.SimpleJDBCConnectionPool;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.query.FreeformQuery;
+import com.vaadin.data.util.query.FreeformQueryDelegate;
+import com.vaadin.data.util.query.OrderBy;
+import com.vaadin.tests.server.container.sqlcontainer.AllTests;
+import com.vaadin.tests.server.container.sqlcontainer.AllTests.DB;
+import com.vaadin.tests.server.container.sqlcontainer.DataGenerator;
+
+public class FreeformQueryTest {
+
+    private static final int offset = AllTests.offset;
+    private JDBCConnectionPool connectionPool;
+
+    @Before
+    public void setUp() throws SQLException {
+
+        try {
+            connectionPool = new SimpleJDBCConnectionPool(AllTests.dbDriver,
+                    AllTests.dbURL, AllTests.dbUser, AllTests.dbPwd, 2, 2);
+        } catch (SQLException e) {
+            e.printStackTrace();
+            Assert.fail(e.getMessage());
+        }
+
+        DataGenerator.addPeopleToDatabase(connectionPool);
+    }
+
+    @After
+    public void tearDown() {
+        if (connectionPool != null) {
+            connectionPool.destroy();
+        }
+    }
+
+    @Test
+    public void construction_legalParameters_shouldSucceed() {
+        FreeformQuery ffQuery = new FreeformQuery("SELECT * FROM foo",
+                Arrays.asList("ID"), connectionPool);
+        Assert.assertArrayEquals(new Object[] { "ID" }, ffQuery
+                .getPrimaryKeyColumns().toArray());
+
+        Assert.assertEquals("SELECT * FROM foo", ffQuery.getQueryString());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void construction_emptyQueryString_shouldFail() {
+        new FreeformQuery("", Arrays.asList("ID"), connectionPool);
+    }
+
+    @Test
+    public void construction_nullPrimaryKeys_shouldSucceed() {
+        new FreeformQuery("SELECT * FROM foo", null, connectionPool);
+    }
+
+    @Test
+    public void construction_nullPrimaryKeys2_shouldSucceed() {
+        new FreeformQuery("SELECT * FROM foo", connectionPool);
+    }
+
+    @Test
+    public void construction_emptyPrimaryKeys_shouldSucceed() {
+        new FreeformQuery("SELECT * FROM foo", connectionPool);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void construction_emptyStringsInPrimaryKeys_shouldFail() {
+        new FreeformQuery("SELECT * FROM foo", Arrays.asList(""),
+                connectionPool);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void construction_nullConnectionPool_shouldFail() {
+        new FreeformQuery("SELECT * FROM foo", Arrays.asList("ID"), null);
+    }
+
+    @Test
+    public void getCount_simpleQuery_returnsFour() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        Assert.assertEquals(4, query.getCount());
+    }
+
+    @Test(expected = SQLException.class)
+    public void getCount_illegalQuery_shouldThrowSQLException()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM asdf",
+                Arrays.asList("ID"), connectionPool);
+        query.getResults(0, 50);
+    }
+
+    @Test
+    public void getCount_simpleQueryTwoMorePeopleAdded_returnsSix()
+            throws SQLException {
+        // Add some people
+        Connection conn = connectionPool.reserveConnection();
+        Statement statement = conn.createStatement();
+        if (AllTests.db == DB.MSSQL) {
+            statement.executeUpdate("insert into people values('Bengt', 30)");
+            statement.executeUpdate("insert into people values('Ingvar', 50)");
+        } else {
+            statement
+                    .executeUpdate("insert into people values(default, 'Bengt', 30)");
+            statement
+                    .executeUpdate("insert into people values(default, 'Ingvar', 50)");
+        }
+        statement.close();
+        conn.commit();
+        connectionPool.releaseConnection(conn);
+
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+
+        Assert.assertEquals(6, query.getCount());
+    }
+
+    @Test
+    public void getCount_moreComplexQuery_returnsThree() throws SQLException {
+        FreeformQuery query = new FreeformQuery(
+                "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'",
+                connectionPool, new String[] { "ID" });
+        Assert.assertEquals(3, query.getCount());
+    }
+
+    @Test
+    public void getCount_normalState_releasesConnection() throws SQLException {
+        FreeformQuery query = new FreeformQuery(
+                "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'",
+                connectionPool, "ID");
+        query.getCount();
+        query.getCount();
+        Assert.assertNotNull(connectionPool.reserveConnection());
+    }
+
+    @Test
+    public void getCount_delegateRegistered_shouldUseDelegate()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(delegate.getCountQuery()).andReturn(
+                "SELECT COUNT(*) FROM people WHERE \"NAME\" LIKE '%lle'");
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+        Assert.assertEquals(3, query.getCount());
+        EasyMock.verify(delegate);
+    }
+
+    @Test
+    public void getCount_delegateRegisteredZeroRows_returnsZero()
+            throws SQLException {
+        DataGenerator.createGarbage(connectionPool);
+        FreeformQuery query = new FreeformQuery("SELECT * FROM GARBAGE",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(delegate.getCountQuery()).andReturn(
+                "SELECT COUNT(*) FROM GARBAGE");
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+        Assert.assertEquals(0, query.getCount());
+        EasyMock.verify(delegate);
+    }
+
+    @Test
+    public void getResults_simpleQuery_returnsFourRecords() throws SQLException {
+        FreeformQuery query = new FreeformQuery(
+                "SELECT \"ID\",\"NAME\" FROM people", Arrays.asList("ID"),
+                connectionPool);
+        query.beginTransaction();
+        ResultSet rs = query.getResults(0, 0);
+
+        Assert.assertTrue(rs.next());
+        Assert.assertEquals(0 + offset, rs.getInt(1));
+        Assert.assertEquals("Ville", rs.getString(2));
+
+        Assert.assertTrue(rs.next());
+        Assert.assertEquals(1 + offset, rs.getInt(1));
+        Assert.assertEquals("Kalle", rs.getString(2));
+
+        Assert.assertTrue(rs.next());
+        Assert.assertEquals(2 + offset, rs.getInt(1));
+        Assert.assertEquals("Pelle", rs.getString(2));
+
+        Assert.assertTrue(rs.next());
+        Assert.assertEquals(3 + offset, rs.getInt(1));
+        Assert.assertEquals("Börje", rs.getString(2));
+
+        Assert.assertFalse(rs.next());
+        query.commit();
+    }
+
+    @Test
+    public void getResults_moreComplexQuery_returnsThreeRecords()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery(
+                "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'",
+                Arrays.asList("ID"), connectionPool);
+        query.beginTransaction();
+        ResultSet rs = query.getResults(0, 0);
+
+        Assert.assertTrue(rs.next());
+        Assert.assertEquals(0 + offset, rs.getInt(1));
+        Assert.assertEquals("Ville", rs.getString(2));
+
+        Assert.assertTrue(rs.next());
+        Assert.assertEquals(1 + offset, rs.getInt(1));
+        Assert.assertEquals("Kalle", rs.getString(2));
+
+        Assert.assertTrue(rs.next());
+        Assert.assertEquals(2 + offset, rs.getInt(1));
+        Assert.assertEquals("Pelle", rs.getString(2));
+
+        Assert.assertFalse(rs.next());
+        query.commit();
+    }
+
+    @Test
+    public void getResults_noDelegate5000Rows_returns5000rows()
+            throws SQLException {
+        DataGenerator.addFiveThousandPeople(connectionPool);
+
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.beginTransaction();
+        ResultSet rs = query.getResults(0, 0);
+        for (int i = 0; i < 5000; i++) {
+            Assert.assertTrue(rs.next());
+        }
+        Assert.assertFalse(rs.next());
+        query.commit();
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void setFilters_noDelegate_shouldFail() {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        filters.add(new Like("name", "%lle"));
+        query.setFilters(filters);
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void setOrderBy_noDelegate_shouldFail() {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.setOrderBy(Arrays.asList(new OrderBy("name", true)));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void storeRow_noDelegateNoTransactionActive_shouldFail()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.storeRow(new RowItem(new SQLContainer(query), new RowId(
+                new Object[] { 1 }), null));
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void storeRow_noDelegate_shouldFail() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        SQLContainer container = EasyMock.createNiceMock(SQLContainer.class);
+        EasyMock.replay(container);
+        query.beginTransaction();
+        query.storeRow(new RowItem(container, new RowId(new Object[] { 1 }),
+                null));
+        query.commit();
+        EasyMock.verify(container);
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void removeRow_noDelegate_shouldFail() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        SQLContainer container = EasyMock.createNiceMock(SQLContainer.class);
+        EasyMock.replay(container);
+        query.beginTransaction();
+        query.removeRow(new RowItem(container, new RowId(new Object[] { 1 }),
+                null));
+        query.commit();
+        EasyMock.verify(container);
+    }
+
+    @Test
+    public void beginTransaction_readOnly_shouldSucceed() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.beginTransaction();
+    }
+
+    @Test
+    public void commit_readOnly_shouldSucceed() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.beginTransaction();
+        query.commit();
+    }
+
+    @Test
+    public void rollback_readOnly_shouldSucceed() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.beginTransaction();
+        query.rollback();
+    }
+
+    @Test(expected = SQLException.class)
+    public void commit_noActiveTransaction_shouldFail() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.commit();
+    }
+
+    @Test(expected = SQLException.class)
+    public void rollback_noActiveTransaction_shouldFail() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.rollback();
+    }
+
+    @Test
+    public void containsRowWithKeys_simpleQueryWithExistingKeys_returnsTrue()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        Assert.assertTrue(query.containsRowWithKey(1));
+    }
+
+    @Test
+    public void containsRowWithKeys_simpleQueryWithNonexistingKeys_returnsTrue()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        Assert.assertFalse(query.containsRowWithKey(1337));
+    }
+
+    // (expected = SQLException.class)
+    @Test
+    public void containsRowWithKeys_simpleQueryWithInvalidKeys_shouldFail()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        Assert.assertFalse(query.containsRowWithKey(38796));
+    }
+
+    @Test
+    public void containsRowWithKeys_queryContainingWhereClauseAndExistingKeys_returnsTrue()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery(
+                "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'",
+                Arrays.asList("ID"), connectionPool);
+        Assert.assertTrue(query.containsRowWithKey(1));
+    }
+
+    @Test
+    public void containsRowWithKeys_queryContainingLowercaseWhereClauseAndExistingKeys_returnsTrue()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery(
+                "select * from people where \"NAME\" like '%lle'",
+                Arrays.asList("ID"), connectionPool);
+        Assert.assertTrue(query.containsRowWithKey(1));
+    }
+
+    @Test
+    public void containsRowWithKeys_nullKeys_shouldFailAndReleaseConnections()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery(
+                "select * from people where \"NAME\" like '%lle'",
+                Arrays.asList("ID"), connectionPool);
+        try {
+            query.containsRowWithKey(new Object[] { null });
+        } catch (SQLException e) {
+            // We should now be able to reserve two connections
+            connectionPool.reserveConnection();
+            connectionPool.reserveConnection();
+        }
+    }
+
+    /*
+     * -------- Tests with a delegate ---------
+     */
+
+    @Test
+    public void setDelegate_noExistingDelegate_shouldRegisterNewDelegate() {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        query.setDelegate(delegate);
+        Assert.assertEquals(delegate, query.getDelegate());
+    }
+
+    @Test
+    public void getResults_hasDelegate_shouldCallDelegate() throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        if (AllTests.db == DB.MSSQL) {
+            EasyMock.expect(delegate.getQueryString(0, 2))
+                    .andReturn(
+                            "SELECT * FROM (SELECT row_number()"
+                                    + "OVER (ORDER BY id ASC) AS rownum, * FROM people)"
+                                    + " AS a WHERE a.rownum BETWEEN 0 AND 2");
+        } else if (AllTests.db == DB.ORACLE) {
+            EasyMock.expect(delegate.getQueryString(0, 2))
+                    .andReturn(
+                            "SELECT * FROM (SELECT  x.*, ROWNUM AS r FROM"
+                                    + " (SELECT * FROM people) x) WHERE r BETWEEN 1 AND 2");
+        } else {
+            EasyMock.expect(delegate.getQueryString(0, 2)).andReturn(
+                    "SELECT * FROM people LIMIT 2 OFFSET 0");
+        }
+        EasyMock.replay(delegate);
+
+        query.setDelegate(delegate);
+        query.beginTransaction();
+        query.getResults(0, 2);
+        EasyMock.verify(delegate);
+        query.commit();
+    }
+
+    @Test
+    public void getResults_delegateImplementsGetQueryString_shouldHonorOffsetAndPagelength()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        if (AllTests.db == DB.MSSQL) {
+            EasyMock.expect(delegate.getQueryString(0, 2))
+                    .andReturn(
+                            "SELECT * FROM (SELECT row_number()"
+                                    + "OVER (ORDER BY id ASC) AS rownum, * FROM people)"
+                                    + " AS a WHERE a.rownum BETWEEN 0 AND 2");
+        } else if (AllTests.db == DB.ORACLE) {
+            EasyMock.expect(delegate.getQueryString(0, 2))
+                    .andReturn(
+                            "SELECT * FROM (SELECT  x.*, ROWNUM AS r FROM"
+                                    + " (SELECT * FROM people) x) WHERE r BETWEEN 1 AND 2");
+        } else {
+            EasyMock.expect(delegate.getQueryString(0, 2)).andReturn(
+                    "SELECT * FROM people LIMIT 2 OFFSET 0");
+        }
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        ResultSet rs = query.getResults(0, 2);
+        int rsoffset = 0;
+        if (AllTests.db == DB.MSSQL) {
+            rsoffset++;
+        }
+        Assert.assertTrue(rs.next());
+        Assert.assertEquals(0 + offset, rs.getInt(1 + rsoffset));
+        Assert.assertEquals("Ville", rs.getString(2 + rsoffset));
+
+        Assert.assertTrue(rs.next());
+        Assert.assertEquals(1 + offset, rs.getInt(1 + rsoffset));
+        Assert.assertEquals("Kalle", rs.getString(2 + rsoffset));
+
+        Assert.assertFalse(rs.next());
+
+        EasyMock.verify(delegate);
+        query.commit();
+    }
+
+    @Test
+    public void getResults_delegateRegistered5000Rows_returns100rows()
+            throws SQLException {
+        DataGenerator.addFiveThousandPeople(connectionPool);
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        if (AllTests.db == DB.MSSQL) {
+            EasyMock.expect(delegate.getQueryString(200, 100))
+                    .andReturn(
+                            "SELECT * FROM (SELECT row_number()"
+                                    + "OVER (ORDER BY id ASC) AS rownum, * FROM people)"
+                                    + " AS a WHERE a.rownum BETWEEN 201 AND 300");
+        } else if (AllTests.db == DB.ORACLE) {
+            EasyMock.expect(delegate.getQueryString(200, 100))
+                    .andReturn(
+                            "SELECT * FROM (SELECT  x.*, ROWNUM AS r FROM"
+                                    + " (SELECT * FROM people ORDER BY ID ASC) x) WHERE r BETWEEN 201 AND 300");
+        } else {
+            EasyMock.expect(delegate.getQueryString(200, 100)).andReturn(
+                    "SELECT * FROM people LIMIT 100 OFFSET 200");
+        }
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        ResultSet rs = query.getResults(200, 100);
+        for (int i = 0; i < 100; i++) {
+            Assert.assertTrue(rs.next());
+            Assert.assertEquals(200 + i + offset, rs.getInt("ID"));
+        }
+        Assert.assertFalse(rs.next());
+        query.commit();
+    }
+
+    @Test
+    public void setFilters_delegateImplementsSetFilters_shouldPassFiltersToDelegate() {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        List<Filter> filters = new ArrayList<Filter>();
+        filters.add(new Like("name", "%lle"));
+        delegate.setFilters(filters);
+
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.setFilters(filters);
+
+        EasyMock.verify(delegate);
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void setFilters_delegateDoesNotImplementSetFilters_shouldFail() {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        List<Filter> filters = new ArrayList<Filter>();
+        filters.add(new Like("name", "%lle"));
+        delegate.setFilters(filters);
+        EasyMock.expectLastCall().andThrow(new UnsupportedOperationException());
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.setFilters(filters);
+
+        EasyMock.verify(delegate);
+    }
+
+    @Test
+    public void setOrderBy_delegateImplementsSetOrderBy_shouldPassArgumentsToDelegate() {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        List<OrderBy> orderBys = Arrays.asList(new OrderBy("name", false));
+        delegate.setOrderBy(orderBys);
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.setOrderBy(orderBys);
+
+        EasyMock.verify(delegate);
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void setOrderBy_delegateDoesNotImplementSetOrderBy_shouldFail() {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        List<OrderBy> orderBys = Arrays.asList(new OrderBy("name", false));
+        delegate.setOrderBy(orderBys);
+        EasyMock.expectLastCall().andThrow(new UnsupportedOperationException());
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.setOrderBy(orderBys);
+
+        EasyMock.verify(delegate);
+    }
+
+    @Test
+    public void setFilters_noDelegateAndNullParameter_shouldSucceed() {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.setFilters(null);
+    }
+
+    @Test
+    public void setOrderBy_noDelegateAndNullParameter_shouldSucceed() {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        query.setOrderBy(null);
+    }
+
+    @Test
+    public void storeRow_delegateImplementsStoreRow_shouldPassToDelegate()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(
+                delegate.storeRow(EasyMock.isA(Connection.class),
+                        EasyMock.isA(RowItem.class))).andReturn(1);
+        SQLContainer container = EasyMock.createNiceMock(SQLContainer.class);
+        EasyMock.replay(delegate, container);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        RowItem row = new RowItem(container, new RowId(new Object[] { 1 }),
+                null);
+        query.storeRow(row);
+        query.commit();
+
+        EasyMock.verify(delegate, container);
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void storeRow_delegateDoesNotImplementStoreRow_shouldFail()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(
+                delegate.storeRow(EasyMock.isA(Connection.class),
+                        EasyMock.isA(RowItem.class))).andThrow(
+                new UnsupportedOperationException());
+        SQLContainer container = EasyMock.createNiceMock(SQLContainer.class);
+        EasyMock.replay(delegate, container);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        RowItem row = new RowItem(container, new RowId(new Object[] { 1 }),
+                null);
+        query.storeRow(row);
+        query.commit();
+
+        EasyMock.verify(delegate, container);
+    }
+
+    @Test
+    public void removeRow_delegateImplementsRemoveRow_shouldPassToDelegate()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(
+                delegate.removeRow(EasyMock.isA(Connection.class),
+                        EasyMock.isA(RowItem.class))).andReturn(true);
+        SQLContainer container = EasyMock.createNiceMock(SQLContainer.class);
+        EasyMock.replay(delegate, container);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        RowItem row = new RowItem(container, new RowId(new Object[] { 1 }),
+                null);
+        query.removeRow(row);
+        query.commit();
+
+        EasyMock.verify(delegate, container);
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void removeRow_delegateDoesNotImplementRemoveRow_shouldFail()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(
+                delegate.removeRow(EasyMock.isA(Connection.class),
+                        EasyMock.isA(RowItem.class))).andThrow(
+                new UnsupportedOperationException());
+        SQLContainer container = EasyMock.createNiceMock(SQLContainer.class);
+        EasyMock.replay(delegate, container);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        RowItem row = new RowItem(container, new RowId(new Object[] { 1 }),
+                null);
+        query.removeRow(row);
+        query.commit();
+
+        EasyMock.verify(delegate, container);
+    }
+
+    @Test
+    public void beginTransaction_delegateRegistered_shouldSucceed()
+            throws UnsupportedOperationException, SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void beginTransaction_transactionAlreadyActive_shouldFail()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+
+        query.beginTransaction();
+        query.beginTransaction();
+    }
+
+    @Test(expected = SQLException.class)
+    public void commit_delegateRegisteredNoActiveTransaction_shouldFail()
+            throws UnsupportedOperationException, SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.commit();
+    }
+
+    @Test
+    public void commit_delegateRegisteredActiveTransaction_shouldSucceed()
+            throws UnsupportedOperationException, SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        query.commit();
+    }
+
+    @Test(expected = SQLException.class)
+    public void commit_delegateRegisteredActiveTransactionDoubleCommit_shouldFail()
+            throws UnsupportedOperationException, SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        query.commit();
+        query.commit();
+    }
+
+    @Test(expected = SQLException.class)
+    public void rollback_delegateRegisteredNoActiveTransaction_shouldFail()
+            throws UnsupportedOperationException, SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.rollback();
+    }
+
+    @Test
+    public void rollback_delegateRegisteredActiveTransaction_shouldSucceed()
+            throws UnsupportedOperationException, SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        query.rollback();
+    }
+
+    @Test(expected = SQLException.class)
+    public void rollback_delegateRegisteredActiveTransactionDoubleRollback_shouldFail()
+            throws UnsupportedOperationException, SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        query.rollback();
+        query.rollback();
+    }
+
+    @Test(expected = SQLException.class)
+    public void rollback_delegateRegisteredCommittedTransaction_shouldFail()
+            throws UnsupportedOperationException, SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        query.commit();
+        query.rollback();
+    }
+
+    @Test(expected = SQLException.class)
+    public void commit_delegateRegisteredRollbackedTransaction_shouldFail()
+            throws UnsupportedOperationException, SQLException {
+        FreeformQuery query = new FreeformQuery("SELECT * FROM people",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.beginTransaction();
+        query.rollback();
+        query.commit();
+    }
+
+    @Test(expected = SQLException.class)
+    public void containsRowWithKeys_delegateRegistered_shouldCallGetContainsRowQueryString()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery(
+                "SELECT * FROM people WHERE name LIKE '%lle'",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(delegate.getContainsRowQueryString(1)).andReturn("");
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        query.containsRowWithKey(1);
+
+        EasyMock.verify(delegate);
+    }
+
+    @Test
+    public void containsRowWithKeys_delegateRegistered_shouldUseResultFromGetContainsRowQueryString()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery(
+                "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        // In order to test that this is the query that is actually used, we use
+        // a non-existing id in place of the existing one.
+        EasyMock.expect(delegate.getContainsRowQueryString(1))
+                .andReturn(
+                        "SELECT * FROM people WHERE \"NAME\" LIKE '%lle' AND \"ID\" = 1337");
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        // The id (key) used should be 1337 as above, for the call with key = 1
+        Assert.assertFalse(query.containsRowWithKey(1));
+
+        EasyMock.verify(delegate);
+    }
+
+    @Test
+    public void containsRowWithKeys_delegateRegisteredGetContainsRowQueryStringNotImplemented_shouldBuildQueryString()
+            throws SQLException {
+        FreeformQuery query = new FreeformQuery(
+                "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'",
+                Arrays.asList("ID"), connectionPool);
+        FreeformQueryDelegate delegate = EasyMock
+                .createMock(FreeformQueryDelegate.class);
+        EasyMock.expect(delegate.getContainsRowQueryString(1)).andThrow(
+                new UnsupportedOperationException());
+        EasyMock.replay(delegate);
+        query.setDelegate(delegate);
+
+        Assert.assertTrue(query.containsRowWithKey(1));
+
+        EasyMock.verify(delegate);
+    }
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/query/QueryBuilderTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/query/QueryBuilderTest.java
new file mode 100644 (file)
index 0000000..8ccca12
--- /dev/null
@@ -0,0 +1,311 @@
+package com.vaadin.tests.server.container.sqlcontainer.query;
+
+import java.util.ArrayList;
+
+import junit.framework.Assert;
+
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.And;
+import com.vaadin.data.util.filter.Between;
+import com.vaadin.data.util.filter.Compare.Equal;
+import com.vaadin.data.util.filter.Compare.Greater;
+import com.vaadin.data.util.filter.Compare.GreaterOrEqual;
+import com.vaadin.data.util.filter.Compare.Less;
+import com.vaadin.data.util.filter.Compare.LessOrEqual;
+import com.vaadin.data.util.filter.IsNull;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.filter.Not;
+import com.vaadin.data.util.filter.Or;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.data.util.query.generator.StatementHelper;
+import com.vaadin.data.util.query.generator.filter.QueryBuilder;
+import com.vaadin.data.util.query.generator.filter.StringDecorator;
+
+public class QueryBuilderTest {
+
+    private StatementHelper mockedStatementHelper(Object... values) {
+        StatementHelper sh = EasyMock.createMock(StatementHelper.class);
+        for (Object val : values) {
+            sh.addParameterValue(val);
+            EasyMock.expectLastCall();
+        }
+        EasyMock.replay(sh);
+        return sh;
+    }
+
+    // escape bad characters and wildcards
+
+    @Test
+    public void getWhereStringForFilter_equals() {
+        StatementHelper sh = mockedStatementHelper("Fido");
+        Equal f = new Equal("NAME", "Fido");
+        Assert.assertEquals("\"NAME\" = ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_greater() {
+        StatementHelper sh = mockedStatementHelper(18);
+        Greater f = new Greater("AGE", 18);
+        Assert.assertEquals("\"AGE\" > ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_less() {
+        StatementHelper sh = mockedStatementHelper(65);
+        Less f = new Less("AGE", 65);
+        Assert.assertEquals("\"AGE\" < ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_greaterOrEqual() {
+        StatementHelper sh = mockedStatementHelper(18);
+        GreaterOrEqual f = new GreaterOrEqual("AGE", 18);
+        Assert.assertEquals("\"AGE\" >= ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_lessOrEqual() {
+        StatementHelper sh = mockedStatementHelper(65);
+        LessOrEqual f = new LessOrEqual("AGE", 65);
+        Assert.assertEquals("\"AGE\" <= ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_simpleStringFilter() {
+        StatementHelper sh = mockedStatementHelper("Vi%");
+        SimpleStringFilter f = new SimpleStringFilter("NAME", "Vi", false, true);
+        Assert.assertEquals("\"NAME\" LIKE ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_simpleStringFilterMatchAnywhere() {
+        StatementHelper sh = mockedStatementHelper("%Vi%");
+        SimpleStringFilter f = new SimpleStringFilter("NAME", "Vi", false,
+                false);
+        Assert.assertEquals("\"NAME\" LIKE ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_simpleStringFilterMatchAnywhereIgnoreCase() {
+        StatementHelper sh = mockedStatementHelper("%VI%");
+        SimpleStringFilter f = new SimpleStringFilter("NAME", "Vi", true, false);
+        Assert.assertEquals("UPPER(\"NAME\") LIKE ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_startsWith() {
+        StatementHelper sh = mockedStatementHelper("Vi%");
+        Like f = new Like("NAME", "Vi%");
+        Assert.assertEquals("\"NAME\" LIKE ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_startsWithNumber() {
+        StatementHelper sh = mockedStatementHelper("1%");
+        Like f = new Like("AGE", "1%");
+        Assert.assertEquals("\"AGE\" LIKE ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_endsWith() {
+        StatementHelper sh = mockedStatementHelper("%lle");
+        Like f = new Like("NAME", "%lle");
+        Assert.assertEquals("\"NAME\" LIKE ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_contains() {
+        StatementHelper sh = mockedStatementHelper("%ill%");
+        Like f = new Like("NAME", "%ill%");
+        Assert.assertEquals("\"NAME\" LIKE ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_between() {
+        StatementHelper sh = mockedStatementHelper(18, 65);
+        Between f = new Between("AGE", 18, 65);
+        Assert.assertEquals("\"AGE\" BETWEEN ? AND ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_caseInsensitive_equals() {
+        StatementHelper sh = mockedStatementHelper("FIDO");
+        Like f = new Like("NAME", "Fido");
+        f.setCaseSensitive(false);
+        Assert.assertEquals("UPPER(\"NAME\") LIKE ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_caseInsensitive_startsWith() {
+        StatementHelper sh = mockedStatementHelper("VI%");
+        Like f = new Like("NAME", "Vi%");
+        f.setCaseSensitive(false);
+        Assert.assertEquals("UPPER(\"NAME\") LIKE ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_caseInsensitive_endsWith() {
+        StatementHelper sh = mockedStatementHelper("%LLE");
+        Like f = new Like("NAME", "%lle");
+        f.setCaseSensitive(false);
+        Assert.assertEquals("UPPER(\"NAME\") LIKE ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilter_caseInsensitive_contains() {
+        StatementHelper sh = mockedStatementHelper("%ILL%");
+        Like f = new Like("NAME", "%ill%");
+        f.setCaseSensitive(false);
+        Assert.assertEquals("UPPER(\"NAME\") LIKE ?",
+                QueryBuilder.getWhereStringForFilter(f, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilters_listOfFilters() {
+        StatementHelper sh = mockedStatementHelper("%lle", 18);
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        filters.add(new Like("NAME", "%lle"));
+        filters.add(new Greater("AGE", 18));
+        Assert.assertEquals(" WHERE \"NAME\" LIKE ? AND \"AGE\" > ?",
+                QueryBuilder.getWhereStringForFilters(filters, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilters_oneAndFilter() {
+        StatementHelper sh = mockedStatementHelper("%lle", 18);
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        filters.add(new And(new Like("NAME", "%lle"), new Greater("AGE", 18)));
+        Assert.assertEquals(" WHERE (\"NAME\" LIKE ? AND \"AGE\" > ?)",
+                QueryBuilder.getWhereStringForFilters(filters, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilters_oneOrFilter() {
+        StatementHelper sh = mockedStatementHelper("%lle", 18);
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        filters.add(new Or(new Like("NAME", "%lle"), new Greater("AGE", 18)));
+        Assert.assertEquals(" WHERE (\"NAME\" LIKE ? OR \"AGE\" > ?)",
+                QueryBuilder.getWhereStringForFilters(filters, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilters_complexCompoundFilters() {
+        StatementHelper sh = mockedStatementHelper("%lle", 18, 65, "Pelle");
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        filters.add(new Or(new And(new Like("NAME", "%lle"), new Or(new Less(
+                "AGE", 18), new Greater("AGE", 65))),
+                new Equal("NAME", "Pelle")));
+        Assert.assertEquals(
+                " WHERE ((\"NAME\" LIKE ? AND (\"AGE\" < ? OR \"AGE\" > ?)) OR \"NAME\" = ?)",
+                QueryBuilder.getWhereStringForFilters(filters, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilters_complexCompoundFiltersAndSingleFilter() {
+        StatementHelper sh = mockedStatementHelper("%lle", 18, 65, "Pelle",
+                "Virtanen");
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        filters.add(new Or(new And(new Like("NAME", "%lle"), new Or(new Less(
+                "AGE", 18), new Greater("AGE", 65))),
+                new Equal("NAME", "Pelle")));
+        filters.add(new Equal("LASTNAME", "Virtanen"));
+        Assert.assertEquals(
+                " WHERE ((\"NAME\" LIKE ? AND (\"AGE\" < ? OR \"AGE\" > ?)) OR \"NAME\" = ?) AND \"LASTNAME\" = ?",
+                QueryBuilder.getWhereStringForFilters(filters, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilters_emptyList_shouldReturnEmptyString() {
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        Assert.assertEquals("", QueryBuilder.getWhereStringForFilters(filters,
+                new StatementHelper()));
+    }
+
+    @Test
+    public void getWhereStringForFilters_NotFilter() {
+        StatementHelper sh = mockedStatementHelper(18);
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        filters.add(new Not(new Equal("AGE", 18)));
+        Assert.assertEquals(" WHERE NOT \"AGE\" = ?",
+                QueryBuilder.getWhereStringForFilters(filters, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilters_complexNegatedFilter() {
+        StatementHelper sh = mockedStatementHelper(65, 18);
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        filters.add(new Not(new Or(new Equal("AGE", 65), new Equal("AGE", 18))));
+        Assert.assertEquals(" WHERE NOT (\"AGE\" = ? OR \"AGE\" = ?)",
+                QueryBuilder.getWhereStringForFilters(filters, sh));
+        EasyMock.verify(sh);
+    }
+
+    @Test
+    public void getWhereStringForFilters_isNull() {
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        filters.add(new IsNull("NAME"));
+        Assert.assertEquals(" WHERE \"NAME\" IS NULL", QueryBuilder
+                .getWhereStringForFilters(filters, new StatementHelper()));
+    }
+
+    @Test
+    public void getWhereStringForFilters_isNotNull() {
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        filters.add(new Not(new IsNull("NAME")));
+        Assert.assertEquals(" WHERE \"NAME\" IS NOT NULL", QueryBuilder
+                .getWhereStringForFilters(filters, new StatementHelper()));
+    }
+
+    @Test
+    public void getWhereStringForFilters_customStringDecorator() {
+        QueryBuilder.setStringDecorator(new StringDecorator("[", "]"));
+        ArrayList<Filter> filters = new ArrayList<Filter>();
+        filters.add(new Not(new IsNull("NAME")));
+        Assert.assertEquals(" WHERE [NAME] IS NOT NULL", QueryBuilder
+                .getWhereStringForFilters(filters, new StatementHelper()));
+        // Reset the default string decorator
+        QueryBuilder.setStringDecorator(new StringDecorator("\"", "\""));
+    }
+}
diff --git a/tests/src/com/vaadin/tests/server/container/sqlcontainer/query/TableQueryTest.java b/tests/src/com/vaadin/tests/server/container/sqlcontainer/query/TableQueryTest.java
new file mode 100644 (file)
index 0000000..600dcac
--- /dev/null
@@ -0,0 +1,619 @@
+package com.vaadin.tests.server.container.sqlcontainer.query;\r
+\r
+import java.sql.Connection;\r
+import java.sql.PreparedStatement;\r
+import java.sql.ResultSet;\r
+import java.sql.SQLException;\r
+import java.sql.Statement;\r
+import java.util.ArrayList;\r
+import java.util.Arrays;\r
+import java.util.List;\r
+\r
+import org.junit.After;\r
+import org.junit.Assert;\r
+import org.junit.Before;\r
+import org.junit.Test;\r
+\r
+import com.vaadin.data.Container.Filter;\r
+import com.vaadin.data.util.OptimisticLockException;\r
+import com.vaadin.data.util.RowItem;\r
+import com.vaadin.data.util.SQLContainer;\r
+import com.vaadin.data.util.connection.JDBCConnectionPool;\r
+import com.vaadin.data.util.connection.SimpleJDBCConnectionPool;\r
+import com.vaadin.data.util.filter.Compare.Equal;\r
+import com.vaadin.data.util.filter.Like;\r
+import com.vaadin.data.util.query.OrderBy;\r
+import com.vaadin.data.util.query.TableQuery;\r
+import com.vaadin.data.util.query.generator.DefaultSQLGenerator;\r
+import com.vaadin.tests.server.container.sqlcontainer.AllTests;\r
+import com.vaadin.tests.server.container.sqlcontainer.AllTests.DB;\r
+import com.vaadin.tests.server.container.sqlcontainer.DataGenerator;\r
+\r
+public class TableQueryTest {\r
+    private static final int offset = AllTests.offset;\r
+    private JDBCConnectionPool connectionPool;\r
+\r
+    @Before\r
+    public void setUp() throws SQLException {\r
+\r
+        try {\r
+            connectionPool = new SimpleJDBCConnectionPool(AllTests.dbDriver,\r
+                    AllTests.dbURL, AllTests.dbUser, AllTests.dbPwd, 2, 2);\r
+        } catch (SQLException e) {\r
+            e.printStackTrace();\r
+            Assert.fail(e.getMessage());\r
+        }\r
+\r
+        DataGenerator.addPeopleToDatabase(connectionPool);\r
+    }\r
+\r
+    @After\r
+    public void tearDown() {\r
+        if (connectionPool != null) {\r
+            connectionPool.destroy();\r
+        }\r
+    }\r
+\r
+    /**********************************************************************\r
+     * TableQuery construction tests\r
+     **********************************************************************/\r
+    @Test\r
+    public void construction_legalParameters_shouldSucceed() {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                new DefaultSQLGenerator());\r
+        Assert.assertArrayEquals(new Object[] { "ID" }, tQuery\r
+                .getPrimaryKeyColumns().toArray());\r
+        boolean correctTableName = "people".equalsIgnoreCase(tQuery\r
+                .getTableName());\r
+        Assert.assertTrue(correctTableName);\r
+    }\r
+\r
+    @Test\r
+    public void construction_legalParameters_defaultGenerator_shouldSucceed() {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        Assert.assertArrayEquals(new Object[] { "ID" }, tQuery\r
+                .getPrimaryKeyColumns().toArray());\r
+        boolean correctTableName = "people".equalsIgnoreCase(tQuery\r
+                .getTableName());\r
+        Assert.assertTrue(correctTableName);\r
+    }\r
+\r
+    @Test(expected = IllegalArgumentException.class)\r
+    public void construction_nonExistingTableName_shouldFail() {\r
+        new TableQuery("skgwaguhsd", connectionPool, new DefaultSQLGenerator());\r
+    }\r
+\r
+    @Test(expected = IllegalArgumentException.class)\r
+    public void construction_emptyTableName_shouldFail() {\r
+        new TableQuery("", connectionPool, new DefaultSQLGenerator());\r
+    }\r
+\r
+    @Test(expected = IllegalArgumentException.class)\r
+    public void construction_nullSqlGenerator_shouldFail() {\r
+        new TableQuery("people", connectionPool, null);\r
+    }\r
+\r
+    @Test(expected = IllegalArgumentException.class)\r
+    public void construction_nullConnectionPool_shouldFail() {\r
+        new TableQuery("people", null, new DefaultSQLGenerator());\r
+    }\r
+\r
+    /**********************************************************************\r
+     * TableQuery row count tests\r
+     **********************************************************************/\r
+    @Test\r
+    public void getCount_simpleQuery_returnsFour() throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        Assert.assertEquals(4, tQuery.getCount());\r
+    }\r
+\r
+    @Test\r
+    public void getCount_simpleQueryTwoMorePeopleAdded_returnsSix()\r
+            throws SQLException {\r
+        // Add some people\r
+        Connection conn = connectionPool.reserveConnection();\r
+        Statement statement = conn.createStatement();\r
+        if (AllTests.db == DB.MSSQL) {\r
+            statement.executeUpdate("insert into people values('Bengt', 30)");\r
+            statement.executeUpdate("insert into people values('Ingvar', 50)");\r
+        } else {\r
+            statement\r
+                    .executeUpdate("insert into people values(default, 'Bengt', 30)");\r
+            statement\r
+                    .executeUpdate("insert into people values(default, 'Ingvar', 50)");\r
+        }\r
+        statement.close();\r
+        conn.commit();\r
+        connectionPool.releaseConnection(conn);\r
+\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+\r
+        Assert.assertEquals(6, tQuery.getCount());\r
+    }\r
+\r
+    @Test\r
+    public void getCount_normalState_releasesConnection() throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.getCount();\r
+        tQuery.getCount();\r
+        Assert.assertNotNull(connectionPool.reserveConnection());\r
+    }\r
+\r
+    /**********************************************************************\r
+     * TableQuery get results tests\r
+     **********************************************************************/\r
+    @Test\r
+    public void getResults_simpleQuery_returnsFourRecords() throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.beginTransaction();\r
+        ResultSet rs = tQuery.getResults(0, 0);\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(0 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Ville", rs.getString(2));\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(1 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Kalle", rs.getString(2));\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(2 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Pelle", rs.getString(2));\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(3 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Börje", rs.getString(2));\r
+\r
+        Assert.assertFalse(rs.next());\r
+        tQuery.commit();\r
+    }\r
+\r
+    @Test\r
+    public void getResults_noDelegate5000Rows_returns5000rows()\r
+            throws SQLException {\r
+        DataGenerator.addFiveThousandPeople(connectionPool);\r
+\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+\r
+        tQuery.beginTransaction();\r
+        ResultSet rs = tQuery.getResults(0, 0);\r
+        for (int i = 0; i < 5000; i++) {\r
+            Assert.assertTrue(rs.next());\r
+        }\r
+        Assert.assertFalse(rs.next());\r
+        tQuery.commit();\r
+    }\r
+\r
+    /**********************************************************************\r
+     * TableQuery transaction management tests\r
+     **********************************************************************/\r
+    @Test\r
+    public void beginTransaction_readOnly_shouldSucceed() throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.beginTransaction();\r
+    }\r
+\r
+    @Test(expected = IllegalStateException.class)\r
+    public void beginTransaction_transactionAlreadyActive_shouldFail()\r
+            throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+\r
+        tQuery.beginTransaction();\r
+        tQuery.beginTransaction();\r
+    }\r
+\r
+    @Test\r
+    public void commit_readOnly_shouldSucceed() throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.beginTransaction();\r
+        tQuery.commit();\r
+    }\r
+\r
+    @Test\r
+    public void rollback_readOnly_shouldSucceed() throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.beginTransaction();\r
+        tQuery.rollback();\r
+    }\r
+\r
+    @Test(expected = SQLException.class)\r
+    public void commit_noActiveTransaction_shouldFail() throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.commit();\r
+    }\r
+\r
+    @Test(expected = SQLException.class)\r
+    public void rollback_noActiveTransaction_shouldFail() throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.rollback();\r
+    }\r
+\r
+    /**********************************************************************\r
+     * TableQuery row query with given keys tests\r
+     **********************************************************************/\r
+    @Test\r
+    public void containsRowWithKeys_existingKeys_returnsTrue()\r
+            throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        Assert.assertTrue(tQuery.containsRowWithKey(1));\r
+    }\r
+\r
+    @Test\r
+    public void containsRowWithKeys_nonexistingKeys_returnsTrue()\r
+            throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+\r
+        Assert.assertFalse(tQuery.containsRowWithKey(1337));\r
+    }\r
+\r
+    @Test\r
+    public void containsRowWithKeys_invalidKeys_shouldFail()\r
+            throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        boolean b = true;\r
+        try {\r
+            b = tQuery.containsRowWithKey("foo");\r
+        } catch (SQLException se) {\r
+            return;\r
+        }\r
+        Assert.assertFalse(b);\r
+    }\r
+\r
+    @Test\r
+    public void containsRowWithKeys_nullKeys_shouldFailAndReleaseConnections()\r
+            throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        try {\r
+            tQuery.containsRowWithKey(new Object[] { null });\r
+        } catch (SQLException e) {\r
+            // We should now be able to reserve two connections\r
+            connectionPool.reserveConnection();\r
+            connectionPool.reserveConnection();\r
+        }\r
+    }\r
+\r
+    /**********************************************************************\r
+     * TableQuery filtering and ordering tests\r
+     **********************************************************************/\r
+    @Test\r
+    public void setFilters_shouldReturnCorrectCount() throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        List<Filter> filters = new ArrayList<Filter>();\r
+        filters.add(new Like("NAME", "%lle"));\r
+        tQuery.setFilters(filters);\r
+        Assert.assertEquals(3, tQuery.getCount());\r
+    }\r
+\r
+    @Test\r
+    public void setOrderByNameAscending_shouldReturnCorrectOrder()\r
+            throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+\r
+        List<OrderBy> orderBys = Arrays.asList(new OrderBy("NAME", true));\r
+        tQuery.setOrderBy(orderBys);\r
+\r
+        tQuery.beginTransaction();\r
+        ResultSet rs;\r
+        rs = tQuery.getResults(0, 0);\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(3 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Börje", rs.getString(2));\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(1 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Kalle", rs.getString(2));\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(2 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Pelle", rs.getString(2));\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(0 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Ville", rs.getString(2));\r
+\r
+        Assert.assertFalse(rs.next());\r
+        tQuery.commit();\r
+    }\r
+\r
+    @Test\r
+    public void setOrderByNameDescending_shouldReturnCorrectOrder()\r
+            throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+\r
+        List<OrderBy> orderBys = Arrays.asList(new OrderBy("NAME", false));\r
+        tQuery.setOrderBy(orderBys);\r
+\r
+        tQuery.beginTransaction();\r
+        ResultSet rs;\r
+        rs = tQuery.getResults(0, 0);\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(0 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Ville", rs.getString(2));\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(2 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Pelle", rs.getString(2));\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(1 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Kalle", rs.getString(2));\r
+\r
+        Assert.assertTrue(rs.next());\r
+        Assert.assertEquals(3 + offset, rs.getInt(1));\r
+        Assert.assertEquals("Börje", rs.getString(2));\r
+\r
+        Assert.assertFalse(rs.next());\r
+        tQuery.commit();\r
+    }\r
+\r
+    @Test\r
+    public void setFilters_nullParameter_shouldSucceed() {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.setFilters(null);\r
+    }\r
+\r
+    @Test\r
+    public void setOrderBy_nullParameter_shouldSucceed() {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.setOrderBy(null);\r
+    }\r
+\r
+    /**********************************************************************\r
+     * TableQuery row removal tests\r
+     **********************************************************************/\r
+    @Test\r
+    public void removeRowThroughContainer_legalRowItem_shouldSucceed()\r
+            throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(tQuery);\r
+        container.setAutoCommit(false);\r
+        Assert.assertTrue(container.removeItem(container.getItemIds()\r
+                .iterator().next()));\r
+\r
+        Assert.assertEquals(4, tQuery.getCount());\r
+        Assert.assertEquals(3, container.size());\r
+        container.commit();\r
+\r
+        Assert.assertEquals(3, tQuery.getCount());\r
+        Assert.assertEquals(3, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void removeRowThroughContainer_nonexistingRowId_shouldFail()\r
+            throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+\r
+        SQLContainer container = new SQLContainer(tQuery);\r
+        container.setAutoCommit(true);\r
+        Assert.assertFalse(container.removeItem("foo"));\r
+    }\r
+\r
+    /**********************************************************************\r
+     * TableQuery row adding / modification tests\r
+     **********************************************************************/\r
+    @Test\r
+    public void insertRowThroughContainer_shouldSucceed() throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.setVersionColumn("ID");\r
+\r
+        SQLContainer container = new SQLContainer(tQuery);\r
+        container.setAutoCommit(false);\r
+\r
+        Object item = container.addItem();\r
+        Assert.assertNotNull(item);\r
+\r
+        Assert.assertEquals(4, tQuery.getCount());\r
+        Assert.assertEquals(5, container.size());\r
+        container.commit();\r
+\r
+        Assert.assertEquals(5, tQuery.getCount());\r
+        Assert.assertEquals(5, container.size());\r
+    }\r
+\r
+    @Test\r
+    public void modifyRowThroughContainer_shouldSucceed() throws SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+\r
+        // In this test the primary key is used as a version column\r
+        tQuery.setVersionColumn("ID");\r
+        SQLContainer container = new SQLContainer(tQuery);\r
+        container.setAutoCommit(false);\r
+\r
+        /* Check that the container size is correct and there is no 'Viljami' */\r
+        Assert.assertEquals(4, container.size());\r
+        List<Filter> filters = new ArrayList<Filter>();\r
+        filters.add(new Equal("NAME", "Viljami"));\r
+        tQuery.setFilters(filters);\r
+        Assert.assertEquals(0, tQuery.getCount());\r
+        tQuery.setFilters(null);\r
+\r
+        /* Fetch first item, modify and commit */\r
+        Object item = container.getItem(container.getItemIds().iterator()\r
+                .next());\r
+        Assert.assertNotNull(item);\r
+\r
+        RowItem ri = (RowItem) item;\r
+        Assert.assertNotNull(ri.getItemProperty("NAME"));\r
+        ri.getItemProperty("NAME").setValue("Viljami");\r
+\r
+        container.commit();\r
+\r
+        // Check that the size is still correct and only 1 'Viljami' is found\r
+        Assert.assertEquals(4, tQuery.getCount());\r
+        Assert.assertEquals(4, container.size());\r
+        tQuery.setFilters(filters);\r
+        Assert.assertEquals(1, tQuery.getCount());\r
+    }\r
+\r
+    @Test\r
+    public void storeRow_noVersionColumn_shouldSucceed()\r
+            throws UnsupportedOperationException, SQLException {\r
+        TableQuery tQuery = new TableQuery("people", connectionPool,\r
+                AllTests.sqlGen);\r
+        SQLContainer container = new SQLContainer(tQuery);\r
+        Object id = container.addItem();\r
+        RowItem row = (RowItem) container.getItem(id);\r
+        row.getItemProperty("NAME").setValue("R2D2");\r
+        row.getItemProperty("AGE").setValue(123);\r
+        tQuery.beginTransaction();\r
+        tQuery.storeRow(row);\r
+        tQuery.commit();\r
+\r
+        Connection conn = connectionPool.reserveConnection();\r
+        PreparedStatement stmt = conn\r
+                .prepareStatement("SELECT * FROM PEOPLE WHERE \"NAME\" = ?");\r
+        stmt.setString(1, "R2D2");\r
+        ResultSet rs = stmt.executeQuery();\r
+        Assert.assertTrue(rs.next());\r
+        rs.close();\r
+        stmt.close();\r
+        connectionPool.releaseConnection(conn);\r
+    }\r
+\r
+    @Test\r
+    public void storeRow_versionSetAndEqualToDBValue_shouldSucceed()\r
+            throws SQLException {\r
+        DataGenerator.addVersionedData(connectionPool);\r
+\r
+        TableQuery tQuery = new TableQuery("versioned", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.setVersionColumn("VERSION");\r
+        SQLContainer container = new SQLContainer(tQuery);\r
+        RowItem row = (RowItem) container.getItem(container.firstItemId());\r
+        Assert.assertEquals("Junk", row.getItemProperty("TEXT").getValue());\r
+\r
+        row.getItemProperty("TEXT").setValue("asdf");\r
+        container.commit();\r
+\r
+        Connection conn = connectionPool.reserveConnection();\r
+        PreparedStatement stmt = conn\r
+                .prepareStatement("SELECT * FROM VERSIONED WHERE \"TEXT\" = ?");\r
+        stmt.setString(1, "asdf");\r
+        ResultSet rs = stmt.executeQuery();\r
+        Assert.assertTrue(rs.next());\r
+        rs.close();\r
+        stmt.close();\r
+        conn.commit();\r
+        connectionPool.releaseConnection(conn);\r
+    }\r
+\r
+    @Test(expected = OptimisticLockException.class)\r
+    public void storeRow_versionSetAndLessThanDBValue_shouldThrowException()\r
+            throws SQLException {\r
+        if (AllTests.db == DB.HSQLDB) {\r
+            throw new OptimisticLockException(\r
+                    "HSQLDB doesn't support row versioning for optimistic locking - don't run this test.",\r
+                    null);\r
+        }\r
+        DataGenerator.addVersionedData(connectionPool);\r
+\r
+        TableQuery tQuery = new TableQuery("versioned", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.setVersionColumn("VERSION");\r
+        SQLContainer container = new SQLContainer(tQuery);\r
+        RowItem row = (RowItem) container.getItem(container.firstItemId());\r
+        Assert.assertEquals("Junk", row.getItemProperty("TEXT").getValue());\r
+\r
+        row.getItemProperty("TEXT").setValue("asdf");\r
+\r
+        // Update the version using another connection.\r
+        Connection conn = connectionPool.reserveConnection();\r
+        PreparedStatement stmt = conn\r
+                .prepareStatement("UPDATE VERSIONED SET \"TEXT\" = ? WHERE \"ID\" = ?");\r
+        stmt.setString(1, "foo");\r
+        stmt.setObject(2, row.getItemProperty("ID").getValue());\r
+        stmt.executeUpdate();\r
+        stmt.close();\r
+        conn.commit();\r
+        connectionPool.releaseConnection(conn);\r
+\r
+        container.commit();\r
+    }\r
+\r
+    @Test\r
+    public void removeRow_versionSetAndEqualToDBValue_shouldSucceed()\r
+            throws SQLException {\r
+        DataGenerator.addVersionedData(connectionPool);\r
+\r
+        TableQuery tQuery = new TableQuery("versioned", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.setVersionColumn("VERSION");\r
+        SQLContainer container = new SQLContainer(tQuery);\r
+        RowItem row = (RowItem) container.getItem(container.firstItemId());\r
+        Assert.assertEquals("Junk", row.getItemProperty("TEXT").getValue());\r
+\r
+        container.removeItem(container.firstItemId());\r
+        container.commit();\r
+\r
+        Connection conn = connectionPool.reserveConnection();\r
+        PreparedStatement stmt = conn\r
+                .prepareStatement("SELECT * FROM VERSIONED WHERE \"TEXT\" = ?");\r
+        stmt.setString(1, "Junk");\r
+        ResultSet rs = stmt.executeQuery();\r
+        Assert.assertFalse(rs.next());\r
+        rs.close();\r
+        stmt.close();\r
+        conn.commit();\r
+        connectionPool.releaseConnection(conn);\r
+    }\r
+\r
+    @Test(expected = OptimisticLockException.class)\r
+    public void removeRow_versionSetAndLessThanDBValue_shouldThrowException()\r
+            throws SQLException {\r
+        if (AllTests.db == AllTests.DB.HSQLDB) {\r
+            // HSQLDB doesn't support versioning, so this is to make the test\r
+            // green.\r
+            throw new OptimisticLockException(null);\r
+        }\r
+        DataGenerator.addVersionedData(connectionPool);\r
+\r
+        TableQuery tQuery = new TableQuery("versioned", connectionPool,\r
+                AllTests.sqlGen);\r
+        tQuery.setVersionColumn("VERSION");\r
+        SQLContainer container = new SQLContainer(tQuery);\r
+        RowItem row = (RowItem) container.getItem(container.firstItemId());\r
+        Assert.assertEquals("Junk", row.getItemProperty("TEXT").getValue());\r
+\r
+        // Update the version using another connection.\r
+        Connection conn = connectionPool.reserveConnection();\r
+        PreparedStatement stmt = conn\r
+                .prepareStatement("UPDATE VERSIONED SET \"TEXT\" = ? WHERE \"ID\" = ?");\r
+        stmt.setString(1, "asdf");\r
+        stmt.setObject(2, row.getItemProperty("ID").getValue());\r
+        stmt.executeUpdate();\r
+        stmt.close();\r
+        conn.commit();\r
+        connectionPool.releaseConnection(conn);\r
+\r
+        container.removeItem(container.firstItemId());\r
+        container.commit();\r
+    }\r
+\r
+}
\ No newline at end of file