You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2015/04/28 12:45:06 UTC

[1/7] cayenne git commit: Refactoring SelectTranslator for better extensibility

Repository: cayenne
Updated Branches:
  refs/heads/master a6b55f792 -> a6d153a28


http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java
index 6608413..1ea09cb 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java
@@ -19,57 +19,47 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import java.sql.Connection;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
 
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.JoinType;
 import org.apache.cayenne.query.Query;
 
 public class TstQueryAssembler extends QueryAssembler {
 
-    protected List<DbRelationship> dbRels;
+	protected List<DbRelationship> dbRels;
 
-    public TstQueryAssembler(Query q, DataNode node, Connection connection) throws SQLException {
-        super(q, node, connection);
-        dbRels = new ArrayList<DbRelationship>();
-    }
+	public TstQueryAssembler(Query q, DbAdapter adapter, EntityResolver resolver) throws SQLException {
+		super(q, adapter, resolver);
+		dbRels = new ArrayList<DbRelationship>();
+	}
 
-    @Override
-    public void dbRelationshipAdded(DbRelationship relationship, JoinType joinType, String joinSplitAlias) {
-        dbRels.add(relationship);
-    }
+	@Override
+	public void dbRelationshipAdded(DbRelationship relationship, JoinType joinType, String joinSplitAlias) {
+		dbRels.add(relationship);
+	}
 
-    @Override
-    public String getCurrentAlias() {
-        return "ta";
-    }
+	@Override
+	public String getCurrentAlias() {
+		return "ta";
+	}
 
-    @Override
-    public void resetJoinStack() {
-        // noop
-    }
+	@Override
+	public void resetJoinStack() {
+		// noop
+	}
 
-    @Override
-    public boolean supportsTableAliases() {
-        return true;
-    }
+	@Override
+	public boolean supportsTableAliases() {
+		return true;
+	}
 
-    @Override
-    public String createSqlString() {
-        return "SELECT * FROM ARTIST";
-    }
-
-    public List<DbAttribute> getAttributes() {
-        return attributes;
-    }
-
-    public List getValues() {
-        return values;
-    }
+	@Override
+	protected void doTranslate() {
+		this.sql = "SELECT * FROM ARTIST";
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/test/java/org/apache/cayenne/merge/ValueForNullIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/merge/ValueForNullIT.java b/cayenne-server/src/test/java/org/apache/cayenne/merge/ValueForNullIT.java
index 6fd121b..fb43eb3 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/merge/ValueForNullIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/merge/ValueForNullIT.java
@@ -30,7 +30,7 @@ import junit.framework.AssertionFailedError;
 import org.apache.cayenne.DataObject;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.access.DataContext;
-import org.apache.cayenne.access.jdbc.ParameterBinding;
+import org.apache.cayenne.access.jdbc.SQLParameterBinding;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
@@ -107,11 +107,11 @@ public class ValueForNullIT extends MergeCase {
 		return super.createMerger(mergerFactory, new DefaultValueForNullProvider() {
 
 			@Override
-			protected ParameterBinding get(DbEntity entity, DbAttribute column) {
+			protected SQLParameterBinding get(DbEntity entity, DbAttribute column) {
 				int type = column.getType();
 				switch (type) {
 				case Types.VARCHAR:
-					return new ParameterBinding(DEFAULT_VALUE_STRING, type, -1);
+					return new SQLParameterBinding(DEFAULT_VALUE_STRING, type, -1);
 				default:
 					throw new AssertionFailedError("should not get here");
 				}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java b/cayenne-server/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java
index f21e0f3..9387468 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java
@@ -31,7 +31,7 @@ import java.util.Map;
 import org.apache.cayenne.CayenneDataObject;
 import org.apache.cayenne.DataObject;
 import org.apache.cayenne.ObjectId;
-import org.apache.cayenne.access.jdbc.ParameterBinding;
+import org.apache.cayenne.access.jdbc.SQLParameterBinding;
 import org.apache.cayenne.access.jdbc.SQLStatement;
 import org.junit.Before;
 import org.junit.Test;
@@ -223,12 +223,12 @@ public class VelocitySQLTemplateProcessorTest {
 	}
 
 	private void assertBindingValue(Object expectedValue, Object binding) {
-		assertTrue("Not a binding!", binding instanceof ParameterBinding);
-		assertEquals(expectedValue, ((ParameterBinding) binding).getValue());
+		assertTrue("Not a binding!", binding instanceof SQLParameterBinding);
+		assertEquals(expectedValue, ((SQLParameterBinding) binding).getValue());
 	}
 
 	private void assertBindingType(int expectedType, Object binding) {
-		assertTrue("Not a binding!", binding instanceof ParameterBinding);
-		assertEquals(expectedType, ((ParameterBinding) binding).getJdbcType());
+		assertTrue("Not a binding!", binding instanceof SQLParameterBinding);
+		assertEquals(expectedType, ((SQLParameterBinding) binding).getJdbcType());
 	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/docs/doc/src/main/resources/RELEASE-NOTES.txt
----------------------------------------------------------------------
diff --git a/docs/doc/src/main/resources/RELEASE-NOTES.txt b/docs/doc/src/main/resources/RELEASE-NOTES.txt
index 05564c8..46dd250 100644
--- a/docs/doc/src/main/resources/RELEASE-NOTES.txt
+++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt
@@ -20,6 +20,7 @@ CAY-1995 Add support for iterators to Select
 CAY-2001 Saving a display state of Project
 CAY-2004 EJBQL: Support for ordering on aggregate expressions
 CAY-2005 Improving generated DISTINCT performance on PostgreSQL
+CAY-2007 Refactoring SelectTranslator for better extensibility
 
 Bug Fixes:
 


[4/7] cayenne git commit: Refactoring SelectTranslator for better extensibility

Posted by aa...@apache.org.
http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java
index a62b4d1..d652885 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java
@@ -19,634 +19,27 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.access.DataNode;
+import java.util.Map;
+
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.ExpressionFactory;
-import org.apache.cayenne.exp.parser.ASTDbPath;
-import org.apache.cayenne.map.DataMap;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbJoin;
-import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.JoinType;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.map.ObjAttribute;
-import org.apache.cayenne.map.ObjEntity;
-import org.apache.cayenne.map.ObjRelationship;
-import org.apache.cayenne.map.PathComponent;
-import org.apache.cayenne.query.PrefetchSelectQuery;
-import org.apache.cayenne.query.PrefetchTreeNode;
-import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SelectQuery;
-import org.apache.cayenne.reflect.ArcProperty;
-import org.apache.cayenne.reflect.AttributeProperty;
-import org.apache.cayenne.reflect.ClassDescriptor;
-import org.apache.cayenne.reflect.PropertyVisitor;
-import org.apache.cayenne.reflect.ToManyProperty;
-import org.apache.cayenne.reflect.ToOneProperty;
-import org.apache.cayenne.util.CayenneMapEntry;
-import org.apache.cayenne.util.EqualsBuilder;
-import org.apache.cayenne.util.HashCodeBuilder;
-
-import java.sql.Connection;
-import java.sql.Types;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 /**
- * A builder of JDBC PreparedStatements based on Cayenne SelectQueries.
- * Translates SelectQuery to parameterized SQL string and wraps it in a
- * PreparedStatement. SelectTranslator is stateful and thread-unsafe.
+ * An abstraction of {@link SelectQuery} translator.
+ * 
+ * @since 4.0 this is an interface.
  */
-public class SelectTranslator extends QueryAssembler {
-
-    protected static final int[] UNSUPPORTED_DISTINCT_TYPES = { Types.BLOB, Types.CLOB, Types.NCLOB,
-            Types.LONGVARBINARY, Types.LONGVARCHAR, Types.LONGNVARCHAR };
-
-    protected static boolean isUnsupportedForDistinct(int type) {
-        for (int unsupportedDistinctType : UNSUPPORTED_DISTINCT_TYPES) {
-            if (unsupportedDistinctType == type) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    JoinStack joinStack;
-    
-    /**
-     * @since 4.0
-     */
-    public SelectTranslator(Query query, DataNode dataNode, Connection connection) {
-        super(query, dataNode, connection);
-    }
-
-    public JoinStack getJoinStack() {
-        if (joinStack == null) {
-            joinStack = createJoinStack();
-        }
-        return joinStack;
-    }
-
-    List<ColumnDescriptor> resultColumns;
-    Map<ObjAttribute, ColumnDescriptor> attributeOverrides;
-    Map<ColumnDescriptor, ObjAttribute> defaultAttributesByColumn;
-
-    boolean suppressingDistinct;
-
-    /**
-     * If set to <code>true</code>, indicates that distinct select query is
-     * required no matter what the original query settings where. This flag can
-     * be set when joins are created using "to-many" relationships.
-     */
-    boolean forcingDistinct;
-
-    protected JoinStack createJoinStack() {
-        return new JoinStack(getAdapter(), queryMetadata.getDataMap(), this);
-    }
-
-    /**
-     * Returns query translated to SQL. This is a main work method of the
-     * SelectTranslator.
-     */
-    @Override
-    public String createSqlString() throws Exception {
-        if (cachedSqlString != null) {
-            return cachedSqlString;
-        }
-
-        DataMap dataMap = queryMetadata.getDataMap();
-        JoinStack joins = getJoinStack();
-
-        QuotingStrategy strategy = getAdapter().getQuotingStrategy();
-        forcingDistinct = false;
-
-        // build column list
-        this.resultColumns = buildResultColumns();
-
-        // build qualifier
-        QualifierTranslator qualifierTranslator = adapter.getQualifierTranslator(this);
-        StringBuilder qualifierBuffer = qualifierTranslator.appendPart(new StringBuilder());
-
-        // build ORDER BY
-        OrderingTranslator orderingTranslator = new OrderingTranslator(this);
-        StringBuilder orderingBuffer = orderingTranslator.appendPart(new StringBuilder());
-
-        // assemble
-        StringBuilder queryBuf = new StringBuilder();
-        queryBuf.append("SELECT ");
-
-        // check if DISTINCT is appropriate
-        // side effect: "suppressingDistinct" flag may end up being flipped here
-        if (forcingDistinct || getSelectQuery().isDistinct()) {
-            suppressingDistinct = false;
-
-            for (ColumnDescriptor column : resultColumns) {
-                if (isUnsupportedForDistinct(column.getJdbcType())) {
-                    suppressingDistinct = true;
-                    break;
-                }
-            }
-
-            if (!suppressingDistinct) {
-            	queryBuf.append(buildDistinctStatement() + " ");
-            }
-        }
-
-        // convert ColumnDescriptors to column names
-        List<String> selectColumnExpList = new ArrayList<String>();
-        for (ColumnDescriptor column : resultColumns) {
-            String fullName = strategy.quotedIdentifier(dataMap, column.getNamePrefix(), column.getName());
-            selectColumnExpList.add(fullName);
-        }
-
-        // append any column expressions used in the order by if this query
-        // uses the DISTINCT modifier
-        if (forcingDistinct || getSelectQuery().isDistinct()) {
-            List<String> orderByColumnList = orderingTranslator.getOrderByColumnList();
-            for (String orderByColumnExp : orderByColumnList) {
-                // Convert to ColumnDescriptors??
-                if (!selectColumnExpList.contains(orderByColumnExp)) {
-                    selectColumnExpList.add(orderByColumnExp);
-                }
-            }
-        }
-
-        appendSelectColumns(queryBuf, selectColumnExpList);
-
-        // append from clause
-        queryBuf.append(" FROM ");
-
-        // append tables and joins
-        joins.appendRootWithQuoteSqlIdentifiers(queryBuf, getRootDbEntity());
-
-        // join parameters will be added to head of query
-        parameterIndex = 0;
-
-        joins.appendJoins(queryBuf);
-        joins.appendQualifier(qualifierBuffer, qualifierBuffer.length() == 0);
-
-        // append qualifier
-        if (qualifierBuffer.length() > 0) {
-            queryBuf.append(" WHERE ");
-            queryBuf.append(qualifierBuffer);
-        }
-
-        // append prebuilt ordering
-        if (orderingBuffer.length() > 0) {
-            queryBuf.append(" ORDER BY ").append(orderingBuffer);
-        }
-
-        if (!isSuppressingDistinct()) {
-            appendLimitAndOffsetClauses(queryBuf);
-        }
-
-        cachedSqlString = queryBuf.toString();
-        return cachedSqlString;
-    }
-    
-	/**
-	 * Allows subclasses to insert their own dialect of DISTINCT statement to
-	 * improve performance.
-	 *
-	 * @return string representing the DISTINCT statement
-	 * @since 4.0
-	 */
-	protected String buildDistinctStatement() {
-		return "DISTINCT";
-	}
-
-    /**
-     * @since 3.1
-     */
-    protected void appendSelectColumns(StringBuilder buffer, List<String> selectColumnExpList) {
-
-        // append columns (unroll the loop's first element)
-        int columnCount = selectColumnExpList.size();
-        buffer.append(selectColumnExpList.get(0));
-
-        // assume there is at least 1 element
-        for (int i = 1; i < columnCount; i++) {
-            buffer.append(", ");
-            buffer.append(selectColumnExpList.get(i));
-        }
-    }
-
-    /**
-     * Handles appending optional limit and offset clauses. This implementation
-     * does nothing, deferring to subclasses to define the LIMIT/OFFSET clause
-     * syntax.
-     * 
-     * @since 3.0
-     */
-    protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-
-    }
-
-    @Override
-    public String getCurrentAlias() {
-        return getJoinStack().getCurrentAlias();
-    }
-
-    /**
-     * Returns a list of ColumnDescriptors for the query columns.
-     * 
-     * @since 1.2
-     */
-    public ColumnDescriptor[] getResultColumns() {
-        if (resultColumns == null || resultColumns.isEmpty()) {
-            return new ColumnDescriptor[0];
-        }
-
-        return resultColumns.toArray(new ColumnDescriptor[resultColumns.size()]);
-    }
-
-    /**
-     * Returns a map of ColumnDescriptors keyed by ObjAttribute for columns that
-     * may need to be reprocessed manually due to incompatible mappings along
-     * the inheritance hierarchy.
-     * 
-     * @since 1.2
-     */
-    public Map<ObjAttribute, ColumnDescriptor> getAttributeOverrides() {
-        if (attributeOverrides != null) {
-            return attributeOverrides;
-        } else {
-            return Collections.emptyMap();
-        }
-    }
-
-    /**
-     * Returns true if SelectTranslator determined that a query requiring
-     * DISTINCT can't be run with DISTINCT keyword for internal reasons. If this
-     * method returns true, DataNode may need to do in-memory distinct
-     * filtering.
-     * 
-     * @since 1.1
-     */
-    public boolean isSuppressingDistinct() {
-        return suppressingDistinct;
-    }
-
-    private SelectQuery<?> getSelectQuery() {
-        return (SelectQuery<?>) getQuery();
-    }
-
-    protected List<ColumnDescriptor> buildResultColumns() {
-
-        this.defaultAttributesByColumn = new HashMap<ColumnDescriptor, ObjAttribute>();
-
-        List<ColumnDescriptor> columns = new ArrayList<ColumnDescriptor>();
-        SelectQuery<?> query = getSelectQuery();
-
-        if (query.getRoot() instanceof DbEntity) {
-            appendDbEntityColumns(columns, query);
-        } else if (getQueryMetadata().getPageSize() > 0) {
-            appendIdColumns(columns, query);
-        } else {
-            appendQueryColumns(columns, query);
-        }
-
-        return columns;
-    }
-
-    <T> List<ColumnDescriptor> appendDbEntityColumns(List<ColumnDescriptor> columns, SelectQuery<T> query) {
-
-        Set<ColumnTracker> attributes = new HashSet<ColumnTracker>();
-
-        DbEntity table = getRootDbEntity();
-        for (DbAttribute dba : table.getAttributes()) {
-            appendColumn(columns, null, dba, attributes, null);
-        }
-
-        return columns;
-    }
-
-    /**
-     * Appends columns needed for object SelectQuery to the provided columns
-     * list.
-     */
-    <T> List<ColumnDescriptor> appendQueryColumns(final List<ColumnDescriptor> columns, SelectQuery<T> query) {
-
-        final Set<ColumnTracker> attributes = new HashSet<ColumnTracker>();
-
-        // fetched attributes include attributes that are either:
-        //
-        // * class properties
-        // * PK
-        // * FK used in relationship
-        // * joined prefetch PK
-
-        ClassDescriptor descriptor = queryMetadata.getClassDescriptor();
-        ObjEntity oe = descriptor.getEntity();
-
-        PropertyVisitor visitor = new PropertyVisitor() {
-
-            public boolean visitAttribute(AttributeProperty property) {
-                ObjAttribute oa = property.getAttribute();
-
-                resetJoinStack();
-                Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator();
-                while (dbPathIterator.hasNext()) {
-                    Object pathPart = dbPathIterator.next();
-
-                    if (pathPart == null) {
-                        throw new CayenneRuntimeException("ObjAttribute has no component: " + oa.getName());
-                    } else if (pathPart instanceof DbRelationship) {
-                        DbRelationship rel = (DbRelationship) pathPart;
-                        dbRelationshipAdded(rel, JoinType.LEFT_OUTER, null);
-                    } else if (pathPart instanceof DbAttribute) {
-                        DbAttribute dbAttr = (DbAttribute) pathPart;
-
-                        appendColumn(columns, oa, dbAttr, attributes, null);
-                    }
-                }
-                return true;
-            }
-
-            public boolean visitToMany(ToManyProperty property) {
-                visitRelationship(property);
-                return true;
-            }
-
-            public boolean visitToOne(ToOneProperty property) {
-                visitRelationship(property);
-                return true;
-            }
-
-            private void visitRelationship(ArcProperty property) {
-                resetJoinStack();
-
-                ObjRelationship rel = property.getRelationship();
-                DbRelationship dbRel = rel.getDbRelationships().get(0);
-
-                List<DbJoin> joins = dbRel.getJoins();
-                for (DbJoin join : joins) {
-                    DbAttribute src = join.getSource();
-                    appendColumn(columns, null, src, attributes, null);
-                }
-            }
-        };
-
-        descriptor.visitAllProperties(visitor);
-
-        // stack should be reset, because all root table attributes go with "t0"
-        // table alias
-        resetJoinStack();
-
-        // add remaining needed attrs from DbEntity
-        DbEntity table = getRootDbEntity();
-        for (DbAttribute dba : table.getPrimaryKeys()) {
-            appendColumn(columns, null, dba, attributes, null);
-        }
-
-        // special handling of a disjoint query...
-
-        if (query instanceof PrefetchSelectQuery) {
-
-            // for each relationship path add PK of the target entity...
-            for (String path : ((PrefetchSelectQuery) query).getResultPaths()) {
-
-                ASTDbPath pathExp = (ASTDbPath) oe.translateToDbPath(ExpressionFactory.exp(path));
-
-                // add joins and find terminating element
-
-                resetJoinStack();
-
-                PathComponent<DbAttribute, DbRelationship> lastComponent = null;
-                for (PathComponent<DbAttribute, DbRelationship> component : table
-                        .resolvePath(pathExp, getPathAliases())) {
-
-					if (component.getRelationship() != null) {
-						// do not invoke dbRelationshipAdded(), invoke
-						// pushJoin() instead. This is to prevent
-						// 'forcingDistinct' flipping to true, that will result
-						// in unneeded extra processing and sometimes in invalid
-						// results (see CAY-1979). Distinctness of each row is
-						// guaranteed by the prefetch query semantics - we
-						// include target ID in the result columns
-						getJoinStack().pushJoin(component.getRelationship(), component.getJoinType(), null);
-					}
-
-                    lastComponent = component;
-                }
-
-                // process terminating element
-                if (lastComponent != null) {
-
-                    DbRelationship relationship = lastComponent.getRelationship();
-
-                    if (relationship != null) {
-
-                        String labelPrefix = pathExp.getPath();
-                        DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
-
-                        for (DbAttribute pk : targetEntity.getPrimaryKeys()) {
-
-                            // note that we my select a source attribute, but
-                            // label it as
-                            // target for simplified snapshot processing
-                            appendColumn(columns, null, pk, attributes, labelPrefix + '.' + pk.getName());
-                        }
-                    }
-                }
-            }
-        }
-
-        // handle joint prefetches directly attached to this query...
-        if (query.getPrefetchTree() != null) {
-
-            for (PrefetchTreeNode prefetch : query.getPrefetchTree().adjacentJointNodes()) {
-
-                // for each prefetch add all joins plus columns from the target
-                // entity
-                Expression prefetchExp = ExpressionFactory.exp(prefetch.getPath());
-                ASTDbPath dbPrefetch = (ASTDbPath) oe.translateToDbPath(prefetchExp);
-
-                resetJoinStack();
-                DbRelationship r = null;
-                for (PathComponent<DbAttribute, DbRelationship> component : table.resolvePath(dbPrefetch,
-                        getPathAliases())) {
-                    r = component.getRelationship();
-                    dbRelationshipAdded(r, JoinType.LEFT_OUTER, null);
-                }
-
-                if (r == null) {
-                    throw new CayenneRuntimeException("Invalid joint prefetch '" + prefetch + "' for entity: "
-                            + oe.getName());
-                }
-
-                // add columns from the target entity, including those that are
-                // matched
-                // against the FK of the source entity. This is needed to
-                // determine
-                // whether optional relationships are null
-
-                // go via target OE to make sure that Java types are mapped
-                // correctly...
-                ObjRelationship targetRel = (ObjRelationship) prefetchExp.evaluate(oe);
-                ObjEntity targetEntity = (ObjEntity) targetRel.getTargetEntity();
-
-                String labelPrefix = dbPrefetch.getPath();
-                for (ObjAttribute oa : targetEntity.getAttributes()) {
-                    Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator();
-                    while (dbPathIterator.hasNext()) {
-                        Object pathPart = dbPathIterator.next();
-
-                        if (pathPart == null) {
-                            throw new CayenneRuntimeException("ObjAttribute has no component: " + oa.getName());
-                        } else if (pathPart instanceof DbRelationship) {
-                            DbRelationship rel = (DbRelationship) pathPart;
-                            dbRelationshipAdded(rel, JoinType.INNER, null);
-                        } else if (pathPart instanceof DbAttribute) {
-                            DbAttribute attribute = (DbAttribute) pathPart;
-
-                            appendColumn(columns, oa, attribute, attributes, labelPrefix + '.' + attribute.getName());
-                        }
-                    }
-                }
-
-                // append remaining target attributes such as keys
-                DbEntity targetDbEntity = (DbEntity) r.getTargetEntity();
-                for (DbAttribute attribute : targetDbEntity.getAttributes()) {
-                    appendColumn(columns, null, attribute, attributes, labelPrefix + '.' + attribute.getName());
-                }
-            }
-        }
-
-        return columns;
-    }
-
-    <T> List<ColumnDescriptor> appendIdColumns(final List<ColumnDescriptor> columns, SelectQuery<T> query) {
-
-        Set<ColumnTracker> skipSet = new HashSet<ColumnTracker>();
-
-        ClassDescriptor descriptor = queryMetadata.getClassDescriptor();
-        ObjEntity oe = descriptor.getEntity();
-        DbEntity dbEntity = oe.getDbEntity();
-        for (ObjAttribute attribute : oe.getPrimaryKeys()) {
-
-            // synthetic objattributes can't reliably lookup their DbAttribute,
-            // so do it manually..
-            DbAttribute dbAttribute = dbEntity.getAttribute(attribute.getDbAttributeName());
-            appendColumn(columns, attribute, dbAttribute, skipSet, null);
-        }
-
-        return columns;
-    }
-
-    private void appendColumn(List<ColumnDescriptor> columns, ObjAttribute objAttribute, DbAttribute attribute,
-            Set<ColumnTracker> skipSet, String label) {
-
-        String alias = getCurrentAlias();
-        if (skipSet.add(new ColumnTracker(alias, attribute))) {
-
-            ColumnDescriptor column = (objAttribute != null) ? new ColumnDescriptor(objAttribute, attribute, alias)
-                    : new ColumnDescriptor(attribute, alias);
-
-            if (label != null) {
-                column.setDataRowKey(label);
-            }
-
-            columns.add(column);
-
-            // TODO: andrus, 5/7/2006 - replace 'columns' collection with this
-            // map, as it
-            // is redundant
-            defaultAttributesByColumn.put(column, objAttribute);
-        } else if (objAttribute != null) {
-
-            // record ObjAttribute override
-            for (ColumnDescriptor column : columns) {
-                if (attribute.getName().equals(column.getName())) {
-                    
-                    if (attributeOverrides == null) {
-                        attributeOverrides = new HashMap<ObjAttribute, ColumnDescriptor>();
-                    }
-
-                    // kick out the original attribute
-                    ObjAttribute original = defaultAttributesByColumn.remove(column);
-
-                    if (original != null) {
-                        attributeOverrides.put(original, column);
-                    }
-
-                    attributeOverrides.put(objAttribute, column);
-                    column.setJavaClass(Void.TYPE.getName());
-                    
-                    break;
-                }
-            }
-        }
-    }
-
-    /**
-     * @since 3.0
-     */
-    @Override
-    public void resetJoinStack() {
-        getJoinStack().resetStack();
-    }
-
-    /**
-     * @since 3.0
-     */
-    @Override
-    public void dbRelationshipAdded(DbRelationship relationship, JoinType joinType, String joinSplitAlias) {
-        if (relationship.isToMany()) {
-            forcingDistinct = true;
-        }
-
-        getJoinStack().pushJoin(relationship, joinType, joinSplitAlias);
-    }
-
-    /**
-     * Always returns true.
-     */
-    @Override
-    public boolean supportsTableAliases() {
-        return true;
-    }
-
-    static final class ColumnTracker {
-
-        private DbAttribute attribute;
-        private String alias;
+public interface SelectTranslator {
 
-        ColumnTracker(String alias, DbAttribute attribute) {
-            this.attribute = attribute;
-            this.alias = alias;
-        }
+	String getSql() throws Exception;
 
-        @Override
-        public boolean equals(Object object) {
-            if (!(object instanceof ColumnTracker)) {
-                return false;
-            }
+	ParameterBinding[] getBindings();
 
-            ColumnTracker other = (ColumnTracker) object;
-            return new EqualsBuilder()
-                    .append(alias, other.alias)
-                    .append(attribute, other.attribute)
-                    .isEquals();
-        }
+	Map<ObjAttribute, ColumnDescriptor> getAttributeOverrides();
 
-        @Override
-        public int hashCode() {
-            return new HashCodeBuilder(31, 5)
-                    .append(alias)
-                    .append(attribute)
-                    .toHashCode();
-        }
+	ColumnDescriptor[] getResultColumns();
 
-    }
+	boolean isSuppressingDistinct();
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TrimmingQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TrimmingQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TrimmingQualifierTranslator.java
index c992cfd..31eb2c9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TrimmingQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TrimmingQualifierTranslator.java
@@ -19,75 +19,74 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import java.io.IOException;
 import java.sql.Types;
 
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.map.DbAttribute;
 
 /**
- * QualifierTranslator that allows translation of qualifiers that perform comparison with
- * CHAR columns. Some databases require trimming the values for this to work.
+ * QualifierTranslator that allows translation of qualifiers that perform
+ * comparison with CHAR columns. Some databases require trimming the values for
+ * this to work.
  * 
  */
 public class TrimmingQualifierTranslator extends QualifierTranslator {
 
-    protected String trimFunction;
+	protected String trimFunction;
 
-    /**
-     * Constructor for TrimmingQualifierTranslator.
-     */
-    public TrimmingQualifierTranslator(QueryAssembler queryAssembler, String trimFunction) {
-        super(queryAssembler);
-        this.trimFunction = trimFunction;
-    }
+	/**
+	 * Constructor for TrimmingQualifierTranslator.
+	 */
+	public TrimmingQualifierTranslator(QueryAssembler queryAssembler, String trimFunction) {
+		super(queryAssembler);
+		this.trimFunction = trimFunction;
+	}
 
-    /**
-     * Adds special handling of CHAR columns.
-     */
-    @Override
-    protected void processColumn(DbAttribute dbAttr) throws IOException {
-        if (dbAttr.getType() == Types.CHAR) {
-            out.append(trimFunction).append("(");
-            super.processColumn(dbAttr);
-            out.append(')');
-        }
-        else {
-            super.processColumn(dbAttr);
-        }
-    }
-    
-    /**
-     * Adds special handling of CHAR columns.
-     */
-    @Override
-    protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) throws IOException {
-        
-        if (dbAttr.getType() == Types.CHAR) {
-            out.append(trimFunction).append("(");
-            super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-            out.append(')');
-        }
-        else {
-            super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-        }                 
-    }
+	/**
+	 * Adds special handling of CHAR columns.
+	 */
+	@Override
+	protected void processColumn(DbAttribute dbAttr) {
+		if (dbAttr.getType() == Types.CHAR) {
+			out.append(trimFunction).append("(");
+			super.processColumn(dbAttr);
+			out.append(')');
+		} else {
+			super.processColumn(dbAttr);
+		}
+	}
 
-    /**
-     * Returns the trimFunction.
-     * 
-     * @return String
-     */
-    public String getTrimFunction() {
-        return trimFunction;
-    }
+	/**
+	 * Adds special handling of CHAR columns.
+	 */
+	@Override
+	protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) {
 
-    /**
-     * Sets the trimFunction.
-     * 
-     * @param trimFunction The trimFunction to set
-     */
-    public void setTrimFunction(String trimFunction) {
-        this.trimFunction = trimFunction;
-    }
+		if (dbAttr.getType() == Types.CHAR) {
+			out.append(trimFunction).append("(");
+			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
+			out.append(')');
+		} else {
+			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
+		}
+	}
+
+	/**
+	 * Returns the trimFunction.
+	 * 
+	 * @return String
+	 */
+	public String getTrimFunction() {
+		return trimFunction;
+	}
+
+	/**
+	 * Sets the trimFunction.
+	 * 
+	 * @param trimFunction
+	 *            The trimFunction to set
+	 */
+	public void setTrimFunction(String trimFunction) {
+		this.trimFunction = trimFunction;
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java
index 1cdbcd0..67f22ca 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java
@@ -34,96 +34,84 @@ import org.apache.cayenne.map.DbAttribute;
 
 public class DB2QualifierTranslator extends TrimmingQualifierTranslator {
 
-    public DB2QualifierTranslator(QueryAssembler queryAssembler, String trimFunction) {
-        super(queryAssembler, trimFunction);
-    }
-
-    @Override
-    protected void appendLiteralDirect(
-            Object val,
-            DbAttribute attr,
-            Expression parentExpression) throws IOException {
-
-        boolean castNeeded = false;
-
-        if (parentExpression != null) {
-            int type = parentExpression.getType();
-
-            castNeeded = attr != null
-                    && (type == Expression.LIKE
-                            || type == Expression.LIKE_IGNORE_CASE
-                            || type == Expression.NOT_LIKE || type == Expression.NOT_LIKE_IGNORE_CASE);
-        }
-
-        if (castNeeded) {
-            out.append("CAST (");
-        }
-
-        super.appendLiteralDirect(val, attr, parentExpression);
-
-        if (castNeeded) {
-            int jdbcType = attr.getType();
-            int len = attr.getMaxLength();
-
-            // determine CAST type
-
-            // LIKE on CHAR may produce unpredictible results
-            // LIKE on LONVARCHAR doesn't seem to be supported
-            if (jdbcType == Types.CHAR || jdbcType == Types.LONGVARCHAR) {
-                jdbcType = Types.VARCHAR;
-
-                // length is required for VARCHAR
-                if (len <= 0) {
-                    len = 254;
-                }
-            }
-
-            out.append(" AS ");
-            String[] types = queryAssembler.getAdapter().externalTypesForJdbcType(
-                    jdbcType);
-
-            if (types == null || types.length == 0) {
-                throw new CayenneRuntimeException(
-                        "Can't find database type for JDBC type '"
-                                + TypesMapping.getSqlNameByType(jdbcType));
-            }
-
-            out.append(types[0]);
-            if (len > 0 && queryAssembler.getAdapter().typeSupportsLength(jdbcType)) {
-                out.append("(");
-                out.append(String.valueOf(len));
-                out.append(")");
-            }
-
-            out.append(")");
-        }
-    }
-    
-    @Override
-    protected void processColumnWithQuoteSqlIdentifiers(
-            DbAttribute dbAttr,
-            Expression pathExp) throws IOException {
-
-        SimpleNode parent = null;
-        if (pathExp instanceof SimpleNode) {
-            parent = (SimpleNode) ((SimpleNode) pathExp).jjtGetParent();
-        }
-
-        // problem in db2 : Comparisons between CLOB and CLOB are not supported.
-        // we need do it by casting the Clob to VARCHAR.
-        if (parent != null
-                && (parent instanceof ASTEqual || parent instanceof ASTNotEqual)
-                && dbAttr.getType() == Types.CLOB
-                && parent.getOperandCount() == 2
-                && parent.getOperand(1) instanceof String) {
-            Integer size = parent.getOperand(1).toString().length() + 1;
-
-            out.append("CAST(");
-            super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-            out.append(" AS VARCHAR(" + size + "))");
-        }
-        else {
-            super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-        }
-    }
+	public DB2QualifierTranslator(QueryAssembler queryAssembler, String trimFunction) {
+		super(queryAssembler, trimFunction);
+	}
+
+	@Override
+	protected void appendLiteralDirect(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
+
+		boolean castNeeded = false;
+
+		if (parentExpression != null) {
+			int type = parentExpression.getType();
+
+			castNeeded = attr != null
+					&& (type == Expression.LIKE || type == Expression.LIKE_IGNORE_CASE || type == Expression.NOT_LIKE || type == Expression.NOT_LIKE_IGNORE_CASE);
+		}
+
+		if (castNeeded) {
+			out.append("CAST (");
+		}
+
+		super.appendLiteralDirect(val, attr, parentExpression);
+
+		if (castNeeded) {
+			int jdbcType = attr.getType();
+			int len = attr.getMaxLength();
+
+			// determine CAST type
+
+			// LIKE on CHAR may produce unpredictible results
+			// LIKE on LONVARCHAR doesn't seem to be supported
+			if (jdbcType == Types.CHAR || jdbcType == Types.LONGVARCHAR) {
+				jdbcType = Types.VARCHAR;
+
+				// length is required for VARCHAR
+				if (len <= 0) {
+					len = 254;
+				}
+			}
+
+			out.append(" AS ");
+			String[] types = queryAssembler.getAdapter().externalTypesForJdbcType(jdbcType);
+
+			if (types == null || types.length == 0) {
+				throw new CayenneRuntimeException("Can't find database type for JDBC type '"
+						+ TypesMapping.getSqlNameByType(jdbcType));
+			}
+
+			out.append(types[0]);
+			if (len > 0 && queryAssembler.getAdapter().typeSupportsLength(jdbcType)) {
+				out.append("(");
+				out.append(String.valueOf(len));
+				out.append(")");
+			}
+
+			out.append(")");
+		}
+	}
+
+	@Override
+	protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) {
+
+		SimpleNode parent = null;
+		if (pathExp instanceof SimpleNode) {
+			parent = (SimpleNode) ((SimpleNode) pathExp).jjtGetParent();
+		}
+
+		// problem in db2 : Comparisons between CLOB and CLOB are not supported.
+		// we need do it by casting the Clob to VARCHAR.
+		if (parent != null && (parent instanceof ASTEqual || parent instanceof ASTNotEqual)
+				&& dbAttr.getType() == Types.CLOB && parent.getOperandCount() == 2
+				&& parent.getOperand(1) instanceof String) {
+			Integer size = parent.getOperand(1).toString().length() + 1;
+
+			out.append("CAST(");
+			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
+			out.append(" AS VARCHAR(" + size + "))");
+		} else {
+			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java
index d2bcf58..1b59288 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.dba.derby;
 
-import java.io.IOException;
 import java.sql.Types;
 
 import org.apache.cayenne.access.translator.select.QueryAssembler;
@@ -31,35 +30,31 @@ import org.apache.cayenne.map.DbAttribute;
 
 public class DerbyQualifierTranslator extends TrimmingQualifierTranslator {
 
-    public DerbyQualifierTranslator(QueryAssembler queryAssembler, String trimFunction) {
-        super(queryAssembler, trimFunction);
-    }
-
-    @Override
-    protected void processColumnWithQuoteSqlIdentifiers(
-            DbAttribute dbAttr,
-            Expression pathExp) throws IOException {
-
-        SimpleNode parent = null;
-        if (pathExp instanceof SimpleNode) {
-            parent = (SimpleNode) ((SimpleNode) pathExp).jjtGetParent();
-        }
-
-        // problem in derby : Comparisons between 'CLOB (UCS_BASIC)' and 'CLOB (UCS_BASIC)' are not supported.
-        // we need do it by casting the Clob to VARCHAR.
-        if (parent != null
-                && (parent instanceof ASTEqual || parent instanceof ASTNotEqual)
-                && dbAttr.getType() == Types.CLOB
-                && parent.getOperandCount() == 2
-                && parent.getOperand(1) instanceof String) {
-            Integer size = parent.getOperand(1).toString().length() + 1;
-
-            out.append("CAST(");
-            super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-            out.append(" AS VARCHAR(" + size + "))");
-        }
-        else {
-            super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-        }
-    }
+	public DerbyQualifierTranslator(QueryAssembler queryAssembler, String trimFunction) {
+		super(queryAssembler, trimFunction);
+	}
+
+	@Override
+	protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) {
+
+		SimpleNode parent = null;
+		if (pathExp instanceof SimpleNode) {
+			parent = (SimpleNode) ((SimpleNode) pathExp).jjtGetParent();
+		}
+
+		// problem in derby : Comparisons between 'CLOB (UCS_BASIC)' and 'CLOB
+		// (UCS_BASIC)' are not supported.
+		// we need do it by casting the Clob to VARCHAR.
+		if (parent != null && (parent instanceof ASTEqual || parent instanceof ASTNotEqual)
+				&& dbAttr.getType() == Types.CLOB && parent.getOperandCount() == 2
+				&& parent.getOperand(1) instanceof String) {
+			Integer size = parent.getOperand(1).toString().length() + 1;
+
+			out.append("CAST(");
+			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
+			out.append(" AS VARCHAR(" + size + "))");
+		} else {
+			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java
index 047799c..f41bdf2 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java
@@ -16,10 +16,8 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
- 
-package org.apache.cayenne.dba.firebird;
 
-import java.io.IOException;
+package org.apache.cayenne.dba.firebird;
 
 import org.apache.cayenne.access.translator.select.QualifierTranslator;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
@@ -27,18 +25,18 @@ import org.apache.cayenne.dba.oracle.OracleQualifierTranslator;
 import org.apache.cayenne.exp.Expression;
 
 public class FirebirdQualifierTranslator extends QualifierTranslator {
-    public FirebirdQualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler);
-    }
+	public FirebirdQualifierTranslator(QueryAssembler queryAssembler) {
+		super(queryAssembler);
+	}
 
-    @Override
-    protected void doAppendPart(Expression rootNode) throws IOException {
-        if (rootNode == null) {
-            return;
-        }
-        // IN statements with more than 1500 values are denied in Firebird
-        // so we need to split one big statement on few smaller ones
-        rootNode = rootNode.transform(new OracleQualifierTranslator.INTrimmer());
-        rootNode.traverse(this);
-    }
+	@Override
+	protected void doAppendPart(Expression rootNode) {
+		if (rootNode == null) {
+			return;
+		}
+		// IN statements with more than 1500 values are denied in Firebird
+		// so we need to split one big statement on few smaller ones
+		rootNode = rootNode.transform(new OracleQualifierTranslator.INTrimmer());
+		rootNode.traverse(this);
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseActionBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseActionBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseActionBuilder.java
index 1078354..5ed5cf2 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseActionBuilder.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseActionBuilder.java
@@ -19,8 +19,6 @@
 
 package org.apache.cayenne.dba.frontbase;
 
-import java.sql.Connection;
-
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -39,8 +37,8 @@ class FrontBaseActionBuilder extends JdbcActionBuilder {
         return new SelectAction(query, dataNode) {
 
             @Override
-            protected SelectTranslator createTranslator(Connection connection) {
-                return new FrontBaseSelectTranslator(query, dataNode, connection);
+            protected SelectTranslator createTranslator() {
+                return new FrontBaseSelectTranslator(query, dataNode.getAdapter(), dataNode.getEntityResolver());
             }
         };
     }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSelectTranslator.java
index bc89501..1e71f5f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSelectTranslator.java
@@ -19,35 +19,34 @@
 
 package org.apache.cayenne.dba.frontbase;
 
-import java.sql.Connection;
-
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.translator.select.SelectTranslator;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
 
 /**
  * @since 1.2
  */
-class FrontBaseSelectTranslator extends SelectTranslator {
+class FrontBaseSelectTranslator extends DefaultSelectTranslator {
 
-    static final String SELECT_PREFIX = "SELECT";
+	static final String SELECT_PREFIX = "SELECT";
 
-    /**
-     * @since 4.0
-     */
-    public FrontBaseSelectTranslator(Query query, DataNode dataNode, Connection connection) {
-        super(query, dataNode, connection);
-    }
+	/**
+	 * @since 4.0
+	 */
+	public FrontBaseSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+		super(query, adapter, entityResolver);
+	}
 
-    @Override
-    protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
+	@Override
+	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
 
-        int limit = queryMetadata.getFetchLimit();
-        if (limit > 0 && buffer.length() > SELECT_PREFIX.length()) {
+		int limit = queryMetadata.getFetchLimit();
+		if (limit > 0 && buffer.length() > SELECT_PREFIX.length()) {
 
-            if (SELECT_PREFIX.equals(buffer.substring(0, SELECT_PREFIX.length()))) {
-                buffer.insert(SELECT_PREFIX.length(), " TOP " + limit);
-            }
-        }
-    }
+			if (SELECT_PREFIX.equals(buffer.substring(0, SELECT_PREFIX.length()))) {
+				buffer.insert(SELECT_PREFIX.length(), " TOP " + limit);
+			}
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectAction.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectAction.java
index 2b5e098..af721ac 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectAction.java
@@ -18,8 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.dba.hsqldb;
 
-import java.sql.Connection;
-
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -30,17 +28,17 @@ import org.apache.cayenne.query.SelectQuery;
  */
 class HSQLSelectAction extends SelectAction {
 
-    <T> HSQLSelectAction(SelectQuery<T> query, DataNode dataNode) {
-        super(query, dataNode);
-    }
+	<T> HSQLSelectAction(SelectQuery<T> query, DataNode dataNode) {
+		super(query, dataNode);
+	}
 
-    @Override
-    protected int getInMemoryOffset(int queryOffset) {
-        return 0;
-    }
+	@Override
+	protected int getInMemoryOffset(int queryOffset) {
+		return 0;
+	}
 
-    @Override
-    protected SelectTranslator createTranslator(Connection connection) {
-        return new HSQLSelectTranslator(query, dataNode, connection);
-    }
+	@Override
+	protected SelectTranslator createTranslator() {
+		return new HSQLSelectTranslator(query, dataNode.getAdapter(), dataNode.getEntityResolver());
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectTranslator.java
index af11ea5..ce3bb58 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectTranslator.java
@@ -19,39 +19,39 @@
 
 package org.apache.cayenne.dba.hsqldb;
 
-import java.sql.Connection;
-
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.translator.select.SelectTranslator;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
 
 /**
  * @since 1.2
  */
-class HSQLSelectTranslator extends SelectTranslator {
-    
-    /**
-     * @since 4.0
-     */
-    public HSQLSelectTranslator(Query query, DataNode dataNode, Connection connection) {
-        super(query, dataNode, connection);
-    }
-
-    @Override
-    protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-        int offset = queryMetadata.getFetchOffset();
-        int limit = queryMetadata.getFetchLimit();
-
-        if (offset > 0 || limit > 0) {
-            buffer.append(" LIMIT ");
-
-            // both OFFSET and LIMIT must be present, so come up with defaults if one of
-            // them is not set by the user
-            if (limit == 0) {
-                limit = Integer.MAX_VALUE;
-            }
-
-            buffer.append(limit).append(" OFFSET ").append(offset);
-        }
-    }
+class HSQLSelectTranslator extends DefaultSelectTranslator {
+
+	/**
+	 * @since 4.0
+	 */
+	public HSQLSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+		super(query, adapter, entityResolver);
+	}
+
+	@Override
+	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
+		int offset = queryMetadata.getFetchOffset();
+		int limit = queryMetadata.getFetchLimit();
+
+		if (offset > 0 || limit > 0) {
+			buffer.append(" LIMIT ");
+
+			// both OFFSET and LIMIT must be present, so come up with defaults
+			// if one of
+			// them is not set by the user
+			if (limit == 0) {
+				limit = Integer.MAX_VALUE;
+			}
+
+			buffer.append(limit).append(" OFFSET ").append(offset);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectAction.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectAction.java
index 2f93ad4..07f05b1 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectAction.java
@@ -18,8 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.dba.ingres;
 
-import java.sql.Connection;
-
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -37,7 +35,7 @@ public class IngresSelectAction extends SelectAction {
     }
 
     @Override
-    protected SelectTranslator createTranslator(Connection connection) {
-        return new IngresSelectTranslator(query, dataNode, connection);
+    protected SelectTranslator createTranslator() {
+        return new IngresSelectTranslator(query, dataNode.getAdapter(), dataNode.getEntityResolver());
     }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectTranslator.java
index 18558ec..f466d5f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectTranslator.java
@@ -18,19 +18,18 @@
  ****************************************************************/
 package org.apache.cayenne.dba.ingres;
 
-import java.sql.Connection;
-
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.translator.select.SelectTranslator;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
 
-public class IngresSelectTranslator extends SelectTranslator {
+public class IngresSelectTranslator extends DefaultSelectTranslator {
     
     /**
      * @since 4.0
      */
-    public IngresSelectTranslator(Query query, DataNode dataNode, Connection connection) {
-        super(query, dataNode, connection);
+    public IngresSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+        super(query, adapter, entityResolver);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectAction.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectAction.java
index 5eb3b4d..a39bd16 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectAction.java
@@ -18,8 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.dba.mysql;
 
-import java.sql.Connection;
-
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -30,17 +28,17 @@ import org.apache.cayenne.query.SelectQuery;
  */
 class MySQLSelectAction extends SelectAction {
 
-    <T> MySQLSelectAction(SelectQuery<T> query, DataNode dataNode) {
-        super(query, dataNode);
-    }
+	<T> MySQLSelectAction(SelectQuery<T> query, DataNode dataNode) {
+		super(query, dataNode);
+	}
 
-    @Override
-    protected int getInMemoryOffset(int queryOffset) {
-        return 0;
-    }
+	@Override
+	protected int getInMemoryOffset(int queryOffset) {
+		return 0;
+	}
 
-    @Override
-    protected SelectTranslator createTranslator(Connection connection) {
-        return new MySQLSelectTranslator(query, dataNode, connection);
-    }
+	@Override
+	protected SelectTranslator createTranslator() {
+		return new MySQLSelectTranslator(query, dataNode.getAdapter(), dataNode.getEntityResolver());
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectTranslator.java
index bd20ef2..c9f0809 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectTranslator.java
@@ -19,40 +19,39 @@
 
 package org.apache.cayenne.dba.mysql;
 
-import java.sql.Connection;
-
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.translator.select.SelectTranslator;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
 
 /**
  * @since 1.2
  */
-class MySQLSelectTranslator extends SelectTranslator {
-
-    /**
-     * @since 4.0
-     */
-    public MySQLSelectTranslator(Query query, DataNode dataNode, Connection connection) {
-        super(query, dataNode, connection);
-    }
-
-    @Override
-    protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-        int offset = queryMetadata.getFetchOffset();
-        int limit = queryMetadata.getFetchLimit();
-
-        if (offset > 0 || limit > 0) {
-            buffer.append(" LIMIT ");
-
-            // both OFFSET and LIMIT must be present, so come up with defaults
-            // if one of
-            // them is not set by the user
-            if (limit == 0) {
-                limit = Integer.MAX_VALUE;
-            }
-
-            buffer.append(limit).append(" OFFSET ").append(offset);
-        }
-    }
+class MySQLSelectTranslator extends DefaultSelectTranslator {
+
+	/**
+	 * @since 4.0
+	 */
+	public MySQLSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+		super(query, adapter, entityResolver);
+	}
+
+	@Override
+	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
+		int offset = queryMetadata.getFetchOffset();
+		int limit = queryMetadata.getFetchLimit();
+
+		if (offset > 0 || limit > 0) {
+			buffer.append(" LIMIT ");
+
+			// both OFFSET and LIMIT must be present, so come up with defaults
+			// if one of
+			// them is not set by the user
+			if (limit == 0) {
+				limit = Integer.MAX_VALUE;
+			}
+
+			buffer.append(limit).append(" OFFSET ").append(offset);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseActionBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseActionBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseActionBuilder.java
index 9ce7b23..fb03bde 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseActionBuilder.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseActionBuilder.java
@@ -19,8 +19,6 @@
 
 package org.apache.cayenne.dba.openbase;
 
-import java.sql.Connection;
-
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -33,18 +31,18 @@ import org.apache.cayenne.query.SelectQuery;
  */
 class OpenBaseActionBuilder extends JdbcActionBuilder {
 
-    OpenBaseActionBuilder(DataNode dataNode) {
-        super(dataNode);
-    }
+	OpenBaseActionBuilder(DataNode dataNode) {
+		super(dataNode);
+	}
 
-    @Override
-    public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
-        return new SelectAction(query, dataNode) {
+	@Override
+	public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
+		return new SelectAction(query, dataNode) {
 
-            @Override
-            protected SelectTranslator createTranslator(Connection connection) {
-                return new OpenBaseSelectTranslator(query, dataNode, connection);
-            }
-        };
-    }
+			@Override
+			protected SelectTranslator createTranslator() {
+				return new OpenBaseSelectTranslator(query, dataNode.getAdapter(), dataNode.getEntityResolver());
+			}
+		};
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseJoinStack.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseJoinStack.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseJoinStack.java
index 98c5548..a9c7579 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseJoinStack.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseJoinStack.java
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.dba.openbase;
 
-import java.io.IOException;
 import java.util.List;
 
 import org.apache.cayenne.access.translator.select.JoinStack;
@@ -31,91 +30,86 @@ import org.apache.cayenne.map.DbJoin;
 import org.apache.cayenne.map.DbRelationship;
 
 /**
- * OpenBase does not support standard JOIN keyword and have strange syntax for defining
- * inner/outer joins
+ * OpenBase does not support standard JOIN keyword and have strange syntax for
+ * defining inner/outer joins
  * 
- * @see http
- *      ://www.openbase.com/help/KnowledgeBase/400_OpenBaseSQL/401_SelectStatements.html
+ * @see http ://www.openbase.com/help/KnowledgeBase/
+ *      400_OpenBaseSQL/401_SelectStatements.html
  * @since 3.0
  */
 class OpenBaseJoinStack extends JoinStack {
 
-    protected OpenBaseJoinStack(DbAdapter dbAdapter, DataMap dataMap, QueryAssembler assembler) {
-        super(dbAdapter, dataMap, assembler);
-    }
-
-    @Override
-    protected void appendJoinSubtree(Appendable out, JoinTreeNode node)
-            throws IOException {
-        DbRelationship relationship = node.getRelationship();
-
-        if (relationship == null) {
-            return;
-        }
-
-        DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
-        String targetAlias = node.getTargetTableAlias();
-
-        out.append(", ").append(targetEntity.getFullyQualifiedName()).append(' ').append(
-                targetAlias);
-
-        for (JoinTreeNode child : node.getChildren()) {
-            appendJoinSubtree(out, child);
-        }
-    }
-
-    @Override
-    protected void appendQualifier(Appendable out, boolean firstQualifierElement)
-            throws IOException {
-        boolean first = firstQualifierElement;
-        for (JoinTreeNode node : rootNode.getChildren()) {
-            if (!first) {
-                out.append(" AND ");
-            }
-            appendQualifierSubtree(out, node);
-            first = false;
-        }
-    }
-
-    protected void appendQualifierSubtree(Appendable out, JoinTreeNode node)
-            throws IOException {
-        DbRelationship relationship = node.getRelationship();
-
-        String srcAlias = node.getSourceTableAlias();
-        String targetAlias = node.getTargetTableAlias();
-
-        List<DbJoin> joins = relationship.getJoins();
-        int len = joins.size();
-        for (int i = 0; i < len; i++) {
-            DbJoin join = joins.get(i);
-
-            if (i > 0) {
-                out.append(" AND ");
-            }
-
-            out.append(srcAlias).append('.').append(join.getSourceName());
-
-            switch (node.getJoinType()) {
-                case INNER:
-                    out.append(" = ");
-                    break;
-                case LEFT_OUTER:
-                    out.append(" * ");
-                    break;
-                default:
-                    throw new IllegalArgumentException("Unsupported join type: "
-                            + node.getJoinType());
-            }
-
-            out.append(targetAlias).append('.').append(join.getTargetName());
-
-        }
-
-        for (JoinTreeNode child : node.getChildren()) {
-            out.append(" AND ");
-            appendQualifierSubtree(out, child);
-        }
-
-    }
+	protected OpenBaseJoinStack(DbAdapter dbAdapter, DataMap dataMap, QueryAssembler assembler) {
+		super(dbAdapter, dataMap, assembler);
+	}
+
+	@Override
+	protected void appendJoinSubtree(StringBuilder out, JoinTreeNode node) {
+		DbRelationship relationship = node.getRelationship();
+
+		if (relationship == null) {
+			return;
+		}
+
+		DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
+		String targetAlias = node.getTargetTableAlias();
+
+		out.append(", ").append(targetEntity.getFullyQualifiedName()).append(' ').append(targetAlias);
+
+		for (JoinTreeNode child : node.getChildren()) {
+			appendJoinSubtree(out, child);
+		}
+	}
+
+	@Override
+	protected void appendQualifier(StringBuilder out, boolean firstQualifierElement) {
+		boolean first = firstQualifierElement;
+		for (JoinTreeNode node : rootNode.getChildren()) {
+			if (!first) {
+				out.append(" AND ");
+			}
+			appendQualifierSubtree(out, node);
+			first = false;
+		}
+	}
+
+	protected void appendQualifierSubtree(StringBuilder out, JoinTreeNode node) {
+		DbRelationship relationship = node.getRelationship();
+
+		String srcAlias = node.getSourceTableAlias();
+		String targetAlias = node.getTargetTableAlias();
+
+		List<DbJoin> joins = relationship.getJoins();
+		int len = joins.size();
+		for (int i = 0; i < len; i++) {
+			DbJoin join = joins.get(i);
+
+			if (i > 0) {
+				out.append(" AND ");
+			}
+
+			out.append(srcAlias).append('.').append(join.getSourceName());
+
+			switch (node.getJoinType()) {
+			case INNER:
+				out.append(" = ");
+				break;
+			case LEFT_OUTER:
+				out.append(" * ");
+				break;
+			default:
+				throw new IllegalArgumentException("Unsupported join type: " + node.getJoinType());
+			}
+
+			out.append(targetAlias).append('.').append(join.getTargetName());
+
+		}
+
+		for (JoinTreeNode child : node.getChildren()) {
+			out.append(" AND ");
+			appendQualifierSubtree(out, child);
+		}
+
+	}
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseQualifierTranslator.java
index df58745..42383b9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseQualifierTranslator.java
@@ -29,146 +29,134 @@ import org.apache.cayenne.exp.parser.PatternMatchNode;
 import org.apache.cayenne.map.DbAttribute;
 
 /**
- * Translates query qualifier to SQL. Used as a helper class by query translators.
+ * Translates query qualifier to SQL. Used as a helper class by query
+ * translators.
  * 
  * @since 1.1
  */
 public class OpenBaseQualifierTranslator extends QualifierTranslator {
 
-    public OpenBaseQualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler);
-    }
-
-    @Override
-    public void startNode(Expression node, Expression parentNode) {
-
-        if (node.getOperandCount() == 2) {
-            // binary nodes are the only ones that currently require this
-            detectObjectMatch(node);
-
-            try {
-                if (parenthesisNeeded(node, parentNode)) {
-                    out.append('(');
-                }
-
-                // super implementation has special handling
-                // of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-                // OpenBase is case-insensitive by default
-                // ...
-            }
-            catch (IOException ioex) {
-                throw new CayenneRuntimeException("Error appending content", ioex);
-            }
-        }
-        else {
-            super.startNode(node, parentNode);
-        }
-    }
-
-    @Override
-    public void endNode(Expression node, Expression parentNode) {
-        if (node.getOperandCount() == 2) {
-
-            try {
-                // check if we need to use objectMatchTranslator to finish building the
-                // expression
-                if (matchingObject) {
-                    appendObjectMatch();
-                }
-
-                if(PatternMatchNode.class.isAssignableFrom(node.getClass())) {
-                    appendLikeEscapeCharacter((PatternMatchNode) node);
-                }
-                
-                if (parenthesisNeeded(node, parentNode)) {
-                    out.append(')');
-                }
-
-                // super implementation has special handling
-                // of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-                // OpenBase is case-insensitive by default
-                // ...
-            }
-            catch (IOException ioex) {
-                throw new CayenneRuntimeException("Error appending content", ioex);
-            }
-        }
-        else {
-            super.endNode(node, parentNode);
-        }
-    }
-
-    @Override
-    protected void appendLiteralDirect(
-            Object val,
-            DbAttribute attr,
-            Expression parentExpression) throws IOException {
-
-        // Special handling of string matching is needed:
-        // Case-sensitive LIKE must be converted to [x][Y][z] format
-        if (val instanceof String
-                && (parentExpression.getType() == Expression.LIKE || parentExpression
-                        .getType() == Expression.NOT_LIKE)) {
-
-            val = caseSensitiveLikePattern((String) val);
-        }
-
-        super.appendLiteralDirect(val, attr, parentExpression);
-    }
-
-    private String caseSensitiveLikePattern(String pattern) {
-        int len = pattern.length();
-        StringBuilder buffer = new StringBuilder(len * 3);
-
-        for (int i = 0; i < len; i++) {
-            char c = pattern.charAt(i);
-            if (c == '%' || c == '?') {
-                buffer.append(c);
-            }
-            else {
-                buffer.append("[").append(c).append("]");
-            }
-        }
-
-        return buffer.toString();
-    }
-
-    @Override
-    public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
-        if (!hasMoreChildren) {
-            return;
-        }
-
-        // super implementation has special handling
-        // of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-        // OpenBase is case-insensitive by default
-        // ...
-
-        try {
-            switch (node.getType()) {
-
-                case Expression.LIKE_IGNORE_CASE:
-                    finishedChildNodeAppendExpression(node, " LIKE ");
-                    break;
-                case Expression.NOT_LIKE_IGNORE_CASE:
-                    finishedChildNodeAppendExpression(node, " NOT LIKE ");
-                    break;
-                default:
-                    super.finishedChild(node, childIndex, hasMoreChildren);
-            }
-        }
-        catch (IOException ioex) {
-            throw new CayenneRuntimeException("Error appending content", ioex);
-        }
-    }
-
-    private void finishedChildNodeAppendExpression(Expression node, String operation)
-            throws IOException {
-        Appendable out = matchingObject ? new StringBuilder() : this.out;
-        out.append(operation);
-        if (matchingObject) {
-            objectMatchTranslator.setOperation(out.toString());
-            objectMatchTranslator.setExpression(node);
-        }
-    }
+	public OpenBaseQualifierTranslator(QueryAssembler queryAssembler) {
+		super(queryAssembler);
+	}
+
+	@Override
+	public void startNode(Expression node, Expression parentNode) {
+
+		if (node.getOperandCount() == 2) {
+			// binary nodes are the only ones that currently require this
+			detectObjectMatch(node);
+
+			if (parenthesisNeeded(node, parentNode)) {
+				out.append('(');
+			}
+
+			// super implementation has special handling
+			// of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
+			// OpenBase is case-insensitive by default
+			// ...
+
+		} else {
+			super.startNode(node, parentNode);
+		}
+	}
+
+	@Override
+	public void endNode(Expression node, Expression parentNode) {
+		if (node.getOperandCount() == 2) {
+
+			try {
+				// check if we need to use objectMatchTranslator to finish
+				// building the
+				// expression
+				if (matchingObject) {
+					appendObjectMatch();
+				}
+
+				if (PatternMatchNode.class.isAssignableFrom(node.getClass())) {
+					appendLikeEscapeCharacter((PatternMatchNode) node);
+				}
+
+				if (parenthesisNeeded(node, parentNode)) {
+					out.append(')');
+				}
+
+				// super implementation has special handling
+				// of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
+				// OpenBase is case-insensitive by default
+				// ...
+			} catch (IOException ioex) {
+				throw new CayenneRuntimeException("Error appending content", ioex);
+			}
+		} else {
+			super.endNode(node, parentNode);
+		}
+	}
+
+	@Override
+	protected void appendLiteralDirect(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
+
+		// Special handling of string matching is needed:
+		// Case-sensitive LIKE must be converted to [x][Y][z] format
+		if (val instanceof String
+				&& (parentExpression.getType() == Expression.LIKE || parentExpression.getType() == Expression.NOT_LIKE)) {
+
+			val = caseSensitiveLikePattern((String) val);
+		}
+
+		super.appendLiteralDirect(val, attr, parentExpression);
+	}
+
+	private String caseSensitiveLikePattern(String pattern) {
+		int len = pattern.length();
+		StringBuilder buffer = new StringBuilder(len * 3);
+
+		for (int i = 0; i < len; i++) {
+			char c = pattern.charAt(i);
+			if (c == '%' || c == '?') {
+				buffer.append(c);
+			} else {
+				buffer.append("[").append(c).append("]");
+			}
+		}
+
+		return buffer.toString();
+	}
+
+	@Override
+	public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
+		if (!hasMoreChildren) {
+			return;
+		}
+
+		// super implementation has special handling
+		// of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
+		// OpenBase is case-insensitive by default
+		// ...
+
+		try {
+			switch (node.getType()) {
+
+			case Expression.LIKE_IGNORE_CASE:
+				finishedChildNodeAppendExpression(node, " LIKE ");
+				break;
+			case Expression.NOT_LIKE_IGNORE_CASE:
+				finishedChildNodeAppendExpression(node, " NOT LIKE ");
+				break;
+			default:
+				super.finishedChild(node, childIndex, hasMoreChildren);
+			}
+		} catch (IOException ioex) {
+			throw new CayenneRuntimeException("Error appending content", ioex);
+		}
+	}
+
+	private void finishedChildNodeAppendExpression(Expression node, String operation) throws IOException {
+		Appendable out = matchingObject ? new StringBuilder() : this.out;
+		out.append(operation);
+		if (matchingObject) {
+			objectMatchTranslator.setOperation(out.toString());
+			objectMatchTranslator.setExpression(node);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSelectTranslator.java
index d0dc5a6..342d0fd 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSelectTranslator.java
@@ -19,36 +19,35 @@
 
 package org.apache.cayenne.dba.openbase;
 
-import java.sql.Connection;
-
-import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
 import org.apache.cayenne.access.translator.select.JoinStack;
-import org.apache.cayenne.access.translator.select.SelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
 
 /**
  * @since 1.2
  */
-class OpenBaseSelectTranslator extends SelectTranslator {
+class OpenBaseSelectTranslator extends DefaultSelectTranslator {
+
+	/**
+	 * @since 4.0
+	 */
+	public OpenBaseSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+		super(query, adapter, entityResolver);
+	}
 
-    /**
-     * @since 4.0
-     */
-    public OpenBaseSelectTranslator(Query query, DataNode dataNode, Connection connection) {
-        super(query, dataNode, connection);
-    }
-    
-    @Override
-    protected JoinStack createJoinStack() {
-        return new OpenBaseJoinStack(getAdapter(), queryMetadata.getDataMap(), this);
-    }
+	@Override
+	protected JoinStack createJoinStack() {
+		return new OpenBaseJoinStack(getAdapter(), queryMetadata.getDataMap(), this);
+	}
 
-    @Override
-    protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-        int limit = queryMetadata.getFetchLimit();
-        if (limit > 0) {
-            buffer.append(" RETURN RESULTS ").append(limit);
-        }
-    }
+	@Override
+	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
+		int limit = queryMetadata.getFetchLimit();
+		if (limit > 0) {
+			buffer.append(" RETURN RESULTS ").append(limit);
+		}
+	}
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8JoinStack.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8JoinStack.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8JoinStack.java
index 0900b56..e27ee90 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8JoinStack.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8JoinStack.java
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.dba.oracle;
 
-import java.io.IOException;
 import java.util.List;
 
 import org.apache.cayenne.access.translator.select.JoinStack;
@@ -36,79 +35,74 @@ import org.apache.cayenne.map.DbRelationship;
 // cloned from OpenBaseJoin stack... need better strategies of reuse...
 class Oracle8JoinStack extends JoinStack {
 
-    Oracle8JoinStack(DbAdapter dbAdapter, DataMap dataMap, QueryAssembler assembler) {
-        super(dbAdapter, dataMap, assembler);
-    }
-
-    @Override
-    protected void appendJoinSubtree(Appendable out, JoinTreeNode node)
-            throws IOException {
-        DbRelationship relationship = node.getRelationship();
-
-        if (relationship == null) {
-            return;
-        }
-
-        DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
-        String targetAlias = node.getTargetTableAlias();
-
-        out.append(", ").append(targetEntity.getFullyQualifiedName()).append(' ').append(
-                targetAlias);
-
-        for (JoinTreeNode child : node.getChildren()) {
-            appendJoinSubtree(out, child);
-        }
-    }
-
-    @Override
-    protected void appendQualifier(Appendable out, boolean firstQualifierElement)
-            throws IOException {
-        boolean first = firstQualifierElement;
-        for (JoinTreeNode node : rootNode.getChildren()) {
-            if (!first) {
-                out.append(" AND ");
-            }
-            appendQualifierSubtree(out, node);
-            first = false;
-        }
-    }
-
-    protected void appendQualifierSubtree(Appendable out, JoinTreeNode node)
-            throws IOException {
-        DbRelationship relationship = node.getRelationship();
-
-        String srcAlias = node.getSourceTableAlias();
-        String targetAlias = node.getTargetTableAlias();
-
-        List<DbJoin> joins = relationship.getJoins();
-        int len = joins.size();
-        for (int i = 0; i < len; i++) {
-            DbJoin join = joins.get(i);
-
-            if (i > 0) {
-                out.append(" AND ");
-            }
-
-            out.append(srcAlias).append('.').append(join.getSourceName());
-
-            switch (node.getJoinType()) {
-                case INNER:
-                    out.append(" = ");
-                    break;
-                case LEFT_OUTER:
-                    out.append(" * ");
-                    break;
-                default:
-                    throw new IllegalArgumentException("Unsupported join type: "
-                            + node.getJoinType());
-            }
-
-            out.append(targetAlias).append('.').append(join.getTargetName());
-        }
-
-        for (JoinTreeNode child : node.getChildren()) {
-            out.append(" AND ");
-            appendQualifierSubtree(out, child);
-        }
-    }
+	Oracle8JoinStack(DbAdapter dbAdapter, DataMap dataMap, QueryAssembler assembler) {
+		super(dbAdapter, dataMap, assembler);
+	}
+
+	@Override
+	protected void appendJoinSubtree(StringBuilder out, JoinTreeNode node) {
+		DbRelationship relationship = node.getRelationship();
+
+		if (relationship == null) {
+			return;
+		}
+
+		DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
+		String targetAlias = node.getTargetTableAlias();
+
+		out.append(", ").append(targetEntity.getFullyQualifiedName()).append(' ').append(targetAlias);
+
+		for (JoinTreeNode child : node.getChildren()) {
+			appendJoinSubtree(out, child);
+		}
+	}
+
+	@Override
+	protected void appendQualifier(StringBuilder out, boolean firstQualifierElement) {
+		boolean first = firstQualifierElement;
+		for (JoinTreeNode node : rootNode.getChildren()) {
+			if (!first) {
+				out.append(" AND ");
+			}
+			appendQualifierSubtree(out, node);
+			first = false;
+		}
+	}
+
+	protected void appendQualifierSubtree(StringBuilder out, JoinTreeNode node) {
+		DbRelationship relationship = node.getRelationship();
+
+		String srcAlias = node.getSourceTableAlias();
+		String targetAlias = node.getTargetTableAlias();
+
+		List<DbJoin> joins = relationship.getJoins();
+		int len = joins.size();
+		for (int i = 0; i < len; i++) {
+			DbJoin join = joins.get(i);
+
+			if (i > 0) {
+				out.append(" AND ");
+			}
+
+			out.append(srcAlias).append('.').append(join.getSourceName());
+
+			switch (node.getJoinType()) {
+			case INNER:
+				out.append(" = ");
+				break;
+			case LEFT_OUTER:
+				out.append(" * ");
+				break;
+			default:
+				throw new IllegalArgumentException("Unsupported join type: " + node.getJoinType());
+			}
+
+			out.append(targetAlias).append('.').append(join.getTargetName());
+		}
+
+		for (JoinTreeNode child : node.getChildren()) {
+			out.append(" AND ");
+			appendQualifierSubtree(out, child);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchAction.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchAction.java
index d90ce6e..59766cb 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchAction.java
@@ -35,7 +35,7 @@ import java.util.List;
 import org.apache.cayenne.CayenneException;
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.OperationObserver;
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.log.JdbcEventLogger;
 import org.apache.cayenne.map.DbAttribute;
@@ -55,10 +55,10 @@ class Oracle8LOBBatchAction implements SQLAction {
     private DbAdapter adapter;
     private JdbcEventLogger logger;
 
-    private static void bind(DbAdapter adapter, PreparedStatement statement, BatchParameterBinding[] bindings)
+    private static void bind(DbAdapter adapter, PreparedStatement statement, ParameterBinding[] bindings)
             throws SQLException, Exception {
 
-        for (BatchParameterBinding b : bindings) {
+        for (ParameterBinding b : bindings) {
             adapter.bindParameter(statement, b.getValue(), b.getStatementPosition(), b.getAttribute().getType(), b
                     .getAttribute().getScale());
         }
@@ -104,7 +104,7 @@ class Oracle8LOBBatchAction implements SQLAction {
             PreparedStatement statement = connection.prepareStatement(updateStr);
             try {
 
-                BatchParameterBinding[] bindings = translator.updateBindings(row);
+                ParameterBinding[] bindings = translator.updateBindings(row);
                 logger.logQueryParameters("bind", bindings);
 
                 bind(adapter, statement, bindings);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java
index 8244ca5..82a95ce 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java
@@ -24,7 +24,7 @@ import java.util.Iterator;
 import java.util.List;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.batch.DefaultBatchTranslator;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
@@ -110,28 +110,28 @@ abstract class Oracle8LOBBatchTranslator extends DefaultBatchTranslator {
     }
     
     @Override
-    protected BatchParameterBinding[] createBindings() {
+    protected ParameterBinding[] createBindings() {
         List<DbAttribute> dbAttributes = query.getDbAttributes();
         int len = dbAttributes.size();
 
-        BatchParameterBinding[] bindings = new BatchParameterBinding[len];
+        ParameterBinding[] bindings = new ParameterBinding[len];
 
         for (int i = 0; i < len; i++) {
             DbAttribute attribute = dbAttributes.get(i);
-            bindings[i] = new BatchParameterBinding(attribute);
+            bindings[i] = new ParameterBinding(attribute);
         }
 
         return bindings;
     }
     
     @Override
-    protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
 
         int len = bindings.length;
 
         for (int i = 0, j = 1; i < len; i++) {
 
-            BatchParameterBinding b = bindings[i];
+            ParameterBinding b = bindings[i];
 
             Object value = row.getValue(i);
             DbAttribute attribute = b.getAttribute();

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8QualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8QualifierTranslator.java
index bed30da..d756392 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8QualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8QualifierTranslator.java
@@ -18,34 +18,32 @@
  ****************************************************************/
 package org.apache.cayenne.dba.oracle;
 
-import java.io.IOException;
-
 import org.apache.cayenne.access.translator.select.QueryAssembler;
 
 /**
- * Extends the TrimmingQualifierTranslator that Cayenne normally uses for Oracle.
- * Overrides doAppendPart() to wrap the qualifierBuffer in parentheses if it contains an
- * "OR" expression. This avoids a bug that can happen on Oracle8 if the query also
- * contains a join.
+ * Extends the TrimmingQualifierTranslator that Cayenne normally uses for
+ * Oracle. Overrides doAppendPart() to wrap the qualifierBuffer in parentheses
+ * if it contains an "OR" expression. This avoids a bug that can happen on
+ * Oracle8 if the query also contains a join.
  * 
  * @since 3.0
  */
 class Oracle8QualifierTranslator extends OracleQualifierTranslator {
 
-    public Oracle8QualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler);
-    }
+	public Oracle8QualifierTranslator(QueryAssembler queryAssembler) {
+		super(queryAssembler);
+	}
 
-    @Override
-    protected void doAppendPart() throws IOException {
-        super.doAppendPart();
+	@Override
+	protected void doAppendPart() {
+		super.doAppendPart();
 
-        if (out instanceof StringBuilder) {
-            StringBuilder buffer = (StringBuilder) out;
-            if (buffer.indexOf(" OR ") != -1) {
-                buffer.insert(0, '(');
-                buffer.append(')');
-            }
-        }
-    }
+		if (out instanceof StringBuilder) {
+			StringBuilder buffer = (StringBuilder) out;
+			if (buffer.indexOf(" OR ") != -1) {
+				buffer.insert(0, '(');
+				buffer.append(')');
+			}
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectAction.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectAction.java
index e3ff158..abef7a7 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectAction.java
@@ -18,8 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.dba.oracle;
 
-import java.sql.Connection;
-
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.query.SelectQuery;
@@ -29,12 +27,12 @@ import org.apache.cayenne.query.SelectQuery;
  */
 class Oracle8SelectAction extends OracleSelectAction {
 
-    <T> Oracle8SelectAction(SelectQuery<T> query, DataNode dataNode) {
-        super(query, dataNode);
-    }
+	<T> Oracle8SelectAction(SelectQuery<T> query, DataNode dataNode) {
+		super(query, dataNode);
+	}
 
-    @Override
-    protected SelectTranslator createTranslator(Connection connection) {
-        return new Oracle8SelectTranslator(query, dataNode, connection);
-    }
+	@Override
+	protected SelectTranslator createTranslator() {
+		return new Oracle8SelectTranslator(query, dataNode.getAdapter(), dataNode.getEntityResolver());
+	}
 }


[3/7] cayenne git commit: Refactoring SelectTranslator for better extensibility

Posted by aa...@apache.org.
http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectTranslator.java
index aa35ada..6d1b16f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectTranslator.java
@@ -18,10 +18,9 @@
  ****************************************************************/
 package org.apache.cayenne.dba.oracle;
 
-import java.sql.Connection;
-
-import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.translator.select.JoinStack;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
 
 /**
@@ -29,20 +28,20 @@ import org.apache.cayenne.query.Query;
  */
 class Oracle8SelectTranslator extends OracleSelectTranslator {
 
-    /**
-     * @since 4.0
-     */
-    public Oracle8SelectTranslator(Query query, DataNode dataNode, Connection connection) {
-        super(query, dataNode, connection);
-    }
-    
-    /**
-     * Returns an old style joint stack for Oracle8 that does not support explicit join
-     * syntax.
-     */
-    @Override
-    protected JoinStack createJoinStack() {
-        return new Oracle8JoinStack(getAdapter(), queryMetadata.getDataMap(), this);
-    }
+	/**
+	 * @since 4.0
+	 */
+	public Oracle8SelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+		super(query, adapter, entityResolver);
+	}
+
+	/**
+	 * Returns an old style joint stack for Oracle8 that does not support
+	 * explicit join syntax.
+	 */
+	@Override
+	protected JoinStack createJoinStack() {
+		return new Oracle8JoinStack(getAdapter(), queryMetadata.getDataMap(), this);
+	}
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java
index b375e3f..1ace92e 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.dba.oracle;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -33,68 +32,65 @@ import org.apache.cayenne.exp.parser.ASTPath;
 import org.apache.commons.collections.Transformer;
 
 /**
- * Oracle qualifier translator. In particular, trims INs with more than 1000 elements to
- * an OR-set of INs with &lt;= 1000 elements
+ * Oracle qualifier translator. In particular, trims INs with more than 1000
+ * elements to an OR-set of INs with &lt;= 1000 elements
  */
 public class OracleQualifierTranslator extends TrimmingQualifierTranslator {
 
-    public OracleQualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler, OracleAdapter.TRIM_FUNCTION);
-    }
+	public OracleQualifierTranslator(QueryAssembler queryAssembler) {
+		super(queryAssembler, OracleAdapter.TRIM_FUNCTION);
+	}
 
-    @Override
-    protected void doAppendPart(Expression rootNode) throws IOException {
-        if (rootNode == null) {
-            return;
-        }
-        rootNode = rootNode.transform(new INTrimmer());
-        rootNode.traverse(this);
-    }
+	@Override
+	protected void doAppendPart(Expression rootNode) {
+		if (rootNode == null) {
+			return;
+		}
+		rootNode = rootNode.transform(new INTrimmer());
+		rootNode.traverse(this);
+	}
 
-    public static class INTrimmer implements Transformer {
+	public static class INTrimmer implements Transformer {
 
-        public Expression trimmedInExpression(Expression exp, int maxInSize) {
-            Expression list = (Expression) exp.getOperand(1);
-            Object[] objects = (Object[]) list.evaluate(null);
+		public Expression trimmedInExpression(Expression exp, int maxInSize) {
+			Expression list = (Expression) exp.getOperand(1);
+			Object[] objects = (Object[]) list.evaluate(null);
 
-            if (objects.length <= maxInSize) {
-                return exp;
-            }
+			if (objects.length <= maxInSize) {
+				return exp;
+			}
 
-            Expression trimmed = trimmedInExpression(
-                    (ASTPath) exp.getOperand(0),
-                    objects,
-                    maxInSize);
-            if (exp instanceof ASTNotIn) {
-                return new ASTNegate(trimmed);
-            }
-            return trimmed;
-        }
+			Expression trimmed = trimmedInExpression((ASTPath) exp.getOperand(0), objects, maxInSize);
+			if (exp instanceof ASTNotIn) {
+				return new ASTNegate(trimmed);
+			}
+			return trimmed;
+		}
 
-        Expression trimmedInExpression(ASTPath path, Object[] values, int maxInSize) {
-            Expression res = null;
+		Expression trimmedInExpression(ASTPath path, Object[] values, int maxInSize) {
+			Expression res = null;
 
-            List<Object> in = new ArrayList<Object>(maxInSize);
-            for (Object v : values) {
-                in.add(v);
-                if (in.size() == maxInSize) {
-                    Expression inExp = new ASTIn(path, new ASTList(in));
-                    res = res != null ? res.orExp(inExp) : inExp;
-                    in = new ArrayList<Object>(maxInSize);
-                }
-            }
-            if (in.size() > 0) {
-                Expression inExp = new ASTIn(path, new ASTList(in));
-                res = res != null ? res.orExp(inExp) : inExp;
-            }
-            return res;
-        }
+			List<Object> in = new ArrayList<Object>(maxInSize);
+			for (Object v : values) {
+				in.add(v);
+				if (in.size() == maxInSize) {
+					Expression inExp = new ASTIn(path, new ASTList(in));
+					res = res != null ? res.orExp(inExp) : inExp;
+					in = new ArrayList<Object>(maxInSize);
+				}
+			}
+			if (in.size() > 0) {
+				Expression inExp = new ASTIn(path, new ASTList(in));
+				res = res != null ? res.orExp(inExp) : inExp;
+			}
+			return res;
+		}
 
-        public Object transform(Object input) {
-            if (input instanceof ASTIn || input instanceof ASTNotIn) {
-                return trimmedInExpression((Expression) input, 1000);
-            }
-            return input;
-        }
-    }
+		public Object transform(Object input) {
+			if (input instanceof ASTIn || input instanceof ASTNotIn) {
+				return trimmedInExpression((Expression) input, 1000);
+			}
+			return input;
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectAction.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectAction.java
index de145f3..f361aa4 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectAction.java
@@ -19,8 +19,6 @@
 
 package org.apache.cayenne.dba.oracle;
 
-import java.sql.Connection;
-
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -31,17 +29,17 @@ import org.apache.cayenne.query.SelectQuery;
  */
 class OracleSelectAction extends SelectAction {
 
-    public <T> OracleSelectAction(SelectQuery<T> query, DataNode dataNode) {
-        super(query, dataNode);
-    }
+	public <T> OracleSelectAction(SelectQuery<T> query, DataNode dataNode) {
+		super(query, dataNode);
+	}
 
-    @Override
-    protected SelectTranslator createTranslator(Connection connection) {
-        return new OracleSelectTranslator(query, dataNode, connection);
-    }
+	@Override
+	protected SelectTranslator createTranslator() {
+		return new OracleSelectTranslator(query, dataNode.getAdapter(), dataNode.getEntityResolver());
+	}
 
-    @Override
-    protected int getInMemoryOffset(int queryOffset) {
-        return 0;
-    }
+	@Override
+	protected int getInMemoryOffset(int queryOffset) {
+		return 0;
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java
index 911a2c2..fa6e95b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java
@@ -19,75 +19,54 @@
 
 package org.apache.cayenne.dba.oracle;
 
-import java.sql.Connection;
-import java.sql.PreparedStatement;
 import java.util.List;
 
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.translator.select.SelectTranslator;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
 
 /**
  * Select translator that implements Oracle-specific optimizations.
  * 
  */
-class OracleSelectTranslator extends SelectTranslator {
-    
-    /**
-     * @since 4.0
-     */
-    public OracleSelectTranslator(Query query, DataNode dataNode, Connection connection) {
-        super(query, dataNode, connection);
-    }
+class OracleSelectTranslator extends DefaultSelectTranslator {
 
-    @Override
-    protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-        int offset = queryMetadata.getFetchOffset();
-        int limit = queryMetadata.getFetchLimit();
+	/**
+	 * @since 4.0
+	 */
+	public OracleSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+		super(query, adapter, entityResolver);
+	}
 
-        if (limit > 0 || offset > 0) {
-            int max = (limit <= 0) ? Integer.MAX_VALUE : limit + offset;
+	@Override
+	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
+		int offset = queryMetadata.getFetchOffset();
+		int limit = queryMetadata.getFetchLimit();
 
-            buffer.insert(0, "select * from ( select tid.*, ROWNUM rnum from (");
-            buffer
-                    .append(") tid where ROWNUM <=")
-                    .append(max)
-                    .append(") where rnum  > ")
-                    .append(offset);
-        }
-    }
-    
-    @Override
-    protected void appendSelectColumns(StringBuilder buffer, List<String> selectColumnExpList) {
-        
-        // we need to add aliases to all columns to make fetch
-        // limit and offset work properly on Oracle (see CAY-1266)
-        
-        // append columns (unroll the loop's first element)
-        int columnCount = selectColumnExpList.size();
-        buffer.append(selectColumnExpList.get(0)).append(" AS c0");
+		if (limit > 0 || offset > 0) {
+			int max = (limit <= 0) ? Integer.MAX_VALUE : limit + offset;
 
-        // assume there is at least 1 element
-        for (int i = 1; i < columnCount; i++) {
-            buffer.append(", ");
-            buffer
-                    .append(selectColumnExpList.get(i))
-                    .append(" AS c" + i);
-        }
-    }
+			buffer.insert(0, "select * from ( select tid.*, ROWNUM rnum from (");
+			buffer.append(") tid where ROWNUM <=").append(max).append(") where rnum  > ").append(offset);
+		}
+	}
 
-    /**
-     * Translates internal query into PreparedStatement, applying Oracle optimizations if
-     * possible.
-     */
-    @Override
-    public PreparedStatement createStatement() throws Exception {
-        String sqlStr = createSqlString();
-        logger.logQuery(sqlStr, values);
-        PreparedStatement stmt = connection.prepareStatement(sqlStr);
+	@Override
+	protected void appendSelectColumns(StringBuilder buffer, List<String> selectColumnExpList) {
 
-        initStatement(stmt);
+		// we need to add aliases to all columns to make fetch
+		// limit and offset work properly on Oracle (see CAY-1266)
 
-        return stmt;
-    }
+		// append columns (unroll the loop's first element)
+		int columnCount = selectColumnExpList.size();
+		buffer.append(selectColumnExpList.get(0)).append(" AS c0");
+
+		// assume there is at least 1 element
+		for (int i = 1; i < columnCount; i++) {
+			buffer.append(", ");
+			buffer.append(selectColumnExpList.get(i)).append(" AS c" + i);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java
index 3904805..8b8e731 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java
@@ -34,102 +34,94 @@ import org.apache.cayenne.exp.parser.PatternMatchNode;
  */
 public class PostgresQualifierTranslator extends TrimmingQualifierTranslator {
 
-    public PostgresQualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler, "RTRIM");
-    }
-
-    @Override
-    public void startNode(Expression node, Expression parentNode) {
-
-        if (node.getOperandCount() == 2) {
-            // binary nodes are the only ones that currently require this
-            detectObjectMatch(node);
-
-            try {
-                if (parenthesisNeeded(node, parentNode)) {
-                    out.append('(');
-                }
-
-                // super implementation has special handling
-                // of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-                // Postgres uses ILIKE
-                // ...
-            }
-            catch (IOException ioex) {
-                throw new CayenneRuntimeException("Error appending content", ioex);
-            }
-        }
-        else {
-            super.startNode(node, parentNode);
-        }
-    }
-
-    @Override
-    public void endNode(Expression node, Expression parentNode) {
-        if (node.getOperandCount() == 2) {
-
-            try {
-                // check if we need to use objectMatchTranslator to finish building the
-                // expression
-                if (matchingObject) {
-                    appendObjectMatch();
-                }
-                
-                if(PatternMatchNode.class.isAssignableFrom(node.getClass())) {
-                    appendLikeEscapeCharacter((PatternMatchNode) node);
-                }
-
-                if (parenthesisNeeded(node, parentNode)) {
-                    out.append(')');
-                }
-
-                // super implementation has special handling
-                // of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-                // Postgres uses ILIKE
-                // ...
-            }
-            catch (IOException ioex) {
-                throw new CayenneRuntimeException("Error appending content", ioex);
-            }
-        }
-        else {
-            super.endNode(node, parentNode);
-        }
-    }
-
-    @Override
-    public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
-        if (!hasMoreChildren) {
-            return;
-        }
-
-        try {
-            // use ILIKE
-
-            switch (node.getType()) {
-
-                case Expression.LIKE_IGNORE_CASE:
-                    finishedChildNodeAppendExpression(node, " ILIKE ");
-                    break;
-                case Expression.NOT_LIKE_IGNORE_CASE:
-                    finishedChildNodeAppendExpression(node, " NOT ILIKE ");
-                    break;
-                default:
-                    super.finishedChild(node, childIndex, hasMoreChildren);
-            }
-        }
-        catch (IOException ioex) {
-            throw new CayenneRuntimeException("Error appending content", ioex);
-        }
-    }
-
-    private void finishedChildNodeAppendExpression(Expression node, String operation)
-            throws IOException {
-        Appendable buf = matchingObject ? new StringBuilder() : this.out;
-        buf.append(operation);
-        if (matchingObject) {
-            objectMatchTranslator.setOperation(buf.toString());
-            objectMatchTranslator.setExpression(node);
-        }
-    }
+	public PostgresQualifierTranslator(QueryAssembler queryAssembler) {
+		super(queryAssembler, "RTRIM");
+	}
+
+	@Override
+	public void startNode(Expression node, Expression parentNode) {
+
+		if (node.getOperandCount() == 2) {
+			// binary nodes are the only ones that currently require this
+			detectObjectMatch(node);
+
+			if (parenthesisNeeded(node, parentNode)) {
+				out.append('(');
+			}
+
+			// super implementation has special handling
+			// of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
+			// Postgres uses ILIKE
+			// ...
+
+		} else {
+			super.startNode(node, parentNode);
+		}
+	}
+
+	@Override
+	public void endNode(Expression node, Expression parentNode) {
+		if (node.getOperandCount() == 2) {
+
+			try {
+				// check if we need to use objectMatchTranslator to finish
+				// building the
+				// expression
+				if (matchingObject) {
+					appendObjectMatch();
+				}
+
+				if (PatternMatchNode.class.isAssignableFrom(node.getClass())) {
+					appendLikeEscapeCharacter((PatternMatchNode) node);
+				}
+
+				if (parenthesisNeeded(node, parentNode)) {
+					out.append(')');
+				}
+
+				// super implementation has special handling
+				// of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
+				// Postgres uses ILIKE
+				// ...
+			} catch (IOException ioex) {
+				throw new CayenneRuntimeException("Error appending content", ioex);
+			}
+		} else {
+			super.endNode(node, parentNode);
+		}
+	}
+
+	@Override
+	public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
+		if (!hasMoreChildren) {
+			return;
+		}
+
+		try {
+			// use ILIKE
+
+			switch (node.getType()) {
+
+			case Expression.LIKE_IGNORE_CASE:
+				finishedChildNodeAppendExpression(node, " ILIKE ");
+				break;
+			case Expression.NOT_LIKE_IGNORE_CASE:
+				finishedChildNodeAppendExpression(node, " NOT ILIKE ");
+				break;
+			default:
+				super.finishedChild(node, childIndex, hasMoreChildren);
+			}
+		} catch (IOException ioex) {
+			throw new CayenneRuntimeException("Error appending content", ioex);
+		}
+	}
+
+	private void finishedChildNodeAppendExpression(Expression node, String operation) throws IOException {
+		Appendable buf = matchingObject ? new StringBuilder() : this.out;
+		buf.append(operation);
+		if (matchingObject) {
+			objectMatchTranslator.setOperation(buf.toString());
+			objectMatchTranslator.setExpression(node);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
index 0833f96..b9e8fe5 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
@@ -18,8 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.dba.postgres;
 
-import java.sql.Connection;
-
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -30,17 +28,17 @@ import org.apache.cayenne.query.SelectQuery;
  */
 class PostgresSelectAction extends SelectAction {
 
-    <T> PostgresSelectAction(SelectQuery<T> query, DataNode dataNode) {
-        super(query, dataNode);
-    }
+	<T> PostgresSelectAction(SelectQuery<T> query, DataNode dataNode) {
+		super(query, dataNode);
+	}
 
-    @Override
-    protected int getInMemoryOffset(int queryOffset) {
-        return 0;
-    }
+	@Override
+	protected int getInMemoryOffset(int queryOffset) {
+		return 0;
+	}
 
-    @Override
-    protected SelectTranslator createTranslator(Connection connection) {
-        return new PostgresSelectTranslator(query, dataNode, connection);
-    }
+	@Override
+	protected SelectTranslator createTranslator() {
+		return new PostgresSelectTranslator(query, dataNode.getAdapter(), dataNode.getEntityResolver());
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectTranslator.java
index 84b3521..b7c1917 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectTranslator.java
@@ -19,23 +19,22 @@
 
 package org.apache.cayenne.dba.postgres;
 
-import java.sql.Connection;
-
-import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.translator.select.SelectTranslator;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
 
 /**
  * @since 1.2
  */
-class PostgresSelectTranslator extends SelectTranslator {
+class PostgresSelectTranslator extends DefaultSelectTranslator {
 
 	/**
 	 * @since 4.0
 	 */
-	public PostgresSelectTranslator(Query query, DataNode dataNode, Connection connection) {
-		super(query, dataNode, connection);
+	public PostgresSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+		super(query, adapter, entityResolver);
 	}
 
 	@Override
@@ -81,7 +80,7 @@ class PostgresSelectTranslator extends SelectTranslator {
 		if (foundKey) {
 			builder.append(")");
 		}
-		
+
 		return builder.toString();
 	}
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectAction.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectAction.java
index d274d70..413a6a6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectAction.java
@@ -18,8 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.dba.sqlserver;
 
-import java.sql.Connection;
-
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -32,8 +30,8 @@ public class SQLServerSelectAction extends SelectAction {
     }
 
     @Override
-    protected SelectTranslator createTranslator(Connection connection) {
-        return new SQLServerSelectTranslator(query, dataNode, connection);
+    protected SelectTranslator createTranslator() {
+        return new SQLServerSelectTranslator(query, dataNode.getAdapter(), dataNode.getEntityResolver());
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectTranslator.java
index 3556c7b..b78877c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectTranslator.java
@@ -18,40 +18,37 @@
  ****************************************************************/
 package org.apache.cayenne.dba.sqlserver;
 
-import java.sql.Connection;
-
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.translator.select.SelectTranslator;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.QueryMetadata;
-
-public class SQLServerSelectTranslator extends SelectTranslator {
-	
-    /**
-     * @since 4.0
-     */
-    public SQLServerSelectTranslator(Query query, DataNode dataNode, Connection connection) {
-        super(query, dataNode, connection);
-    }
-    
-    @Override
-    protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-        QueryMetadata metadata = getQuery().getMetaData(getEntityResolver());
-        
-        int limit = metadata.getFetchLimit();
-        int offset = metadata.getFetchOffset();
-        
-        if (limit > 0) {
-        	String sql = buffer.toString();
-        	
-        	// If contains distinct insert top limit after
-        	if (sql.startsWith("SELECT DISTINCT ")) {
-        		buffer.replace(0, 15, "SELECT DISTINCT TOP " + (offset + limit));	
-        		
-        	} else {
-        		buffer.replace(0, 6, "SELECT TOP " + (offset + limit));	
-        	}
-        }
-    }
+
+public class SQLServerSelectTranslator extends DefaultSelectTranslator {
+
+	/**
+	 * @since 4.0
+	 */
+	public SQLServerSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+		super(query, adapter, entityResolver);
+	}
+
+	@Override
+	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
+
+		int limit = queryMetadata.getFetchLimit();
+		int offset = queryMetadata.getFetchOffset();
+
+		if (limit > 0) {
+			String sql = buffer.toString();
+
+			// If contains distinct insert top limit after
+			if (sql.startsWith("SELECT DISTINCT ")) {
+				buffer.replace(0, 15, "SELECT DISTINCT TOP " + (offset + limit));
+
+			} else {
+				buffer.replace(0, 6, "SELECT TOP " + (offset + limit));
+			}
+		}
+	}
 
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
index 2c0649f..0b4fa68 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.dba.sqlserver;
 
-import java.io.IOException;
 import java.sql.Types;
 import java.util.ArrayList;
 import java.util.List;
@@ -33,87 +32,84 @@ import org.apache.cayenne.map.DbAttribute;
  */
 class SQLServerTrimmingQualifierTranslator extends TrimmingQualifierTranslator {
 
-    // since LIKE IGNORE CASE requires more contextual information than the super
-    // translator can provide, we are using an internal element stack to trace translation
-    // context.. Maybe it is a good idea to introduce it in the superclass?
-    private List<Expression> expressionStack;
-
-    SQLServerTrimmingQualifierTranslator(QueryAssembler queryAssembler,
-            String trimFunction) {
-        super(queryAssembler, trimFunction);
-        expressionStack = new ArrayList<Expression>();
-    }
-
-    @Override
-    public void startNode(Expression node, Expression parentNode) {
-        push(node);
-        super.startNode(node, parentNode);
-    }
-
-    @Override
-    protected void processColumn(DbAttribute dbAttr) throws IOException {
-
-        Expression node = peek(1);
-
-        boolean likeCI = node != null
-                && dbAttr.getType() == Types.CLOB
-                && (node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE);
-
-        if (likeCI) {
-            out.append("CAST(");
-        }
-
-        super.processColumn(dbAttr);
-
-        if (likeCI) {
-            out.append(" AS NVARCHAR(MAX))");
-        }
-    }
-
-    @Override
-    protected void processColumnWithQuoteSqlIdentifiers(
-            DbAttribute dbAttr,
-            Expression pathExp) throws IOException {
-        Expression node = peek(1);
-
-        boolean likeCI = node != null
-                && dbAttr.getType() == Types.CLOB
-                && (node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE);
-
-        if (likeCI) {
-            out.append("CAST(");
-        }
-
-        super.processColumnWithQuoteSqlIdentifiers(dbAttr, node);
-
-        if (likeCI) {
-            out.append(" AS NVARCHAR(MAX))");
-        }
-    }
-
-    @Override
-    public void endNode(Expression node, Expression parentNode) {
-        super.endNode(node, parentNode);
-        pop();
-    }
-
-    private void push(Expression node) {
-        expressionStack.add(node);
-    }
-
-    private void pop() {
-        int len = expressionStack.size();
-        if (len > 0) {
-            expressionStack.remove(len - 1);
-        }
-    }
-
-    private Expression peek(int tailIndex) {
-        int index = expressionStack.size() - tailIndex - 1;
-        if (index < 0) {
-            return null;
-        }
-
-        return expressionStack.get(index);
-    }
+	// since LIKE IGNORE CASE requires more contextual information than the
+	// super
+	// translator can provide, we are using an internal element stack to trace
+	// translation
+	// context.. Maybe it is a good idea to introduce it in the superclass?
+	private List<Expression> expressionStack;
+
+	SQLServerTrimmingQualifierTranslator(QueryAssembler queryAssembler, String trimFunction) {
+		super(queryAssembler, trimFunction);
+		expressionStack = new ArrayList<Expression>();
+	}
+
+	@Override
+	public void startNode(Expression node, Expression parentNode) {
+		push(node);
+		super.startNode(node, parentNode);
+	}
+
+	@Override
+	protected void processColumn(DbAttribute dbAttr) {
+
+		Expression node = peek(1);
+
+		boolean likeCI = node != null && dbAttr.getType() == Types.CLOB
+				&& (node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE);
+
+		if (likeCI) {
+			out.append("CAST(");
+		}
+
+		super.processColumn(dbAttr);
+
+		if (likeCI) {
+			out.append(" AS NVARCHAR(MAX))");
+		}
+	}
+
+	@Override
+	protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) {
+		Expression node = peek(1);
+
+		boolean likeCI = node != null && dbAttr.getType() == Types.CLOB
+				&& (node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE);
+
+		if (likeCI) {
+			out.append("CAST(");
+		}
+
+		super.processColumnWithQuoteSqlIdentifiers(dbAttr, node);
+
+		if (likeCI) {
+			out.append(" AS NVARCHAR(MAX))");
+		}
+	}
+
+	@Override
+	public void endNode(Expression node, Expression parentNode) {
+		super.endNode(node, parentNode);
+		pop();
+	}
+
+	private void push(Expression node) {
+		expressionStack.add(node);
+	}
+
+	private void pop() {
+		int len = expressionStack.size();
+		if (len > 0) {
+			expressionStack.remove(len - 1);
+		}
+	}
+
+	private Expression peek(int tailIndex) {
+		int index = expressionStack.size() - tailIndex - 1;
+		if (index < 0) {
+			return null;
+		}
+
+		return expressionStack.get(index);
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/log/CommonsJdbcEventLogger.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/log/CommonsJdbcEventLogger.java b/cayenne-server/src/main/java/org/apache/cayenne/log/CommonsJdbcEventLogger.java
index 43cd923..d31f89f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/log/CommonsJdbcEventLogger.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/log/CommonsJdbcEventLogger.java
@@ -25,8 +25,8 @@ import java.util.List;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ExtendedEnumeration;
-import org.apache.cayenne.access.jdbc.ParameterBinding;
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.jdbc.SQLParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
 import org.apache.cayenne.conn.DataSourceInfo;
@@ -44,397 +44,426 @@ import org.apache.commons.logging.LogFactory;
  */
 public class CommonsJdbcEventLogger implements JdbcEventLogger {
 
-    private static final Log logger = LogFactory.getLog(CommonsJdbcEventLogger.class);
-
-    private static final int TRIM_VALUES_THRESHOLD = 30;
-
-    protected long queryExecutionTimeLoggingThreshold;
-
-    public CommonsJdbcEventLogger(@Inject RuntimeProperties runtimeProperties) {
-        this.queryExecutionTimeLoggingThreshold = runtimeProperties.getLong(
-                Constants.QUERY_EXECUTION_TIME_LOGGING_THRESHOLD_PROPERTY, 0);
-    }
-
-    void sqlLiteralForObject(StringBuilder buffer, Object object) {
-        if (object == null) {
-            buffer.append("NULL");
-        } else if (object instanceof String) {
-            buffer.append('\'');
-            // lets escape quotes
-            String literal = (String) object;
-            if (literal.length() > TRIM_VALUES_THRESHOLD) {
-                literal = literal.substring(0, TRIM_VALUES_THRESHOLD) + "...";
-            }
-
-            int curPos = 0;
-            int endPos = 0;
-
-            while ((endPos = literal.indexOf('\'', curPos)) >= 0) {
-                buffer.append(literal.substring(curPos, endPos + 1)).append('\'');
-                curPos = endPos + 1;
-            }
-
-            if (curPos < literal.length())
-                buffer.append(literal.substring(curPos));
-
-            buffer.append('\'');
-        }
-        // handle byte pretty formatting
-        else if (object instanceof Byte) {
-            IDUtil.appendFormattedByte(buffer, ((Byte) object).byteValue());
-        } else if (object instanceof Number) {
-            // process numeric value (do something smart in the future)
-            buffer.append(object);
-        } else if (object instanceof java.sql.Date) {
-            buffer.append('\'').append(object).append('\'');
-        } else if (object instanceof java.sql.Time) {
-            buffer.append('\'').append(object).append('\'');
-        } else if (object instanceof java.util.Date) {
-            long time = ((java.util.Date) object).getTime();
-            buffer.append('\'').append(new java.sql.Timestamp(time)).append('\'');
-        } else if (object instanceof java.util.Calendar) {
-            long time = ((java.util.Calendar) object).getTimeInMillis();
-            buffer.append(object.getClass().getName()).append('(').append(new java.sql.Timestamp(time)).append(')');
-        } else if (object instanceof Character) {
-            buffer.append(((Character) object).charValue());
-        } else if (object instanceof Boolean) {
-            buffer.append('\'').append(object).append('\'');
-        } else if (object instanceof Enum<?>) {
-            // buffer.append(object.getClass().getName()).append(".");
-            buffer.append(((Enum<?>) object).name()).append("=");
-            if (object instanceof ExtendedEnumeration) {
-                Object value = ((ExtendedEnumeration) object).getDatabaseValue();
-                if (value instanceof String)
-                    buffer.append("'");
-                buffer.append(value);
-                if (value instanceof String)
-                    buffer.append("'");
-            } else {
-                buffer.append(((Enum<?>) object).ordinal());
-                // FIXME -- this isn't quite right
-            }
-        } else if (object instanceof ParameterBinding) {
-            sqlLiteralForObject(buffer, ((ParameterBinding) object).getValue());
-        } else if (object.getClass().isArray()) {
-            buffer.append("< ");
-
-            int len = Array.getLength(object);
-            boolean trimming = false;
-            if (len > TRIM_VALUES_THRESHOLD) {
-                len = TRIM_VALUES_THRESHOLD;
-                trimming = true;
-            }
-
-            for (int i = 0; i < len; i++) {
-                if (i > 0) {
-                    buffer.append(",");
-                }
-                sqlLiteralForObject(buffer, Array.get(object, i));
-            }
-
-            if (trimming) {
-                buffer.append("...");
-            }
-
-            buffer.append('>');
-        } else {
-            buffer.append(object.getClass().getName()).append("@").append(System.identityHashCode(object));
-        }
-    }
-
-    @Override
-    public void log(String message) {
-        if (message != null) {
-            logger.info(message);
-        }
-    }
-
-    @Override
-    public void logConnect(String dataSource) {
-        if (isLoggable()) {
-            logger.info("Connecting. JNDI path: " + dataSource);
-        }
-    }
-
-    @Override
-    public void logConnect(String url, String userName, String password) {
-        if (isLoggable()) {
-            StringBuilder buf = new StringBuilder("Opening connection: ");
-
-            // append URL on the same line to make log somewhat grep-friendly
-            buf.append(url);
-            buf.append("\n\tLogin: ").append(userName);
-            buf.append("\n\tPassword: *******");
-
-            logger.info(buf.toString());
-        }
-    }
-
-    @Override
-    public void logPoolCreated(DataSourceInfo dsi) {
-        if (isLoggable()) {
-            StringBuilder buf = new StringBuilder("Created connection pool: ");
-
-            if (dsi != null) {
-                // append URL on the same line to make log somewhat
-                // grep-friendly
-                buf.append(dsi.getDataSourceUrl());
-
-                if (dsi.getAdapterClassName() != null) {
-                    buf.append("\n\tCayenne DbAdapter: ").append(dsi.getAdapterClassName());
-                }
-
-                buf.append("\n\tDriver class: ").append(dsi.getJdbcDriver());
-
-                if (dsi.getMinConnections() >= 0) {
-                    buf.append("\n\tMin. connections in the pool: ").append(dsi.getMinConnections());
-                }
-                if (dsi.getMaxConnections() >= 0) {
-                    buf.append("\n\tMax. connections in the pool: ").append(dsi.getMaxConnections());
-                }
-            } else {
-                buf.append(" pool information unavailable");
-            }
-
-            logger.info(buf.toString());
-        }
-    }
-
-    @Override
-    public void logConnectSuccess() {
-        logger.info("+++ Connecting: SUCCESS.");
-    }
-
-    @Override
-    public void logConnectFailure(Throwable th) {
-        logger.info("*** Connecting: FAILURE.", th);
-    }
-
-    @Override
-    public void logGeneratedKey(DbAttribute attribute, Object value) {
-        if (isLoggable()) {
-            String entity = attribute.getEntity().getName();
-            String key = attribute.getName();
-
-            logger.info("Generated PK: " + entity + "." + key + " = " + value);
-        }
-    }
-
-    @Override
-    public void logQuery(String queryStr, List<?> params) {
-        logQuery(queryStr, null, params, -1);
-    }
-
-    @Deprecated
-    private void buildLog(StringBuilder buffer, String prefix, String postfix, List<DbAttribute> attributes,
-            List<?> parameters, boolean isInserting) {
-        if (parameters != null && parameters.size() > 0) {
-            DbAttribute attribute = null;
-            Iterator<DbAttribute> attributeIterator = null;
-            int position = 0;
-
-            if (attributes != null)
-                attributeIterator = attributes.iterator();
-
-            for (Object parameter : parameters) {
-                // If at the beginning, output the prefix, otherwise a
-                // separator.
-                if (position++ == 0)
-                    buffer.append(prefix);
-                else
-                    buffer.append(", ");
-
-                // Find the next attribute and SKIP generated attributes. Only
-                // skip when logging inserts, though. Should show previously
-                // generated keys on DELETE, UPDATE, or SELECT.
-                while (attributeIterator != null && attributeIterator.hasNext()) {
-                    attribute = attributeIterator.next();
-
-                    if (isInserting == false || attribute.isGenerated() == false)
-                        break;
-                }
-
-                buffer.append(position);
-
-                if (attribute != null) {
-                    buffer.append("->");
-                    buffer.append(attribute.getName());
-                }
-
-                buffer.append(":");
-                sqlLiteralForObject(buffer, parameter);
-            }
-
-            buffer.append(postfix);
-        }
-    }
-
-   
-
-    private boolean isInserting(String query) {
-        if (query == null || query.length() == 0)
-            return false;
-
-        char firstCharacter = query.charAt(0);
-
-        if (firstCharacter == 'I' || firstCharacter == 'i')
-            return true;
-        else
-            return false;
-    }
-
-    @Override
-    public void logQuery(String queryStr, List<DbAttribute> attrs, List<?> params, long time) {
-        if (isLoggable()) {
-            StringBuilder buffer = new StringBuilder(queryStr);
-            buildLog(buffer, " [bind: ", "]", attrs, params, isInserting(queryStr));
-
-            // log preparation time only if it is something significant
-            if (time > 5) {
-                buffer.append(" - prepared in ").append(time).append(" ms.");
-            }
-
-            logger.info(buffer.toString());
-        }
-    }
-
-    @Deprecated
-    @Override
-    public void logQueryParameters(String label, List<DbAttribute> attrs, List<Object> parameters, boolean isInserting) {
-        String prefix = "[" + label + ": ";
-        if (isLoggable() && parameters.size() > 0) {
-            StringBuilder buffer = new StringBuilder();
-            buildLog(buffer, prefix, "]", attrs, parameters, isInserting);
-            logger.info(buffer.toString());
-        }
-    }
-
-    @Override
-    public void logQueryParameters(String label, BatchParameterBinding[] bindings) {
-
-        if (isLoggable() && bindings.length > 0) {
-
-            boolean hasIncluded = false;
-            StringBuilder buffer = null;
-
-            int len = bindings.length;
-            for (int i = 0, j = 1; i < len; i++) {
-                BatchParameterBinding b = bindings[i];
-
-                if (b.isExcluded()) {
-                    continue;
-                }
-
-                if (hasIncluded) {
-                    buffer.append(", ");
-                } else {
-                    hasIncluded = true;
-                    buffer = new StringBuilder();
-                    buffer.append("[").append(label).append(": ");
-                }
-
-                DbAttribute attribute = b.getAttribute();
-
-                buffer.append(j++);
-
-                if (attribute != null) {
-                    buffer.append("->");
-                    buffer.append(attribute.getName());
-                }
-
-                buffer.append(":");
-                sqlLiteralForObject(buffer, b.getValue());
-            }
-
-            if (hasIncluded) {
-                buffer.append("]");
-
-                logger.info(buffer.toString());
-            }
-        }
-    }
-
-    @Override
-    public void logSelectCount(int count, long time) {
-        logSelectCount(count, time, null);
-    }
-
-    @Override
-    public void logSelectCount(int count, long time, String sql) {
-
-        if (isLoggable()) {
-            StringBuilder buf = new StringBuilder();
-
-            if (count == 1) {
-                buf.append("=== returned 1 row.");
-            } else {
-                buf.append("=== returned ").append(count).append(" rows.");
-            }
-
-            if (time >= 0) {
-                buf.append(" - took ").append(time).append(" ms.");
-            }
-
-            logger.info(buf.toString());
-        }
-
-        if (queryExecutionTimeLoggingThreshold > 0 && time > queryExecutionTimeLoggingThreshold) {
-            StringBuilder buf = new StringBuilder();
-            buf.append("Query time exceeded threshold (").append(time).append(" ms): ");
-            buf.append(sql);
-            String message = buf.toString();
-            logger.warn(message, new CayenneRuntimeException(message));
-        }
-    }
-
-    @Override
-    public void logUpdateCount(int count) {
-        if (isLoggable()) {
-            if (count < 0) {
-                logger.info("=== updated ? rows");
-            } else {
-                String countStr = (count == 1) ? "=== updated 1 row." : "=== updated " + count + " rows.";
-                logger.info(countStr);
-            }
-        }
-    }
-
-    @Override
-    public void logBeginTransaction(String transactionLabel) {
-        logger.info("--- " + transactionLabel);
-    }
-
-    @Override
-    public void logCommitTransaction(String transactionLabel) {
-        logger.info("+++ " + transactionLabel);
-    }
-
-    @Override
-    public void logRollbackTransaction(String transactionLabel) {
-        logger.info("*** " + transactionLabel);
-    }
-
-    @Override
-    public void logQueryError(Throwable th) {
-        if (isLoggable()) {
-            if (th != null) {
-                th = Util.unwindException(th);
-            }
-
-            logger.info("*** error.", th);
-
-            if (th instanceof SQLException) {
-                SQLException sqlException = ((SQLException) th).getNextException();
-                while (sqlException != null) {
-                    logger.info("*** nested SQL error.", sqlException);
-                    sqlException = sqlException.getNextException();
-                }
-            }
-        }
-    }
-
-    @Override
-    public boolean isLoggable() {
-        return logger.isInfoEnabled();
-    }
+	private static final Log logger = LogFactory.getLog(CommonsJdbcEventLogger.class);
+
+	private static final int TRIM_VALUES_THRESHOLD = 30;
+
+	protected long queryExecutionTimeLoggingThreshold;
+
+	public CommonsJdbcEventLogger(@Inject RuntimeProperties runtimeProperties) {
+		this.queryExecutionTimeLoggingThreshold = runtimeProperties.getLong(
+				Constants.QUERY_EXECUTION_TIME_LOGGING_THRESHOLD_PROPERTY, 0);
+	}
+
+	void sqlLiteralForObject(StringBuilder buffer, Object object) {
+		if (object == null) {
+			buffer.append("NULL");
+		} else if (object instanceof String) {
+			buffer.append('\'');
+			// lets escape quotes
+			String literal = (String) object;
+			if (literal.length() > TRIM_VALUES_THRESHOLD) {
+				literal = literal.substring(0, TRIM_VALUES_THRESHOLD) + "...";
+			}
+
+			int curPos = 0;
+			int endPos = 0;
+
+			while ((endPos = literal.indexOf('\'', curPos)) >= 0) {
+				buffer.append(literal.substring(curPos, endPos + 1)).append('\'');
+				curPos = endPos + 1;
+			}
+
+			if (curPos < literal.length())
+				buffer.append(literal.substring(curPos));
+
+			buffer.append('\'');
+		}
+		// handle byte pretty formatting
+		else if (object instanceof Byte) {
+			IDUtil.appendFormattedByte(buffer, ((Byte) object).byteValue());
+		} else if (object instanceof Number) {
+			// process numeric value (do something smart in the future)
+			buffer.append(object);
+		} else if (object instanceof java.sql.Date) {
+			buffer.append('\'').append(object).append('\'');
+		} else if (object instanceof java.sql.Time) {
+			buffer.append('\'').append(object).append('\'');
+		} else if (object instanceof java.util.Date) {
+			long time = ((java.util.Date) object).getTime();
+			buffer.append('\'').append(new java.sql.Timestamp(time)).append('\'');
+		} else if (object instanceof java.util.Calendar) {
+			long time = ((java.util.Calendar) object).getTimeInMillis();
+			buffer.append(object.getClass().getName()).append('(').append(new java.sql.Timestamp(time)).append(')');
+		} else if (object instanceof Character) {
+			buffer.append(((Character) object).charValue());
+		} else if (object instanceof Boolean) {
+			buffer.append('\'').append(object).append('\'');
+		} else if (object instanceof Enum<?>) {
+			// buffer.append(object.getClass().getName()).append(".");
+			buffer.append(((Enum<?>) object).name()).append("=");
+			if (object instanceof ExtendedEnumeration) {
+				Object value = ((ExtendedEnumeration) object).getDatabaseValue();
+				if (value instanceof String)
+					buffer.append("'");
+				buffer.append(value);
+				if (value instanceof String)
+					buffer.append("'");
+			} else {
+				buffer.append(((Enum<?>) object).ordinal());
+				// FIXME -- this isn't quite right
+			}
+		} else if (object instanceof SQLParameterBinding) {
+			sqlLiteralForObject(buffer, ((SQLParameterBinding) object).getValue());
+		} else if (object.getClass().isArray()) {
+			buffer.append("< ");
+
+			int len = Array.getLength(object);
+			boolean trimming = false;
+			if (len > TRIM_VALUES_THRESHOLD) {
+				len = TRIM_VALUES_THRESHOLD;
+				trimming = true;
+			}
+
+			for (int i = 0; i < len; i++) {
+				if (i > 0) {
+					buffer.append(",");
+				}
+				sqlLiteralForObject(buffer, Array.get(object, i));
+			}
+
+			if (trimming) {
+				buffer.append("...");
+			}
+
+			buffer.append('>');
+		} else {
+			buffer.append(object.getClass().getName()).append("@").append(System.identityHashCode(object));
+		}
+	}
+
+	@Override
+	public void log(String message) {
+		if (message != null) {
+			logger.info(message);
+		}
+	}
+
+	@Override
+	public void logConnect(String dataSource) {
+		if (isLoggable()) {
+			logger.info("Connecting. JNDI path: " + dataSource);
+		}
+	}
+
+	@Override
+	public void logConnect(String url, String userName, String password) {
+		if (isLoggable()) {
+			StringBuilder buf = new StringBuilder("Opening connection: ");
+
+			// append URL on the same line to make log somewhat grep-friendly
+			buf.append(url);
+			buf.append("\n\tLogin: ").append(userName);
+			buf.append("\n\tPassword: *******");
+
+			logger.info(buf.toString());
+		}
+	}
+
+	@Override
+	public void logPoolCreated(DataSourceInfo dsi) {
+		if (isLoggable()) {
+			StringBuilder buf = new StringBuilder("Created connection pool: ");
+
+			if (dsi != null) {
+				// append URL on the same line to make log somewhat
+				// grep-friendly
+				buf.append(dsi.getDataSourceUrl());
+
+				if (dsi.getAdapterClassName() != null) {
+					buf.append("\n\tCayenne DbAdapter: ").append(dsi.getAdapterClassName());
+				}
+
+				buf.append("\n\tDriver class: ").append(dsi.getJdbcDriver());
+
+				if (dsi.getMinConnections() >= 0) {
+					buf.append("\n\tMin. connections in the pool: ").append(dsi.getMinConnections());
+				}
+				if (dsi.getMaxConnections() >= 0) {
+					buf.append("\n\tMax. connections in the pool: ").append(dsi.getMaxConnections());
+				}
+			} else {
+				buf.append(" pool information unavailable");
+			}
+
+			logger.info(buf.toString());
+		}
+	}
+
+	@Override
+	public void logConnectSuccess() {
+		logger.info("+++ Connecting: SUCCESS.");
+	}
+
+	@Override
+	public void logConnectFailure(Throwable th) {
+		logger.info("*** Connecting: FAILURE.", th);
+	}
+
+	@Override
+	public void logGeneratedKey(DbAttribute attribute, Object value) {
+		if (isLoggable()) {
+			String entity = attribute.getEntity().getName();
+			String key = attribute.getName();
+
+			logger.info("Generated PK: " + entity + "." + key + " = " + value);
+		}
+	}
+
+	/**
+	 * @deprecated since 4.0 use
+	 *             {@link #logQuery(String, ParameterBinding[], long)}.
+	 */
+	@Deprecated
+	@Override
+	public void logQuery(String queryStr, List<?> params) {
+		logQuery(queryStr, null, params, -1);
+	}
+
+	/**
+	 * @deprecated since 4.0 uses old style of parameter logging.
+	 */
+	@Deprecated
+	private void buildLog(StringBuilder buffer, String prefix, String postfix, List<DbAttribute> attributes,
+			List<?> parameters, boolean isInserting) {
+		if (parameters != null && parameters.size() > 0) {
+			Iterator<DbAttribute> attributeIterator = null;
+			int position = 0;
+
+			if (attributes != null)
+				attributeIterator = attributes.iterator();
+
+			for (Object parameter : parameters) {
+				// If at the beginning, output the prefix, otherwise a
+				// separator.
+				if (position++ == 0)
+					buffer.append(prefix);
+				else
+					buffer.append(", ");
+
+				// Find the next attribute and SKIP generated attributes. Only
+				// skip when logging inserts, though. Should show previously
+				// generated keys on DELETE, UPDATE, or SELECT.
+				DbAttribute attribute = null;
+				while (attributeIterator != null && attributeIterator.hasNext()) {
+					attribute = attributeIterator.next();
+
+					if (isInserting == false || attribute.isGenerated() == false)
+						break;
+				}
+
+				buffer.append(position);
+
+				if (attribute != null) {
+					buffer.append("->");
+					buffer.append(attribute.getName());
+				}
+
+				buffer.append(":");
+				sqlLiteralForObject(buffer, parameter);
+			}
+
+			buffer.append(postfix);
+		}
+	}
+
+	private boolean isInserting(String query) {
+		if (query == null || query.length() == 0)
+			return false;
+
+		char firstCharacter = query.charAt(0);
+
+		if (firstCharacter == 'I' || firstCharacter == 'i')
+			return true;
+		else
+			return false;
+	}
+
+	@Override
+	public void logQuery(String sql, ParameterBinding[] bindings, long translatedIn) {
+		if (isLoggable()) {
+
+			StringBuilder buffer = new StringBuilder(sql).append(" ");
+			appendParameters(buffer, "bind", bindings);
+
+			if (buffer.length() > 0) {
+				logger.info(buffer.toString());
+			}
+		}
+	}
+
+	@Deprecated
+	@Override
+	public void logQuery(String queryStr, List<DbAttribute> attrs, List<?> params, long time) {
+		if (isLoggable()) {
+			StringBuilder buffer = new StringBuilder(queryStr);
+			buildLog(buffer, " [bind: ", "]", attrs, params, isInserting(queryStr));
+
+			// log preparation time only if it is something significant
+			if (time > 5) {
+				buffer.append(" - prepared in ").append(time).append(" ms.");
+			}
+
+			logger.info(buffer.toString());
+		}
+	}
+
+	@Deprecated
+	@Override
+	public void logQueryParameters(String label, List<DbAttribute> attrs, List<Object> parameters, boolean isInserting) {
+		String prefix = "[" + label + ": ";
+		if (isLoggable() && parameters.size() > 0) {
+			StringBuilder buffer = new StringBuilder();
+			buildLog(buffer, prefix, "]", attrs, parameters, isInserting);
+			logger.info(buffer.toString());
+		}
+	}
+
+	@Override
+	public void logQueryParameters(String label, ParameterBinding[] bindings) {
+
+		if (isLoggable() && bindings.length > 0) {
+
+			StringBuilder buffer = new StringBuilder();
+			appendParameters(buffer, label, bindings);
+
+			if (buffer.length() > 0) {
+				logger.info(buffer.toString());
+			}
+		}
+	}
+
+	private void appendParameters(StringBuilder buffer, String label, ParameterBinding[] bindings) {
+
+		int len = bindings.length;
+		if (len > 0) {
+
+			boolean hasIncluded = false;
+
+			for (int i = 0, j = 1; i < len; i++) {
+				ParameterBinding b = bindings[i];
+
+				if (b.isExcluded()) {
+					continue;
+				}
+
+				if (hasIncluded) {
+					buffer.append(", ");
+				} else {
+					hasIncluded = true;
+					buffer.append("[").append(label).append(": ");
+				}
+
+				DbAttribute attribute = b.getAttribute();
+
+				buffer.append(j++);
+
+				if (attribute != null) {
+					buffer.append("->");
+					buffer.append(attribute.getName());
+				}
+
+				buffer.append(":");
+				sqlLiteralForObject(buffer, b.getValue());
+			}
+
+			if (hasIncluded) {
+				buffer.append("]");
+			}
+		}
+	}
+
+	@Override
+	public void logSelectCount(int count, long time) {
+		logSelectCount(count, time, null);
+	}
+
+	@Override
+	public void logSelectCount(int count, long time, String sql) {
+
+		if (isLoggable()) {
+			StringBuilder buf = new StringBuilder();
+
+			if (count == 1) {
+				buf.append("=== returned 1 row.");
+			} else {
+				buf.append("=== returned ").append(count).append(" rows.");
+			}
+
+			if (time >= 0) {
+				buf.append(" - took ").append(time).append(" ms.");
+			}
+
+			logger.info(buf.toString());
+		}
+
+		if (queryExecutionTimeLoggingThreshold > 0 && time > queryExecutionTimeLoggingThreshold) {
+			StringBuilder buf = new StringBuilder();
+			buf.append("Query time exceeded threshold (").append(time).append(" ms): ");
+			buf.append(sql);
+			String message = buf.toString();
+			logger.warn(message, new CayenneRuntimeException(message));
+		}
+	}
+
+	@Override
+	public void logUpdateCount(int count) {
+		if (isLoggable()) {
+			if (count < 0) {
+				logger.info("=== updated ? rows");
+			} else {
+				String countStr = (count == 1) ? "=== updated 1 row." : "=== updated " + count + " rows.";
+				logger.info(countStr);
+			}
+		}
+	}
+
+	@Override
+	public void logBeginTransaction(String transactionLabel) {
+		logger.info("--- " + transactionLabel);
+	}
+
+	@Override
+	public void logCommitTransaction(String transactionLabel) {
+		logger.info("+++ " + transactionLabel);
+	}
+
+	@Override
+	public void logRollbackTransaction(String transactionLabel) {
+		logger.info("*** " + transactionLabel);
+	}
+
+	@Override
+	public void logQueryError(Throwable th) {
+		if (isLoggable()) {
+			if (th != null) {
+				th = Util.unwindException(th);
+			}
+
+			logger.info("*** error.", th);
+
+			if (th instanceof SQLException) {
+				SQLException sqlException = ((SQLException) th).getNextException();
+				while (sqlException != null) {
+					logger.info("*** nested SQL error.", sqlException);
+					sqlException = sqlException.getNextException();
+				}
+			}
+		}
+	}
+
+	@Override
+	public boolean isLoggable() {
+		return logger.isInfoEnabled();
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/log/JdbcEventLogger.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/log/JdbcEventLogger.java b/cayenne-server/src/main/java/org/apache/cayenne/log/JdbcEventLogger.java
index 59e3f1e..7e89611 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/log/JdbcEventLogger.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/log/JdbcEventLogger.java
@@ -20,7 +20,7 @@ package org.apache.cayenne.log;
 
 import java.util.List;
 
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.conn.DataSourceInfo;
 import org.apache.cayenne.map.DbAttribute;
 
@@ -54,18 +54,33 @@ public interface JdbcEventLogger {
 
 	void logGeneratedKey(DbAttribute attribute, Object value);
 
+	/**
+	 * @deprecated since 4.0 use
+	 *             {@link #logQuery(String, ParameterBinding[], long)}.
+	 */
+	@Deprecated
 	void logQuery(String sql, List<?> params);
 
+	/**
+	 * @deprecated since 4.0 use
+	 *             {@link #logQuery(String, ParameterBinding[], long)}.
+	 */
+	@Deprecated
 	void logQuery(String sql, List<DbAttribute> attrs, List<?> params, long time);
 
 	/**
 	 * @since 4.0
 	 */
-	void logQueryParameters(String label, BatchParameterBinding[] bindings);
+	void logQuery(String sql, ParameterBinding[] bindings, long translatedIn);
+
+	/**
+	 * @since 4.0
+	 */
+	void logQueryParameters(String label, ParameterBinding[] bindings);
 
 	/**
 	 * @deprecated since 4.0 in favor of
-	 *             {@link #logQueryParameters(String, BatchParameterBinding[])}
+	 *             {@link #logQueryParameters(String, ParameterBinding[])}
 	 */
 	@Deprecated
 	void logQueryParameters(String label, List<DbAttribute> attrs, List<Object> parameters, boolean isInserting);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/log/NoopJdbcEventLogger.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/log/NoopJdbcEventLogger.java b/cayenne-server/src/main/java/org/apache/cayenne/log/NoopJdbcEventLogger.java
index 5db1115..03f29ab 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/log/NoopJdbcEventLogger.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/log/NoopJdbcEventLogger.java
@@ -20,7 +20,7 @@ package org.apache.cayenne.log;
 
 import java.util.List;
 
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.conn.DataSourceInfo;
 import org.apache.cayenne.map.DbAttribute;
 
@@ -29,78 +29,96 @@ import org.apache.cayenne.map.DbAttribute;
  */
 public class NoopJdbcEventLogger implements JdbcEventLogger {
 
-    private static final NoopJdbcEventLogger instance = new NoopJdbcEventLogger();
+	private static final NoopJdbcEventLogger instance = new NoopJdbcEventLogger();
 
-    public static NoopJdbcEventLogger getInstance() {
-        return instance;
-    }
+	public static NoopJdbcEventLogger getInstance() {
+		return instance;
+	}
 
-    private NoopJdbcEventLogger() {
+	private NoopJdbcEventLogger() {
 
-    }
+	}
 
-    public void log(String message) {
-    }
+	@Override
+	public void log(String message) {
+	}
 
-    public void logConnect(String dataSource) {
-    }
+	@Override
+	public void logConnect(String dataSource) {
+	}
 
-    public void logConnect(String url, String userName, String password) {
-    }
+	@Override
+	public void logConnect(String url, String userName, String password) {
+	}
 
-    public void logPoolCreated(DataSourceInfo dsi) {
-    }
+	@Override
+	public void logPoolCreated(DataSourceInfo dsi) {
+	}
 
-    public void logConnectSuccess() {
-    }
+	@Override
+	public void logConnectSuccess() {
+	}
 
-    public void logConnectFailure(Throwable th) {
-    }
+	@Override
+	public void logConnectFailure(Throwable th) {
+	}
 
-    public void logGeneratedKey(DbAttribute attribute, Object value) {
-    }
+	@Override
+	public void logGeneratedKey(DbAttribute attribute, Object value) {
+	}
 
-    public void logQuery(String sql, List<?> params) {
-    }
+	@Override
+	public void logQuery(String sql, List<?> params) {
+	}
 
-    public void logQuery(String sql, List<DbAttribute> attrs, List<?> params, long time) {
-    }
-
-    @Override
-    @Deprecated
-    public void logQueryParameters(
-            String label,
-            List<DbAttribute> attrs,
-            List<Object> parameters,
-            boolean isInserting) {
-    }
-    
-    @Override
-    public void logQueryParameters(String label, BatchParameterBinding[] bindings) {
-    }
-
-    public void logSelectCount(int count, long time) {
-    }
-
-    public void logSelectCount(int count, long time, String sql) {
-    }
+	@Deprecated
+	@Override
+	public void logQuery(String sql, List<DbAttribute> attrs, List<?> params, long time) {
+	}
 	
-    public void logUpdateCount(int count) {
-    }
-
-    public void logBeginTransaction(String transactionLabel) {
-    }
-
-    public void logCommitTransaction(String transactionLabel) {
-    }
-
-    public void logRollbackTransaction(String transactionLabel) {
-    }
-
-    public void logQueryError(Throwable th) {
-    }
-
-    public boolean isLoggable() {
-        return false;
-    }
+	@Override
+	public void logQuery(String sql, ParameterBinding[] bindings, long translatedIn) {		
+	}
+
+	@Override
+	@Deprecated
+	public void logQueryParameters(String label, List<DbAttribute> attrs, List<Object> parameters, boolean isInserting) {
+	}
+
+	@Override
+	public void logQueryParameters(String label, ParameterBinding[] bindings) {
+	}
+
+	@Override
+	public void logSelectCount(int count, long time) {
+	}
+
+	@Override
+	public void logSelectCount(int count, long time, String sql) {
+	}
+
+	@Override
+	public void logUpdateCount(int count) {
+	}
+
+	@Override
+	public void logBeginTransaction(String transactionLabel) {
+	}
+
+	@Override
+	public void logCommitTransaction(String transactionLabel) {
+	}
+
+	@Override
+	public void logRollbackTransaction(String transactionLabel) {
+	}
+
+	@Override
+	public void logQueryError(Throwable th) {
+	}
+
+	@Override
+	public boolean isLoggable() {
+		return false;
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/merge/DefaultValueForNullProvider.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/merge/DefaultValueForNullProvider.java b/cayenne-server/src/main/java/org/apache/cayenne/merge/DefaultValueForNullProvider.java
index ae63f53..6db83c0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/merge/DefaultValueForNullProvider.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/merge/DefaultValueForNullProvider.java
@@ -23,25 +23,25 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import org.apache.cayenne.access.jdbc.ParameterBinding;
+import org.apache.cayenne.access.jdbc.SQLParameterBinding;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 
 public class DefaultValueForNullProvider implements ValueForNullProvider {
 
-    private Map<String, ParameterBinding> values = new HashMap<String, ParameterBinding>();
+    private Map<String, SQLParameterBinding> values = new HashMap<String, SQLParameterBinding>();
 
     public void set(DbEntity entity, DbAttribute column, Object value, int type) {
-        values.put(createKey(entity, column), new ParameterBinding(value, type, column
+        values.put(createKey(entity, column), new SQLParameterBinding(value, type, column
                 .getAttributePrecision()));
     }
 
-    protected ParameterBinding get(DbEntity entity, DbAttribute column) {
+    protected SQLParameterBinding get(DbEntity entity, DbAttribute column) {
         return values.get(createKey(entity, column));
     }
 
     public List<String> createSql(DbEntity entity, DbAttribute column) {
-        ParameterBinding value = get(entity, column);
+        SQLParameterBinding value = get(entity, column);
         if (value == null) {
             return Collections.emptyList();
         }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
index 43e807a..8529a77 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
@@ -24,7 +24,7 @@ import java.io.Writer;
 import java.util.Collection;
 import java.util.Iterator;
 
-import org.apache.cayenne.access.jdbc.ParameterBinding;
+import org.apache.cayenne.access.jdbc.SQLParameterBinding;
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.util.ConversionUtil;
 import org.apache.velocity.context.InternalContextAdapter;
@@ -81,7 +81,7 @@ public class BindDirective extends Directive {
 
 	/**
 	 * Extracts the value of the object property to render and passes control to
-	 * {@link #render(InternalContextAdapter, Writer, ParameterBinding)} to do
+	 * {@link #render(InternalContextAdapter, Writer, SQLParameterBinding)} to do
 	 * the actual rendering.
 	 */
 	@Override
@@ -130,10 +130,10 @@ public class BindDirective extends Directive {
 					+ ") at line " + node.getLine() + ", column " + node.getColumn());
 		}
 
-		render(context, writer, new ParameterBinding(value, jdbcType, scale));
+		render(context, writer, new SQLParameterBinding(value, jdbcType, scale));
 	}
 
-	protected void render(InternalContextAdapter context, Writer writer, ParameterBinding binding) throws IOException {
+	protected void render(InternalContextAdapter context, Writer writer, SQLParameterBinding binding) throws IOException {
 
 		bind(context, binding);
 		writer.write('?');
@@ -146,7 +146,7 @@ public class BindDirective extends Directive {
 	/**
 	 * Adds value to the list of bindings in the context.
 	 */
-	protected void bind(InternalContextAdapter context, ParameterBinding binding) {
+	protected void bind(InternalContextAdapter context, SQLParameterBinding binding) {
 
 		Collection bindings = (Collection) context.getInternalUserContext().get(
 				VelocitySQLTemplateProcessor.BINDINGS_LIST_KEY);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindEqualDirective.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindEqualDirective.java b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindEqualDirective.java
index b773cf5..f109b82 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindEqualDirective.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindEqualDirective.java
@@ -22,7 +22,7 @@ package org.apache.cayenne.velocity;
 import java.io.IOException;
 import java.io.Writer;
 
-import org.apache.cayenne.access.jdbc.ParameterBinding;
+import org.apache.cayenne.access.jdbc.SQLParameterBinding;
 import org.apache.velocity.context.InternalContextAdapter;
 
 /**
@@ -43,7 +43,7 @@ public class BindEqualDirective extends BindDirective {
     protected void render(
         InternalContextAdapter context,
         Writer writer,
-        ParameterBinding binding)
+        SQLParameterBinding binding)
         throws IOException {
 
         if (binding.getValue() != null) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java
index a49a8d6..3b17732 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java
@@ -22,7 +22,7 @@ package org.apache.cayenne.velocity;
 import java.io.IOException;
 import java.io.Writer;
 
-import org.apache.cayenne.access.jdbc.ParameterBinding;
+import org.apache.cayenne.access.jdbc.SQLParameterBinding;
 import org.apache.velocity.context.InternalContextAdapter;
 
 /**
@@ -43,7 +43,7 @@ public class BindNotEqualDirective extends BindDirective {
     protected void render(
             InternalContextAdapter context,
             Writer writer,
-            ParameterBinding binding) throws IOException {
+            SQLParameterBinding binding) throws IOException {
 
         if (binding.getValue() != null) {
             bind(context, binding);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java
index ed1f7d0..35785d3 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java
@@ -27,7 +27,7 @@ import java.util.Map;
 
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
-import org.apache.cayenne.access.jdbc.ParameterBinding;
+import org.apache.cayenne.access.jdbc.SQLParameterBinding;
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.velocity.context.InternalContextAdapter;
 import org.apache.velocity.exception.MethodInvocationException;
@@ -94,7 +94,7 @@ public class BindObjectEqualDirective extends BindDirective {
 
             renderColumn(context, writer, sqlColumnsArray[i], i);
             writer.write(' ');
-            render(context, writer, new ParameterBinding(value, jdbcType, -1));
+            render(context, writer, new SQLParameterBinding(value, jdbcType, -1));
         }
 
         return true;
@@ -151,7 +151,7 @@ public class BindObjectEqualDirective extends BindDirective {
     protected void render(
             InternalContextAdapter context,
             Writer writer,
-            ParameterBinding binding) throws IOException {
+            SQLParameterBinding binding) throws IOException {
 
         if (binding.getValue() != null) {
             bind(context, binding);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java
index 0b610ab..bf76993 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java
@@ -21,7 +21,7 @@ package org.apache.cayenne.velocity;
 import java.io.IOException;
 import java.io.Writer;
 
-import org.apache.cayenne.access.jdbc.ParameterBinding;
+import org.apache.cayenne.access.jdbc.SQLParameterBinding;
 import org.apache.velocity.context.InternalContextAdapter;
 
 /**
@@ -57,7 +57,7 @@ public class BindObjectNotEqualDirective extends BindObjectEqualDirective {
     protected void render(
             InternalContextAdapter context,
             Writer writer,
-            ParameterBinding binding) throws IOException {
+            SQLParameterBinding binding) throws IOException {
 
         if (binding.getValue() != null) {
             bind(context, binding);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
index 6246821..2e6ab40 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
@@ -28,7 +28,7 @@ import java.util.Map;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.jdbc.ParameterBinding;
+import org.apache.cayenne.access.jdbc.SQLParameterBinding;
 import org.apache.cayenne.access.jdbc.SQLStatement;
 import org.apache.cayenne.access.jdbc.SQLTemplateProcessor;
 import org.apache.cayenne.exp.ExpressionException;
@@ -152,7 +152,7 @@ public class VelocitySQLTemplateProcessor implements SQLTemplateProcessor {
 	}
 
 	SQLStatement processTemplate(String template, SimpleNode parsedTemplate, Map<String, Object> parameters) {
-		List<ParameterBinding> bindings = new ArrayList<ParameterBinding>();
+		List<SQLParameterBinding> bindings = new ArrayList<SQLParameterBinding>();
 		List<ColumnDescriptor> results = new ArrayList<ColumnDescriptor>();
 		parameters.put(BINDINGS_LIST_KEY, bindings);
 		parameters.put(RESULT_COLUMNS_LIST_KEY, results);
@@ -165,7 +165,7 @@ public class VelocitySQLTemplateProcessor implements SQLTemplateProcessor {
 			throw new CayenneRuntimeException("Error processing Velocity template", e);
 		}
 
-		ParameterBinding[] bindingsArray = new ParameterBinding[bindings.size()];
+		SQLParameterBinding[] bindingsArray = new SQLParameterBinding[bindings.size()];
 		bindings.toArray(bindingsArray);
 
 		ColumnDescriptor[] resultsArray = new ColumnDescriptor[results.size()];

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/SQLStatementTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/SQLStatementTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/SQLStatementTest.java
index 88791b5..e0cc575 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/SQLStatementTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/SQLStatementTest.java
@@ -31,7 +31,7 @@ public class SQLStatementTest {
 
     @Test
     public void testConstructor() throws Exception {
-        ParameterBinding[] bindings = new ParameterBinding[0];
+        SQLParameterBinding[] bindings = new SQLParameterBinding[0];
         SQLStatement statement = new SQLStatement("abc", bindings);
         assertEquals("abc", statement.getSql());
         assertSame(bindings, statement.getBindings());

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java
index 439fb20..0fdc48b 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java
@@ -19,6 +19,7 @@
 
 package org.apache.cayenne.access.translator.batch;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.JdbcAdapter;
 import org.apache.cayenne.di.AdhocObjectFactory;
@@ -54,13 +55,13 @@ public class DefaultBatchTranslatorIT extends ServerCase {
             }
 
             @Override
-            protected BatchParameterBinding[] createBindings() {
-                return new BatchParameterBinding[0];
+            protected ParameterBinding[] createBindings() {
+                return new ParameterBinding[0];
             }
 
             @Override
-            protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
-                return new BatchParameterBinding[0];
+            protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+                return new ParameterBinding[0];
             }
         };
 
@@ -80,13 +81,13 @@ public class DefaultBatchTranslatorIT extends ServerCase {
             }
 
             @Override
-            protected BatchParameterBinding[] createBindings() {
-                return new BatchParameterBinding[0];
+            protected ParameterBinding[] createBindings() {
+                return new ParameterBinding[0];
             }
 
             @Override
-            protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
-                return new BatchParameterBinding[0];
+            protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+                return new ParameterBinding[0];
             }
         };
 
@@ -115,13 +116,13 @@ public class DefaultBatchTranslatorIT extends ServerCase {
             }
 
             @Override
-            protected BatchParameterBinding[] createBindings() {
-                return new BatchParameterBinding[0];
+            protected ParameterBinding[] createBindings() {
+                return new ParameterBinding[0];
             }
 
             @Override
-            protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
-                return new BatchParameterBinding[0];
+            protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+                return new ParameterBinding[0];
             }
         };
 


[6/7] cayenne git commit: Refactoring SelectTranslator for better extensibility

Posted by aa...@apache.org.
Refactoring SelectTranslator for better extensibility

    * Removing checked exceptions from translator API... we no longer pretend to write to Appendable, and
      will write to StringBuilder instead
    * unifying ParameterBinding objects between query types
    * fixing a bug in SelecTranslator - iterators will not work when supress distinct is in effect
    * removing Connection from translators; SQLAction should take care of all connection ops


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/81f4fb50
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/81f4fb50
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/81f4fb50

Branch: refs/heads/master
Commit: 81f4fb50b75895fee1c0a1b73b173ca97634abfa
Parents: a6b55f7
Author: aadamchik <aa...@apache.org>
Authored: Tue Apr 28 12:29:07 2015 +0200
Committer: aadamchik <aa...@apache.org>
Committed: Tue Apr 28 12:43:55 2015 +0200

----------------------------------------------------------------------
 .../CryptoBatchTranslatorFactoryDecorator.java  |   8 +-
 .../crypto/transformer/BindingsTransformer.java |   4 +-
 .../transformer/DefaultBindingsTransformer.java |   6 +-
 .../transformer/DefaultTransformerFactory.java  |   6 +-
 .../crypto/transformer/TransformerFactory.java  |   4 +-
 .../apache/cayenne/access/jdbc/BatchAction.java |  10 +-
 .../cayenne/access/jdbc/ParameterBinding.java   |  68 --
 .../access/jdbc/SQLParameterBinding.java        |  62 ++
 .../cayenne/access/jdbc/SQLStatement.java       |  10 +-
 .../cayenne/access/jdbc/SQLTemplateAction.java  |   2 +-
 .../cayenne/access/jdbc/SelectAction.java       | 344 ++++---
 .../access/translator/ParameterBinding.java     |  81 ++
 .../translator/batch/BatchParameterBinding.java |  79 --
 .../translator/batch/BatchTranslator.java       |   5 +-
 .../batch/DefaultBatchTranslator.java           |  11 +-
 .../translator/batch/DeleteBatchTranslator.java |  11 +-
 .../translator/batch/InsertBatchTranslator.java |  11 +-
 .../batch/SoftDeleteBatchTranslator.java        |  13 +-
 .../translator/batch/UpdateBatchTranslator.java |  11 +-
 .../select/DefaultSelectTranslator.java         | 630 ++++++++++++
 .../access/translator/select/JoinStack.java     | 349 ++++---
 .../translator/select/OrderingTranslator.java   | 166 ++--
 .../translator/select/QualifierTranslator.java  | 949 +++++++++----------
 .../translator/select/QueryAssembler.java       | 302 +++---
 .../translator/select/QueryAssemblerHelper.java | 885 +++++++++--------
 .../translator/select/SelectTranslator.java     | 631 +-----------
 .../select/TrimmingQualifierTranslator.java     | 113 ++-
 .../cayenne/dba/db2/DB2QualifierTranslator.java | 172 ++--
 .../dba/derby/DerbyQualifierTranslator.java     |  59 +-
 .../firebird/FirebirdQualifierTranslator.java   |  30 +-
 .../dba/frontbase/FrontBaseActionBuilder.java   |   6 +-
 .../frontbase/FrontBaseSelectTranslator.java    |  41 +-
 .../cayenne/dba/hsqldb/HSQLSelectAction.java    |  24 +-
 .../dba/hsqldb/HSQLSelectTranslator.java        |  60 +-
 .../cayenne/dba/ingres/IngresSelectAction.java  |   6 +-
 .../dba/ingres/IngresSelectTranslator.java      |  13 +-
 .../cayenne/dba/mysql/MySQLSelectAction.java    |  24 +-
 .../dba/mysql/MySQLSelectTranslator.java        |  61 +-
 .../dba/openbase/OpenBaseActionBuilder.java     |  26 +-
 .../cayenne/dba/openbase/OpenBaseJoinStack.java | 158 ++-
 .../openbase/OpenBaseQualifierTranslator.java   | 262 +++--
 .../dba/openbase/OpenBaseSelectTranslator.java  |  45 +-
 .../cayenne/dba/oracle/Oracle8JoinStack.java    | 146 ++-
 .../dba/oracle/Oracle8LOBBatchAction.java       |   8 +-
 .../dba/oracle/Oracle8LOBBatchTranslator.java   |  12 +-
 .../dba/oracle/Oracle8QualifierTranslator.java  |  38 +-
 .../cayenne/dba/oracle/Oracle8SelectAction.java |  16 +-
 .../dba/oracle/Oracle8SelectTranslator.java     |  35 +-
 .../dba/oracle/OracleQualifierTranslator.java   | 104 +-
 .../cayenne/dba/oracle/OracleSelectAction.java  |  24 +-
 .../dba/oracle/OracleSelectTranslator.java      |  89 +-
 .../postgres/PostgresQualifierTranslator.java   | 188 ++--
 .../dba/postgres/PostgresSelectAction.java      |  24 +-
 .../dba/postgres/PostgresSelectTranslator.java  |  15 +-
 .../dba/sqlserver/SQLServerSelectAction.java    |   6 +-
 .../sqlserver/SQLServerSelectTranslator.java    |  65 +-
 .../SQLServerTrimmingQualifierTranslator.java   | 164 ++--
 .../cayenne/log/CommonsJdbcEventLogger.java     | 819 ++++++++--------
 .../org/apache/cayenne/log/JdbcEventLogger.java |  21 +-
 .../apache/cayenne/log/NoopJdbcEventLogger.java | 142 +--
 .../merge/DefaultValueForNullProvider.java      |  10 +-
 .../apache/cayenne/velocity/BindDirective.java  |  10 +-
 .../cayenne/velocity/BindEqualDirective.java    |   4 +-
 .../cayenne/velocity/BindNotEqualDirective.java |   4 +-
 .../velocity/BindObjectEqualDirective.java      |   6 +-
 .../velocity/BindObjectNotEqualDirective.java   |   4 +-
 .../velocity/VelocitySQLTemplateProcessor.java  |   6 +-
 .../cayenne/access/jdbc/SQLStatementTest.java   |   2 +-
 .../batch/DefaultBatchTranslatorIT.java         |  25 +-
 .../select/DefaultSelectTranslatorIT.java       | 787 +++++++++++++++
 .../translator/select/OrderingTranslatorIT.java | 216 ++---
 .../select/QualifierTranslatorIT.java           | 197 ++--
 .../translator/select/QueryAssemblerIT.java     |  62 +-
 .../translator/select/SelectTranslatorIT.java   | 794 ----------------
 .../translator/select/TstQueryAssembler.java    |  64 +-
 .../apache/cayenne/merge/ValueForNullIT.java    |   6 +-
 .../VelocitySQLTemplateProcessorTest.java       |  10 +-
 docs/doc/src/main/resources/RELEASE-NOTES.txt   |   1 +
 78 files changed, 4870 insertions(+), 5012 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java
index cbb19f2..e86e564 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java
@@ -18,7 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.batch;
 
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.access.translator.batch.BatchTranslatorFactory;
 import org.apache.cayenne.crypto.transformer.BindingsTransformer;
@@ -65,16 +65,16 @@ public class CryptoBatchTranslatorFactoryDecorator implements BatchTranslatorFac
             }
 
             @Override
-            public BatchParameterBinding[] getBindings() {
+            public ParameterBinding[] getBindings() {
                 return delegateTranslator.getBindings();
             }
 
             @Override
-            public BatchParameterBinding[] updateBindings(BatchQueryRow row) {
+            public ParameterBinding[] updateBindings(BatchQueryRow row) {
 
                 ensureEncryptorCompiled();
 
-                BatchParameterBinding[] bindings = delegateTranslator.updateBindings(row);
+                ParameterBinding[] bindings = delegateTranslator.updateBindings(row);
 
                 if (encryptor != null) {
                     encryptor.transform(bindings);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
index 2d782ee..c6f9577 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
@@ -18,12 +18,12 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.transformer;
 
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 
 /**
  * @since 4.0
  */
 public interface BindingsTransformer {
 
-    void transform(BatchParameterBinding[] bindings);
+    void transform(ParameterBinding[] bindings);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
index 0ef8caf..db31eea 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
@@ -18,7 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.transformer;
 
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.crypto.transformer.bytes.BytesEncryptor;
 import org.apache.cayenne.crypto.transformer.value.ValueEncryptor;
 
@@ -38,12 +38,12 @@ public class DefaultBindingsTransformer implements BindingsTransformer {
     }
 
     @Override
-    public void transform(BatchParameterBinding[] bindings) {
+    public void transform(ParameterBinding[] bindings) {
 
         int len = positions.length;
 
         for (int i = 0; i < len; i++) {
-            BatchParameterBinding b = bindings[positions[i]];
+            ParameterBinding b = bindings[positions[i]];
             Object transformed = transformers[i].encrypt(encryptor, b.getValue());
             b.setValue(transformed);
         }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
index 3b3e73a..178a2f8 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
@@ -23,7 +23,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.crypto.map.ColumnMapper;
 import org.apache.cayenne.crypto.transformer.bytes.BytesTransformerFactory;
 import org.apache.cayenne.crypto.transformer.value.ValueDecryptor;
@@ -90,7 +90,7 @@ public class DefaultTransformerFactory implements TransformerFactory {
     }
 
     @Override
-    public BindingsTransformer encryptor(BatchParameterBinding[] bindings) {
+    public BindingsTransformer encryptor(ParameterBinding[] bindings) {
         int len = bindings.length;
         List<Integer> cryptoColumns = null;
 
@@ -115,7 +115,7 @@ public class DefaultTransformerFactory implements TransformerFactory {
 
             for (int i = 0; i < dlen; i++) {
                 int pos = cryptoColumns.get(i);
-                BatchParameterBinding b = bindings[pos];
+                ParameterBinding b = bindings[pos];
                 positions[i] = pos;
                 transformers[i] = transformerFactory.encryptor(b.getAttribute());
             }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
index 0d6ed2b..9bc28a8 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
@@ -19,7 +19,7 @@
 package org.apache.cayenne.crypto.transformer;
 
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 
 /**
  * A factory that creates encryption transformers used for processing batch
@@ -29,7 +29,7 @@ import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
  */
 public interface TransformerFactory {
 
-    BindingsTransformer encryptor(BatchParameterBinding[] bindings);
+    BindingsTransformer encryptor(ParameterBinding[] bindings);
 
     MapTransformer decryptor(ColumnDescriptor[] columns, Object sampleRow);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
index 7efe4e3..ca3d1ef 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
@@ -33,7 +33,7 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.OptimisticLockException;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.TypesMapping;
@@ -53,10 +53,10 @@ public class BatchAction extends BaseSQLAction {
     protected BatchQuery query;
     protected RowDescriptor keyRowDescriptor;
 
-    private static void bind(DbAdapter adapter, PreparedStatement statement, BatchParameterBinding[] bindings)
+    private static void bind(DbAdapter adapter, PreparedStatement statement, ParameterBinding[] bindings)
             throws SQLException, Exception {
 
-        for (BatchParameterBinding b : bindings) {
+        for (ParameterBinding b : bindings) {
             if (!b.isExcluded()) {
                 adapter.bindParameter(statement, b.getValue(), b.getStatementPosition(), b.getAttribute().getType(), b
                         .getAttribute().getScale());
@@ -114,7 +114,7 @@ public class BatchAction extends BaseSQLAction {
         try {
             for (BatchQueryRow row : query.getRows()) {
 
-                BatchParameterBinding[] bindings = translator.updateBindings(row);
+                ParameterBinding[] bindings = translator.updateBindings(row);
                 logger.logQueryParameters("batch bind", bindings);
                 bind(adapter, statement, bindings);
 
@@ -171,7 +171,7 @@ public class BatchAction extends BaseSQLAction {
         try {
             for (BatchQueryRow row : query.getRows()) {
 
-                BatchParameterBinding[] bindings = translator.updateBindings(row);
+                ParameterBinding[] bindings = translator.updateBindings(row);
                 logger.logQueryParameters("bind", bindings);
 
                 bind(adapter, statement, bindings);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ParameterBinding.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ParameterBinding.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ParameterBinding.java
deleted file mode 100644
index 8eb5f2b..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ParameterBinding.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-
-package org.apache.cayenne.access.jdbc;
-
-/**
- * Describes PreparedStatement parameter binding.
- * 
- * @since 1.1
- */
-public class ParameterBinding {
-
-    protected int jdbcType;
-    protected int scale;
-    protected Object value;
-
-    public ParameterBinding(Object value, int jdbcType, int scale) {
-        this.value = value;
-        this.jdbcType = jdbcType;
-        this.scale = scale;
-    }
-
-    public int getJdbcType() {
-        return jdbcType;
-    }
-
-    /**
-     * @since 3.0
-     */
-    public int getScale() {
-        return scale;
-    }
-
-    public Object getValue() {
-        return value;
-    }
-
-    public void setJdbcType(int i) {
-        jdbcType = i;
-    }
-
-    /**
-     * @since 3.0
-     */
-    public void setScale(int i) {
-        scale = i;
-    }
-
-    public void setValue(Object object) {
-        value = object;
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLParameterBinding.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLParameterBinding.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLParameterBinding.java
new file mode 100644
index 0000000..f2b2369
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLParameterBinding.java
@@ -0,0 +1,62 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.jdbc;
+
+/**
+ * Describes PreparedStatement parameter binding.
+ * 
+ * @since 4.0
+ */
+public class SQLParameterBinding {
+
+	protected int jdbcType;
+	protected int scale;
+	protected Object value;
+
+	public SQLParameterBinding(Object value, int jdbcType, int scale) {
+		this.value = value;
+		this.jdbcType = jdbcType;
+		this.scale = scale;
+	}
+
+	public int getJdbcType() {
+		return jdbcType;
+	}
+
+	public int getScale() {
+		return scale;
+	}
+
+	public Object getValue() {
+		return value;
+	}
+
+	public void setJdbcType(int i) {
+		jdbcType = i;
+	}
+
+	public void setScale(int i) {
+		scale = i;
+	}
+
+	public void setValue(Object object) {
+		value = object;
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLStatement.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLStatement.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLStatement.java
index b13050a..33d385d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLStatement.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLStatement.java
@@ -28,13 +28,13 @@ package org.apache.cayenne.access.jdbc;
 public class SQLStatement {
 
     protected String sql;
-    protected ParameterBinding[] bindings;
+    protected SQLParameterBinding[] bindings;
     protected ColumnDescriptor[] resultColumns;
 
     public SQLStatement() {
     }
 
-    public SQLStatement(String sql, ParameterBinding[] bindings) {
+    public SQLStatement(String sql, SQLParameterBinding[] bindings) {
         this(sql, null, bindings);
     }
 
@@ -42,7 +42,7 @@ public class SQLStatement {
      * @since 1.2
      */
     public SQLStatement(String sql, ColumnDescriptor[] resultColumns,
-            ParameterBinding[] bindings) {
+            SQLParameterBinding[] bindings) {
 
         setSql(sql);
         setBindings(bindings);
@@ -63,7 +63,7 @@ public class SQLStatement {
         resultColumns = descriptors;
     }
 
-    public ParameterBinding[] getBindings() {
+    public SQLParameterBinding[] getBindings() {
         return bindings;
     }
 
@@ -71,7 +71,7 @@ public class SQLStatement {
         return sql;
     }
 
-    public void setBindings(ParameterBinding[] bindings) {
+    public void setBindings(SQLParameterBinding[] bindings) {
         this.bindings = bindings;
     }
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
index 9c76fe9..97a1067 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
@@ -343,7 +343,7 @@ public class SQLTemplateAction implements SQLAction {
 	/**
 	 * Binds parameters to the PreparedStatement.
 	 */
-	protected void bind(PreparedStatement preparedStatement, ParameterBinding[] bindings) throws SQLException,
+	protected void bind(PreparedStatement preparedStatement, SQLParameterBinding[] bindings) throws SQLException,
 			Exception {
 		// bind parameters
 		if (bindings.length > 0) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
index f1e6419..00785c9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
@@ -30,7 +30,11 @@ import org.apache.cayenne.ResultIterator;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.log.JdbcEventLogger;
 import org.apache.cayenne.query.PrefetchProcessor;
 import org.apache.cayenne.query.PrefetchTreeNode;
 import org.apache.cayenne.query.QueryMetadata;
@@ -43,149 +47,199 @@ import org.apache.cayenne.query.SelectQuery;
  */
 public class SelectAction extends BaseSQLAction {
 
-    protected SelectQuery<?> query;
-
-    /**
-     * @since 4.0
-     */
-    public SelectAction(SelectQuery<?> query, DataNode dataNode) {
-        super(dataNode);
-        this.query = query;
-    }
-
-    protected SelectTranslator createTranslator(Connection connection) {
-        return new SelectTranslator(query, dataNode, connection);
-    }
-
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    @Override
-    public void performAction(Connection connection, OperationObserver observer) throws SQLException, Exception {
-
-        final long t1 = System.currentTimeMillis();
-
-        final SelectTranslator translator = createTranslator(connection);
-        PreparedStatement prepStmt = translator.createStatement();
-
-        // TODO: ugly... 'createSqlString' is already called inside
-        // 'createStatement', but calling it here again to store for logging
-        // purposes
-        final String sqlString = translator.createSqlString();
-
-        ResultSet rs;
-
-        // need to run in try-catch block to close statement properly if
-        // exception happens
-        try {
-            rs = prepStmt.executeQuery();
-        } catch (Exception ex) {
-            prepStmt.close();
-            throw ex;
-        }
-        QueryMetadata md = query.getMetaData(dataNode.getEntityResolver());
-        RowDescriptor descriptor = new RowDescriptorBuilder().setColumns(translator.getResultColumns()).getDescriptor(
-                dataNode.getAdapter().getExtendedTypes());
-        
-        RowReader<?> rowReader = dataNode.rowReader(descriptor, md, translator.getAttributeOverrides());
-
-        JDBCResultIterator workerIterator = new JDBCResultIterator(prepStmt, rs, rowReader);
-
-        ResultIterator it = workerIterator;
-
-        if (observer.isIteratedResult()) {
-            it = new ConnectionAwareResultIterator(it, connection) {
-                @Override
-                protected void doClose() {
-                    dataNode.getJdbcEventLogger().logSelectCount(rowCounter, System.currentTimeMillis() - t1, sqlString);
-                    super.doClose();
-                }
-            };
-        }
-
-        // wrap result iterator if distinct has to be suppressed
-        if (translator.isSuppressingDistinct()) {
-
-            // a joint prefetch warrants full row compare
-
-            final boolean[] compareFullRows = new boolean[1];
-
-            final PrefetchTreeNode rootPrefetch = md.getPrefetchTree();
-
-            if (rootPrefetch != null) {
-                rootPrefetch.traverse(new PrefetchProcessor() {
-
-                    public void finishPrefetch(PrefetchTreeNode node) {
-                    }
-
-                    public boolean startDisjointPrefetch(PrefetchTreeNode node) {
-                        // continue to children only if we are at root
-                        return rootPrefetch == node;
-                    }
-
-                    public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
-                        // continue to children only if we are at root
-                        return rootPrefetch == node;
-                    }
-
-                    public boolean startUnknownPrefetch(PrefetchTreeNode node) {
-                        // continue to children only if we are at root
-                        return rootPrefetch == node;
-                    }
-
-                    public boolean startJointPrefetch(PrefetchTreeNode node) {
-                        if (rootPrefetch != node) {
-                            compareFullRows[0] = true;
-                            return false;
-                        }
-
-                        return true;
-                    }
-
-                    public boolean startPhantomPrefetch(PrefetchTreeNode node) {
-                        return true;
-                    }
-                });
-            }
-
-            it = new DistinctResultIterator(workerIterator, translator.getRootDbEntity(), compareFullRows[0]);
-        }
-
-        // wrap iterator in a fetch limit checker ... there are a few cases when
-        // in-memory
-        // fetch limit is a noop, however in a general case this is needed, as
-        // the SQL
-        // result count does not directly correspond to the number of objects
-        // returned
-        // from Cayenne.
-
-        int fetchLimit = query.getFetchLimit();
-        int offset = translator.isSuppressingDistinct() ? query.getFetchOffset() : getInMemoryOffset(query
-                .getFetchOffset());
-        if (fetchLimit > 0 || offset > 0) {
-            it = new LimitResultIterator(it, offset, fetchLimit);
-        }
-
-        // TODO: Should do something about closing ResultSet and
-        // PreparedStatement in this
-        // method, instead of relying on DefaultResultIterator to do that later
-
-        if (observer.isIteratedResult()) {
-            try {
-                observer.nextRows(translator.getQuery(), it);
-            } catch (Exception ex) {
-                it.close();
-                throw ex;
-            }
-        } else {
-            List<DataRow> resultRows;
-            try {
-                resultRows = it.allRows();
-            } finally {
-                it.close();
-            }
-
-            dataNode.getJdbcEventLogger().logSelectCount(resultRows.size(), System.currentTimeMillis() - t1, sqlString);
-
-            observer.nextRows(query, resultRows);
-        }
-    }
+	private static void bind(DbAdapter adapter, PreparedStatement statement, ParameterBinding[] bindings)
+			throws SQLException, Exception {
+
+		for (ParameterBinding b : bindings) {
+
+			if (b.isExcluded()) {
+				continue;
+			}
+
+			// null DbAttributes are a result of inferior qualifier
+			// processing (qualifier can't map parameters to DbAttributes
+			// and therefore only supports standard java types now) hence, a
+			// special moronic case here:
+			if (b.getAttribute() == null) {
+				statement.setObject(b.getStatementPosition(), b.getValue());
+			} else {
+				adapter.bindParameter(statement, b.getValue(), b.getStatementPosition(), b.getAttribute().getType(), b
+						.getAttribute().getScale());
+			}
+		}
+
+	}
+
+	protected SelectQuery<?> query;
+	protected QueryMetadata queryMetadata;
+
+	/**
+	 * @since 4.0
+	 */
+	public SelectAction(SelectQuery<?> query, DataNode dataNode) {
+		super(dataNode);
+		this.query = query;
+		this.queryMetadata = query.getMetaData(dataNode.getEntityResolver());
+	}
+
+	protected SelectTranslator createTranslator() {
+		return new DefaultSelectTranslator(query, dataNode.getAdapter(), dataNode.getEntityResolver());
+	}
+
+	@SuppressWarnings({ "unchecked", "rawtypes", "resource" })
+	@Override
+	public void performAction(Connection connection, OperationObserver observer) throws SQLException, Exception {
+
+		final long t1 = System.currentTimeMillis();
+
+		JdbcEventLogger logger = dataNode.getJdbcEventLogger();
+		SelectTranslator translator = createTranslator();
+		final String sql = translator.getSql();
+
+		ParameterBinding[] bindings = translator.getBindings();
+		PreparedStatement statement = connection.prepareStatement(sql);
+		bind(dataNode.getAdapter(), statement, bindings);
+
+		int fetchSize = queryMetadata.getStatementFetchSize();
+		if (fetchSize != 0) {
+			statement.setFetchSize(fetchSize);
+		}
+
+		logger.logQuery(sql, bindings, System.currentTimeMillis() - t1);
+
+		ResultSet rs;
+
+		// need to run in try-catch block to close statement properly if
+		// exception happens
+		try {
+			rs = statement.executeQuery();
+		} catch (Exception ex) {
+			statement.close();
+			throw ex;
+		}
+		RowDescriptor descriptor = new RowDescriptorBuilder().setColumns(translator.getResultColumns()).getDescriptor(
+				dataNode.getAdapter().getExtendedTypes());
+
+		RowReader<?> rowReader = dataNode.rowReader(descriptor, queryMetadata, translator.getAttributeOverrides());
+
+		ResultIterator it = new JDBCResultIterator(statement, rs, rowReader);
+		it = forIteratedResult(it, observer, connection, t1, sql);
+		it = forSuppressedDistinct(it, translator);
+		it = forFetchLimit(it, translator);
+
+		// TODO: Should do something about closing ResultSet and
+		// PreparedStatement in this method, instead of relying on
+		// DefaultResultIterator to do that later
+
+		if (observer.isIteratedResult()) {
+			try {
+				observer.nextRows(query, it);
+			} catch (Exception ex) {
+				it.close();
+				throw ex;
+			}
+		} else {
+			List<DataRow> resultRows;
+			try {
+				resultRows = it.allRows();
+			} finally {
+				it.close();
+			}
+
+			dataNode.getJdbcEventLogger().logSelectCount(resultRows.size(), System.currentTimeMillis() - t1, sql);
+
+			observer.nextRows(query, resultRows);
+		}
+	}
+
+	private <T> ResultIterator<T> forIteratedResult(ResultIterator<T> iterator, OperationObserver observer,
+			Connection connection, final long queryStartedAt, final String sql) {
+		if (!observer.isIteratedResult()) {
+			return iterator;
+		}
+
+		return new ConnectionAwareResultIterator<T>(iterator, connection) {
+			@Override
+			protected void doClose() {
+				dataNode.getJdbcEventLogger().logSelectCount(rowCounter, System.currentTimeMillis() - queryStartedAt,
+						sql);
+				super.doClose();
+			}
+		};
+	}
+
+	private <T> ResultIterator<T> forFetchLimit(ResultIterator<T> iterator, SelectTranslator translator) {
+		// wrap iterator in a fetch limit checker ... there are a few cases when
+		// in-memory fetch limit is a noop, however in a general case this is
+		// needed, as the SQL result count does not directly correspond to the
+		// number of objects returned from Cayenne.
+
+		int fetchLimit = query.getFetchLimit();
+		int offset = translator.isSuppressingDistinct() ? query.getFetchOffset() : getInMemoryOffset(query
+				.getFetchOffset());
+
+		if (fetchLimit > 0 || offset > 0) {
+			return new LimitResultIterator<T>(iterator, offset, fetchLimit);
+		} else {
+			return iterator;
+		}
+	}
+
+	private <T> ResultIterator<T> forSuppressedDistinct(ResultIterator<T> iterator, SelectTranslator translator) {
+		if (!translator.isSuppressingDistinct()) {
+			return iterator;
+		}
+
+		// wrap result iterator if distinct has to be suppressed
+
+		// a joint prefetch warrants full row compare
+
+		final boolean[] compareFullRows = new boolean[1];
+		final PrefetchTreeNode rootPrefetch = queryMetadata.getPrefetchTree();
+
+		if (rootPrefetch != null) {
+			rootPrefetch.traverse(new PrefetchProcessor() {
+
+				@Override
+				public void finishPrefetch(PrefetchTreeNode node) {
+				}
+
+				@Override
+				public boolean startDisjointPrefetch(PrefetchTreeNode node) {
+					// continue to children only if we are at root
+					return rootPrefetch == node;
+				}
+
+				@Override
+				public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
+					// continue to children only if we are at root
+					return rootPrefetch == node;
+				}
+
+				@Override
+				public boolean startUnknownPrefetch(PrefetchTreeNode node) {
+					// continue to children only if we are at root
+					return rootPrefetch == node;
+				}
+
+				@Override
+				public boolean startJointPrefetch(PrefetchTreeNode node) {
+					if (rootPrefetch != node) {
+						compareFullRows[0] = true;
+						return false;
+					}
+
+					return true;
+				}
+
+				@Override
+				public boolean startPhantomPrefetch(PrefetchTreeNode node) {
+					return true;
+				}
+			});
+		}
+
+		return new DistinctResultIterator<T>(iterator, queryMetadata.getDbEntity(), compareFullRows[0]);
+	}
+
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java
new file mode 100644
index 0000000..7a6a7e6
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java
@@ -0,0 +1,81 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.access.translator;
+
+import org.apache.cayenne.map.DbAttribute;
+
+/**
+ * Describes a PreparedStatement parameter binding mapped to a DbAttribute.
+ * 
+ * @since 4.0
+ */
+public class ParameterBinding {
+
+	static final int EXCLUDED_POSITION = -1;
+
+	private DbAttribute attribute;
+	private Object value;
+	private int statementPosition;
+
+	public ParameterBinding(DbAttribute attribute) {
+		this.attribute = attribute;
+		this.statementPosition = EXCLUDED_POSITION;
+	}
+
+	public DbAttribute getAttribute() {
+		return attribute;
+	}
+
+	public Object getValue() {
+		return value;
+	}
+
+	public void setValue(Object value) {
+		this.value = value;
+	}
+
+	public int getStatementPosition() {
+		return statementPosition;
+	}
+
+	public void setStatementPosition(int statementPosition) {
+		this.statementPosition = statementPosition;
+	}
+
+	public boolean isExcluded() {
+		return statementPosition == EXCLUDED_POSITION;
+	}
+
+	/**
+	 * Marks the binding object as excluded for the current iteration.
+	 */
+	public void exclude() {
+		this.statementPosition = EXCLUDED_POSITION;
+		this.value = null;
+	}
+
+	/**
+	 * Sets the value of the binding and initializes statement position var,
+	 * thus "including" this binding in the current iteration.
+	 */
+	public void include(int statementPosition, Object value) {
+		this.statementPosition = statementPosition;
+		this.value = value;
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchParameterBinding.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchParameterBinding.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchParameterBinding.java
deleted file mode 100644
index a37a145..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchParameterBinding.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-package org.apache.cayenne.access.translator.batch;
-
-import org.apache.cayenne.map.DbAttribute;
-
-/**
- * @since 4.0
- */
-public class BatchParameterBinding {
-
-    static final int EXCLUDED_POSITION = -1;
-
-    private DbAttribute attribute;
-    private Object value;
-    private int statementPosition;
-
-    public BatchParameterBinding(DbAttribute attribute) {
-        this.attribute = attribute;
-        this.statementPosition = EXCLUDED_POSITION;
-    }
-
-    public DbAttribute getAttribute() {
-        return attribute;
-    }
-
-    public Object getValue() {
-        return value;
-    }
-
-    public void setValue(Object value) {
-        this.value = value;
-    }
-
-    public int getStatementPosition() {
-        return statementPosition;
-    }
-
-    public void setStatementPosition(int statementPosition) {
-        this.statementPosition = statementPosition;
-    }
-
-    public boolean isExcluded() {
-        return statementPosition == EXCLUDED_POSITION;
-    }
-
-    /**
-     * Marks the binding object as excluded for the current iteration.
-     */
-    public void exclude() {
-        this.statementPosition = EXCLUDED_POSITION;
-        this.value = null;
-    }
-
-    /**
-     * Sets the value of the binding and initializes statement position var,
-     * thus "including" this binding in the current iteration.
-     */
-    public void include(int statementPosition, Object value) {
-        this.statementPosition = statementPosition;
-        this.value = value;
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java
index 989fcf1..04a4902 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java
@@ -19,6 +19,7 @@
 
 package org.apache.cayenne.access.translator.batch;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.query.BatchQueryRow;
 
 /**
@@ -36,7 +37,7 @@ public interface BatchTranslator {
     /**
      * Returns the widest possible array of bindings for this query.
      */
-    BatchParameterBinding[] getBindings();
+    ParameterBinding[] getBindings();
 
     /**
      * Updates internal bindings to be used with a given row, returning updated
@@ -46,5 +47,5 @@ public interface BatchTranslator {
      * parameter). Usually the returned array is actually the same object reused
      * for every iteration, only with changed object state.
      */
-    BatchParameterBinding[] updateBindings(BatchQueryRow row);
+    ParameterBinding[] updateBindings(BatchQueryRow row);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
index 82f945e..732ec64 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
@@ -20,6 +20,7 @@ package org.apache.cayenne.access.translator.batch;
 
 import java.sql.Types;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -39,7 +40,7 @@ public abstract class DefaultBatchTranslator implements BatchTranslator {
 
     protected boolean translated;
     protected String sql;
-    protected BatchParameterBinding[] bindings;
+    protected ParameterBinding[] bindings;
 
     public DefaultBatchTranslator(BatchQuery query, DbAdapter adapter, String trimFunction) {
         this.query = query;
@@ -66,22 +67,22 @@ public abstract class DefaultBatchTranslator implements BatchTranslator {
     }
 
     @Override
-    public BatchParameterBinding[] getBindings() {
+    public ParameterBinding[] getBindings() {
         ensureTranslated();
         return bindings;
     }
     
     @Override
-    public BatchParameterBinding[] updateBindings(BatchQueryRow row) {
+    public ParameterBinding[] updateBindings(BatchQueryRow row) {
         ensureTranslated();
         return doUpdateBindings(row);
     }
 
     protected abstract String createSql();
 
-    protected abstract BatchParameterBinding[] createBindings();
+    protected abstract ParameterBinding[] createBindings();
     
-    protected abstract BatchParameterBinding[] doUpdateBindings(BatchQueryRow row);
+    protected abstract ParameterBinding[] doUpdateBindings(BatchQueryRow row);
 
     /**
      * Appends the name of the column to the query buffer. Subclasses use this

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
index bf91404..e6636da 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
@@ -22,6 +22,7 @@ package org.apache.cayenne.access.translator.batch;
 import java.util.Iterator;
 import java.util.List;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -71,23 +72,23 @@ public class DeleteBatchTranslator extends DefaultBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] createBindings() {
+    protected ParameterBinding[] createBindings() {
         DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
         List<DbAttribute> attributes = deleteBatch.getDbAttributes();
         int len = attributes.size();
 
-        BatchParameterBinding[] bindings = new BatchParameterBinding[len];
+        ParameterBinding[] bindings = new ParameterBinding[len];
 
         for (int i = 0; i < len; i++) {
             DbAttribute a = attributes.get(i);
-            bindings[i] = new BatchParameterBinding(a);
+            bindings[i] = new ParameterBinding(a);
         }
 
         return bindings;
     }
 
     @Override
-    protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
 
         int len = bindings.length;
 
@@ -95,7 +96,7 @@ public class DeleteBatchTranslator extends DefaultBatchTranslator {
 
         for (int i = 0, j = 1; i < len; i++) {
 
-            BatchParameterBinding b = bindings[i];
+            ParameterBinding b = bindings[i];
 
             // skip null attributes... they are translated as "IS NULL"
             if (deleteBatch.isNull(b.getAttribute())) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
index 43ce612..3deb552 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.access.translator.batch;
 
 import java.util.List;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -81,15 +82,15 @@ public class InsertBatchTranslator extends DefaultBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] createBindings() {
+    protected ParameterBinding[] createBindings() {
         List<DbAttribute> attributes = query.getDbAttributes();
         int len = attributes.size();
 
-        BatchParameterBinding[] bindings = new BatchParameterBinding[len];
+        ParameterBinding[] bindings = new ParameterBinding[len];
 
         for (int i = 0; i < len; i++) {
             DbAttribute a = attributes.get(i);
-            bindings[i] = new BatchParameterBinding(a);
+            bindings[i] = new ParameterBinding(a);
 
             // include/exclude state depends on DbAttribute only and can be
             // precompiled here
@@ -106,12 +107,12 @@ public class InsertBatchTranslator extends DefaultBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
         int len = bindings.length;
 
         for (int i = 0, j = 1; i < len; i++) {
 
-            BatchParameterBinding b = bindings[i];
+            ParameterBinding b = bindings[i];
 
             // exclusions are permanent
             if (!b.isExcluded()) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
index 7311cc5..4e793e5 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.access.translator.batch;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -53,16 +54,16 @@ public class SoftDeleteBatchTranslator extends DeleteBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] createBindings() {
+    protected ParameterBinding[] createBindings() {
 
-        BatchParameterBinding[] superBindings = super.createBindings();
+        ParameterBinding[] superBindings = super.createBindings();
 
         int slen = superBindings.length;
 
-        BatchParameterBinding[] bindings = new BatchParameterBinding[slen + 1];
+        ParameterBinding[] bindings = new ParameterBinding[slen + 1];
 
         DbAttribute deleteAttribute = query.getDbEntity().getAttribute(deletedFieldName);
-        bindings[0] = new BatchParameterBinding(deleteAttribute);
+        bindings[0] = new ParameterBinding(deleteAttribute);
         bindings[0].include(1, true);
         
         System.arraycopy(superBindings, 0, bindings, 1, slen);
@@ -71,7 +72,7 @@ public class SoftDeleteBatchTranslator extends DeleteBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
         int len = bindings.length;
 
         DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
@@ -79,7 +80,7 @@ public class SoftDeleteBatchTranslator extends DeleteBatchTranslator {
         // skip position 0... Otherwise follow super algorithm
         for (int i = 1, j = 2; i < len; i++) {
 
-            BatchParameterBinding b = bindings[i];
+            ParameterBinding b = bindings[i];
 
             // skip null attributes... they are translated as "IS NULL"
             if (deleteBatch.isNull(b.getAttribute())) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
index 61a49b3..a44a373 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
@@ -22,6 +22,7 @@ package org.apache.cayenne.access.translator.batch;
 import java.util.Iterator;
 import java.util.List;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -78,7 +79,7 @@ public class UpdateBatchTranslator extends DefaultBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] createBindings() {
+    protected ParameterBinding[] createBindings() {
         UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
 
         List<DbAttribute> updatedDbAttributes = updateBatch.getUpdatedAttributes();
@@ -87,23 +88,23 @@ public class UpdateBatchTranslator extends DefaultBatchTranslator {
         int ul = updatedDbAttributes.size();
         int ql = qualifierAttributes.size();
 
-        BatchParameterBinding[] bindings = new BatchParameterBinding[ul + ql];
+        ParameterBinding[] bindings = new ParameterBinding[ul + ql];
 
         for (int i = 0; i < ul; i++) {
             DbAttribute a = updatedDbAttributes.get(i);
-            bindings[i] = new BatchParameterBinding(a);
+            bindings[i] = new ParameterBinding(a);
         }
 
         for (int i = 0; i < ql; i++) {
             DbAttribute a = qualifierAttributes.get(i);
-            bindings[ul + i] = new BatchParameterBinding(a);
+            bindings[ul + i] = new ParameterBinding(a);
         }
 
         return bindings;
     }
 
     @Override
-    protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
 
         UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
new file mode 100644
index 0000000..38b5763
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
@@ -0,0 +1,630 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.access.translator.select;
+
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.JoinType;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.map.PathComponent;
+import org.apache.cayenne.query.PrefetchSelectQuery;
+import org.apache.cayenne.query.PrefetchTreeNode;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.reflect.ArcProperty;
+import org.apache.cayenne.reflect.AttributeProperty;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.reflect.PropertyVisitor;
+import org.apache.cayenne.reflect.ToManyProperty;
+import org.apache.cayenne.reflect.ToOneProperty;
+import org.apache.cayenne.util.CayenneMapEntry;
+import org.apache.cayenne.util.EqualsBuilder;
+import org.apache.cayenne.util.HashCodeBuilder;
+
+/**
+ * @since 4.0
+ */
+public class DefaultSelectTranslator extends QueryAssembler implements SelectTranslator {
+
+	protected static final int[] UNSUPPORTED_DISTINCT_TYPES = { Types.BLOB, Types.CLOB, Types.NCLOB,
+			Types.LONGVARBINARY, Types.LONGVARCHAR, Types.LONGNVARCHAR };
+
+	protected static boolean isUnsupportedForDistinct(int type) {
+		for (int unsupportedDistinctType : UNSUPPORTED_DISTINCT_TYPES) {
+			if (unsupportedDistinctType == type) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	JoinStack joinStack;
+
+	public DefaultSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+		super(query, adapter, entityResolver);
+	}
+
+	protected JoinStack getJoinStack() {
+		if (joinStack == null) {
+			joinStack = createJoinStack();
+		}
+		return joinStack;
+	}
+
+	List<ColumnDescriptor> resultColumns;
+	Map<ObjAttribute, ColumnDescriptor> attributeOverrides;
+	Map<ColumnDescriptor, ObjAttribute> defaultAttributesByColumn;
+
+	boolean suppressingDistinct;
+
+	/**
+	 * If set to <code>true</code>, indicates that distinct select query is
+	 * required no matter what the original query settings where. This flag can
+	 * be set when joins are created using "to-many" relationships.
+	 */
+	boolean forcingDistinct;
+
+	protected JoinStack createJoinStack() {
+		return new JoinStack(getAdapter(), queryMetadata.getDataMap(), this);
+	}
+
+	@Override
+	protected void doTranslate() {
+
+		DataMap dataMap = queryMetadata.getDataMap();
+		JoinStack joins = getJoinStack();
+
+		QuotingStrategy strategy = getAdapter().getQuotingStrategy();
+		forcingDistinct = false;
+
+		// build column list
+		this.resultColumns = buildResultColumns();
+
+		// build qualifier
+		QualifierTranslator qualifierTranslator = adapter.getQualifierTranslator(this);
+		StringBuilder qualifierBuffer = qualifierTranslator.appendPart(new StringBuilder());
+
+		// build ORDER BY
+		OrderingTranslator orderingTranslator = new OrderingTranslator(this);
+		StringBuilder orderingBuffer = orderingTranslator.appendPart(new StringBuilder());
+
+		// assemble
+		StringBuilder queryBuf = new StringBuilder();
+		queryBuf.append("SELECT ");
+
+		// check if DISTINCT is appropriate
+		// side effect: "suppressingDistinct" flag may end up being flipped here
+		if (forcingDistinct || getSelectQuery().isDistinct()) {
+			suppressingDistinct = false;
+
+			for (ColumnDescriptor column : resultColumns) {
+				if (isUnsupportedForDistinct(column.getJdbcType())) {
+					suppressingDistinct = true;
+					break;
+				}
+			}
+
+			if (!suppressingDistinct) {
+				queryBuf.append(buildDistinctStatement() + " ");
+			}
+		}
+
+		// convert ColumnDescriptors to column names
+		List<String> selectColumnExpList = new ArrayList<String>();
+		for (ColumnDescriptor column : resultColumns) {
+			String fullName = strategy.quotedIdentifier(dataMap, column.getNamePrefix(), column.getName());
+			selectColumnExpList.add(fullName);
+		}
+
+		// append any column expressions used in the order by if this query
+		// uses the DISTINCT modifier
+		if (forcingDistinct || getSelectQuery().isDistinct()) {
+			List<String> orderByColumnList = orderingTranslator.getOrderByColumnList();
+			for (String orderByColumnExp : orderByColumnList) {
+				// Convert to ColumnDescriptors??
+				if (!selectColumnExpList.contains(orderByColumnExp)) {
+					selectColumnExpList.add(orderByColumnExp);
+				}
+			}
+		}
+
+		appendSelectColumns(queryBuf, selectColumnExpList);
+
+		// append from clause
+		queryBuf.append(" FROM ");
+
+		// append tables and joins
+		joins.appendRootWithQuoteSqlIdentifiers(queryBuf, getQueryMetadata().getDbEntity());
+
+		joins.appendJoins(queryBuf);
+		joins.appendQualifier(qualifierBuffer, qualifierBuffer.length() == 0);
+
+		// append qualifier
+		if (qualifierBuffer.length() > 0) {
+			queryBuf.append(" WHERE ");
+			queryBuf.append(qualifierBuffer);
+		}
+
+		// append prebuilt ordering
+		if (orderingBuffer.length() > 0) {
+			queryBuf.append(" ORDER BY ").append(orderingBuffer);
+		}
+
+		if (!isSuppressingDistinct()) {
+			appendLimitAndOffsetClauses(queryBuf);
+		}
+
+		this.sql = queryBuf.toString();
+	}
+
+	/**
+	 * Allows subclasses to insert their own dialect of DISTINCT statement to
+	 * improve performance.
+	 *
+	 * @return string representing the DISTINCT statement
+	 * @since 4.0
+	 */
+	protected String buildDistinctStatement() {
+		return "DISTINCT";
+	}
+
+	/**
+	 * @since 3.1
+	 */
+	protected void appendSelectColumns(StringBuilder buffer, List<String> selectColumnExpList) {
+
+		// append columns (unroll the loop's first element)
+		int columnCount = selectColumnExpList.size();
+		buffer.append(selectColumnExpList.get(0));
+
+		// assume there is at least 1 element
+		for (int i = 1; i < columnCount; i++) {
+			buffer.append(", ");
+			buffer.append(selectColumnExpList.get(i));
+		}
+	}
+
+	/**
+	 * Handles appending optional limit and offset clauses. This implementation
+	 * does nothing, deferring to subclasses to define the LIMIT/OFFSET clause
+	 * syntax.
+	 * 
+	 * @since 3.0
+	 */
+	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
+
+	}
+
+	@Override
+	public String getCurrentAlias() {
+		return getJoinStack().getCurrentAlias();
+	}
+
+	/**
+	 * Returns a list of ColumnDescriptors for the query columns.
+	 * 
+	 * @since 1.2
+	 */
+	public ColumnDescriptor[] getResultColumns() {
+		if (resultColumns == null || resultColumns.isEmpty()) {
+			return new ColumnDescriptor[0];
+		}
+
+		return resultColumns.toArray(new ColumnDescriptor[resultColumns.size()]);
+	}
+
+	/**
+	 * Returns a map of ColumnDescriptors keyed by ObjAttribute for columns that
+	 * may need to be reprocessed manually due to incompatible mappings along
+	 * the inheritance hierarchy.
+	 * 
+	 * @since 1.2
+	 */
+	public Map<ObjAttribute, ColumnDescriptor> getAttributeOverrides() {
+		if (attributeOverrides != null) {
+			return attributeOverrides;
+		} else {
+			return Collections.emptyMap();
+		}
+	}
+
+	/**
+	 * Returns true if SelectTranslator determined that a query requiring
+	 * DISTINCT can't be run with DISTINCT keyword for internal reasons. If this
+	 * method returns true, DataNode may need to do in-memory distinct
+	 * filtering.
+	 * 
+	 * @since 1.1
+	 */
+	public boolean isSuppressingDistinct() {
+		return suppressingDistinct;
+	}
+
+	private SelectQuery<?> getSelectQuery() {
+		return (SelectQuery<?>) getQuery();
+	}
+
+	protected List<ColumnDescriptor> buildResultColumns() {
+
+		this.defaultAttributesByColumn = new HashMap<ColumnDescriptor, ObjAttribute>();
+
+		List<ColumnDescriptor> columns = new ArrayList<ColumnDescriptor>();
+		SelectQuery<?> query = getSelectQuery();
+
+		if (query.getRoot() instanceof DbEntity) {
+			appendDbEntityColumns(columns, query);
+		} else if (getQueryMetadata().getPageSize() > 0) {
+			appendIdColumns(columns, query);
+		} else {
+			appendQueryColumns(columns, query);
+		}
+
+		return columns;
+	}
+
+	<T> List<ColumnDescriptor> appendDbEntityColumns(List<ColumnDescriptor> columns, SelectQuery<T> query) {
+
+		Set<ColumnTracker> attributes = new HashSet<ColumnTracker>();
+
+		DbEntity table = getQueryMetadata().getDbEntity();
+		for (DbAttribute dba : table.getAttributes()) {
+			appendColumn(columns, null, dba, attributes, null);
+		}
+
+		return columns;
+	}
+
+	/**
+	 * Appends columns needed for object SelectQuery to the provided columns
+	 * list.
+	 */
+	<T> List<ColumnDescriptor> appendQueryColumns(final List<ColumnDescriptor> columns, SelectQuery<T> query) {
+
+		final Set<ColumnTracker> attributes = new HashSet<ColumnTracker>();
+
+		// fetched attributes include attributes that are either:
+		//
+		// * class properties
+		// * PK
+		// * FK used in relationship
+		// * joined prefetch PK
+
+		ClassDescriptor descriptor = queryMetadata.getClassDescriptor();
+		ObjEntity oe = descriptor.getEntity();
+
+		PropertyVisitor visitor = new PropertyVisitor() {
+
+			public boolean visitAttribute(AttributeProperty property) {
+				ObjAttribute oa = property.getAttribute();
+
+				resetJoinStack();
+				Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator();
+				while (dbPathIterator.hasNext()) {
+					Object pathPart = dbPathIterator.next();
+
+					if (pathPart == null) {
+						throw new CayenneRuntimeException("ObjAttribute has no component: " + oa.getName());
+					} else if (pathPart instanceof DbRelationship) {
+						DbRelationship rel = (DbRelationship) pathPart;
+						dbRelationshipAdded(rel, JoinType.LEFT_OUTER, null);
+					} else if (pathPart instanceof DbAttribute) {
+						DbAttribute dbAttr = (DbAttribute) pathPart;
+
+						appendColumn(columns, oa, dbAttr, attributes, null);
+					}
+				}
+				return true;
+			}
+
+			public boolean visitToMany(ToManyProperty property) {
+				visitRelationship(property);
+				return true;
+			}
+
+			public boolean visitToOne(ToOneProperty property) {
+				visitRelationship(property);
+				return true;
+			}
+
+			private void visitRelationship(ArcProperty property) {
+				resetJoinStack();
+
+				ObjRelationship rel = property.getRelationship();
+				DbRelationship dbRel = rel.getDbRelationships().get(0);
+
+				List<DbJoin> joins = dbRel.getJoins();
+				for (DbJoin join : joins) {
+					DbAttribute src = join.getSource();
+					appendColumn(columns, null, src, attributes, null);
+				}
+			}
+		};
+
+		descriptor.visitAllProperties(visitor);
+
+		// stack should be reset, because all root table attributes go with "t0"
+		// table alias
+		resetJoinStack();
+
+		// add remaining needed attrs from DbEntity
+		DbEntity table = getQueryMetadata().getDbEntity();
+		for (DbAttribute dba : table.getPrimaryKeys()) {
+			appendColumn(columns, null, dba, attributes, null);
+		}
+
+		// special handling of a disjoint query...
+
+		if (query instanceof PrefetchSelectQuery) {
+
+			// for each relationship path add PK of the target entity...
+			for (String path : ((PrefetchSelectQuery) query).getResultPaths()) {
+
+				ASTDbPath pathExp = (ASTDbPath) oe.translateToDbPath(ExpressionFactory.exp(path));
+
+				// add joins and find terminating element
+
+				resetJoinStack();
+
+				PathComponent<DbAttribute, DbRelationship> lastComponent = null;
+				for (PathComponent<DbAttribute, DbRelationship> component : table
+						.resolvePath(pathExp, getPathAliases())) {
+
+					if (component.getRelationship() != null) {
+						// do not invoke dbRelationshipAdded(), invoke
+						// pushJoin() instead. This is to prevent
+						// 'forcingDistinct' flipping to true, that will result
+						// in unneeded extra processing and sometimes in invalid
+						// results (see CAY-1979). Distinctness of each row is
+						// guaranteed by the prefetch query semantics - we
+						// include target ID in the result columns
+						getJoinStack().pushJoin(component.getRelationship(), component.getJoinType(), null);
+					}
+
+					lastComponent = component;
+				}
+
+				// process terminating element
+				if (lastComponent != null) {
+
+					DbRelationship relationship = lastComponent.getRelationship();
+
+					if (relationship != null) {
+
+						String labelPrefix = pathExp.getPath();
+						DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
+
+						for (DbAttribute pk : targetEntity.getPrimaryKeys()) {
+
+							// note that we my select a source attribute, but
+							// label it as
+							// target for simplified snapshot processing
+							appendColumn(columns, null, pk, attributes, labelPrefix + '.' + pk.getName());
+						}
+					}
+				}
+			}
+		}
+
+		// handle joint prefetches directly attached to this query...
+		if (query.getPrefetchTree() != null) {
+
+			for (PrefetchTreeNode prefetch : query.getPrefetchTree().adjacentJointNodes()) {
+
+				// for each prefetch add all joins plus columns from the target
+				// entity
+				Expression prefetchExp = ExpressionFactory.exp(prefetch.getPath());
+				ASTDbPath dbPrefetch = (ASTDbPath) oe.translateToDbPath(prefetchExp);
+
+				resetJoinStack();
+				DbRelationship r = null;
+				for (PathComponent<DbAttribute, DbRelationship> component : table.resolvePath(dbPrefetch,
+						getPathAliases())) {
+					r = component.getRelationship();
+					dbRelationshipAdded(r, JoinType.LEFT_OUTER, null);
+				}
+
+				if (r == null) {
+					throw new CayenneRuntimeException("Invalid joint prefetch '" + prefetch + "' for entity: "
+							+ oe.getName());
+				}
+
+				// add columns from the target entity, including those that are
+				// matched
+				// against the FK of the source entity. This is needed to
+				// determine
+				// whether optional relationships are null
+
+				// go via target OE to make sure that Java types are mapped
+				// correctly...
+				ObjRelationship targetRel = (ObjRelationship) prefetchExp.evaluate(oe);
+				ObjEntity targetEntity = (ObjEntity) targetRel.getTargetEntity();
+
+				String labelPrefix = dbPrefetch.getPath();
+				for (ObjAttribute oa : targetEntity.getAttributes()) {
+					Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator();
+					while (dbPathIterator.hasNext()) {
+						Object pathPart = dbPathIterator.next();
+
+						if (pathPart == null) {
+							throw new CayenneRuntimeException("ObjAttribute has no component: " + oa.getName());
+						} else if (pathPart instanceof DbRelationship) {
+							DbRelationship rel = (DbRelationship) pathPart;
+							dbRelationshipAdded(rel, JoinType.INNER, null);
+						} else if (pathPart instanceof DbAttribute) {
+							DbAttribute attribute = (DbAttribute) pathPart;
+
+							appendColumn(columns, oa, attribute, attributes, labelPrefix + '.' + attribute.getName());
+						}
+					}
+				}
+
+				// append remaining target attributes such as keys
+				DbEntity targetDbEntity = (DbEntity) r.getTargetEntity();
+				for (DbAttribute attribute : targetDbEntity.getAttributes()) {
+					appendColumn(columns, null, attribute, attributes, labelPrefix + '.' + attribute.getName());
+				}
+			}
+		}
+
+		return columns;
+	}
+
+	<T> List<ColumnDescriptor> appendIdColumns(final List<ColumnDescriptor> columns, SelectQuery<T> query) {
+
+		Set<ColumnTracker> skipSet = new HashSet<ColumnTracker>();
+
+		ClassDescriptor descriptor = queryMetadata.getClassDescriptor();
+		ObjEntity oe = descriptor.getEntity();
+		DbEntity dbEntity = oe.getDbEntity();
+		for (ObjAttribute attribute : oe.getPrimaryKeys()) {
+
+			// synthetic objattributes can't reliably lookup their DbAttribute,
+			// so do it manually..
+			DbAttribute dbAttribute = dbEntity.getAttribute(attribute.getDbAttributeName());
+			appendColumn(columns, attribute, dbAttribute, skipSet, null);
+		}
+
+		return columns;
+	}
+
+	private void appendColumn(List<ColumnDescriptor> columns, ObjAttribute objAttribute, DbAttribute attribute,
+			Set<ColumnTracker> skipSet, String label) {
+
+		String alias = getCurrentAlias();
+		if (skipSet.add(new ColumnTracker(alias, attribute))) {
+
+			ColumnDescriptor column = (objAttribute != null) ? new ColumnDescriptor(objAttribute, attribute, alias)
+					: new ColumnDescriptor(attribute, alias);
+
+			if (label != null) {
+				column.setDataRowKey(label);
+			}
+
+			columns.add(column);
+
+			// TODO: andrus, 5/7/2006 - replace 'columns' collection with this
+			// map, as it
+			// is redundant
+			defaultAttributesByColumn.put(column, objAttribute);
+		} else if (objAttribute != null) {
+
+			// record ObjAttribute override
+			for (ColumnDescriptor column : columns) {
+				if (attribute.getName().equals(column.getName())) {
+
+					if (attributeOverrides == null) {
+						attributeOverrides = new HashMap<ObjAttribute, ColumnDescriptor>();
+					}
+
+					// kick out the original attribute
+					ObjAttribute original = defaultAttributesByColumn.remove(column);
+
+					if (original != null) {
+						attributeOverrides.put(original, column);
+					}
+
+					attributeOverrides.put(objAttribute, column);
+					column.setJavaClass(Void.TYPE.getName());
+
+					break;
+				}
+			}
+		}
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	@Override
+	public void resetJoinStack() {
+		getJoinStack().resetStack();
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	@Override
+	public void dbRelationshipAdded(DbRelationship relationship, JoinType joinType, String joinSplitAlias) {
+		if (relationship.isToMany()) {
+			forcingDistinct = true;
+		}
+
+		getJoinStack().pushJoin(relationship, joinType, joinSplitAlias);
+	}
+
+	/**
+	 * Always returns true.
+	 */
+	@Override
+	public boolean supportsTableAliases() {
+		return true;
+	}
+
+	static final class ColumnTracker {
+
+		private DbAttribute attribute;
+		private String alias;
+
+		ColumnTracker(String alias, DbAttribute attribute) {
+			this.attribute = attribute;
+			this.alias = alias;
+		}
+
+		@Override
+		public boolean equals(Object object) {
+			if (!(object instanceof ColumnTracker)) {
+				return false;
+			}
+
+			ColumnTracker other = (ColumnTracker) object;
+			return new EqualsBuilder().append(alias, other.alias).append(attribute, other.attribute).isEquals();
+		}
+
+		@Override
+		public int hashCode() {
+			return new HashCodeBuilder(31, 5).append(alias).append(attribute).toHashCode();
+		}
+
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java
index 440f7b1..416f14c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.access.translator.select;
 
-import java.io.IOException;
 import java.util.List;
 
 import org.apache.cayenne.dba.DbAdapter;
@@ -45,178 +44,178 @@ import org.apache.commons.collections.Transformer;
  */
 public class JoinStack {
 
-    protected JoinTreeNode rootNode;
-    protected JoinTreeNode topNode;
-    private QuotingStrategy quotingStrategy;
-
-    private int aliasCounter;
-
-    /**
-     * Helper class to process DbEntity qualifiers
-     */
-    private QualifierTranslator qualifierTranslator;
-
-    protected JoinStack(DbAdapter dbAdapter, DataMap dataMap, QueryAssembler assembler) {
-        this.rootNode = new JoinTreeNode(this);
-        this.rootNode.setTargetTableAlias(newAlias());
-
-        this.quotingStrategy = dbAdapter.getQuotingStrategy();
-        this.qualifierTranslator = dbAdapter.getQualifierTranslator(assembler);
-
-        resetStack();
-    }
-
-    String getCurrentAlias() {
-        return topNode.getTargetTableAlias();
-    }
-
-    /**
-     * Returns the number of configured joins.
-     */
-    protected int size() {
-        // do not count root as a join
-        return rootNode.size() - 1;
-    }
-
-    void appendRootWithQuoteSqlIdentifiers(Appendable out, DbEntity rootEntity) throws IOException {
-
-        out.append(quotingStrategy.quotedFullyQualifiedName(rootEntity));
-        out.append(' ');
-        out.append(quotingStrategy.quotedIdentifier(rootEntity, rootNode.getTargetTableAlias()));
-    }
-
-    /**
-     * Appends all configured joins to the provided output object.
-     */
-    protected void appendJoins(Appendable out) throws IOException {
-
-        // skip root, recursively append its children
-        for (JoinTreeNode child : rootNode.getChildren()) {
-            appendJoinSubtree(out, child);
-        }
-    }
-
-    protected void appendJoinSubtree(Appendable out, JoinTreeNode node) throws IOException {
-
-        DbRelationship relationship = node.getRelationship();
-
-        DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
-        String srcAlias = node.getSourceTableAlias();
-        String targetAlias = node.getTargetTableAlias();
-
-        switch (node.getJoinType()) {
-        case INNER:
-            out.append(" JOIN");
-            break;
-        case LEFT_OUTER:
-            out.append(" LEFT JOIN");
-            break;
-        default:
-            throw new IllegalArgumentException("Unsupported join type: " + node.getJoinType());
-        }
-
-        out.append(' ');
-        out.append(quotingStrategy.quotedFullyQualifiedName(targetEntity));
-
-        out.append(' ');
-        out.append(quotingStrategy.quotedIdentifier(targetEntity, targetAlias));
-        out.append(" ON (");
-
-        List<DbJoin> joins = relationship.getJoins();
-        int len = joins.size();
-        for (int i = 0; i < len; i++) {
-            DbJoin join = joins.get(i);
-            if (i > 0) {
-                out.append(" AND ");
-            }
-
-            out.append(quotingStrategy.quotedIdentifier(relationship.getSourceEntity(), srcAlias, join.getSourceName()));
-            out.append(" = ");
-            out.append(quotingStrategy.quotedIdentifier(targetEntity, targetAlias, join.getTargetName()));
-        }
-
-        /**
-         * Attaching root Db entity's qualifier
-         */
-        Expression dbQualifier = targetEntity.getQualifier();
-        if (dbQualifier != null) {
-            dbQualifier = dbQualifier.transform(new JoinedDbEntityQualifierTransformer(node));
-
-            if (len > 0) {
-                out.append(" AND ");
-            }
-            qualifierTranslator.setOut(out);
-            qualifierTranslator.doAppendPart(dbQualifier);
-        }
-
-        out.append(')');
-
-        for (JoinTreeNode child : node.getChildren()) {
-            appendJoinSubtree(out, child);
-        }
-    }
-
-    /**
-     * Append join information to the qualifier - the part after "WHERE".
-     */
-    protected void appendQualifier(Appendable out, boolean firstQualifierElement) throws IOException {
-        // nothing as standard join is performed before "WHERE"
-    }
-
-    /**
-     * Pops the stack all the way to the root node.
-     */
-    void resetStack() {
-        topNode = rootNode;
-    }
-
-    /**
-     * Finds or creates a JoinTreeNode for the given arguments and sets it as
-     * the next current join.
-     */
-    void pushJoin(DbRelationship relationship, JoinType joinType, String alias) {
-        topNode = topNode.findOrCreateChild(relationship, joinType, alias);
-    }
-
-    protected String newAlias() {
-        return "t" + aliasCounter++;
-    }
-
-    /**
-     * Class to translate *joined* DB Entity qualifiers annotation to *current*
-     * Obj-entity qualifiers annotation This is done by changing all Obj-paths
-     * to concatenated Db-paths to root entity and rejecting all original
-     * Db-paths
-     */
-    class JoinedDbEntityQualifierTransformer implements Transformer {
-
-        StringBuilder pathToRoot;
-
-        JoinedDbEntityQualifierTransformer(JoinTreeNode node) {
-            pathToRoot = new StringBuilder();
-            while (node != null && node.getRelationship() != null) {
-                String relName = node.getRelationship().getName();
-
-                /**
-                 * We must be in the same join as 'node', otherwise incorrect
-                 * join statement like JOIN t1 ... ON (t0.id=t1.id AND
-                 * t2.qualifier=0) could be generated
-                 */
-                if (node.getJoinType() == JoinType.LEFT_OUTER) {
-                    relName += Entity.OUTER_JOIN_INDICATOR;
-                }
-                relName += ObjEntity.PATH_SEPARATOR;
-
-                pathToRoot.insert(0, relName);
-                node = node.getParent();
-            }
-        }
-
-        public Object transform(Object input) {
-            if (input instanceof ASTObjPath) {
-                return new ASTDbPath(pathToRoot.toString() + ((SimpleNode) input).getOperand(0));
-            }
-            return input;
-        }
-    }
+	protected JoinTreeNode rootNode;
+	protected JoinTreeNode topNode;
+	private QuotingStrategy quotingStrategy;
+
+	private int aliasCounter;
+
+	/**
+	 * Helper class to process DbEntity qualifiers
+	 */
+	private QualifierTranslator qualifierTranslator;
+
+	protected JoinStack(DbAdapter dbAdapter, DataMap dataMap, QueryAssembler assembler) {
+		this.rootNode = new JoinTreeNode(this);
+		this.rootNode.setTargetTableAlias(newAlias());
+
+		this.quotingStrategy = dbAdapter.getQuotingStrategy();
+		this.qualifierTranslator = dbAdapter.getQualifierTranslator(assembler);
+
+		resetStack();
+	}
+
+	String getCurrentAlias() {
+		return topNode.getTargetTableAlias();
+	}
+
+	/**
+	 * Returns the number of configured joins.
+	 */
+	protected int size() {
+		// do not count root as a join
+		return rootNode.size() - 1;
+	}
+
+	void appendRootWithQuoteSqlIdentifiers(StringBuilder out, DbEntity rootEntity) {
+
+		out.append(quotingStrategy.quotedFullyQualifiedName(rootEntity));
+		out.append(' ');
+		out.append(quotingStrategy.quotedIdentifier(rootEntity, rootNode.getTargetTableAlias()));
+	}
+
+	/**
+	 * Appends all configured joins to the provided output object.
+	 */
+	protected void appendJoins(StringBuilder out) {
+
+		// skip root, recursively append its children
+		for (JoinTreeNode child : rootNode.getChildren()) {
+			appendJoinSubtree(out, child);
+		}
+	}
+
+	protected void appendJoinSubtree(StringBuilder out, JoinTreeNode node) {
+
+		DbRelationship relationship = node.getRelationship();
+
+		DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
+		String srcAlias = node.getSourceTableAlias();
+		String targetAlias = node.getTargetTableAlias();
+
+		switch (node.getJoinType()) {
+		case INNER:
+			out.append(" JOIN");
+			break;
+		case LEFT_OUTER:
+			out.append(" LEFT JOIN");
+			break;
+		default:
+			throw new IllegalArgumentException("Unsupported join type: " + node.getJoinType());
+		}
+
+		out.append(' ');
+		out.append(quotingStrategy.quotedFullyQualifiedName(targetEntity));
+
+		out.append(' ');
+		out.append(quotingStrategy.quotedIdentifier(targetEntity, targetAlias));
+		out.append(" ON (");
+
+		List<DbJoin> joins = relationship.getJoins();
+		int len = joins.size();
+		for (int i = 0; i < len; i++) {
+			DbJoin join = joins.get(i);
+			if (i > 0) {
+				out.append(" AND ");
+			}
+
+			out.append(quotingStrategy.quotedIdentifier(relationship.getSourceEntity(), srcAlias, join.getSourceName()));
+			out.append(" = ");
+			out.append(quotingStrategy.quotedIdentifier(targetEntity, targetAlias, join.getTargetName()));
+		}
+
+		/**
+		 * Attaching root Db entity's qualifier
+		 */
+		Expression dbQualifier = targetEntity.getQualifier();
+		if (dbQualifier != null) {
+			dbQualifier = dbQualifier.transform(new JoinedDbEntityQualifierTransformer(node));
+
+			if (len > 0) {
+				out.append(" AND ");
+			}
+			qualifierTranslator.setOut(out);
+			qualifierTranslator.doAppendPart(dbQualifier);
+		}
+
+		out.append(')');
+
+		for (JoinTreeNode child : node.getChildren()) {
+			appendJoinSubtree(out, child);
+		}
+	}
+
+	/**
+	 * Append join information to the qualifier - the part after "WHERE".
+	 */
+	protected void appendQualifier(StringBuilder out, boolean firstQualifierElement) {
+		// nothing as standard join is performed before "WHERE"
+	}
+
+	/**
+	 * Pops the stack all the way to the root node.
+	 */
+	void resetStack() {
+		topNode = rootNode;
+	}
+
+	/**
+	 * Finds or creates a JoinTreeNode for the given arguments and sets it as
+	 * the next current join.
+	 */
+	void pushJoin(DbRelationship relationship, JoinType joinType, String alias) {
+		topNode = topNode.findOrCreateChild(relationship, joinType, alias);
+	}
+
+	protected String newAlias() {
+		return "t" + aliasCounter++;
+	}
+
+	/**
+	 * Class to translate *joined* DB Entity qualifiers annotation to *current*
+	 * Obj-entity qualifiers annotation This is done by changing all Obj-paths
+	 * to concatenated Db-paths to root entity and rejecting all original
+	 * Db-paths
+	 */
+	class JoinedDbEntityQualifierTransformer implements Transformer {
+
+		StringBuilder pathToRoot;
+
+		JoinedDbEntityQualifierTransformer(JoinTreeNode node) {
+			pathToRoot = new StringBuilder();
+			while (node != null && node.getRelationship() != null) {
+				String relName = node.getRelationship().getName();
+
+				/**
+				 * We must be in the same join as 'node', otherwise incorrect
+				 * join statement like JOIN t1 ... ON (t0.id=t1.id AND
+				 * t2.qualifier=0) could be generated
+				 */
+				if (node.getJoinType() == JoinType.LEFT_OUTER) {
+					relName += Entity.OUTER_JOIN_INDICATOR;
+				}
+				relName += ObjEntity.PATH_SEPARATOR;
+
+				pathToRoot.insert(0, relName);
+				node = node.getParent();
+			}
+		}
+
+		public Object transform(Object input) {
+			if (input instanceof ASTObjPath) {
+				return new ASTDbPath(pathToRoot.toString() + ((SimpleNode) input).getOperand(0));
+			}
+			return input;
+		}
+	}
 }


[5/7] cayenne git commit: Refactoring SelectTranslator for better extensibility

Posted by aa...@apache.org.
http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingTranslator.java
index b875374..4a16678 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingTranslator.java
@@ -19,7 +19,6 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -35,88 +34,85 @@ import org.apache.cayenne.query.SelectQuery;
  */
 public class OrderingTranslator extends QueryAssemblerHelper {
 
-    protected List<String> orderByColumnList = new ArrayList<String>();
-
-    public OrderingTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler);
-    }
-
-    /**
-     * Translates query Ordering list to SQL ORDER BY clause. Ordering list is obtained
-     * from <code>queryAssembler</code>'s query object. In a process of building of
-     * ORDER BY clause, <code>queryAssembler</code> is notified when a join needs to be
-     * added.
-     * 
-     * @since 3.0
-     */
-    @Override
-    protected void doAppendPart() throws IOException {
-
-        Query q = queryAssembler.getQuery();
-
-        // only select queries can have ordering...
-        if (q == null || !(q instanceof SelectQuery)) {
-            return;
-        }
-
-        Iterator<Ordering> it = ((SelectQuery<?>) q).getOrderings().iterator();
-
-        Appendable mainBuffer = this.out;
-
-        try {
-            while (it.hasNext()) {
-                Ordering ord = it.next();
-
-                // reset buffer to collect SQL for the single column, that we'll be reusing 
-                this.out = new StringBuilder();
-
-                if (ord.isCaseInsensitive()) {
-                    out.append("UPPER(");
-                }
-
-                Expression exp = ord.getSortSpec();
-
-                if (exp.getType() == Expression.OBJ_PATH) {
-                    appendObjPath(exp);
-                }
-                else if (exp.getType() == Expression.DB_PATH) {
-                    appendDbPath(exp);
-                }
-                else {
-                    throw new CayenneRuntimeException("Unsupported ordering expression: "
-                            + exp);
-                }
-
-                // Close UPPER() modifier
-                if (ord.isCaseInsensitive()) {
-                    out.append(")");
-                }
-
-                String columnSQL = out.toString();
-                mainBuffer.append(columnSQL);
-                orderByColumnList.add(columnSQL);
-
-                // "ASC" is a noop, omit it from the query
-                if (!ord.isAscending()) {
-                    mainBuffer.append(" DESC");
-                }
-
-                if (it.hasNext()) {
-                    mainBuffer.append(", ");
-                }
-            }
-        }
-        finally {
-            this.out = mainBuffer;
-        }
-    }
-
-    /**
-     * Returns the column expressions (not Expressions) used in the order by clause. E.g.,
-     * in the case of an case-insensitive order by, an element of the list would be
-     * <code>UPPER(&lt;column reference&gt;)</code>
-     */
-    public List<String> getOrderByColumnList() {
-        return orderByColumnList;
-    }
+	protected List<String> orderByColumnList = new ArrayList<String>();
+
+	public OrderingTranslator(QueryAssembler queryAssembler) {
+		super(queryAssembler);
+	}
+
+	/**
+	 * Translates query Ordering list to SQL ORDER BY clause. Ordering list is
+	 * obtained from <code>queryAssembler</code>'s query object. In a process of
+	 * building of ORDER BY clause, <code>queryAssembler</code> is notified when
+	 * a join needs to be added.
+	 * 
+	 * @since 3.0
+	 */
+	@Override
+	protected void doAppendPart() {
+
+		Query q = queryAssembler.getQuery();
+
+		// only select queries can have ordering...
+		if (q == null || !(q instanceof SelectQuery)) {
+			return;
+		}
+
+		Iterator<Ordering> it = ((SelectQuery<?>) q).getOrderings().iterator();
+
+		StringBuilder mainBuffer = this.out;
+
+		try {
+			while (it.hasNext()) {
+				Ordering ord = it.next();
+
+				// reset buffer to collect SQL for the single column, that we'll
+				// be reusing
+				this.out = new StringBuilder();
+
+				if (ord.isCaseInsensitive()) {
+					out.append("UPPER(");
+				}
+
+				Expression exp = ord.getSortSpec();
+
+				if (exp.getType() == Expression.OBJ_PATH) {
+					appendObjPath(exp);
+				} else if (exp.getType() == Expression.DB_PATH) {
+					appendDbPath(exp);
+				} else {
+					throw new CayenneRuntimeException("Unsupported ordering expression: " + exp);
+				}
+
+				// Close UPPER() modifier
+				if (ord.isCaseInsensitive()) {
+					out.append(")");
+				}
+
+				String columnSQL = out.toString();
+				mainBuffer.append(columnSQL);
+				orderByColumnList.add(columnSQL);
+
+				// "ASC" is a noop, omit it from the query
+				if (!ord.isAscending()) {
+					mainBuffer.append(" DESC");
+				}
+
+				if (it.hasNext()) {
+					mainBuffer.append(", ");
+				}
+			}
+		} finally {
+			this.out = mainBuffer;
+		}
+	}
+
+	/**
+	 * Returns the column expressions (not Expressions) used in the order by
+	 * clause. E.g., in the case of an case-insensitive order by, an element of
+	 * the list would be <code>UPPER(&lt;column reference&gt;)</code>
+	 */
+	public List<String> getOrderByColumnList() {
+		return orderByColumnList;
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
index 4fa0fcc..c5fd13a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
@@ -43,381 +43,358 @@ import org.apache.commons.collections.IteratorUtils;
 import org.apache.commons.collections.Transformer;
 
 /**
- * Translates query qualifier to SQL. Used as a helper class by query translators.
+ * Translates query qualifier to SQL. Used as a helper class by query
+ * translators.
  */
 public class QualifierTranslator extends QueryAssemblerHelper implements TraversalHandler {
 
-    protected DataObjectMatchTranslator objectMatchTranslator;
-    protected boolean matchingObject;
-    protected boolean caseInsensitive;
-
-    public QualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler);
-
-        caseInsensitive = false;
-    }
-
-    /**
-     * Translates query qualifier to SQL WHERE clause. Qualifier is obtained from the
-     * parent queryAssembler.
-     * 
-     * @since 3.0
-     */
-    @Override
-    protected void doAppendPart() throws IOException {
-        doAppendPart(extractQualifier());
-    }
-
-    public void setCaseInsensitive(boolean caseInsensitive) {
-        this.caseInsensitive = caseInsensitive;
-    }
-
-    /**
-     * Translates query qualifier to SQL WHERE clause. Qualifier is a method parameter.
-     * 
-     * @since 3.0
-     */
-    protected void doAppendPart(Expression rootNode) throws IOException {
-        if (rootNode == null) {
-            return;
-        }
-        rootNode.traverse(this);
-    }
-
-    protected Expression extractQualifier() {
-        Query q = queryAssembler.getQuery();
-
-        Expression qualifier = ((SelectQuery<?>) q).getQualifier();
-
-        // append Entity qualifiers, taking inheritance into account
-        ObjEntity entity = getObjEntity();
-
-        if (entity != null) {
-
-            ClassDescriptor descriptor = queryAssembler
-                    .getEntityResolver()
-                    .getClassDescriptor(entity.getName());
-            Expression entityQualifier = descriptor
-                    .getEntityInheritanceTree()
-                    .qualifierForEntityAndSubclasses();
-            if (entityQualifier != null) {
-                qualifier = (qualifier != null)
-                        ? qualifier.andExp(entityQualifier)
-                        : entityQualifier;
-            }
-        }
-
-        /**
-         * Attaching root Db entity's qualifier
-         */
-        if (getDbEntity() != null) {
-            Expression dbQualifier = getDbEntity().getQualifier();
-            if (dbQualifier != null) {
-                dbQualifier = dbQualifier.transform(new DbEntityQualifierTransformer());
-
-                qualifier = qualifier == null ? dbQualifier : qualifier
-                        .andExp(dbQualifier);
-            }
-        }
-
-        return qualifier;
-    }
-
-    /**
-     * Called before processing an expression to initialize objectMatchTranslator if
-     * needed.
-     */
-    protected void detectObjectMatch(Expression exp) {
-        // On demand initialization of
-        // objectMatchTranslator is not possible since there may be null
-        // object values that would not allow to detect the need for
-        // such translator in the right time (e.g.: null = dbpath)
-
-        matchingObject = false;
-
-        if (exp.getOperandCount() != 2) {
-            // only binary expressions are supported
-            return;
-        }
-
-        // check if there are DataObjects among direct children of the Expression
-        for (int i = 0; i < 2; i++) {
-            Object op = exp.getOperand(i);
-            if (op instanceof Persistent || op instanceof ObjectId) {
-                matchingObject = true;
-
-                if (objectMatchTranslator == null) {
-                    objectMatchTranslator = new DataObjectMatchTranslator();
-                }
-                else {
-                    objectMatchTranslator.reset();
-                }
-                break;
-            }
-        }
-    }
-
-    protected void appendObjectMatch() throws IOException {
-        if (!matchingObject || objectMatchTranslator == null) {
-            throw new IllegalStateException("An invalid attempt to append object match.");
-        }
-
-        // turn off special handling, so that all the methods behave as a superclass's
-        // impl.
-        matchingObject = false;
-
-        boolean first = true;
-
-        DbRelationship relationship = objectMatchTranslator.getRelationship();
-        if (!relationship.isToMany() && !relationship.isToPK()) {
-            queryAssembler.dbRelationshipAdded(
-                    relationship,
-                    JoinType.INNER,
-                    objectMatchTranslator.getJoinSplitAlias());
-        }
-
-        Iterator<String> it = objectMatchTranslator.keys();
-        while (it.hasNext()) {
-            if (first) {
-                first = false;
-            }
-            else {
-                out.append(" AND ");
-            }
-
-            String key = it.next();
-            DbAttribute attr = objectMatchTranslator.getAttribute(key);
-            Object val = objectMatchTranslator.getValue(key);
-
-            processColumn(attr);
-            out.append(objectMatchTranslator.getOperation());
-            appendLiteral(val, attr, objectMatchTranslator.getExpression());
-        }
-
-        objectMatchTranslator.reset();
-    }
-
-    public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
-
-        if (!hasMoreChildren) {
-            return;
-        }
-
-        Appendable out = (matchingObject) ? new StringBuilder() : this.out;
-
-        try {
-            switch (node.getType()) {
-                case Expression.AND:
-                    out.append(" AND ");
-                    break;
-                case Expression.OR:
-                    out.append(" OR ");
-                    break;
-                case Expression.EQUAL_TO:
-                    // translate NULL as IS NULL
-                    if (childIndex == 0
-                            && node.getOperandCount() == 2
-                            && node.getOperand(1) == null) {
-                        out.append(" IS ");
-                    }
-                    else {
-                        out.append(" = ");
-                    }
-                    break;
-                case Expression.NOT_EQUAL_TO:
-                    // translate NULL as IS NOT NULL
-                    if (childIndex == 0
-                            && node.getOperandCount() == 2
-                            && node.getOperand(1) == null) {
-                        out.append(" IS NOT ");
-                    }
-                    else {
-                        out.append(" <> ");
-                    }
-                    break;
-                case Expression.LESS_THAN:
-                    out.append(" < ");
-                    break;
-                case Expression.GREATER_THAN:
-                    out.append(" > ");
-                    break;
-                case Expression.LESS_THAN_EQUAL_TO:
-                    out.append(" <= ");
-                    break;
-                case Expression.GREATER_THAN_EQUAL_TO:
-                    out.append(" >= ");
-                    break;
-                case Expression.IN:
-                    out.append(" IN ");
-                    break;
-                case Expression.NOT_IN:
-                    out.append(" NOT IN ");
-                    break;
-                case Expression.LIKE:
-                    out.append(" LIKE ");
-                    break;
-                case Expression.NOT_LIKE:
-                    out.append(" NOT LIKE ");
-                    break;
-                case Expression.LIKE_IGNORE_CASE:
-                    if (caseInsensitive) {
-                        out.append(" LIKE ");
-                    }
-                    else {
-                        out.append(") LIKE UPPER(");
-                    }
-                    break;
-                case Expression.NOT_LIKE_IGNORE_CASE:
-                    if (caseInsensitive) {
-                        out.append(" NOT LIKE ");
-                    }
-                    else {
-                        out.append(") NOT LIKE UPPER(");
-                    }
-                    break;
-                case Expression.ADD:
-                    out.append(" + ");
-                    break;
-                case Expression.SUBTRACT:
-                    out.append(" - ");
-                    break;
-                case Expression.MULTIPLY:
-                    out.append(" * ");
-                    break;
-                case Expression.DIVIDE:
-                    out.append(" / ");
-                    break;
-                case Expression.BETWEEN:
-                    if (childIndex == 0)
-                        out.append(" BETWEEN ");
-                    else if (childIndex == 1)
-                        out.append(" AND ");
-                    break;
-                case Expression.NOT_BETWEEN:
-                    if (childIndex == 0)
-                        out.append(" NOT BETWEEN ");
-                    else if (childIndex == 1)
-                        out.append(" AND ");
-                    break;
-                case Expression.BITWISE_OR:
-                    out.append(" ").append(operandForBitwiseOr()).append(" ");
-                    break;
-                case Expression.BITWISE_AND:
-                    out.append(" ").append(operandForBitwiseAnd()).append(" ");
-                    break;
-                case Expression.BITWISE_XOR:
-                    out.append(" ").append(operandForBitwiseXor()).append(" ");
-                    break;
-                case Expression.BITWISE_LEFT_SHIFT:
-                	out.append(" ").append(operandForBitwiseLeftShift()).append(" ");
-                	break;
-                case Expression.BITWISE_RIGHT_SHIFT:
-                	out.append(" ").append(operandForBitwiseRightShift()).append("" );
-                	break;
-            }
-        }
-        catch (IOException ioex) {
-            throw new CayenneRuntimeException("Error appending content", ioex);
-        }
-
-        if (matchingObject) {
-            objectMatchTranslator.setOperation(out.toString());
-            objectMatchTranslator.setExpression(node);
-        }
-    }
-
-    /**
-     * @since 3.1
-     */
-    protected String operandForBitwiseNot() {
-        return "~";
-    }
-
-    /**
-     * @since 3.1
-     */
-    protected String operandForBitwiseOr() {
-        return "|";
-    }
-
-    /**
-     * @since 3.1
-     */
-    protected String operandForBitwiseAnd() {
-        return "&";
-    }
-    
-    /**
-     * @since 3.1
-     */
-    protected String operandForBitwiseXor() {
-        return "^";
-    }
-    
-    /**
-     * @since 4.0
-     */
-    protected String operandForBitwiseLeftShift() {
-        return "<<";
-    }
-    
-    /**
-     * @since 4.0
-     */
-    protected String operandForBitwiseRightShift() {
-        return ">>";
-    }
-
-    public void startNode(Expression node, Expression parentNode) {
-        int count = node.getOperandCount();
-
-        if (count == 2) {
-            // binary nodes are the only ones that currently require this
-            detectObjectMatch(node);
-        }
-
-        try {
-
-            if (parenthesisNeeded(node, parentNode)) {
-                out.append('(');
-            }
-
-            if (count == 0) {
-                // not all databases handle true/false
-                if (node.getType() == Expression.TRUE) {
-                    out.append("1 = 1");
-                }
-                if (node.getType() == Expression.FALSE) {
-                    out.append("1 = 0");
-                }
-            }
-
-            if (count == 1) {
-                if (node.getType() == Expression.NEGATIVE)
-                    out.append('-');
-                // ignore POSITIVE - it is a NOOP
-                // else if(node.getType() == Expression.POSITIVE)
-                // qualBuf.append('+');
-                else if (node.getType() == Expression.NOT)
-                    out.append("NOT ");
-                else if (node.getType() == Expression.BITWISE_NOT) {
-                    out.append(operandForBitwiseNot());
-                }
-            }
-            else if ((node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE)
-                    && !caseInsensitive) {
-                out.append("UPPER(");
-            }
-        }
-        catch (IOException ioex) {
-            throw new CayenneRuntimeException("Error appending content", ioex);
-        }
-    }
-
-    /**
-     * @since 1.1
-     */
-    public void endNode(Expression node, Expression parentNode) {
+	protected DataObjectMatchTranslator objectMatchTranslator;
+	protected boolean matchingObject;
+	protected boolean caseInsensitive;
+
+	public QualifierTranslator(QueryAssembler queryAssembler) {
+		super(queryAssembler);
+
+		caseInsensitive = false;
+	}
+
+	/**
+	 * Translates query qualifier to SQL WHERE clause. Qualifier is obtained
+	 * from the parent queryAssembler.
+	 * 
+	 * @since 3.0
+	 */
+	@Override
+	protected void doAppendPart() {
+		doAppendPart(extractQualifier());
+	}
+
+	public void setCaseInsensitive(boolean caseInsensitive) {
+		this.caseInsensitive = caseInsensitive;
+	}
+
+	/**
+	 * Translates query qualifier to SQL WHERE clause. Qualifier is a method
+	 * parameter.
+	 * 
+	 * @since 3.0
+	 */
+	protected void doAppendPart(Expression rootNode) {
+		if (rootNode == null) {
+			return;
+		}
+		rootNode.traverse(this);
+	}
+
+	protected Expression extractQualifier() {
+		Query q = queryAssembler.getQuery();
+
+		Expression qualifier = ((SelectQuery<?>) q).getQualifier();
+
+		// append Entity qualifiers, taking inheritance into account
+		ObjEntity entity = getObjEntity();
+
+		if (entity != null) {
+
+			ClassDescriptor descriptor = queryAssembler.getEntityResolver().getClassDescriptor(entity.getName());
+			Expression entityQualifier = descriptor.getEntityInheritanceTree().qualifierForEntityAndSubclasses();
+			if (entityQualifier != null) {
+				qualifier = (qualifier != null) ? qualifier.andExp(entityQualifier) : entityQualifier;
+			}
+		}
+
+		/**
+		 * Attaching root Db entity's qualifier
+		 */
+		if (getDbEntity() != null) {
+			Expression dbQualifier = getDbEntity().getQualifier();
+			if (dbQualifier != null) {
+				dbQualifier = dbQualifier.transform(new DbEntityQualifierTransformer());
+
+				qualifier = qualifier == null ? dbQualifier : qualifier.andExp(dbQualifier);
+			}
+		}
+
+		return qualifier;
+	}
+
+	/**
+	 * Called before processing an expression to initialize
+	 * objectMatchTranslator if needed.
+	 */
+	protected void detectObjectMatch(Expression exp) {
+		// On demand initialization of
+		// objectMatchTranslator is not possible since there may be null
+		// object values that would not allow to detect the need for
+		// such translator in the right time (e.g.: null = dbpath)
+
+		matchingObject = false;
+
+		if (exp.getOperandCount() != 2) {
+			// only binary expressions are supported
+			return;
+		}
+
+		// check if there are DataObjects among direct children of the
+		// Expression
+		for (int i = 0; i < 2; i++) {
+			Object op = exp.getOperand(i);
+			if (op instanceof Persistent || op instanceof ObjectId) {
+				matchingObject = true;
+
+				if (objectMatchTranslator == null) {
+					objectMatchTranslator = new DataObjectMatchTranslator();
+				} else {
+					objectMatchTranslator.reset();
+				}
+				break;
+			}
+		}
+	}
+
+	protected void appendObjectMatch() throws IOException {
+		if (!matchingObject || objectMatchTranslator == null) {
+			throw new IllegalStateException("An invalid attempt to append object match.");
+		}
+
+		// turn off special handling, so that all the methods behave as a
+		// superclass's
+		// impl.
+		matchingObject = false;
+
+		boolean first = true;
+
+		DbRelationship relationship = objectMatchTranslator.getRelationship();
+		if (!relationship.isToMany() && !relationship.isToPK()) {
+			queryAssembler.dbRelationshipAdded(relationship, JoinType.INNER, objectMatchTranslator.getJoinSplitAlias());
+		}
+
+		Iterator<String> it = objectMatchTranslator.keys();
+		while (it.hasNext()) {
+			if (first) {
+				first = false;
+			} else {
+				out.append(" AND ");
+			}
+
+			String key = it.next();
+			DbAttribute attr = objectMatchTranslator.getAttribute(key);
+			Object val = objectMatchTranslator.getValue(key);
+
+			processColumn(attr);
+			out.append(objectMatchTranslator.getOperation());
+			appendLiteral(val, attr, objectMatchTranslator.getExpression());
+		}
+
+		objectMatchTranslator.reset();
+	}
+
+	public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
+
+		if (!hasMoreChildren) {
+			return;
+		}
+
+		Appendable out = (matchingObject) ? new StringBuilder() : this.out;
+
+		try {
+			switch (node.getType()) {
+			case Expression.AND:
+				out.append(" AND ");
+				break;
+			case Expression.OR:
+				out.append(" OR ");
+				break;
+			case Expression.EQUAL_TO:
+				// translate NULL as IS NULL
+				if (childIndex == 0 && node.getOperandCount() == 2 && node.getOperand(1) == null) {
+					out.append(" IS ");
+				} else {
+					out.append(" = ");
+				}
+				break;
+			case Expression.NOT_EQUAL_TO:
+				// translate NULL as IS NOT NULL
+				if (childIndex == 0 && node.getOperandCount() == 2 && node.getOperand(1) == null) {
+					out.append(" IS NOT ");
+				} else {
+					out.append(" <> ");
+				}
+				break;
+			case Expression.LESS_THAN:
+				out.append(" < ");
+				break;
+			case Expression.GREATER_THAN:
+				out.append(" > ");
+				break;
+			case Expression.LESS_THAN_EQUAL_TO:
+				out.append(" <= ");
+				break;
+			case Expression.GREATER_THAN_EQUAL_TO:
+				out.append(" >= ");
+				break;
+			case Expression.IN:
+				out.append(" IN ");
+				break;
+			case Expression.NOT_IN:
+				out.append(" NOT IN ");
+				break;
+			case Expression.LIKE:
+				out.append(" LIKE ");
+				break;
+			case Expression.NOT_LIKE:
+				out.append(" NOT LIKE ");
+				break;
+			case Expression.LIKE_IGNORE_CASE:
+				if (caseInsensitive) {
+					out.append(" LIKE ");
+				} else {
+					out.append(") LIKE UPPER(");
+				}
+				break;
+			case Expression.NOT_LIKE_IGNORE_CASE:
+				if (caseInsensitive) {
+					out.append(" NOT LIKE ");
+				} else {
+					out.append(") NOT LIKE UPPER(");
+				}
+				break;
+			case Expression.ADD:
+				out.append(" + ");
+				break;
+			case Expression.SUBTRACT:
+				out.append(" - ");
+				break;
+			case Expression.MULTIPLY:
+				out.append(" * ");
+				break;
+			case Expression.DIVIDE:
+				out.append(" / ");
+				break;
+			case Expression.BETWEEN:
+				if (childIndex == 0)
+					out.append(" BETWEEN ");
+				else if (childIndex == 1)
+					out.append(" AND ");
+				break;
+			case Expression.NOT_BETWEEN:
+				if (childIndex == 0)
+					out.append(" NOT BETWEEN ");
+				else if (childIndex == 1)
+					out.append(" AND ");
+				break;
+			case Expression.BITWISE_OR:
+				out.append(" ").append(operandForBitwiseOr()).append(" ");
+				break;
+			case Expression.BITWISE_AND:
+				out.append(" ").append(operandForBitwiseAnd()).append(" ");
+				break;
+			case Expression.BITWISE_XOR:
+				out.append(" ").append(operandForBitwiseXor()).append(" ");
+				break;
+			case Expression.BITWISE_LEFT_SHIFT:
+				out.append(" ").append(operandForBitwiseLeftShift()).append(" ");
+				break;
+			case Expression.BITWISE_RIGHT_SHIFT:
+				out.append(" ").append(operandForBitwiseRightShift()).append("");
+				break;
+			}
+		} catch (IOException ioex) {
+			throw new CayenneRuntimeException("Error appending content", ioex);
+		}
+
+		if (matchingObject) {
+			objectMatchTranslator.setOperation(out.toString());
+			objectMatchTranslator.setExpression(node);
+		}
+	}
+
+	/**
+	 * @since 3.1
+	 */
+	protected String operandForBitwiseNot() {
+		return "~";
+	}
+
+	/**
+	 * @since 3.1
+	 */
+	protected String operandForBitwiseOr() {
+		return "|";
+	}
+
+	/**
+	 * @since 3.1
+	 */
+	protected String operandForBitwiseAnd() {
+		return "&";
+	}
+
+	/**
+	 * @since 3.1
+	 */
+	protected String operandForBitwiseXor() {
+		return "^";
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	protected String operandForBitwiseLeftShift() {
+		return "<<";
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	protected String operandForBitwiseRightShift() {
+		return ">>";
+	}
+
+	public void startNode(Expression node, Expression parentNode) {
+		int count = node.getOperandCount();
+
+		if (count == 2) {
+			// binary nodes are the only ones that currently require this
+			detectObjectMatch(node);
+		}
+
+		if (parenthesisNeeded(node, parentNode)) {
+			out.append('(');
+		}
+
+		if (count == 0) {
+			// not all databases handle true/false
+			if (node.getType() == Expression.TRUE) {
+				out.append("1 = 1");
+			}
+			if (node.getType() == Expression.FALSE) {
+				out.append("1 = 0");
+			}
+		}
+
+		if (count == 1) {
+			if (node.getType() == Expression.NEGATIVE)
+				out.append('-');
+			// ignore POSITIVE - it is a NOOP
+			// else if(node.getType() == Expression.POSITIVE)
+			// qualBuf.append('+');
+			else if (node.getType() == Expression.NOT)
+				out.append("NOT ");
+			else if (node.getType() == Expression.BITWISE_NOT) {
+				out.append(operandForBitwiseNot());
+			}
+		} else if ((node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE)
+				&& !caseInsensitive) {
+			out.append("UPPER(");
+		}
+
+	}
+
+	/**
+	 * @since 1.1
+	 */
+	public void endNode(Expression node, Expression parentNode) {
 
 		try {
 			// check if we need to use objectMatchTranslator to finish building
@@ -444,128 +421,110 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 			if (parenthesisNeeded) {
 				out.append(')');
 			}
+		} catch (IOException ioex) {
+			throw new CayenneRuntimeException("Error appending content", ioex);
+		}
+	}
+
+	public void objectNode(Object leaf, Expression parentNode) {
+
+		try {
+			if (parentNode.getType() == Expression.OBJ_PATH) {
+				appendObjPath(parentNode);
+			} else if (parentNode.getType() == Expression.DB_PATH) {
+				appendDbPath(parentNode);
+			} else if (parentNode.getType() == Expression.LIST) {
+				appendList(parentNode, paramsDbType(parentNode));
+			} else {
+				appendLiteral(leaf, paramsDbType(parentNode), parentNode);
+			}
+		} catch (IOException ioex) {
+			throw new CayenneRuntimeException("Error appending content", ioex);
+		}
+	}
+
+	protected boolean parenthesisNeeded(Expression node, Expression parentNode) {
+		if (parentNode == null)
+			return false;
+
+		// only unary expressions can go w/o parenthesis
+		if (node.getOperandCount() > 1)
+			return true;
+
+		if (node.getType() == Expression.OBJ_PATH)
+			return false;
+
+		if (node.getType() == Expression.DB_PATH)
+			return false;
+
+		return true;
+	}
+
+	private final void appendList(Expression listExpr, DbAttribute paramDesc) throws IOException {
+		Iterator<?> it = null;
+		Object list = listExpr.getOperand(0);
+		if (list instanceof List) {
+			it = ((List<?>) list).iterator();
+		} else if (list instanceof Object[]) {
+			it = IteratorUtils.arrayIterator((Object[]) list);
+		} else {
+			String className = (list != null) ? list.getClass().getName() : "<null>";
+			throw new IllegalArgumentException("Unsupported type for the list expressions: " + className);
+		}
+
+		// process first element outside the loop
+		// (unroll loop to avoid condition checking
+		if (it.hasNext())
+			appendLiteral(it.next(), paramDesc, listExpr);
+		else
+			return;
+
+		while (it.hasNext()) {
+			out.append(", ");
+			appendLiteral(it.next(), paramDesc, listExpr);
+		}
+	}
+
+	@Override
+	protected void appendLiteral(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
+
+		if (!matchingObject) {
+			super.appendLiteral(val, attr, parentExpression);
+		} else if (val == null || (val instanceof Persistent)) {
+			objectMatchTranslator.setDataObject((Persistent) val);
+		} else if (val instanceof ObjectId) {
+			objectMatchTranslator.setObjectId((ObjectId) val);
+		} else {
+			throw new IllegalArgumentException("Attempt to use literal other than DataObject during object match.");
+		}
+	}
+
+	@Override
+	protected void processRelTermination(DbRelationship rel, JoinType joinType, String joinSplitAlias) {
+
+		if (!matchingObject) {
+			super.processRelTermination(rel, joinType, joinSplitAlias);
+		} else {
+			if (rel.isToMany()) {
+				// append joins
+				queryAssembler.dbRelationshipAdded(rel, joinType, joinSplitAlias);
+			}
+			objectMatchTranslator.setRelationship(rel, joinSplitAlias);
+		}
+	}
+
+	/**
+	 * Class to translate DB Entity qualifiers annotation to Obj-entity
+	 * qualifiers annotation This is done by changing all Obj-paths to Db-paths
+	 * and rejecting all original Db-paths
+	 */
+	class DbEntityQualifierTransformer implements Transformer {
+
+		public Object transform(Object input) {
+			if (input instanceof ASTObjPath) {
+				return new ASTDbPath(((SimpleNode) input).getOperand(0));
+			}
+			return input;
 		}
-        catch (IOException ioex) {
-            throw new CayenneRuntimeException("Error appending content", ioex);
-        }
-    }
-
-    public void objectNode(Object leaf, Expression parentNode) {
-
-        try {
-            if (parentNode.getType() == Expression.OBJ_PATH) {
-                appendObjPath(parentNode);
-            }
-            else if (parentNode.getType() == Expression.DB_PATH) {
-                appendDbPath(parentNode);
-            }
-            else if (parentNode.getType() == Expression.LIST) {
-                appendList(parentNode, paramsDbType(parentNode));
-            }
-            else {
-                appendLiteral(leaf, paramsDbType(parentNode), parentNode);
-            }
-        }
-        catch (IOException ioex) {
-            throw new CayenneRuntimeException("Error appending content", ioex);
-        }
-    }
-
-    protected boolean parenthesisNeeded(Expression node, Expression parentNode) {
-        if (parentNode == null)
-            return false;
-
-        // only unary expressions can go w/o parenthesis
-        if (node.getOperandCount() > 1)
-            return true;
-
-        if (node.getType() == Expression.OBJ_PATH)
-            return false;
-
-        if (node.getType() == Expression.DB_PATH)
-            return false;
-
-        return true;
-    }
-
-    private final void appendList(Expression listExpr, DbAttribute paramDesc)
-            throws IOException {
-        Iterator<?> it = null;
-        Object list = listExpr.getOperand(0);
-        if (list instanceof List) {
-            it = ((List<?>) list).iterator();
-        }
-        else if (list instanceof Object[]) {
-            it = IteratorUtils.arrayIterator((Object[]) list);
-        }
-        else {
-            String className = (list != null) ? list.getClass().getName() : "<null>";
-            throw new IllegalArgumentException(
-                    "Unsupported type for the list expressions: " + className);
-        }
-
-        // process first element outside the loop
-        // (unroll loop to avoid condition checking
-        if (it.hasNext())
-            appendLiteral(it.next(), paramDesc, listExpr);
-        else
-            return;
-
-        while (it.hasNext()) {
-            out.append(", ");
-            appendLiteral(it.next(), paramDesc, listExpr);
-        }
-    }
-
-    @Override
-    protected void appendLiteral(Object val, DbAttribute attr, Expression parentExpression)
-            throws IOException {
-
-        if (!matchingObject) {
-            super.appendLiteral(val, attr, parentExpression);
-        }
-        else if (val == null || (val instanceof Persistent)) {
-            objectMatchTranslator.setDataObject((Persistent) val);
-        }
-        else if (val instanceof ObjectId) {
-            objectMatchTranslator.setObjectId((ObjectId) val);
-        }
-        else {
-            throw new IllegalArgumentException(
-                    "Attempt to use literal other than DataObject during object match.");
-        }
-    }
-
-    @Override
-    protected void processRelTermination(
-            DbRelationship rel,
-            JoinType joinType,
-            String joinSplitAlias) throws IOException {
-
-        if (!matchingObject) {
-            super.processRelTermination(rel, joinType, joinSplitAlias);
-        }
-        else {
-            if (rel.isToMany()) {
-                // append joins
-                queryAssembler.dbRelationshipAdded(rel, joinType, joinSplitAlias);
-            }
-            objectMatchTranslator.setRelationship(rel, joinSplitAlias);
-        }
-    }
-
-    /**
-     * Class to translate DB Entity qualifiers annotation to Obj-entity qualifiers
-     * annotation This is done by changing all Obj-paths to Db-paths and rejecting all
-     * original Db-paths
-     */
-    class DbEntityQualifierTransformer implements Transformer {
-
-        public Object transform(Object input) {
-            if (input instanceof ASTObjPath) {
-                return new ASTDbPath(((SimpleNode) input).getOperand(0));
-            }
-            return input;
-        }
-    }
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
index d83e6b2..af1ade2 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
@@ -19,21 +19,16 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import java.sql.Connection;
-import java.sql.PreparedStatement;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
-import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.log.JdbcEventLogger;
 import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.JoinType;
-import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.QueryMetadata;
 
@@ -42,176 +37,127 @@ import org.apache.cayenne.query.QueryMetadata;
  */
 public abstract class QueryAssembler {
 
-    protected Query query;
-    protected QueryMetadata queryMetadata;
-    protected String cachedSqlString;
-    protected Connection connection;
-    protected DbAdapter adapter;
-    protected EntityResolver entityResolver;
-    protected JdbcEventLogger logger;
-
-    /**
-     * Holds PreparedStatement values.
-     */
-    protected List<Object> values = new ArrayList<Object>();
-
-    /**
-     * PreparedStatement attributes matching entries in <code>values</code>
-     * list.
-     */
-    protected List<DbAttribute> attributes = new ArrayList<DbAttribute>();
-
-    /**
-     * The index parameter will be inserted at in parameter list
-     */
-    protected int parameterIndex;
-
-    /**
-     * @since 4.0
-     */
-    public QueryAssembler(Query query, DataNode dataNode, Connection connection) {
-        this.logger = dataNode.getJdbcEventLogger();
-        this.entityResolver = dataNode.getEntityResolver();
-        this.adapter = dataNode.getAdapter();
-        this.query = query;
-        this.connection = connection;
-        this.queryMetadata = query.getMetaData(entityResolver);
-    }
-
-    /**
-     * Returns aliases for the path splits defined in the query.
-     * 
-     * @since 3.0
-     */
-    protected Map<String, String> getPathAliases() {
-        return query.getMetaData(entityResolver).getPathSplitAliases();
-    }
-
-    public EntityResolver getEntityResolver() {
-        return entityResolver;
-    }
-
-    public DbAdapter getAdapter() {
-        return adapter;
-    }
-
-    /**
-     * Returns query object being processed.
-     */
-    public Query getQuery() {
-        return query;
-    }
-
-    public QueryMetadata getQueryMetadata() {
-        return queryMetadata;
-    }
-
-    /**
-     * @since 3.1
-     */
-    public JdbcEventLogger getJdbcEventLogger() {
-        return logger;
-    }
-
-    public DbEntity getRootDbEntity() {
-        return queryMetadata.getDbEntity();
-    }
-
-    public ObjEntity getRootEntity() {
-        return queryMetadata.getObjEntity();
-    }
-
-    /**
-     * A callback invoked by a child qualifier or ordering processor allowing
-     * query assembler to reset its join stack.
-     * 
-     * @since 3.0
-     */
-    public abstract void resetJoinStack();
-
-    /**
-     * Returns an alias of the table which is currently at the top of the join
-     * stack.
-     * 
-     * @since 3.0
-     */
-    public abstract String getCurrentAlias();
-
-    /**
-     * Appends a join with given semantics to the query.
-     * 
-     * @since 3.0
-     */
-    public abstract void dbRelationshipAdded(DbRelationship relationship, JoinType joinType, String joinSplitAlias);
-
-    /**
-     * Translates query into sql string. This is a workhorse method of
-     * QueryAssembler. It is called internally from <code>createStatement</code>
-     * . Usually there is no need to invoke it explicitly.
-     */
-    public abstract String createSqlString() throws Exception;
-
-    /**
-     * Returns <code>true</code> if table aliases are supported. Default
-     * implementation returns false.
-     */
-    public boolean supportsTableAliases() {
-        return false;
-    }
-
-    /**
-     * Registers <code>anObject</code> as a PreparedStatement parameter.
-     * 
-     * @param anObject
-     *            object that represents a value of DbAttribute
-     * @param dbAttr
-     *            DbAttribute being processed.
-     */
-    public void addToParamList(DbAttribute dbAttr, Object anObject) {
-        attributes.add(parameterIndex, dbAttr);
-        values.add(parameterIndex++, anObject);
-    }
-
-    /**
-     * Translates internal query into PreparedStatement.
-     */
-    public PreparedStatement createStatement() throws Exception {
-        long t1 = System.currentTimeMillis();
-        String sqlStr = createSqlString();
-        logger.logQuery(sqlStr, attributes, values, System.currentTimeMillis() - t1);
-        PreparedStatement stmt = connection.prepareStatement(sqlStr);
-        initStatement(stmt);
-        return stmt;
-    }
-
-    /**
-     * Initializes prepared statements with collected parameters. Called
-     * internally from "createStatement". Cayenne users shouldn't normally call
-     * it directly.
-     */
-    protected void initStatement(PreparedStatement stmt) throws Exception {
-        if (values != null && values.size() > 0) {
-            int len = values.size();
-            for (int i = 0; i < len; i++) {
-                Object val = values.get(i);
-
-                DbAttribute attr = attributes.get(i);
-
-                // null DbAttributes are a result of inferior qualifier
-                // processing
-                // (qualifier can't map parameters to DbAttributes and therefore
-                // only supports standard java types now)
-                // hence, a special moronic case here:
-                if (attr == null) {
-                    stmt.setObject(i + 1, val);
-                } else {
-                    adapter.bindParameter(stmt, val, i + 1, attr.getType(), attr.getScale());
-                }
-            }
-        }
-
-        if (queryMetadata.getStatementFetchSize() != 0) {
-            stmt.setFetchSize(queryMetadata.getStatementFetchSize());
-        }
-    }
+	protected Query query;
+	protected QueryMetadata queryMetadata;
+	protected boolean translated;
+	protected String sql;
+	protected DbAdapter adapter;
+	protected EntityResolver entityResolver;
+	protected List<ParameterBinding> bindings;
+
+	/**
+	 * @since 4.0
+	 */
+	public QueryAssembler(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+		this.entityResolver = entityResolver;
+		this.adapter = adapter;
+		this.query = query;
+		this.queryMetadata = query.getMetaData(entityResolver);
+		this.bindings = new ArrayList<ParameterBinding>();
+	}
+
+	/**
+	 * Returns aliases for the path splits defined in the query.
+	 * 
+	 * @since 3.0
+	 */
+	protected Map<String, String> getPathAliases() {
+		return queryMetadata.getPathSplitAliases();
+	}
+
+	public EntityResolver getEntityResolver() {
+		return entityResolver;
+	}
+
+	public DbAdapter getAdapter() {
+		return adapter;
+	}
+
+	/**
+	 * Returns query object being processed.
+	 */
+	public Query getQuery() {
+		return query;
+	}
+
+	public QueryMetadata getQueryMetadata() {
+		return queryMetadata;
+	}
+
+	/**
+	 * A callback invoked by a child qualifier or ordering processor allowing
+	 * query assembler to reset its join stack.
+	 * 
+	 * @since 3.0
+	 */
+	public abstract void resetJoinStack();
+
+	/**
+	 * Returns an alias of the table which is currently at the top of the join
+	 * stack.
+	 * 
+	 * @since 3.0
+	 */
+	public abstract String getCurrentAlias();
+
+	/**
+	 * Appends a join with given semantics to the query.
+	 * 
+	 * @since 3.0
+	 */
+	public abstract void dbRelationshipAdded(DbRelationship relationship, JoinType joinType, String joinSplitAlias);
+
+	/**
+	 * Translates query into an SQL string formatted to use in a
+	 * PreparedStatement.
+	 */
+	public String getSql() {
+		ensureTranslated();
+		return sql;
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	protected void ensureTranslated() {
+		if (!translated) {
+			doTranslate();
+			translated = true;
+		}
+	}
+	
+	/**
+	 * @since 4.0
+	 */
+	protected abstract void doTranslate();
+
+	/**
+	 * Returns <code>true</code> if table aliases are supported. Default
+	 * implementation returns false.
+	 */
+	public boolean supportsTableAliases() {
+		return false;
+	}
+
+	/**
+	 * Registers <code>anObject</code> as a PreparedStatement parameter.
+	 * 
+	 * @param anObject
+	 *            object that represents a value of DbAttribute
+	 * @param dbAttr
+	 *            DbAttribute being processed.
+	 */
+	public void addToParamList(DbAttribute dbAttr, Object anObject) {
+		ParameterBinding binding = new ParameterBinding(dbAttr);
+		binding.setValue(anObject);
+		binding.setStatementPosition(bindings.size() + 1);
+		bindings.add(binding);
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	public ParameterBinding[] getBindings() {
+		return bindings.toArray(new ParameterBinding[bindings.size()]);
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
index 3152518..e56bc2c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
@@ -49,449 +49,444 @@ import org.apache.cayenne.util.CayenneMapEntry;
  */
 public abstract class QueryAssemblerHelper {
 
-    protected QueryAssembler queryAssembler;
-    protected Appendable out;
-    protected QuotingStrategy strategy;
-
-    /**
-     * Creates QueryAssemblerHelper initializing with parent
-     * {@link QueryAssembler} and output buffer object.
-     */
-    public QueryAssemblerHelper(QueryAssembler queryAssembler) {
-        this.queryAssembler = queryAssembler;
-        strategy = queryAssembler.getAdapter().getQuotingStrategy();
-    }
-
-    public ObjEntity getObjEntity() {
-        return queryAssembler.getRootEntity();
-    }
-
-    public DbEntity getDbEntity() {
-        return queryAssembler.getRootDbEntity();
-    }
-
-    /**
-     * @since 3.0
-     */
-    public <T extends Appendable> T appendPart(T out) throws IOException {
-        this.out = out;
-        doAppendPart();
-        return out;
-    }
-
-    /**
-     * Sets ouput buffer
-     */
-    void setOut(Appendable out) {
-        this.out = out;
-    }
-
-    /**
-     * @return output buffer
-     */
-    Appendable getOut() {
-        return out;
-    }
-
-    /**
-     * @since 3.0
-     */
-    protected abstract void doAppendPart() throws IOException;
-
-    /**
-     * <p>Outputs the standard JDBC (database agnostic) expression for supplying
-     * the escape character to the database server when supplying a LIKE clause.
-     * This has been factored-out because some database adaptors handle LIKE
-     * differently and they need access to this common method in order not to
-     * repeat this code.
-     * <p>
-     * If there is no escape character defined then this method will not output
-     * anything. An escape character of 0 will mean no escape character.
-     * 
-     * @since 3.1
-     */
-    protected void appendLikeEscapeCharacter(PatternMatchNode patternMatchNode) throws IOException {
-        char escapeChar = patternMatchNode.getEscapeChar();
-
-        if ('?' == escapeChar) {
-            throw new CayenneRuntimeException("the escape character of '?' is illegal for LIKE clauses.");
-        }
-
-        if (0 != escapeChar) {
-            out.append(" {escape '");
-            out.append(escapeChar);
-            out.append("'}");
-        }
-    }
-
-    /**
-     * Processes parts of the OBJ_PATH expression.
-     */
-    protected void appendObjPath(Expression pathExp) throws IOException {
-
-        queryAssembler.resetJoinStack();
-        String joinSplitAlias = null;
-
-        for (PathComponent<ObjAttribute, ObjRelationship> component : getObjEntity().resolvePath(pathExp,
-                queryAssembler.getPathAliases())) {
-
-            if (component.isAlias()) {
-                joinSplitAlias = component.getName();
-                for (PathComponent<ObjAttribute, ObjRelationship> aliasPart : component.getAliasedPath()) {
-
-                    ObjRelationship relationship = aliasPart.getRelationship();
-
-                    if (relationship == null) {
-                        throw new IllegalStateException("Non-relationship aliased path part: " + aliasPart.getName());
-                    }
-
-                    if (aliasPart.isLast() && component.isLast()) {
-                        processRelTermination(relationship, aliasPart.getJoinType(), joinSplitAlias);
-                    } else {
-                        // find and add joins ....
-                        for (DbRelationship dbRel : relationship.getDbRelationships()) {
-                            queryAssembler.dbRelationshipAdded(dbRel, aliasPart.getJoinType(), joinSplitAlias);
-                        }
-                    }
-                }
-
-                continue;
-            }
-
-            ObjRelationship relationship = component.getRelationship();
-            ObjAttribute attribute = component.getAttribute();
-
-            if (relationship != null) {
-
-                // if this is a last relationship in the path,
-                // it needs special handling
-                if (component.isLast()) {
-                    processRelTermination(relationship, component.getJoinType(), joinSplitAlias);
-                } else {
-                    // find and add joins ....
-                    for (DbRelationship dbRel : relationship.getDbRelationships()) {
-                        queryAssembler.dbRelationshipAdded(dbRel, component.getJoinType(), joinSplitAlias);
-                    }
-                }
-            } else {
-                Iterator<CayenneMapEntry> dbPathIterator = attribute.getDbPathIterator();
-                while (dbPathIterator.hasNext()) {
-                    Object pathPart = dbPathIterator.next();
-
-                    if (pathPart == null) {
-                        throw new CayenneRuntimeException("ObjAttribute has no component: " + attribute.getName());
-                    } else if (pathPart instanceof DbRelationship) {
-                        queryAssembler.dbRelationshipAdded((DbRelationship) pathPart, JoinType.INNER, joinSplitAlias);
-                    } else if (pathPart instanceof DbAttribute) {
-                        processColumnWithQuoteSqlIdentifiers((DbAttribute) pathPart, pathExp);
-                    }
-                }
-
-            }
-        }
-    }
-
-    protected void appendDbPath(Expression pathExp) throws IOException {
-
-        queryAssembler.resetJoinStack();
-        String joinSplitAlias = null;
-
-        for (PathComponent<DbAttribute, DbRelationship> component : getDbEntity().resolvePath(pathExp,
-                queryAssembler.getPathAliases())) {
-
-            if (component.isAlias()) {
-                joinSplitAlias = component.getName();
-                for (PathComponent<DbAttribute, DbRelationship> aliasPart : component.getAliasedPath()) {
-
-                    DbRelationship relationship = aliasPart.getRelationship();
-
-                    if (relationship == null) {
-                        throw new IllegalStateException("Non-relationship aliased path part: " + aliasPart.getName());
-                    }
-
-                    if (aliasPart.isLast() && component.isLast()) {
-                        processRelTermination(relationship, aliasPart.getJoinType(), joinSplitAlias);
-                    } else {
-                        queryAssembler.dbRelationshipAdded(relationship, component.getJoinType(), joinSplitAlias);
-                    }
-                }
-
-                continue;
-            }
-
-            DbRelationship relationship = component.getRelationship();
-
-            if (relationship != null) {
-
-                // if this is a last relationship in the path,
-                // it needs special handling
-                if (component.isLast()) {
-                    processRelTermination(relationship, component.getJoinType(), joinSplitAlias);
-                } else {
-                    // find and add joins ....
-                    queryAssembler.dbRelationshipAdded(relationship, component.getJoinType(), joinSplitAlias);
-                }
-            } else {
-                processColumnWithQuoteSqlIdentifiers(component.getAttribute(), pathExp);
-            }
-        }
-    }
-
-    protected void processColumn(DbAttribute dbAttr) throws IOException {
-        processColumnWithQuoteSqlIdentifiers(dbAttr, null);
-    }
-
-    protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) throws IOException {
-
-        String alias = (queryAssembler.supportsTableAliases()) ? queryAssembler.getCurrentAlias() : null;
-        out.append(strategy.quotedIdentifier(dbAttr.getEntity(), alias, dbAttr.getName()));
-    }
-
-    /**
-     * Appends SQL code to the query buffer to handle <code>val</code> as a
-     * parameter to the PreparedStatement being built. Adds <code>val</code>
-     * into QueryAssembler parameter list.
-     * <p>
-     * If <code>val</code> is null, "NULL" is appended to the query.
-     * </p>
-     * <p>
-     * If <code>val</code> is a DataObject, its primary key value is used as a
-     * parameter. <i>Only objects with a single column primary key can be
-     * used.</i>
-     * 
-     * @param val
-     *            object that should be appended as a literal to the query. Must
-     *            be of one of "standard JDBC" types, null or a DataObject.
-     * @param attr
-     *            DbAttribute that has information on what type of parameter is
-     *            being appended.
-     */
-    protected void appendLiteral(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
-
-        if (val == null) {
-            out.append("NULL");
-        } else if (val instanceof Persistent) {
-            // TODO: see cay1796
-            // This check is unlikely to happen,
-            // since Expression got ObjectId from Persistent object.
-            // Left for future research.
-            ObjectId id = ((Persistent) val).getObjectId();
-
-            // check if this id is acceptable to be a parameter
-            if (id == null) {
-                throw new CayenneRuntimeException("Can't use TRANSIENT object as a query parameter.");
-            }
-
-            if (id.isTemporary()) {
-                throw new CayenneRuntimeException("Can't use NEW object as a query parameter.");
-            }
-
-            Map<String, Object> snap = id.getIdSnapshot();
-            if (snap.size() != 1) {
-                StringBuilder msg = new StringBuilder();
-                msg.append("Object must have a single primary key column ").append("to serve as a query parameter. ")
-                        .append("This object has ").append(snap.size()).append(": ").append(snap);
-
-                throw new CayenneRuntimeException(msg.toString());
-            }
-
-            // checks have been passed, use id value
-            appendLiteralDirect(snap.get(snap.keySet().iterator().next()), attr, parentExpression);
-        } else if(val instanceof ObjectId){
-            
-            ObjectId id = (ObjectId)val;
-            
-            if (id.isTemporary()) {
-                throw new CayenneRuntimeException("Can't use NEW object as a query parameter.");
-            }
-
-            Map<String, Object> snap = id.getIdSnapshot();
-            if (snap.size() != 1) {
-                StringBuilder msg = new StringBuilder();
-                msg.append("Object must have a single primary key column ").append("to serve as a query parameter. ")
-                        .append("This object has ").append(snap.size()).append(": ").append(snap);
-
-                throw new CayenneRuntimeException(msg.toString());
-            }
-
-            // checks have been passed, use id value
-            appendLiteralDirect(snap.get(snap.keySet().iterator().next()), attr, parentExpression);
-        } else {
-            appendLiteralDirect(val, attr, parentExpression);
-        }
-    }
-
-    /**
-     * Appends SQL code to the query buffer to handle <code>val</code> as a
-     * parameter to the PreparedStatement being built. Adds <code>val</code>
-     * into QueryAssembler parameter list.
-     */
-    protected void appendLiteralDirect(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
-        out.append('?');
-
-        // we are hoping that when processing parameter list,
-        // the correct type will be
-        // guessed without looking at DbAttribute...
-        queryAssembler.addToParamList(attr, val);
-    }
-
-    /**
-     * Returns database type of expression parameters or null if it can not be
-     * determined.
-     */
-    protected DbAttribute paramsDbType(Expression e) {
-        int len = e.getOperandCount();
-
-        // for unary expressions, find parent binary - this is a hack mainly to
-        // support
-        // ASTList
-        if (len < 2) {
-
-            if (e instanceof SimpleNode) {
-                Expression parent = (Expression) ((SimpleNode) e).jjtGetParent();
-                if (parent != null) {
-                    return paramsDbType(parent);
-                }
-            }
-
-            return null;
-        }
-
-        // naive algorithm:
-
-        // if at least one of the sibling operands is a
-        // OBJ_PATH or DB_PATH expression, use its attribute type as
-        // a final answer.
-
-        // find attribute or relationship matching the value
-        DbAttribute attribute = null;
-        DbRelationship relationship = null;
-        for (int i = 0; i < len; i++) {
-            Object op = e.getOperand(i);
-
-            if (op instanceof Expression) {
-                Expression expression = (Expression) op;
-                if (expression.getType() == Expression.OBJ_PATH) {
-                    PathComponent<ObjAttribute, ObjRelationship> last = getObjEntity().lastPathComponent(expression,
-                            queryAssembler.getPathAliases());
-
-                    // TODO: handle EmbeddableAttribute
-                    // if (last instanceof EmbeddableAttribute)
-                    // break;
-
-                    if (last.getAttribute() != null) {
-                        attribute = last.getAttribute().getDbAttribute();
-                        break;
-                    } else if (last.getRelationship() != null) {
-                        List<DbRelationship> dbPath = last.getRelationship().getDbRelationships();
-                        if (dbPath.size() > 0) {
-                            relationship = dbPath.get(dbPath.size() - 1);
-                            break;
-                        }
-                    }
-                } else if (expression.getType() == Expression.DB_PATH) {
-                    PathComponent<DbAttribute, DbRelationship> last = getDbEntity().lastPathComponent(expression,
-                            queryAssembler.getPathAliases());
-                    if (last.getAttribute() != null) {
-                        attribute = last.getAttribute();
-                        break;
-                    } else if (last.getRelationship() != null) {
-                        relationship = last.getRelationship();
-                        break;
-                    }
-                }
-            }
-        }
-
-        if (attribute != null) {
-            return attribute;
-        }
-
-        if (relationship != null) {
-            // Can't properly handle multiple joins....
-            if (relationship.getJoins().size() == 1) {
-                DbJoin join = relationship.getJoins().get(0);
-                return join.getSource();
-            }
-        }
-
-        return null;
-    }
-
-    /**
-     * Processes case when an OBJ_PATH expression ends with relationship. If
-     * this is a "to many" relationship, a join is added and a column expression
-     * for the target entity primary key. If this is a "to one" relationship,
-     * column expression for the source foreign key is added.
-     * 
-     * @since 3.0
-     */
-    protected void processRelTermination(ObjRelationship rel, JoinType joinType, String joinSplitAlias)
-            throws IOException {
-
-        Iterator<DbRelationship> dbRels = rel.getDbRelationships().iterator();
-
-        // scan DbRelationships
-        while (dbRels.hasNext()) {
-            DbRelationship dbRel = dbRels.next();
-
-            // if this is a last relationship in the path,
-            // it needs special handling
-            if (!dbRels.hasNext()) {
-                processRelTermination(dbRel, joinType, joinSplitAlias);
-            } else {
-                // find and add joins ....
-                queryAssembler.dbRelationshipAdded(dbRel, joinType, joinSplitAlias);
-            }
-        }
-    }
-
-    /**
-     * Handles case when a DB_NAME expression ends with relationship. If this is
-     * a "to many" relationship, a join is added and a column expression for the
-     * target entity primary key. If this is a "to one" relationship, column
-     * expression for the source foreign key is added.
-     * 
-     * @since 3.0
-     */
-    protected void processRelTermination(DbRelationship rel, JoinType joinType, String joinSplitAlias)
-            throws IOException {
-
-        if (rel.isToMany()) {
-            // append joins
-            queryAssembler.dbRelationshipAdded(rel, joinType, joinSplitAlias);
-        }
-
-        // get last DbRelationship on the list
-        List<DbJoin> joins = rel.getJoins();
-        if (joins.size() != 1) {
-            StringBuilder msg = new StringBuilder();
-            msg.append("OBJ_PATH expressions are only supported ").append("for a single-join relationships. ")
-                    .append("This relationship has ").append(joins.size()).append(" joins.");
-
-            throw new CayenneRuntimeException(msg.toString());
-        }
-
-        DbJoin join = joins.get(0);
-
-        DbAttribute attribute = null;
-
-        if (rel.isToMany()) {
-            DbEntity ent = (DbEntity) join.getRelationship().getTargetEntity();
-            Collection<DbAttribute> pk = ent.getPrimaryKeys();
-            if (pk.size() != 1) {
-                StringBuilder msg = new StringBuilder();
-                msg.append("DB_NAME expressions can only support ").append("targets with a single column PK. ")
-                        .append("This entity has ").append(pk.size()).append(" columns in primary key.");
-
-                throw new CayenneRuntimeException(msg.toString());
-            }
-
-            attribute = pk.iterator().next();
-        } else {
-            attribute = join.getSource();
-        }
-
-        processColumn(attribute);
-    }
+	protected QueryAssembler queryAssembler;
+	protected StringBuilder out;
+	protected QuotingStrategy strategy;
+
+	/**
+	 * Creates QueryAssemblerHelper initializing with parent
+	 * {@link QueryAssembler} and output buffer object.
+	 */
+	public QueryAssemblerHelper(QueryAssembler queryAssembler) {
+		this.queryAssembler = queryAssembler;
+		strategy = queryAssembler.getAdapter().getQuotingStrategy();
+	}
+
+	public ObjEntity getObjEntity() {
+		return queryAssembler.getQueryMetadata().getObjEntity();
+	}
+
+	public DbEntity getDbEntity() {
+		return queryAssembler.getQueryMetadata().getDbEntity();
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	public StringBuilder appendPart(StringBuilder out) {
+		this.out = out;
+		doAppendPart();
+		return out;
+	}
+
+	/**
+	 * Sets ouput buffer
+	 */
+	void setOut(StringBuilder out) {
+		this.out = out;
+	}
+
+	/**
+	 * @return output buffer
+	 */
+	StringBuilder getOut() {
+		return out;
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	protected abstract void doAppendPart();
+
+	/**
+	 * <p>
+	 * Outputs the standard JDBC (database agnostic) expression for supplying
+	 * the escape character to the database server when supplying a LIKE clause.
+	 * This has been factored-out because some database adaptors handle LIKE
+	 * differently and they need access to this common method in order not to
+	 * repeat this code.
+	 * <p>
+	 * If there is no escape character defined then this method will not output
+	 * anything. An escape character of 0 will mean no escape character.
+	 * 
+	 * @since 3.1
+	 */
+	protected void appendLikeEscapeCharacter(PatternMatchNode patternMatchNode) throws IOException {
+		char escapeChar = patternMatchNode.getEscapeChar();
+
+		if ('?' == escapeChar) {
+			throw new CayenneRuntimeException("the escape character of '?' is illegal for LIKE clauses.");
+		}
+
+		if (0 != escapeChar) {
+			out.append(" {escape '");
+			out.append(escapeChar);
+			out.append("'}");
+		}
+	}
+
+	/**
+	 * Processes parts of the OBJ_PATH expression.
+	 */
+	protected void appendObjPath(Expression pathExp) {
+
+		queryAssembler.resetJoinStack();
+		String joinSplitAlias = null;
+
+		for (PathComponent<ObjAttribute, ObjRelationship> component : getObjEntity().resolvePath(pathExp,
+				queryAssembler.getPathAliases())) {
+
+			if (component.isAlias()) {
+				joinSplitAlias = component.getName();
+				for (PathComponent<ObjAttribute, ObjRelationship> aliasPart : component.getAliasedPath()) {
+
+					ObjRelationship relationship = aliasPart.getRelationship();
+
+					if (relationship == null) {
+						throw new IllegalStateException("Non-relationship aliased path part: " + aliasPart.getName());
+					}
+
+					if (aliasPart.isLast() && component.isLast()) {
+						processRelTermination(relationship, aliasPart.getJoinType(), joinSplitAlias);
+					} else {
+						// find and add joins ....
+						for (DbRelationship dbRel : relationship.getDbRelationships()) {
+							queryAssembler.dbRelationshipAdded(dbRel, aliasPart.getJoinType(), joinSplitAlias);
+						}
+					}
+				}
+
+				continue;
+			}
+
+			ObjRelationship relationship = component.getRelationship();
+			ObjAttribute attribute = component.getAttribute();
+
+			if (relationship != null) {
+
+				// if this is a last relationship in the path,
+				// it needs special handling
+				if (component.isLast()) {
+					processRelTermination(relationship, component.getJoinType(), joinSplitAlias);
+				} else {
+					// find and add joins ....
+					for (DbRelationship dbRel : relationship.getDbRelationships()) {
+						queryAssembler.dbRelationshipAdded(dbRel, component.getJoinType(), joinSplitAlias);
+					}
+				}
+			} else {
+				Iterator<CayenneMapEntry> dbPathIterator = attribute.getDbPathIterator();
+				while (dbPathIterator.hasNext()) {
+					Object pathPart = dbPathIterator.next();
+
+					if (pathPart == null) {
+						throw new CayenneRuntimeException("ObjAttribute has no component: " + attribute.getName());
+					} else if (pathPart instanceof DbRelationship) {
+						queryAssembler.dbRelationshipAdded((DbRelationship) pathPart, JoinType.INNER, joinSplitAlias);
+					} else if (pathPart instanceof DbAttribute) {
+						processColumnWithQuoteSqlIdentifiers((DbAttribute) pathPart, pathExp);
+					}
+				}
+
+			}
+		}
+	}
+
+	protected void appendDbPath(Expression pathExp) {
+
+		queryAssembler.resetJoinStack();
+		String joinSplitAlias = null;
+
+		for (PathComponent<DbAttribute, DbRelationship> component : getDbEntity().resolvePath(pathExp,
+				queryAssembler.getPathAliases())) {
+
+			if (component.isAlias()) {
+				joinSplitAlias = component.getName();
+				for (PathComponent<DbAttribute, DbRelationship> aliasPart : component.getAliasedPath()) {
+
+					DbRelationship relationship = aliasPart.getRelationship();
+
+					if (relationship == null) {
+						throw new IllegalStateException("Non-relationship aliased path part: " + aliasPart.getName());
+					}
+
+					if (aliasPart.isLast() && component.isLast()) {
+						processRelTermination(relationship, aliasPart.getJoinType(), joinSplitAlias);
+					} else {
+						queryAssembler.dbRelationshipAdded(relationship, component.getJoinType(), joinSplitAlias);
+					}
+				}
+
+				continue;
+			}
+
+			DbRelationship relationship = component.getRelationship();
+
+			if (relationship != null) {
+
+				// if this is a last relationship in the path,
+				// it needs special handling
+				if (component.isLast()) {
+					processRelTermination(relationship, component.getJoinType(), joinSplitAlias);
+				} else {
+					// find and add joins ....
+					queryAssembler.dbRelationshipAdded(relationship, component.getJoinType(), joinSplitAlias);
+				}
+			} else {
+				processColumnWithQuoteSqlIdentifiers(component.getAttribute(), pathExp);
+			}
+		}
+	}
+
+	protected void processColumn(DbAttribute dbAttr) {
+		processColumnWithQuoteSqlIdentifiers(dbAttr, null);
+	}
+
+	protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) {
+
+		String alias = (queryAssembler.supportsTableAliases()) ? queryAssembler.getCurrentAlias() : null;
+		out.append(strategy.quotedIdentifier(dbAttr.getEntity(), alias, dbAttr.getName()));
+	}
+
+	/**
+	 * Appends SQL code to the query buffer to handle <code>val</code> as a
+	 * parameter to the PreparedStatement being built. Adds <code>val</code>
+	 * into QueryAssembler parameter list.
+	 * <p>
+	 * If <code>val</code> is null, "NULL" is appended to the query.
+	 * </p>
+	 * <p>
+	 * If <code>val</code> is a DataObject, its primary key value is used as a
+	 * parameter. <i>Only objects with a single column primary key can be
+	 * used.</i>
+	 * 
+	 * @param val
+	 *            object that should be appended as a literal to the query. Must
+	 *            be of one of "standard JDBC" types, null or a DataObject.
+	 * @param attr
+	 *            DbAttribute that has information on what type of parameter is
+	 *            being appended.
+	 */
+	protected void appendLiteral(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
+
+		if (val == null) {
+			out.append("NULL");
+		} else if (val instanceof Persistent) {
+			// TODO: see cay1796
+			// This check is unlikely to happen,
+			// since Expression got ObjectId from Persistent object.
+			// Left for future research.
+			ObjectId id = ((Persistent) val).getObjectId();
+
+			// check if this id is acceptable to be a parameter
+			if (id == null) {
+				throw new CayenneRuntimeException("Can't use TRANSIENT object as a query parameter.");
+			}
+
+			if (id.isTemporary()) {
+				throw new CayenneRuntimeException("Can't use NEW object as a query parameter.");
+			}
+
+			Map<String, Object> snap = id.getIdSnapshot();
+			if (snap.size() != 1) {
+				StringBuilder msg = new StringBuilder();
+				msg.append("Object must have a single primary key column ").append("to serve as a query parameter. ")
+						.append("This object has ").append(snap.size()).append(": ").append(snap);
+
+				throw new CayenneRuntimeException(msg.toString());
+			}
+
+			// checks have been passed, use id value
+			appendLiteralDirect(snap.get(snap.keySet().iterator().next()), attr, parentExpression);
+		} else if (val instanceof ObjectId) {
+
+			ObjectId id = (ObjectId) val;
+
+			if (id.isTemporary()) {
+				throw new CayenneRuntimeException("Can't use NEW object as a query parameter.");
+			}
+
+			Map<String, Object> snap = id.getIdSnapshot();
+			if (snap.size() != 1) {
+				StringBuilder msg = new StringBuilder();
+				msg.append("Object must have a single primary key column ").append("to serve as a query parameter. ")
+						.append("This object has ").append(snap.size()).append(": ").append(snap);
+
+				throw new CayenneRuntimeException(msg.toString());
+			}
+
+			// checks have been passed, use id value
+			appendLiteralDirect(snap.get(snap.keySet().iterator().next()), attr, parentExpression);
+		} else {
+			appendLiteralDirect(val, attr, parentExpression);
+		}
+	}
+
+	/**
+	 * Appends SQL code to the query buffer to handle <code>val</code> as a
+	 * parameter to the PreparedStatement being built. Adds <code>val</code>
+	 * into QueryAssembler parameter list.
+	 */
+	protected void appendLiteralDirect(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
+		out.append('?');
+		queryAssembler.addToParamList(attr, val);
+	}
+
+	/**
+	 * Returns database type of expression parameters or null if it can not be
+	 * determined.
+	 */
+	protected DbAttribute paramsDbType(Expression e) {
+		int len = e.getOperandCount();
+
+		// for unary expressions, find parent binary - this is a hack mainly to
+		// support
+		// ASTList
+		if (len < 2) {
+
+			if (e instanceof SimpleNode) {
+				Expression parent = (Expression) ((SimpleNode) e).jjtGetParent();
+				if (parent != null) {
+					return paramsDbType(parent);
+				}
+			}
+
+			return null;
+		}
+
+		// naive algorithm:
+
+		// if at least one of the sibling operands is a
+		// OBJ_PATH or DB_PATH expression, use its attribute type as
+		// a final answer.
+
+		// find attribute or relationship matching the value
+		DbAttribute attribute = null;
+		DbRelationship relationship = null;
+		for (int i = 0; i < len; i++) {
+			Object op = e.getOperand(i);
+
+			if (op instanceof Expression) {
+				Expression expression = (Expression) op;
+				if (expression.getType() == Expression.OBJ_PATH) {
+					PathComponent<ObjAttribute, ObjRelationship> last = getObjEntity().lastPathComponent(expression,
+							queryAssembler.getPathAliases());
+
+					// TODO: handle EmbeddableAttribute
+					// if (last instanceof EmbeddableAttribute)
+					// break;
+
+					if (last.getAttribute() != null) {
+						attribute = last.getAttribute().getDbAttribute();
+						break;
+					} else if (last.getRelationship() != null) {
+						List<DbRelationship> dbPath = last.getRelationship().getDbRelationships();
+						if (dbPath.size() > 0) {
+							relationship = dbPath.get(dbPath.size() - 1);
+							break;
+						}
+					}
+				} else if (expression.getType() == Expression.DB_PATH) {
+					PathComponent<DbAttribute, DbRelationship> last = getDbEntity().lastPathComponent(expression,
+							queryAssembler.getPathAliases());
+					if (last.getAttribute() != null) {
+						attribute = last.getAttribute();
+						break;
+					} else if (last.getRelationship() != null) {
+						relationship = last.getRelationship();
+						break;
+					}
+				}
+			}
+		}
+
+		if (attribute != null) {
+			return attribute;
+		}
+
+		if (relationship != null) {
+			// Can't properly handle multiple joins....
+			if (relationship.getJoins().size() == 1) {
+				DbJoin join = relationship.getJoins().get(0);
+				return join.getSource();
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Processes case when an OBJ_PATH expression ends with relationship. If
+	 * this is a "to many" relationship, a join is added and a column expression
+	 * for the target entity primary key. If this is a "to one" relationship,
+	 * column expression for the source foreign key is added.
+	 * 
+	 * @since 3.0
+	 */
+	protected void processRelTermination(ObjRelationship rel, JoinType joinType, String joinSplitAlias) {
+
+		Iterator<DbRelationship> dbRels = rel.getDbRelationships().iterator();
+
+		// scan DbRelationships
+		while (dbRels.hasNext()) {
+			DbRelationship dbRel = dbRels.next();
+
+			// if this is a last relationship in the path,
+			// it needs special handling
+			if (!dbRels.hasNext()) {
+				processRelTermination(dbRel, joinType, joinSplitAlias);
+			} else {
+				// find and add joins ....
+				queryAssembler.dbRelationshipAdded(dbRel, joinType, joinSplitAlias);
+			}
+		}
+	}
+
+	/**
+	 * Handles case when a DB_NAME expression ends with relationship. If this is
+	 * a "to many" relationship, a join is added and a column expression for the
+	 * target entity primary key. If this is a "to one" relationship, column
+	 * expression for the source foreign key is added.
+	 * 
+	 * @since 3.0
+	 */
+	protected void processRelTermination(DbRelationship rel, JoinType joinType, String joinSplitAlias) {
+
+		if (rel.isToMany()) {
+			// append joins
+			queryAssembler.dbRelationshipAdded(rel, joinType, joinSplitAlias);
+		}
+
+		// get last DbRelationship on the list
+		List<DbJoin> joins = rel.getJoins();
+		if (joins.size() != 1) {
+			StringBuilder msg = new StringBuilder();
+			msg.append("OBJ_PATH expressions are only supported ").append("for a single-join relationships. ")
+					.append("This relationship has ").append(joins.size()).append(" joins.");
+
+			throw new CayenneRuntimeException(msg.toString());
+		}
+
+		DbJoin join = joins.get(0);
+
+		DbAttribute attribute = null;
+
+		if (rel.isToMany()) {
+			DbEntity ent = (DbEntity) join.getRelationship().getTargetEntity();
+			Collection<DbAttribute> pk = ent.getPrimaryKeys();
+			if (pk.size() != 1) {
+				StringBuilder msg = new StringBuilder();
+				msg.append("DB_NAME expressions can only support ").append("targets with a single column PK. ")
+						.append("This entity has ").append(pk.size()).append(" columns in primary key.");
+
+				throw new CayenneRuntimeException(msg.toString());
+			}
+
+			attribute = pk.iterator().next();
+		} else {
+			attribute = join.getSource();
+		}
+
+		processColumn(attribute);
+	}
 }


[2/7] cayenne git commit: Refactoring SelectTranslator for better extensibility

Posted by aa...@apache.org.
http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java
new file mode 100644
index 0000000..612f13a
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java
@@ -0,0 +1,787 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.select;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.ExpressionException;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.log.JdbcEventLogger;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.query.PrefetchTreeNode;
+import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.SortOrder;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.testdo.testmap.ArtistExhibit;
+import org.apache.cayenne.testdo.testmap.CompoundPainting;
+import org.apache.cayenne.testdo.testmap.Painting;
+import org.apache.cayenne.unit.UnitDbAdapter;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class DefaultSelectTranslatorIT extends ServerCase {
+
+	@Inject
+	private DataContext context;
+
+	@Inject
+	private UnitDbAdapter unitAdapter;
+
+	@Inject
+	private DBHelper dbHelper;
+
+	@Inject
+	private JdbcEventLogger logger;
+
+	@Inject
+	private DataNode dataNode;
+
+	/**
+	 * Tests query creation with qualifier and ordering.
+	 */
+	@Test
+	public void testCreateSqlString1() throws Exception {
+		// query with qualifier and ordering
+		SelectQuery<Artist> q = new SelectQuery<Artist>(Artist.class, ExpressionFactory.likeExp("artistName", "a%"));
+		q.addOrdering("dateOfBirth", SortOrder.ASCENDING);
+
+		String generatedSql = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver())
+				.getSql();
+
+		// do some simple assertions to make sure all parts are in
+		assertNotNull(generatedSql);
+		assertTrue(generatedSql.startsWith("SELECT "));
+		assertTrue(generatedSql.indexOf(" FROM ") > 0);
+		assertTrue(generatedSql.indexOf(" WHERE ") > generatedSql.indexOf(" FROM "));
+		assertTrue(generatedSql.indexOf(" ORDER BY ") > generatedSql.indexOf(" WHERE "));
+	}
+
+	/**
+	 * Tests query creation with qualifier and ordering.
+	 */
+	@Test
+	public void testDbEntityQualifier() throws Exception {
+
+		SelectQuery<Artist> q = new SelectQuery<Artist>(Artist.class);
+		final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
+		final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
+
+		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+				dataNode.getEntityResolver());
+
+		entity.setQualifier(ExpressionFactory.exp("ARTIST_NAME = \"123\""));
+		middleEntity.setQualifier(ExpressionFactory.exp("GROUP_ID = 1987"));
+
+		try {
+
+			String generatedSql = transl.getSql();
+
+			// do some simple assertions to make sure all parts are in
+			assertNotNull(generatedSql);
+			assertTrue(generatedSql.startsWith("SELECT "));
+			assertTrue(generatedSql.indexOf(" FROM ") > 0);
+			if (generatedSql.contains("RTRIM")) {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
+			} else if (generatedSql.contains("TRIM")) {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
+			} else {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
+			}
+		} finally {
+			entity.setQualifier(null);
+			middleEntity.setQualifier(null);
+		}
+	}
+
+	@Test
+	public void testDbEntityQualifier_OuterJoin() throws Exception {
+
+		SelectQuery<Painting> q = SelectQuery.query(Painting.class);
+		q.addOrdering("toArtist+.artistName", SortOrder.ASCENDING);
+
+		final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
+		final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
+
+		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+				dataNode.getEntityResolver());
+
+		entity.setQualifier(ExpressionFactory.exp("ARTIST_NAME = \"123\""));
+		middleEntity.setQualifier(ExpressionFactory.exp("GROUP_ID = 1987"));
+
+		try {
+
+			String generatedSql = transl.getSql();
+
+			// do some simple assertions to make sure all parts are in
+			assertNotNull(generatedSql);
+			assertTrue(generatedSql.startsWith("SELECT "));
+			assertTrue(generatedSql.indexOf(" FROM ") > 0);
+			if (generatedSql.contains("RTRIM")) {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
+			} else if (generatedSql.contains("TRIM")) {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
+			} else {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
+			}
+
+		} finally {
+			entity.setQualifier(null);
+			middleEntity.setQualifier(null);
+		}
+	}
+
+	@Test
+	public void testDbEntityQualifier_FlattenedRel() throws Exception {
+
+		SelectQuery<Artist> q = new SelectQuery<Artist>(Artist.class, ExpressionFactory.matchExp("groupArray.name",
+				"bar"));
+
+		final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
+		final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
+
+		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+				dataNode.getEntityResolver());
+
+		entity.setQualifier(ExpressionFactory.exp("ARTIST_NAME = \"123\""));
+		middleEntity.setQualifier(ExpressionFactory.exp("GROUP_ID = 1987"));
+
+		try {
+
+			String generatedSql = transl.getSql();
+
+			// do some simple assertions to make sure all parts are in
+			assertNotNull(generatedSql);
+			assertTrue(generatedSql.startsWith("SELECT "));
+			assertTrue(generatedSql.indexOf(" FROM ") > 0);
+			if (generatedSql.contains("RTRIM")) {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
+			} else if (generatedSql.contains("TRIM")) {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
+			} else {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
+			}
+
+		} finally {
+			entity.setQualifier(null);
+			middleEntity.setQualifier(null);
+		}
+	}
+
+	@Test
+	public void testDbEntityQualifier_RelatedMatch() throws Exception {
+
+		SelectQuery<Artist> q = new SelectQuery(Painting.class,
+				ExpressionFactory.matchExp("toArtist.artistName", "foo"));
+
+		final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
+		final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
+
+		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+				dataNode.getEntityResolver());
+
+		entity.setQualifier(ExpressionFactory.exp("ARTIST_NAME = \"123\""));
+		middleEntity.setQualifier(ExpressionFactory.exp("GROUP_ID = 1987"));
+
+		try {
+
+			String generatedSql = transl.getSql();
+
+			// do some simple assertions to make sure all parts are in
+			assertNotNull(generatedSql);
+			assertTrue(generatedSql.startsWith("SELECT "));
+			assertTrue(generatedSql.indexOf(" FROM ") > 0);
+			if (generatedSql.contains("RTRIM")) {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
+			} else if (generatedSql.contains("TRIM")) {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
+			} else {
+				assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
+			}
+
+		} finally {
+			entity.setQualifier(null);
+			middleEntity.setQualifier(null);
+		}
+	}
+
+	/**
+	 * Tests query creation with "distinct" specified.
+	 */
+	@Test
+	public void testCreateSqlString2() throws Exception {
+		// query with "distinct" set
+		SelectQuery q = new SelectQuery(Artist.class);
+		q.setDistinct(true);
+
+		String generatedSql = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver())
+				.getSql();
+
+		// do some simple assertions to make sure all parts are in
+		assertNotNull(generatedSql);
+		assertTrue(generatedSql.startsWith("SELECT DISTINCT"));
+	}
+
+	/**
+	 * Test aliases when the same table used in more then 1 relationship. Check
+	 * translation of relationship path "ArtistExhibit.toArtist.artistName" and
+	 * "ArtistExhibit.toExhibit.toGallery.paintingArray.toArtist.artistName".
+	 */
+	@Test
+	public void testCreateSqlString5() throws Exception {
+		// query with qualifier and ordering
+		SelectQuery q = new SelectQuery(ArtistExhibit.class);
+		q.setQualifier(ExpressionFactory.likeExp("toArtist.artistName", "a%"));
+		q.andQualifier(ExpressionFactory.likeExp("toExhibit.toGallery.paintingArray.toArtist.artistName", "a%"));
+
+		String generatedSql = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver())
+				.getSql();
+		// logObj.warn("Query: " + generatedSql);
+
+		// do some simple assertions to make sure all parts are in
+		assertNotNull(generatedSql);
+		assertTrue(generatedSql.startsWith("SELECT "));
+		assertTrue(generatedSql.indexOf(" FROM ") > 0);
+		assertTrue(generatedSql.indexOf(" WHERE ") > generatedSql.indexOf(" FROM "));
+
+		// check that there are 2 distinct aliases for the ARTIST table
+		int ind1 = generatedSql.indexOf("ARTIST t", generatedSql.indexOf(" FROM "));
+		assertTrue(ind1 > 0);
+
+		int ind2 = generatedSql.indexOf("ARTIST t", ind1 + 1);
+		assertTrue(ind2 > 0);
+
+		assertTrue(generatedSql.charAt(ind1 + "ARTIST t".length()) != generatedSql.charAt(ind2 + "ARTIST t".length()));
+	}
+
+	/**
+	 * Test aliases when the same table used in more then 1 relationship. Check
+	 * translation of relationship path "ArtistExhibit.toArtist.artistName" and
+	 * "ArtistExhibit.toArtist.paintingArray.paintingTitle".
+	 */
+	@Test
+	public void testCreateSqlString6() throws Exception {
+		// query with qualifier and ordering
+		SelectQuery q = new SelectQuery(ArtistExhibit.class);
+		q.setQualifier(ExpressionFactory.likeExp("toArtist.artistName", "a%"));
+		q.andQualifier(ExpressionFactory.likeExp("toArtist.paintingArray.paintingTitle", "p%"));
+
+		String generatedSql = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver())
+				.getSql();
+
+		// do some simple assertions to make sure all parts are in
+		assertNotNull(generatedSql);
+		assertTrue(generatedSql.startsWith("SELECT "));
+		assertTrue(generatedSql.indexOf(" FROM ") > 0);
+		assertTrue(generatedSql.indexOf(" WHERE ") > generatedSql.indexOf(" FROM "));
+
+		// check that there is only one distinct alias for the ARTIST
+		// table
+		int ind1 = generatedSql.indexOf("ARTIST t", generatedSql.indexOf(" FROM "));
+		assertTrue(ind1 > 0);
+
+		int ind2 = generatedSql.indexOf("ARTIST t", ind1 + 1);
+		assertTrue(generatedSql, ind2 < 0);
+	}
+
+	/**
+	 * Test query when qualifying on the same attribute more than once. Check
+	 * translation "Artist.dateOfBirth > ? AND Artist.dateOfBirth < ?".
+	 */
+	@Test
+	public void testCreateSqlString7() throws Exception {
+		SelectQuery q = new SelectQuery(Artist.class);
+		q.setQualifier(ExpressionFactory.greaterExp("dateOfBirth", new Date()));
+		q.andQualifier(ExpressionFactory.lessExp("dateOfBirth", new Date()));
+
+		String generatedSql = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver())
+				.getSql();
+		// logObj.warn("Query: " + generatedSql);
+
+		// do some simple assertions to make sure all parts are in
+		assertNotNull(generatedSql);
+		assertTrue(generatedSql.startsWith("SELECT "));
+
+		int i1 = generatedSql.indexOf(" FROM ");
+		assertTrue(i1 > 0);
+
+		int i2 = generatedSql.indexOf(" WHERE ");
+		assertTrue(i2 > i1);
+
+		int i3 = generatedSql.indexOf("DATE_OF_BIRTH", i2 + 1);
+		assertTrue(i3 > i2);
+
+		int i4 = generatedSql.indexOf("DATE_OF_BIRTH", i3 + 1);
+		assertTrue("No second DOB comparison: " + i4 + ", " + i3, i4 > i3);
+	}
+
+	/**
+	 * Test query when qualifying on the same attribute accessed over
+	 * relationship, more than once. Check translation
+	 * "Painting.toArtist.dateOfBirth > ? AND Painting.toArtist.dateOfBirth <
+	 * ?".
+	 */
+	@Test
+	public void testCreateSqlString8() throws Exception {
+		SelectQuery q = new SelectQuery();
+		q.setRoot(Painting.class);
+		q.setQualifier(ExpressionFactory.greaterExp("toArtist.dateOfBirth", new Date()));
+		q.andQualifier(ExpressionFactory.lessExp("toArtist.dateOfBirth", new Date()));
+
+		String generatedSql = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver())
+				.getSql();
+
+		// do some simple assertions to make sure all parts are in
+		assertNotNull(generatedSql);
+		assertTrue(generatedSql.startsWith("SELECT "));
+
+		int i1 = generatedSql.indexOf(" FROM ");
+		assertTrue(i1 > 0);
+
+		int i2 = generatedSql.indexOf(" WHERE ");
+		assertTrue(i2 > i1);
+
+		int i3 = generatedSql.indexOf("DATE_OF_BIRTH", i2 + 1);
+		assertTrue(i3 > i2);
+
+		int i4 = generatedSql.indexOf("DATE_OF_BIRTH", i3 + 1);
+		assertTrue("No second DOB comparison: " + i4 + ", " + i3, i4 > i3);
+	}
+
+	@Test
+	public void testCreateSqlString9() throws Exception {
+		// query for a compound ObjEntity with qualifier
+		SelectQuery q = new SelectQuery(CompoundPainting.class, ExpressionFactory.likeExp("artistName", "a%"));
+
+		String sql = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver()).getSql();
+
+		// do some simple assertions to make sure all parts are in
+		assertNotNull(sql);
+		assertTrue(sql.startsWith("SELECT "));
+
+		int i1 = sql.indexOf(" FROM ");
+		assertTrue(i1 > 0);
+
+		int i2 = sql.indexOf("PAINTING");
+		assertTrue(i2 > 0);
+
+		int i3 = sql.indexOf("ARTIST");
+		assertTrue(i3 > 0);
+
+		int i4 = sql.indexOf("GALLERY");
+		assertTrue(i4 > 0);
+
+		int i5 = sql.indexOf("PAINTING_INFO");
+		assertTrue(i5 > 0);
+
+		int i6 = sql.indexOf("ARTIST_NAME");
+		assertTrue(i6 > 0);
+
+		int i7 = sql.indexOf("ESTIMATED_PRICE");
+		assertTrue(i7 > 0);
+
+		int i8 = sql.indexOf("GALLERY_NAME");
+		assertTrue(i8 > 0);
+
+		int i9 = sql.indexOf("PAINTING_TITLE");
+		assertTrue(i9 > 0);
+
+		int i10 = sql.indexOf("TEXT_REVIEW");
+		assertTrue(i10 > 0);
+
+		int i11 = sql.indexOf("PAINTING_ID");
+		assertTrue(i11 > 0);
+
+		int i12 = sql.indexOf("ARTIST_ID");
+		assertTrue(i12 > 0);
+
+		int i13 = sql.indexOf("GALLERY_ID");
+		assertTrue(i13 > 0);
+
+	}
+
+	@Test
+	public void testCreateSqlString10() throws Exception {
+		// query with to-many joint prefetches
+		SelectQuery q = new SelectQuery(Artist.class);
+		q.addPrefetch(Artist.PAINTING_ARRAY_PROPERTY).setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+
+		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+				dataNode.getEntityResolver());
+		String sql = transl.getSql();
+		assertNotNull(sql);
+		assertTrue(sql.startsWith("SELECT "));
+
+		int i1 = sql.indexOf("ARTIST_ID");
+		assertTrue(sql, i1 > 0);
+
+		int i2 = sql.indexOf("FROM");
+		assertTrue(sql, i2 > 0);
+
+		assertTrue(sql, sql.indexOf("PAINTING_ID") > 0);
+
+		// assert we have one join
+		assertEquals(1, transl.joinStack.size());
+	}
+
+	@Test
+	public void testCreateSqlString11() throws Exception {
+		// query with joint prefetches and other joins
+		SelectQuery q = new SelectQuery(Artist.class, ExpressionFactory.exp("paintingArray.paintingTitle = 'a'"));
+		q.addPrefetch(Artist.PAINTING_ARRAY_PROPERTY).setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+
+		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+				dataNode.getEntityResolver());
+
+		transl.getSql();
+
+		// assert we only have one join
+		assertEquals(2, transl.joinStack.size());
+	}
+
+	@Test
+	public void testCreateSqlString12() throws Exception {
+		// query with to-one joint prefetches
+		SelectQuery q = new SelectQuery(Painting.class);
+		q.addPrefetch(Painting.TO_ARTIST_PROPERTY).setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+
+		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+				dataNode.getEntityResolver());
+
+		String sql = transl.getSql();
+		assertNotNull(sql);
+		assertTrue(sql.startsWith("SELECT "));
+
+		int i1 = sql.indexOf("ARTIST_ID");
+		assertTrue(sql, i1 > 0);
+
+		int i2 = sql.indexOf("FROM");
+		assertTrue(sql, i2 > 0);
+
+		assertTrue(sql, sql.indexOf("PAINTING_ID") > 0);
+
+		// assert we have one join
+		assertEquals(1, transl.joinStack.size());
+	}
+
+	@Test
+	public void testCreateSqlString13() throws Exception {
+		// query with invalid joint prefetches
+		SelectQuery q = new SelectQuery(Painting.class);
+		q.addPrefetch("invalid.invalid").setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+
+		try {
+			new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver()).getSql();
+			fail("Invalid jointPrefetch must have thrown...");
+		} catch (ExpressionException e) {
+			// expected
+		}
+	}
+
+	@Test
+	public void testCreateSqlStringWithQuoteSqlIdentifiers() throws Exception {
+
+		try {
+			SelectQuery q = new SelectQuery(Artist.class);
+			DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
+			entity.getDataMap().setQuotingSQLIdentifiers(true);
+			q.addOrdering("dateOfBirth", SortOrder.ASCENDING);
+
+			String charStart = unitAdapter.getIdentifiersStartQuote();
+			String charEnd = unitAdapter.getIdentifiersEndQuote();
+
+			String s = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver()).getSql();
+			assertTrue(s.startsWith("SELECT "));
+			int iFrom = s.indexOf(" FROM ");
+			assertTrue(iFrom > 0);
+			int artistName = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_NAME" + charEnd);
+			assertTrue(artistName > 0 && artistName < iFrom);
+			int artistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
+			assertTrue(artistId > 0 && artistId < iFrom);
+			int dateOfBirth = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd);
+			assertTrue(dateOfBirth > 0 && dateOfBirth < iFrom);
+			int iArtist = s.indexOf(charStart + "ARTIST" + charEnd + " " + charStart + "t0" + charEnd);
+			assertTrue(iArtist > iFrom);
+			int iOrderBy = s.indexOf(" ORDER BY ");
+			int dateOfBirth2 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd,
+					iOrderBy);
+			assertTrue(iOrderBy > iArtist);
+			assertTrue(dateOfBirth2 > iOrderBy);
+
+		} finally {
+			DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
+			entity.getDataMap().setQuotingSQLIdentifiers(false);
+		}
+
+	}
+
+	@Test
+	public void testCreateSqlStringWithQuoteSqlIdentifiers2() throws Exception {
+
+		try {
+			SelectQuery q = new SelectQuery(Artist.class);
+			DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
+			entity.getDataMap().setQuotingSQLIdentifiers(true);
+			q.setQualifier(ExpressionFactory.greaterExp("dateOfBirth", new Date()));
+			q.andQualifier(ExpressionFactory.lessExp("dateOfBirth", new Date()));
+
+			String charStart = unitAdapter.getIdentifiersStartQuote();
+			String charEnd = unitAdapter.getIdentifiersEndQuote();
+
+			String s = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver()).getSql();
+
+			assertTrue(s.startsWith("SELECT "));
+			int iFrom = s.indexOf(" FROM ");
+			assertTrue(iFrom > 0);
+			int artistName = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_NAME" + charEnd);
+			assertTrue(artistName > 0 && artistName < iFrom);
+			int artistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
+			assertTrue(artistId > 0 && artistId < iFrom);
+			int dateOfBirth = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd);
+			assertTrue(dateOfBirth > 0 && dateOfBirth < iFrom);
+			int iArtist = s.indexOf(charStart + "ARTIST" + charEnd + " " + charStart + "t0" + charEnd);
+			assertTrue(iArtist > iFrom);
+			int iWhere = s.indexOf(" WHERE ");
+			assertTrue(iWhere > iArtist);
+
+			int dateOfBirth2 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd
+					+ " > ?");
+			assertTrue(dateOfBirth2 > iWhere);
+
+			int iAnd = s.indexOf(" AND ");
+			assertTrue(iAnd > iWhere);
+			int dateOfBirth3 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd
+					+ " < ?");
+			assertTrue(dateOfBirth3 > iAnd);
+
+		} finally {
+			DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
+			entity.getDataMap().setQuotingSQLIdentifiers(false);
+		}
+	}
+
+	@Test
+	public void testCreateSqlStringWithQuoteSqlIdentifiers3() throws Exception {
+
+		// query with joint prefetches and other joins
+		// and with QuoteSqlIdentifiers = true
+		try {
+			SelectQuery q = new SelectQuery(Artist.class, ExpressionFactory.exp("paintingArray.paintingTitle = 'a'"));
+			q.addPrefetch(Artist.PAINTING_ARRAY.joint());
+
+			DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
+			entity.getDataMap().setQuotingSQLIdentifiers(true);
+
+			String charStart = unitAdapter.getIdentifiersStartQuote();
+			String charEnd = unitAdapter.getIdentifiersEndQuote();
+
+			String s = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver()).getSql();
+
+			assertTrue(s.startsWith("SELECT DISTINCT "));
+			int iFrom = s.indexOf(" FROM ");
+			assertTrue(iFrom > 0);
+			int artistName = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_NAME" + charEnd);
+			assertTrue(artistName > 0 && artistName < iFrom);
+			int artistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
+			assertTrue(artistId > 0 && artistId < iFrom);
+			int dateOfBirth = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd);
+			assertTrue(dateOfBirth > 0 && dateOfBirth < iFrom);
+			int estimatedPrice = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ESTIMATED_PRICE" + charEnd);
+			assertTrue(estimatedPrice > 0 && estimatedPrice < iFrom);
+			int paintingDescription = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "PAINTING_DESCRIPTION"
+					+ charEnd);
+			assertTrue(paintingDescription > 0 && paintingDescription < iFrom);
+			int paintingTitle = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "PAINTING_TITLE" + charEnd);
+			assertTrue(paintingTitle > 0 && paintingTitle < iFrom);
+			int artistIdT1 = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
+			assertTrue(artistIdT1 > 0 && artistIdT1 < iFrom);
+			int galleryId = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "GALLERY_ID" + charEnd);
+			assertTrue(galleryId > 0 && galleryId < iFrom);
+			int paintingId = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "PAINTING_ID" + charEnd);
+			assertTrue(paintingId > 0 && paintingId < iFrom);
+			int iArtist = s.indexOf(charStart + "ARTIST" + charEnd + " " + charStart + "t0" + charEnd);
+			assertTrue(iArtist > iFrom);
+			int iLeftJoin = s.indexOf("LEFT JOIN");
+			assertTrue(iLeftJoin > iFrom);
+			int iPainting = s.indexOf(charStart + "PAINTING" + charEnd + " " + charStart + "t1" + charEnd);
+			assertTrue(iPainting > iLeftJoin);
+			int iOn = s.indexOf(" ON ");
+			assertTrue(iOn > iLeftJoin);
+			int iArtistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
+			assertTrue(iArtistId > iOn);
+			int iArtistIdT1 = s
+					.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
+			assertTrue(iArtistIdT1 > iOn);
+			int i = s.indexOf("=", iLeftJoin);
+			assertTrue(iArtistIdT1 > i || iArtistId > i);
+			int iJoin = s.indexOf("JOIN");
+			assertTrue(iJoin > iLeftJoin);
+			int iPainting2 = s.indexOf(charStart + "PAINTING" + charEnd + " " + charStart + "t2" + charEnd);
+			assertTrue(iPainting2 > iJoin);
+			int iOn2 = s.indexOf(" ON ");
+			assertTrue(iOn2 > iJoin);
+			int iArtistId2 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iJoin);
+			assertTrue(iArtistId2 > iOn2);
+			int iArtistId2T2 = s.indexOf(charStart + "t2" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iJoin);
+			assertTrue(iArtistId2T2 > iOn2);
+			int i2 = s.indexOf("=", iJoin);
+			assertTrue(iArtistId2T2 > i2 || iArtistId2 > i2);
+			int iWhere = s.indexOf(" WHERE ");
+			assertTrue(iWhere > iJoin);
+
+			int paintingTitle2 = s.indexOf(charStart + "t2" + charEnd + "." + charStart + "PAINTING_TITLE" + charEnd
+					+ " = ?");
+			assertTrue(paintingTitle2 > iWhere);
+
+		} finally {
+			DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
+			entity.getDataMap().setQuotingSQLIdentifiers(false);
+		}
+	}
+
+	@Test
+	public void testCreateSqlStringWithQuoteSqlIdentifiers4() throws Exception {
+
+		// query with to-one joint prefetches
+		// and with QuoteSqlIdentifiers = true
+		try {
+			SelectQuery q = new SelectQuery(Painting.class);
+			q.addPrefetch(Painting.TO_ARTIST_PROPERTY).setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+
+			DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
+			entity.getDataMap().setQuotingSQLIdentifiers(true);
+
+			String charStart = unitAdapter.getIdentifiersStartQuote();
+			String charEnd = unitAdapter.getIdentifiersEndQuote();
+
+			String s = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver()).getSql();
+
+			assertTrue(s.startsWith("SELECT "));
+			int iFrom = s.indexOf(" FROM ");
+			assertTrue(iFrom > 0);
+
+			int paintingDescription = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "PAINTING_DESCRIPTION"
+					+ charEnd);
+			assertTrue(paintingDescription > 0 && paintingDescription < iFrom);
+			int paintingTitle = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "PAINTING_TITLE" + charEnd);
+			assertTrue(paintingTitle > 0 && paintingTitle < iFrom);
+			int artistIdT1 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
+			assertTrue(artistIdT1 > 0 && artistIdT1 < iFrom);
+			int estimatedPrice = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ESTIMATED_PRICE" + charEnd);
+			assertTrue(estimatedPrice > 0 && estimatedPrice < iFrom);
+			int galleryId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "GALLERY_ID" + charEnd);
+			assertTrue(galleryId > 0 && galleryId < iFrom);
+			int paintingId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "PAINTING_ID" + charEnd);
+			assertTrue(paintingId > 0 && paintingId < iFrom);
+			int artistName = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_NAME" + charEnd);
+			assertTrue(artistName > 0 && artistName < iFrom);
+			int artistId = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
+			assertTrue(artistId > 0 && artistId < iFrom);
+			int dateOfBirth = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd);
+			assertTrue(dateOfBirth > 0 && dateOfBirth < iFrom);
+			int iPainting = s.indexOf(charStart + "PAINTING" + charEnd + " " + charStart + "t0" + charEnd);
+			assertTrue(iPainting > iFrom);
+
+			int iLeftJoin = s.indexOf("LEFT JOIN");
+			assertTrue(iLeftJoin > iFrom);
+			int iArtist = s.indexOf(charStart + "ARTIST" + charEnd + " " + charStart + "t1" + charEnd);
+			assertTrue(iArtist > iLeftJoin);
+			int iOn = s.indexOf(" ON ");
+			assertTrue(iOn > iLeftJoin);
+			int iArtistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
+			assertTrue(iArtistId > iOn);
+			int iArtistIdT1 = s
+					.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
+			assertTrue(iArtistIdT1 > iOn);
+			int i = s.indexOf("=", iLeftJoin);
+			assertTrue(iArtistIdT1 > i || iArtistId > i);
+
+		} finally {
+			DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
+			entity.getDataMap().setQuotingSQLIdentifiers(false);
+		}
+	}
+
+	/**
+	 * Tests columns generated for a simple object query.
+	 */
+	@Test
+	public void testBuildResultColumns1() throws Exception {
+		SelectQuery q = new SelectQuery(Painting.class);
+		DefaultSelectTranslator tr = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver());
+
+		List<?> columns = tr.buildResultColumns();
+
+		// all DbAttributes must be included
+		DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
+		for (final DbAttribute a : entity.getAttributes()) {
+			ColumnDescriptor c = new ColumnDescriptor(a, "t0");
+			assertTrue("No descriptor for " + a + ", columns: " + columns, columns.contains(c));
+		}
+	}
+
+	/**
+	 * Tests columns generated for an object query with joint prefetch.
+	 */
+	@Test
+	public void testBuildResultColumns2() throws Exception {
+		SelectQuery q = new SelectQuery(Painting.class);
+		q.addPrefetch(Painting.TO_ARTIST_PROPERTY).setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+		DefaultSelectTranslator tr = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver());
+
+		List<?> columns = tr.buildResultColumns();
+
+		// assert root entity columns
+		DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
+		for (final DbAttribute a : entity.getAttributes()) {
+			ColumnDescriptor c = new ColumnDescriptor(a, "t0");
+			assertTrue("No descriptor for " + a + ", columns: " + columns, columns.contains(c));
+		}
+
+		// assert joined columns
+		DbEntity joined = context.getEntityResolver().getDbEntity("ARTIST");
+		for (final DbAttribute a : joined.getAttributes()) {
+
+			// skip ARTIST PK, it is joined from painting
+			if (Artist.ARTIST_ID_PK_COLUMN.equals(a.getName())) {
+				continue;
+			}
+
+			ColumnDescriptor c = new ColumnDescriptor(a, "t1");
+			c.setDataRowKey("toArtist." + a.getName());
+			assertTrue("No descriptor for " + a + ", columns: " + columns, columns.contains(c));
+		}
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingTranslatorIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingTranslatorIT.java
index c051637..f2784f4 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingTranslatorIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingTranslatorIT.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.access.translator.select;
 
+import static org.junit.Assert.assertTrue;
+
 import org.apache.cayenne.TranslationCase;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.di.Inject;
@@ -29,162 +31,144 @@ import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.ServerCaseDataSourceFactory;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
-import java.sql.Connection;
-
-import static org.junit.Assert.assertTrue;
-
 @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
 public class OrderingTranslatorIT extends ServerCase {
 
-    @Inject
-    private DataNode node;
-
-    @Inject
-    private ServerCaseDataSourceFactory dataSourceFactory;
-
-    private Connection connection;
-
-    @Before
-    public void setUp() throws Exception {
-        this.connection = dataSourceFactory.getSharedDataSource().getConnection();
-    }
+	@Inject
+	private DataNode node;
 
-    @After
-    public void tearDown() throws Exception {
-        connection.close();
-    }
+	@Inject
+	private ServerCaseDataSourceFactory dataSourceFactory;
 
-    /**
-     * Tests ascending ordering on string attribute.
-     */
-    @Test
-    public void testDoTranslation1() throws Exception {
-        SelectQuery q = new SelectQuery(Artist.class);
-        q.addOrdering("artistName", SortOrder.ASCENDING);
+	/**
+	 * Tests ascending ordering on string attribute.
+	 */
+	@Test
+	public void testDoTranslation1() throws Exception {
+		SelectQuery q = new SelectQuery(Artist.class);
+		q.addOrdering("artistName", SortOrder.ASCENDING);
 
-        TstQueryAssembler qa = new TstQueryAssembler(q, node, connection);
+		TstQueryAssembler qa = new TstQueryAssembler(q, node.getAdapter(), node.getEntityResolver());
 
-        TranslationCase tstCase = new TranslationCase("Artist", null, "ta.ARTIST_NAME");
+		TranslationCase tstCase = new TranslationCase("Artist", null, "ta.ARTIST_NAME");
 
-        StringBuilder out = new StringBuilder();
-        new OrderingTranslator(qa).appendPart(out);
+		StringBuilder out = new StringBuilder();
+		new OrderingTranslator(qa).appendPart(out);
 
-        assertTrue(out.length() > 0);
-        tstCase.assertTranslatedWell(out.toString());
-    }
+		assertTrue(out.length() > 0);
+		tstCase.assertTranslatedWell(out.toString());
+	}
 
-    /**
-     * Tests descending ordering on string attribute.
-     */
-    @Test
-    public void testDoTranslation2() throws Exception {
-        SelectQuery q = new SelectQuery(Artist.class);
-        q.addOrdering("artistName", SortOrder.DESCENDING);
+	/**
+	 * Tests descending ordering on string attribute.
+	 */
+	@Test
+	public void testDoTranslation2() throws Exception {
+		SelectQuery q = new SelectQuery(Artist.class);
+		q.addOrdering("artistName", SortOrder.DESCENDING);
 
-        TstQueryAssembler qa = new TstQueryAssembler(q, node, connection);
+		TstQueryAssembler qa = new TstQueryAssembler(q, node.getAdapter(), node.getEntityResolver());
 
-        TranslationCase tstCase = new TranslationCase("Artist", null, "ta.ARTIST_NAME DESC");
+		TranslationCase tstCase = new TranslationCase("Artist", null, "ta.ARTIST_NAME DESC");
 
-        StringBuilder out = new StringBuilder();
-        new OrderingTranslator(qa).appendPart(out);
+		StringBuilder out = new StringBuilder();
+		new OrderingTranslator(qa).appendPart(out);
 
-        assertTrue(out.length() > 0);
-        tstCase.assertTranslatedWell(out.toString());
-    }
+		assertTrue(out.length() > 0);
+		tstCase.assertTranslatedWell(out.toString());
+	}
 
-    /**
-     * Tests ascending case-insensitive ordering on string attribute.
-     */
-    @Test
-    public void testDoTranslation4() throws Exception {
-        SelectQuery q = new SelectQuery(Artist.class);
-        q.addOrdering("artistName", SortOrder.ASCENDING_INSENSITIVE);
+	/**
+	 * Tests ascending case-insensitive ordering on string attribute.
+	 */
+	@Test
+	public void testDoTranslation4() throws Exception {
+		SelectQuery q = new SelectQuery(Artist.class);
+		q.addOrdering("artistName", SortOrder.ASCENDING_INSENSITIVE);
 
-        TstQueryAssembler qa = new TstQueryAssembler(q, node, connection);
+		TstQueryAssembler qa = new TstQueryAssembler(q, node.getAdapter(), node.getEntityResolver());
 
-        TranslationCase tstCase = new TranslationCase("Artist", null, "UPPER(ta.ARTIST_NAME)");
+		TranslationCase tstCase = new TranslationCase("Artist", null, "UPPER(ta.ARTIST_NAME)");
 
-        StringBuilder out = new StringBuilder();
-        new OrderingTranslator(qa).appendPart(out);
+		StringBuilder out = new StringBuilder();
+		new OrderingTranslator(qa).appendPart(out);
 
-        assertTrue(out.length() > 0);
-        String orderBySql = out.toString();
-        assertTrue(orderBySql.contains("UPPER("));
-        tstCase.assertTranslatedWell(orderBySql);
-    }
+		assertTrue(out.length() > 0);
+		String orderBySql = out.toString();
+		assertTrue(orderBySql.contains("UPPER("));
+		tstCase.assertTranslatedWell(orderBySql);
+	}
 
-    @Test
-    public void testDoTranslation5() throws Exception {
-        SelectQuery q = new SelectQuery(Artist.class);
-        q.addOrdering("artistName", SortOrder.DESCENDING_INSENSITIVE);
-        q.addOrdering("paintingArray.estimatedPrice", SortOrder.ASCENDING);
+	@Test
+	public void testDoTranslation5() throws Exception {
+		SelectQuery q = new SelectQuery(Artist.class);
+		q.addOrdering("artistName", SortOrder.DESCENDING_INSENSITIVE);
+		q.addOrdering("paintingArray.estimatedPrice", SortOrder.ASCENDING);
 
-        TstQueryAssembler qa = new TstQueryAssembler(q, node, connection);
+		TstQueryAssembler qa = new TstQueryAssembler(q, node.getAdapter(), node.getEntityResolver());
 
-        TranslationCase tstCase = new TranslationCase("Artist", null, "UPPER(ta.ARTIST_NAME) DESC, ta.ESTIMATED_PRICE");
+		TranslationCase tstCase = new TranslationCase("Artist", null, "UPPER(ta.ARTIST_NAME) DESC, ta.ESTIMATED_PRICE");
 
-        StringBuilder out = new StringBuilder();
-        new OrderingTranslator(qa).appendPart(out);
+		StringBuilder out = new StringBuilder();
+		new OrderingTranslator(qa).appendPart(out);
 
-        assertTrue(out.length() > 0);
-        String orderBySql = out.toString();
+		assertTrue(out.length() > 0);
+		String orderBySql = out.toString();
 
-        // Check there is an UPPER modifier
-        int indexOfUpper = orderBySql.indexOf("UPPER(");
-        assertTrue(indexOfUpper != -1);
+		// Check there is an UPPER modifier
+		int indexOfUpper = orderBySql.indexOf("UPPER(");
+		assertTrue(indexOfUpper != -1);
 
-        // and ensure there is only ONE upper modifier
-        assertTrue(orderBySql.indexOf("UPPER(", indexOfUpper + 1) == -1);
-        tstCase.assertTranslatedWell(orderBySql);
-    }
+		// and ensure there is only ONE upper modifier
+		assertTrue(orderBySql.indexOf("UPPER(", indexOfUpper + 1) == -1);
+		tstCase.assertTranslatedWell(orderBySql);
+	}
 
-    @Test
-    public void testDoTranslation6() throws Exception {
-        SelectQuery q = new SelectQuery(Artist.class);
-        q.addOrdering("artistName", SortOrder.ASCENDING_INSENSITIVE);
-        q.addOrdering("paintingArray.estimatedPrice", SortOrder.ASCENDING_INSENSITIVE);
+	@Test
+	public void testDoTranslation6() throws Exception {
+		SelectQuery q = new SelectQuery(Artist.class);
+		q.addOrdering("artistName", SortOrder.ASCENDING_INSENSITIVE);
+		q.addOrdering("paintingArray.estimatedPrice", SortOrder.ASCENDING_INSENSITIVE);
 
-        TstQueryAssembler qa = new TstQueryAssembler(q, node, connection);
+		TstQueryAssembler qa = new TstQueryAssembler(q, node.getAdapter(), node.getEntityResolver());
 
-        TranslationCase tstCase = new TranslationCase("Artist", null,
-                "UPPER(ta.ARTIST_NAME), UPPER(ta.ESTIMATED_PRICE)");
+		TranslationCase tstCase = new TranslationCase("Artist", null,
+				"UPPER(ta.ARTIST_NAME), UPPER(ta.ESTIMATED_PRICE)");
 
-        StringBuilder out = new StringBuilder();
-        new OrderingTranslator(qa).appendPart(out);
+		StringBuilder out = new StringBuilder();
+		new OrderingTranslator(qa).appendPart(out);
 
-        assertTrue(out.length() > 0);
-        String orderBySql = out.toString();
+		assertTrue(out.length() > 0);
+		String orderBySql = out.toString();
 
-        // Check there is at least one UPPER modifier
-        int indexOfUpper = orderBySql.indexOf("UPPER(");
-        assertTrue(indexOfUpper != -1);
+		// Check there is at least one UPPER modifier
+		int indexOfUpper = orderBySql.indexOf("UPPER(");
+		assertTrue(indexOfUpper != -1);
 
-        // and ensure there is another after it
-        assertTrue(orderBySql.indexOf("UPPER(", indexOfUpper + 1) != -1);
+		// and ensure there is another after it
+		assertTrue(orderBySql.indexOf("UPPER(", indexOfUpper + 1) != -1);
 
-        tstCase.assertTranslatedWell(orderBySql);
-    }
+		tstCase.assertTranslatedWell(orderBySql);
+	}
 
-    @Test
-    public void testDoTranslation3() throws Exception {
-        SelectQuery q = new SelectQuery(Artist.class);
+	@Test
+	public void testDoTranslation3() throws Exception {
+		SelectQuery<Artist> q = SelectQuery.query(Artist.class);
 
-        q.addOrdering("artistName", SortOrder.DESCENDING);
-        q.addOrdering("paintingArray.estimatedPrice", SortOrder.ASCENDING);
+		q.addOrdering("artistName", SortOrder.DESCENDING);
+		q.addOrdering("paintingArray.estimatedPrice", SortOrder.ASCENDING);
 
-        TstQueryAssembler qa = new TstQueryAssembler(q, node, connection);
+		TstQueryAssembler qa = new TstQueryAssembler(q, node.getAdapter(), node.getEntityResolver());
 
-        TranslationCase tstCase = new TranslationCase("Artist", null, "ta.ARTIST_NAME DESC, ta.ESTIMATED_PRICE");
+		TranslationCase tstCase = new TranslationCase("Artist", null, "ta.ARTIST_NAME DESC, ta.ESTIMATED_PRICE");
 
-        StringBuilder out = new StringBuilder();
-        new OrderingTranslator(qa).appendPart(out);
+		StringBuilder out = new StringBuilder();
+		new OrderingTranslator(qa).appendPart(out);
 
-        assertTrue(out.length() > 0);
-        String orderBySql = out.toString();
-        tstCase.assertTranslatedWell(orderBySql);
-    }
+		assertTrue(out.length() > 0);
+		String orderBySql = out.toString();
+		tstCase.assertTranslatedWell(orderBySql);
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
index e4ff4ef..6475873 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
@@ -19,6 +19,10 @@
 
 package org.apache.cayenne.access.translator.select;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.access.DataNode;
@@ -38,115 +42,96 @@ import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.ServerCaseDataSourceFactory;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
-import java.sql.Connection;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.fail;
-
 @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
 public class QualifierTranslatorIT extends ServerCase {
 
-    @Inject
-    private DataNode node;
-
-    @Inject
-    private ServerCaseDataSourceFactory dataSourceFactory;
-
-    private Connection connection;
-
-    @Before
-    public void setUp() throws Exception {
-        this.connection = dataSourceFactory.getSharedDataSource().getConnection();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        connection.close();
-    }
-
-    @Test
-    public void testNonQualifiedQuery() throws Exception {
-        TstQueryAssembler qa = new TstQueryAssembler(new MockQuery(), node, connection);
-
-        try {
-            new QualifierTranslator(qa).appendPart(new StringBuilder());
-            fail();
-        } catch (ClassCastException ccex) {
-            // exception expected
-        }
-    }
-
-    @Test
-    public void testNullQualifier() throws Exception {
-        TstQueryAssembler qa = new TstQueryAssembler(new SelectQuery<Object>(), node, connection);
-
-        StringBuilder out = new StringBuilder();
-        new QualifierTranslator(qa).appendPart(out);
-        assertEquals(0, out.length());
-    }
-
-    @Test
-    public void testUnary() throws Exception {
-        doExpressionTest(new TstUnaryExpSuite());
-    }
-
-    @Test
-    public void testBinary() throws Exception {
-        doExpressionTest(new TstBinaryExpSuite());
-    }
-
-    @Test
-    public void testTernary() throws Exception {
-        doExpressionTest(new TstTernaryExpSuite());
-    }
-
-    @Test
-    public void testExtras() throws Exception {
-        ObjectId oid1 = new ObjectId("Gallery", "GALLERY_ID", 1);
-        ObjectId oid2 = new ObjectId("Gallery", "GALLERY_ID", 2);
-        Gallery g1 = new Gallery();
-        Gallery g2 = new Gallery();
-        g1.setObjectId(oid1);
-        g2.setObjectId(oid2);
-
-        Expression e1 = ExpressionFactory.matchExp("toGallery", g1);
-        Expression e2 = e1.orExp(ExpressionFactory.matchExp("toGallery", g2));
-
-        TstExpressionCase extraCase = new TstExpressionCase("Exhibit", e2,
-                "(ta.GALLERY_ID = ?) OR (ta.GALLERY_ID = ?)", 4, 4);
-
-        TstExpressionSuite suite = new TstExpressionSuite() {
-        };
-        suite.addCase(extraCase);
-        doExpressionTest(suite);
-    }
-
-    private void doExpressionTest(TstExpressionSuite suite) throws Exception {
-
-        TstExpressionCase[] cases = suite.cases();
-
-        int len = cases.length;
-        for (int i = 0; i < len; i++) {
-            try {
-
-                ObjEntity entity = node.getEntityResolver().getObjEntity(cases[i].getRootEntity());
-                assertNotNull(entity);
-                SelectQuery q = new SelectQuery(entity);
-                q.setQualifier(cases[i].getCayenneExp());
-
-                TstQueryAssembler qa = new TstQueryAssembler(q, node, connection);
-
-                StringBuilder out = new StringBuilder();
-                new QualifierTranslator(qa).appendPart(out);
-                cases[i].assertTranslatedWell(out.toString());
-            } catch (Exception ex) {
-                throw new CayenneRuntimeException("Failed case: [" + i + "]: " + cases[i], ex);
-            }
-        }
-    }
+	@Inject
+	private DataNode node;
+
+	@Inject
+	private ServerCaseDataSourceFactory dataSourceFactory;
+
+	@Test
+	public void testNonQualifiedQuery() throws Exception {
+		TstQueryAssembler qa = new TstQueryAssembler(new MockQuery(), node.getAdapter(), node.getEntityResolver());
+
+		try {
+			new QualifierTranslator(qa).appendPart(new StringBuilder());
+			fail();
+		} catch (ClassCastException ccex) {
+			// exception expected
+		}
+	}
+
+	@Test
+	public void testNullQualifier() throws Exception {
+		TstQueryAssembler qa = new TstQueryAssembler(new SelectQuery<Object>(), node.getAdapter(),
+				node.getEntityResolver());
+
+		StringBuilder out = new StringBuilder();
+		new QualifierTranslator(qa).appendPart(out);
+		assertEquals(0, out.length());
+	}
+
+	@Test
+	public void testUnary() throws Exception {
+		doExpressionTest(new TstUnaryExpSuite());
+	}
+
+	@Test
+	public void testBinary() throws Exception {
+		doExpressionTest(new TstBinaryExpSuite());
+	}
+
+	@Test
+	public void testTernary() throws Exception {
+		doExpressionTest(new TstTernaryExpSuite());
+	}
+
+	@Test
+	public void testExtras() throws Exception {
+		ObjectId oid1 = new ObjectId("Gallery", "GALLERY_ID", 1);
+		ObjectId oid2 = new ObjectId("Gallery", "GALLERY_ID", 2);
+		Gallery g1 = new Gallery();
+		Gallery g2 = new Gallery();
+		g1.setObjectId(oid1);
+		g2.setObjectId(oid2);
+
+		Expression e1 = ExpressionFactory.matchExp("toGallery", g1);
+		Expression e2 = e1.orExp(ExpressionFactory.matchExp("toGallery", g2));
+
+		TstExpressionCase extraCase = new TstExpressionCase("Exhibit", e2,
+				"(ta.GALLERY_ID = ?) OR (ta.GALLERY_ID = ?)", 4, 4);
+
+		TstExpressionSuite suite = new TstExpressionSuite() {
+		};
+		suite.addCase(extraCase);
+		doExpressionTest(suite);
+	}
+
+	private void doExpressionTest(TstExpressionSuite suite) throws Exception {
+
+		TstExpressionCase[] cases = suite.cases();
+
+		int len = cases.length;
+		for (int i = 0; i < len; i++) {
+			try {
+
+				ObjEntity entity = node.getEntityResolver().getObjEntity(cases[i].getRootEntity());
+				assertNotNull(entity);
+				SelectQuery q = new SelectQuery(entity);
+				q.setQualifier(cases[i].getCayenneExp());
+
+				TstQueryAssembler qa = new TstQueryAssembler(q, node.getAdapter(), node.getEntityResolver());
+
+				StringBuilder out = new StringBuilder();
+				new QualifierTranslator(qa).appendPart(out);
+				cases[i].assertTranslatedWell(out.toString());
+			} catch (Exception ex) {
+				throw new CayenneRuntimeException("Failed case: [" + i + "]: " + cases[i], ex);
+			}
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QueryAssemblerIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QueryAssemblerIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QueryAssemblerIT.java
index 006e73c..05c51e6 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QueryAssemblerIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QueryAssemblerIT.java
@@ -19,65 +19,43 @@
 
 package org.apache.cayenne.access.translator.select;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
-import org.apache.cayenne.unit.di.server.ServerCaseDataSourceFactory;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.sql.Connection;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-
 @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
 public class QueryAssemblerIT extends ServerCase {
 
-    @Inject
-    private DataNode dataNode;
-
-    @Inject
-    private ServerCaseDataSourceFactory dataSourceFactory;
-
-    private Connection connection;
-
-    private TstQueryAssembler qa;
-
-    @Before
-    public void setUp() throws Exception {
-        this.connection = dataSourceFactory.getSharedDataSource().getConnection();
-        this.qa = new TstQueryAssembler(new SelectQuery<Object>(), dataNode, connection);
-    }
+	@Inject
+	private DataNode node;
 
-    @After
-    public void tearDown() throws Exception {
-        connection.close();
-    }
+	private TstQueryAssembler qa;
 
-    @Test
-    public void testGetQuery() throws Exception {
-        assertNotNull(qa.getQuery());
-    }
+	@Before
+	public void setUp() throws Exception {
+		this.qa = new TstQueryAssembler(new SelectQuery<Object>(), node.getAdapter(), node.getEntityResolver());
+	}
 
-    @Test
-    public void testAddToParamList() throws Exception {
+	@Test
+	public void testGetQuery() throws Exception {
+		assertNotNull(qa.getQuery());
+	}
 
-        assertEquals(0, qa.getAttributes().size());
-        assertEquals(0, qa.getValues().size());
+	@Test
+	public void testAddToParamList() throws Exception {
 
-        qa.addToParamList(new DbAttribute(), new Object());
-        assertEquals(1, qa.getAttributes().size());
-        assertEquals(1, qa.getValues().size());
-    }
+		assertEquals(0, qa.getBindings().length);
 
-    @Test
-    public void testCreateStatement() throws Exception {
-        assertNotNull(qa.createStatement());
-    }
+		qa.addToParamList(new DbAttribute(), new Object());
+		assertEquals(1, qa.getBindings().length);
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/SelectTranslatorIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/SelectTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/SelectTranslatorIT.java
deleted file mode 100644
index b0ff3f8..0000000
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/SelectTranslatorIT.java
+++ /dev/null
@@ -1,794 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-
-package org.apache.cayenne.access.translator.select;
-
-import org.apache.cayenne.access.DataContext;
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.di.Inject;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.ExpressionException;
-import org.apache.cayenne.exp.ExpressionFactory;
-import org.apache.cayenne.log.JdbcEventLogger;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.query.PrefetchTreeNode;
-import org.apache.cayenne.query.SelectQuery;
-import org.apache.cayenne.query.SortOrder;
-import org.apache.cayenne.test.jdbc.DBHelper;
-import org.apache.cayenne.testdo.testmap.Artist;
-import org.apache.cayenne.testdo.testmap.ArtistExhibit;
-import org.apache.cayenne.testdo.testmap.CompoundPainting;
-import org.apache.cayenne.testdo.testmap.Painting;
-import org.apache.cayenne.unit.UnitDbAdapter;
-import org.apache.cayenne.unit.di.server.CayenneProjects;
-import org.apache.cayenne.unit.di.server.ServerCase;
-import org.apache.cayenne.unit.di.server.ServerCaseDataSourceFactory;
-import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.sql.Connection;
-import java.util.Date;
-import java.util.List;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
-public class SelectTranslatorIT extends ServerCase {
-
-    @Inject
-    private ServerCaseDataSourceFactory dataSourceFactory;
-
-    @Inject
-    private DataContext context;
-
-    @Inject
-    private UnitDbAdapter unitAdapter;
-
-    @Inject
-    private DBHelper dbHelper;
-
-    @Inject
-    private JdbcEventLogger logger;
-
-    @Inject
-    private DataNode dataNode;
-
-    private Connection connection;
-
-    @Before
-    public void setUp() throws Exception {
-        this.connection = dataSourceFactory.getSharedDataSource().getConnection();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        connection.close();
-    }
-
-    /**
-     * Tests query creation with qualifier and ordering.
-     */
-    @Test
-    public void testCreateSqlString1() throws Exception {
-        // query with qualifier and ordering
-        SelectQuery<Artist> q = new SelectQuery<Artist>(Artist.class, ExpressionFactory.likeExp("artistName", "a%"));
-        q.addOrdering("dateOfBirth", SortOrder.ASCENDING);
-
-        String generatedSql = new SelectTranslator(q, dataNode, connection).createSqlString();
-
-        // do some simple assertions to make sure all parts are in
-        assertNotNull(generatedSql);
-        assertTrue(generatedSql.startsWith("SELECT "));
-        assertTrue(generatedSql.indexOf(" FROM ") > 0);
-        assertTrue(generatedSql.indexOf(" WHERE ") > generatedSql.indexOf(" FROM "));
-        assertTrue(generatedSql.indexOf(" ORDER BY ") > generatedSql.indexOf(" WHERE "));
-    }
-
-    /**
-     * Tests query creation with qualifier and ordering.
-     */
-    @Test
-    public void testDbEntityQualifier() throws Exception {
-
-        SelectQuery<Artist> q = new SelectQuery<Artist>(Artist.class);
-        final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
-        final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
-
-        SelectTranslator transl = new SelectTranslator(q, dataNode, connection);
-
-        entity.setQualifier(Expression.fromString("ARTIST_NAME = \"123\""));
-        middleEntity.setQualifier(Expression.fromString("GROUP_ID = 1987"));
-
-        try {
-
-            String generatedSql = transl.createSqlString();
-
-            // do some simple assertions to make sure all parts are in
-            assertNotNull(generatedSql);
-            assertTrue(generatedSql.startsWith("SELECT "));
-            assertTrue(generatedSql.indexOf(" FROM ") > 0);
-            if (generatedSql.contains("RTRIM")) {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
-            } else if (generatedSql.contains("TRIM")) {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
-            } else {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
-            }
-        } finally {
-            entity.setQualifier(null);
-            middleEntity.setQualifier(null);
-        }
-    }
-
-    @Test
-    public void testDbEntityQualifier_OuterJoin() throws Exception {
-
-        SelectQuery q = new SelectQuery(Painting.class);
-        q.addOrdering("toArtist+.artistName", SortOrder.ASCENDING);
-
-        final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
-        final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
-
-        SelectTranslator transl = new SelectTranslator(q, dataNode, connection);
-
-        entity.setQualifier(Expression.fromString("ARTIST_NAME = \"123\""));
-        middleEntity.setQualifier(Expression.fromString("GROUP_ID = 1987"));
-
-        try {
-
-            String generatedSql = transl.createSqlString();
-
-            // do some simple assertions to make sure all parts are in
-            assertNotNull(generatedSql);
-            assertTrue(generatedSql.startsWith("SELECT "));
-            assertTrue(generatedSql.indexOf(" FROM ") > 0);
-            if (generatedSql.contains("RTRIM")) {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
-            } else if (generatedSql.contains("TRIM")) {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
-            } else {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
-            }
-
-        } finally {
-            entity.setQualifier(null);
-            middleEntity.setQualifier(null);
-        }
-    }
-
-    @Test
-    public void testDbEntityQualifier_FlattenedRel() throws Exception {
-
-        SelectQuery<Artist> q = new SelectQuery<Artist>(Artist.class, ExpressionFactory.matchExp("groupArray.name",
-                "bar"));
-
-        final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
-        final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
-
-        SelectTranslator transl = new SelectTranslator(q, dataNode, connection);
-
-        entity.setQualifier(Expression.fromString("ARTIST_NAME = \"123\""));
-        middleEntity.setQualifier(Expression.fromString("GROUP_ID = 1987"));
-
-        try {
-
-            String generatedSql = transl.createSqlString();
-
-            // do some simple assertions to make sure all parts are in
-            assertNotNull(generatedSql);
-            assertTrue(generatedSql.startsWith("SELECT "));
-            assertTrue(generatedSql.indexOf(" FROM ") > 0);
-            if (generatedSql.contains("RTRIM")) {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
-            } else if (generatedSql.contains("TRIM")) {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
-            } else {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
-            }
-
-        } finally {
-            entity.setQualifier(null);
-            middleEntity.setQualifier(null);
-        }
-    }
-
-    @Test
-    public void testDbEntityQualifier_RelatedMatch() throws Exception {
-
-        SelectQuery<Artist> q = new SelectQuery(Painting.class,
-                ExpressionFactory.matchExp("toArtist.artistName", "foo"));
-
-        final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
-        final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
-
-        SelectTranslator transl = new SelectTranslator(q, dataNode, connection);
-
-        entity.setQualifier(Expression.fromString("ARTIST_NAME = \"123\""));
-        middleEntity.setQualifier(Expression.fromString("GROUP_ID = 1987"));
-
-        try {
-
-            String generatedSql = transl.createSqlString();
-
-            // do some simple assertions to make sure all parts are in
-            assertNotNull(generatedSql);
-            assertTrue(generatedSql.startsWith("SELECT "));
-            assertTrue(generatedSql.indexOf(" FROM ") > 0);
-            if (generatedSql.contains("RTRIM")) {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
-            } else if (generatedSql.contains("TRIM")) {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
-            } else {
-                assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
-            }
-
-        } finally {
-            entity.setQualifier(null);
-            middleEntity.setQualifier(null);
-        }
-    }
-
-    /**
-     * Tests query creation with "distinct" specified.
-     */
-    @Test
-    public void testCreateSqlString2() throws Exception {
-        // query with "distinct" set
-        SelectQuery q = new SelectQuery(Artist.class);
-        q.setDistinct(true);
-
-        String generatedSql = new SelectTranslator(q, dataNode, connection).createSqlString();
-
-        // do some simple assertions to make sure all parts are in
-        assertNotNull(generatedSql);
-        assertTrue(generatedSql.startsWith("SELECT DISTINCT"));
-    }
-
-    /**
-     * Test aliases when the same table used in more then 1 relationship. Check
-     * translation of relationship path "ArtistExhibit.toArtist.artistName" and
-     * "ArtistExhibit.toExhibit.toGallery.paintingArray.toArtist.artistName".
-     */
-    @Test
-    public void testCreateSqlString5() throws Exception {
-        // query with qualifier and ordering
-        SelectQuery q = new SelectQuery(ArtistExhibit.class);
-        q.setQualifier(ExpressionFactory.likeExp("toArtist.artistName", "a%"));
-        q.andQualifier(ExpressionFactory.likeExp("toExhibit.toGallery.paintingArray.toArtist.artistName", "a%"));
-
-        String generatedSql = new SelectTranslator(q, dataNode, connection).createSqlString();
-        // logObj.warn("Query: " + generatedSql);
-
-        // do some simple assertions to make sure all parts are in
-        assertNotNull(generatedSql);
-        assertTrue(generatedSql.startsWith("SELECT "));
-        assertTrue(generatedSql.indexOf(" FROM ") > 0);
-        assertTrue(generatedSql.indexOf(" WHERE ") > generatedSql.indexOf(" FROM "));
-
-        // check that there are 2 distinct aliases for the ARTIST table
-        int ind1 = generatedSql.indexOf("ARTIST t", generatedSql.indexOf(" FROM "));
-        assertTrue(ind1 > 0);
-
-        int ind2 = generatedSql.indexOf("ARTIST t", ind1 + 1);
-        assertTrue(ind2 > 0);
-
-        assertTrue(generatedSql.charAt(ind1 + "ARTIST t".length()) != generatedSql.charAt(ind2 + "ARTIST t".length()));
-    }
-
-    /**
-     * Test aliases when the same table used in more then 1 relationship. Check
-     * translation of relationship path "ArtistExhibit.toArtist.artistName" and
-     * "ArtistExhibit.toArtist.paintingArray.paintingTitle".
-     */
-    @Test
-    public void testCreateSqlString6() throws Exception {
-        // query with qualifier and ordering
-        SelectQuery q = new SelectQuery(ArtistExhibit.class);
-        q.setQualifier(ExpressionFactory.likeExp("toArtist.artistName", "a%"));
-        q.andQualifier(ExpressionFactory.likeExp("toArtist.paintingArray.paintingTitle", "p%"));
-
-        String generatedSql = new SelectTranslator(q, dataNode, connection).createSqlString();
-
-        // do some simple assertions to make sure all parts are in
-        assertNotNull(generatedSql);
-        assertTrue(generatedSql.startsWith("SELECT "));
-        assertTrue(generatedSql.indexOf(" FROM ") > 0);
-        assertTrue(generatedSql.indexOf(" WHERE ") > generatedSql.indexOf(" FROM "));
-
-        // check that there is only one distinct alias for the ARTIST
-        // table
-        int ind1 = generatedSql.indexOf("ARTIST t", generatedSql.indexOf(" FROM "));
-        assertTrue(ind1 > 0);
-
-        int ind2 = generatedSql.indexOf("ARTIST t", ind1 + 1);
-        assertTrue(generatedSql, ind2 < 0);
-    }
-
-    /**
-     * Test query when qualifying on the same attribute more than once. Check
-     * translation "Artist.dateOfBirth > ? AND Artist.dateOfBirth < ?".
-     */
-    @Test
-    public void testCreateSqlString7() throws Exception {
-        SelectQuery q = new SelectQuery(Artist.class);
-        q.setQualifier(ExpressionFactory.greaterExp("dateOfBirth", new Date()));
-        q.andQualifier(ExpressionFactory.lessExp("dateOfBirth", new Date()));
-
-        String generatedSql = new SelectTranslator(q, dataNode, connection).createSqlString();
-        // logObj.warn("Query: " + generatedSql);
-
-        // do some simple assertions to make sure all parts are in
-        assertNotNull(generatedSql);
-        assertTrue(generatedSql.startsWith("SELECT "));
-
-        int i1 = generatedSql.indexOf(" FROM ");
-        assertTrue(i1 > 0);
-
-        int i2 = generatedSql.indexOf(" WHERE ");
-        assertTrue(i2 > i1);
-
-        int i3 = generatedSql.indexOf("DATE_OF_BIRTH", i2 + 1);
-        assertTrue(i3 > i2);
-
-        int i4 = generatedSql.indexOf("DATE_OF_BIRTH", i3 + 1);
-        assertTrue("No second DOB comparison: " + i4 + ", " + i3, i4 > i3);
-    }
-
-    /**
-     * Test query when qualifying on the same attribute accessed over
-     * relationship, more than once. Check translation
-     * "Painting.toArtist.dateOfBirth > ? AND Painting.toArtist.dateOfBirth <
-     * ?".
-     */
-    @Test
-    public void testCreateSqlString8() throws Exception {
-        SelectQuery q = new SelectQuery();
-        q.setRoot(Painting.class);
-        q.setQualifier(ExpressionFactory.greaterExp("toArtist.dateOfBirth", new Date()));
-        q.andQualifier(ExpressionFactory.lessExp("toArtist.dateOfBirth", new Date()));
-
-        String generatedSql = new SelectTranslator(q, dataNode, connection).createSqlString();
-
-        // do some simple assertions to make sure all parts are in
-        assertNotNull(generatedSql);
-        assertTrue(generatedSql.startsWith("SELECT "));
-
-        int i1 = generatedSql.indexOf(" FROM ");
-        assertTrue(i1 > 0);
-
-        int i2 = generatedSql.indexOf(" WHERE ");
-        assertTrue(i2 > i1);
-
-        int i3 = generatedSql.indexOf("DATE_OF_BIRTH", i2 + 1);
-        assertTrue(i3 > i2);
-
-        int i4 = generatedSql.indexOf("DATE_OF_BIRTH", i3 + 1);
-        assertTrue("No second DOB comparison: " + i4 + ", " + i3, i4 > i3);
-    }
-
-    @Test
-    public void testCreateSqlString9() throws Exception {
-        // query for a compound ObjEntity with qualifier
-        SelectQuery q = new SelectQuery(CompoundPainting.class, ExpressionFactory.likeExp("artistName", "a%"));
-
-        String sql = new SelectTranslator(q, dataNode, connection).createSqlString();
-
-        // do some simple assertions to make sure all parts are in
-        assertNotNull(sql);
-        assertTrue(sql.startsWith("SELECT "));
-
-        int i1 = sql.indexOf(" FROM ");
-        assertTrue(i1 > 0);
-
-        int i2 = sql.indexOf("PAINTING");
-        assertTrue(i2 > 0);
-
-        int i3 = sql.indexOf("ARTIST");
-        assertTrue(i3 > 0);
-
-        int i4 = sql.indexOf("GALLERY");
-        assertTrue(i4 > 0);
-
-        int i5 = sql.indexOf("PAINTING_INFO");
-        assertTrue(i5 > 0);
-
-        int i6 = sql.indexOf("ARTIST_NAME");
-        assertTrue(i6 > 0);
-
-        int i7 = sql.indexOf("ESTIMATED_PRICE");
-        assertTrue(i7 > 0);
-
-        int i8 = sql.indexOf("GALLERY_NAME");
-        assertTrue(i8 > 0);
-
-        int i9 = sql.indexOf("PAINTING_TITLE");
-        assertTrue(i9 > 0);
-
-        int i10 = sql.indexOf("TEXT_REVIEW");
-        assertTrue(i10 > 0);
-
-        int i11 = sql.indexOf("PAINTING_ID");
-        assertTrue(i11 > 0);
-
-        int i12 = sql.indexOf("ARTIST_ID");
-        assertTrue(i12 > 0);
-
-        int i13 = sql.indexOf("GALLERY_ID");
-        assertTrue(i13 > 0);
-
-    }
-
-    @Test
-    public void testCreateSqlString10() throws Exception {
-        // query with to-many joint prefetches
-        SelectQuery q = new SelectQuery(Artist.class);
-        q.addPrefetch(Artist.PAINTING_ARRAY_PROPERTY).setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
-
-        SelectTranslator transl = new SelectTranslator(q, dataNode, connection);
-        String sql = transl.createSqlString();
-        assertNotNull(sql);
-        assertTrue(sql.startsWith("SELECT "));
-
-        int i1 = sql.indexOf("ARTIST_ID");
-        assertTrue(sql, i1 > 0);
-
-        int i2 = sql.indexOf("FROM");
-        assertTrue(sql, i2 > 0);
-
-        assertTrue(sql, sql.indexOf("PAINTING_ID") > 0);
-
-        // assert we have one join
-        assertEquals(1, transl.joinStack.size());
-    }
-
-    @Test
-    public void testCreateSqlString11() throws Exception {
-        // query with joint prefetches and other joins
-        SelectQuery q = new SelectQuery(Artist.class, Expression.fromString("paintingArray.paintingTitle = 'a'"));
-        q.addPrefetch(Artist.PAINTING_ARRAY_PROPERTY).setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
-
-        SelectTranslator transl = new SelectTranslator(q, dataNode, connection);
-
-        transl.createSqlString();
-
-        // assert we only have one join
-        assertEquals(2, transl.joinStack.size());
-    }
-
-    @Test
-    public void testCreateSqlString12() throws Exception {
-        // query with to-one joint prefetches
-        SelectQuery q = new SelectQuery(Painting.class);
-        q.addPrefetch(Painting.TO_ARTIST_PROPERTY).setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
-
-        SelectTranslator transl = new SelectTranslator(q, dataNode, connection);
-
-        String sql = transl.createSqlString();
-        assertNotNull(sql);
-        assertTrue(sql.startsWith("SELECT "));
-
-        int i1 = sql.indexOf("ARTIST_ID");
-        assertTrue(sql, i1 > 0);
-
-        int i2 = sql.indexOf("FROM");
-        assertTrue(sql, i2 > 0);
-
-        assertTrue(sql, sql.indexOf("PAINTING_ID") > 0);
-
-        // assert we have one join
-        assertEquals(1, transl.joinStack.size());
-    }
-
-    @Test
-    public void testCreateSqlString13() throws Exception {
-        // query with invalid joint prefetches
-        SelectQuery q = new SelectQuery(Painting.class);
-        q.addPrefetch("invalid.invalid").setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
-
-        try {
-            new SelectTranslator(q, dataNode, connection).createSqlString();
-            fail("Invalid jointPrefetch must have thrown...");
-        } catch (ExpressionException e) {
-            // expected
-        }
-    }
-
-    @Test
-    public void testCreateSqlStringWithQuoteSqlIdentifiers() throws Exception {
-
-        try {
-            SelectQuery q = new SelectQuery(Artist.class);
-            DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
-            entity.getDataMap().setQuotingSQLIdentifiers(true);
-            q.addOrdering("dateOfBirth", SortOrder.ASCENDING);
-
-            String charStart = unitAdapter.getIdentifiersStartQuote();
-            String charEnd = unitAdapter.getIdentifiersEndQuote();
-
-            String s = new SelectTranslator(q, dataNode, connection).createSqlString();
-            assertTrue(s.startsWith("SELECT "));
-            int iFrom = s.indexOf(" FROM ");
-            assertTrue(iFrom > 0);
-            int artistName = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_NAME" + charEnd);
-            assertTrue(artistName > 0 && artistName < iFrom);
-            int artistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
-            assertTrue(artistId > 0 && artistId < iFrom);
-            int dateOfBirth = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd);
-            assertTrue(dateOfBirth > 0 && dateOfBirth < iFrom);
-            int iArtist = s.indexOf(charStart + "ARTIST" + charEnd + " " + charStart + "t0" + charEnd);
-            assertTrue(iArtist > iFrom);
-            int iOrderBy = s.indexOf(" ORDER BY ");
-            int dateOfBirth2 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd,
-                    iOrderBy);
-            assertTrue(iOrderBy > iArtist);
-            assertTrue(dateOfBirth2 > iOrderBy);
-
-        } finally {
-            DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
-            entity.getDataMap().setQuotingSQLIdentifiers(false);
-        }
-
-    }
-
-    @Test
-    public void testCreateSqlStringWithQuoteSqlIdentifiers2() throws Exception {
-
-        try {
-            SelectQuery q = new SelectQuery(Artist.class);
-            DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
-            entity.getDataMap().setQuotingSQLIdentifiers(true);
-            q.setQualifier(ExpressionFactory.greaterExp("dateOfBirth", new Date()));
-            q.andQualifier(ExpressionFactory.lessExp("dateOfBirth", new Date()));
-
-            String charStart = unitAdapter.getIdentifiersStartQuote();
-            String charEnd = unitAdapter.getIdentifiersEndQuote();
-
-            String s = new SelectTranslator(q, dataNode, connection).createSqlString();
-
-            assertTrue(s.startsWith("SELECT "));
-            int iFrom = s.indexOf(" FROM ");
-            assertTrue(iFrom > 0);
-            int artistName = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_NAME" + charEnd);
-            assertTrue(artistName > 0 && artistName < iFrom);
-            int artistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
-            assertTrue(artistId > 0 && artistId < iFrom);
-            int dateOfBirth = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd);
-            assertTrue(dateOfBirth > 0 && dateOfBirth < iFrom);
-            int iArtist = s.indexOf(charStart + "ARTIST" + charEnd + " " + charStart + "t0" + charEnd);
-            assertTrue(iArtist > iFrom);
-            int iWhere = s.indexOf(" WHERE ");
-            assertTrue(iWhere > iArtist);
-
-            int dateOfBirth2 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd
-                    + " > ?");
-            assertTrue(dateOfBirth2 > iWhere);
-
-            int iAnd = s.indexOf(" AND ");
-            assertTrue(iAnd > iWhere);
-            int dateOfBirth3 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd
-                    + " < ?");
-            assertTrue(dateOfBirth3 > iAnd);
-
-        } finally {
-            DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
-            entity.getDataMap().setQuotingSQLIdentifiers(false);
-        }
-    }
-
-    @Test
-    public void testCreateSqlStringWithQuoteSqlIdentifiers3() throws Exception {
-
-        // query with joint prefetches and other joins
-        // and with QuoteSqlIdentifiers = true
-        try {
-            SelectQuery q = new SelectQuery(Artist.class, Expression.fromString("paintingArray.paintingTitle = 'a'"));
-            q.addPrefetch(Artist.PAINTING_ARRAY_PROPERTY).setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
-
-            DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
-            entity.getDataMap().setQuotingSQLIdentifiers(true);
-
-            String charStart = unitAdapter.getIdentifiersStartQuote();
-            String charEnd = unitAdapter.getIdentifiersEndQuote();
-
-            String s = new SelectTranslator(q, dataNode, connection).createSqlString();
-
-            assertTrue(s.startsWith("SELECT DISTINCT "));
-            int iFrom = s.indexOf(" FROM ");
-            assertTrue(iFrom > 0);
-            int artistName = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_NAME" + charEnd);
-            assertTrue(artistName > 0 && artistName < iFrom);
-            int artistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
-            assertTrue(artistId > 0 && artistId < iFrom);
-            int dateOfBirth = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd);
-            assertTrue(dateOfBirth > 0 && dateOfBirth < iFrom);
-            int estimatedPrice = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ESTIMATED_PRICE" + charEnd);
-            assertTrue(estimatedPrice > 0 && estimatedPrice < iFrom);
-            int paintingDescription = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "PAINTING_DESCRIPTION"
-                    + charEnd);
-            assertTrue(paintingDescription > 0 && paintingDescription < iFrom);
-            int paintingTitle = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "PAINTING_TITLE" + charEnd);
-            assertTrue(paintingTitle > 0 && paintingTitle < iFrom);
-            int artistIdT1 = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
-            assertTrue(artistIdT1 > 0 && artistIdT1 < iFrom);
-            int galleryId = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "GALLERY_ID" + charEnd);
-            assertTrue(galleryId > 0 && galleryId < iFrom);
-            int paintingId = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "PAINTING_ID" + charEnd);
-            assertTrue(paintingId > 0 && paintingId < iFrom);
-            int iArtist = s.indexOf(charStart + "ARTIST" + charEnd + " " + charStart + "t0" + charEnd);
-            assertTrue(iArtist > iFrom);
-            int iLeftJoin = s.indexOf("LEFT JOIN");
-            assertTrue(iLeftJoin > iFrom);
-            int iPainting = s.indexOf(charStart + "PAINTING" + charEnd + " " + charStart + "t1" + charEnd);
-            assertTrue(iPainting > iLeftJoin);
-            int iOn = s.indexOf(" ON ");
-            assertTrue(iOn > iLeftJoin);
-            int iArtistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
-            assertTrue(iArtistId > iOn);
-            int iArtistIdT1 = s
-                    .indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
-            assertTrue(iArtistIdT1 > iOn);
-            int i = s.indexOf("=", iLeftJoin);
-            assertTrue(iArtistIdT1 > i || iArtistId > i);
-            int iJoin = s.indexOf("JOIN");
-            assertTrue(iJoin > iLeftJoin);
-            int iPainting2 = s.indexOf(charStart + "PAINTING" + charEnd + " " + charStart + "t2" + charEnd);
-            assertTrue(iPainting2 > iJoin);
-            int iOn2 = s.indexOf(" ON ");
-            assertTrue(iOn2 > iJoin);
-            int iArtistId2 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iJoin);
-            assertTrue(iArtistId2 > iOn2);
-            int iArtistId2T2 = s.indexOf(charStart + "t2" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iJoin);
-            assertTrue(iArtistId2T2 > iOn2);
-            int i2 = s.indexOf("=", iJoin);
-            assertTrue(iArtistId2T2 > i2 || iArtistId2 > i2);
-            int iWhere = s.indexOf(" WHERE ");
-            assertTrue(iWhere > iJoin);
-
-            int paintingTitle2 = s.indexOf(charStart + "t2" + charEnd + "." + charStart + "PAINTING_TITLE" + charEnd
-                    + " = ?");
-            assertTrue(paintingTitle2 > iWhere);
-
-        } finally {
-            DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
-            entity.getDataMap().setQuotingSQLIdentifiers(false);
-        }
-    }
-
-    @Test
-    public void testCreateSqlStringWithQuoteSqlIdentifiers4() throws Exception {
-
-        // query with to-one joint prefetches
-        // and with QuoteSqlIdentifiers = true
-        try {
-            SelectQuery q = new SelectQuery(Painting.class);
-            q.addPrefetch(Painting.TO_ARTIST_PROPERTY).setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
-
-            DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
-            entity.getDataMap().setQuotingSQLIdentifiers(true);
-
-            String charStart = unitAdapter.getIdentifiersStartQuote();
-            String charEnd = unitAdapter.getIdentifiersEndQuote();
-
-            String s = new SelectTranslator(q, dataNode, connection).createSqlString();
-
-            assertTrue(s.startsWith("SELECT "));
-            int iFrom = s.indexOf(" FROM ");
-            assertTrue(iFrom > 0);
-
-            int paintingDescription = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "PAINTING_DESCRIPTION"
-                    + charEnd);
-            assertTrue(paintingDescription > 0 && paintingDescription < iFrom);
-            int paintingTitle = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "PAINTING_TITLE" + charEnd);
-            assertTrue(paintingTitle > 0 && paintingTitle < iFrom);
-            int artistIdT1 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
-            assertTrue(artistIdT1 > 0 && artistIdT1 < iFrom);
-            int estimatedPrice = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ESTIMATED_PRICE" + charEnd);
-            assertTrue(estimatedPrice > 0 && estimatedPrice < iFrom);
-            int galleryId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "GALLERY_ID" + charEnd);
-            assertTrue(galleryId > 0 && galleryId < iFrom);
-            int paintingId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "PAINTING_ID" + charEnd);
-            assertTrue(paintingId > 0 && paintingId < iFrom);
-            int artistName = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_NAME" + charEnd);
-            assertTrue(artistName > 0 && artistName < iFrom);
-            int artistId = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
-            assertTrue(artistId > 0 && artistId < iFrom);
-            int dateOfBirth = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd);
-            assertTrue(dateOfBirth > 0 && dateOfBirth < iFrom);
-            int iPainting = s.indexOf(charStart + "PAINTING" + charEnd + " " + charStart + "t0" + charEnd);
-            assertTrue(iPainting > iFrom);
-
-            int iLeftJoin = s.indexOf("LEFT JOIN");
-            assertTrue(iLeftJoin > iFrom);
-            int iArtist = s.indexOf(charStart + "ARTIST" + charEnd + " " + charStart + "t1" + charEnd);
-            assertTrue(iArtist > iLeftJoin);
-            int iOn = s.indexOf(" ON ");
-            assertTrue(iOn > iLeftJoin);
-            int iArtistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
-            assertTrue(iArtistId > iOn);
-            int iArtistIdT1 = s
-                    .indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
-            assertTrue(iArtistIdT1 > iOn);
-            int i = s.indexOf("=", iLeftJoin);
-            assertTrue(iArtistIdT1 > i || iArtistId > i);
-
-        } finally {
-            DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
-            entity.getDataMap().setQuotingSQLIdentifiers(false);
-        }
-    }
-
-    /**
-     * Tests columns generated for a simple object query.
-     */
-    @Test
-    public void testBuildResultColumns1() throws Exception {
-        SelectQuery q = new SelectQuery(Painting.class);
-        SelectTranslator tr = new SelectTranslator(q, dataNode, connection);
-
-        List<?> columns = tr.buildResultColumns();
-
-        // all DbAttributes must be included
-        DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
-        for (final DbAttribute a : entity.getAttributes()) {
-            ColumnDescriptor c = new ColumnDescriptor(a, "t0");
-            assertTrue("No descriptor for " + a + ", columns: " + columns, columns.contains(c));
-        }
-    }
-
-    /**
-     * Tests columns generated for an object query with joint prefetch.
-     */
-    @Test
-    public void testBuildResultColumns2() throws Exception {
-        SelectQuery q = new SelectQuery(Painting.class);
-        q.addPrefetch(Painting.TO_ARTIST_PROPERTY).setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
-        SelectTranslator tr = new SelectTranslator(q, dataNode, connection);
-
-        List<?> columns = tr.buildResultColumns();
-
-        // assert root entity columns
-        DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
-        for (final DbAttribute a : entity.getAttributes()) {
-            ColumnDescriptor c = new ColumnDescriptor(a, "t0");
-            assertTrue("No descriptor for " + a + ", columns: " + columns, columns.contains(c));
-        }
-
-        // assert joined columns
-        DbEntity joined = context.getEntityResolver().getDbEntity("ARTIST");
-        for (final DbAttribute a : joined.getAttributes()) {
-
-            // skip ARTIST PK, it is joined from painting
-            if (Artist.ARTIST_ID_PK_COLUMN.equals(a.getName())) {
-                continue;
-            }
-
-            ColumnDescriptor c = new ColumnDescriptor(a, "t1");
-            c.setDataRowKey("toArtist." + a.getName());
-            assertTrue("No descriptor for " + a + ", columns: " + columns, columns.contains(c));
-        }
-    }
-
-}


[7/7] cayenne git commit: CAY-2003 cdbimport doesn't work properly with several includeTable tags

Posted by aa...@apache.org.
CAY-2003 cdbimport doesn't work properly with several includeTable tags

missing record in RELEASE-NOTES


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/a6d153a2
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/a6d153a2
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/a6d153a2

Branch: refs/heads/master
Commit: a6d153a282b51d0c911efd16ccf6100b1400cb9e
Parents: 81f4fb5
Author: aadamchik <aa...@apache.org>
Authored: Tue Apr 28 12:44:27 2015 +0200
Committer: aadamchik <aa...@apache.org>
Committed: Tue Apr 28 12:44:27 2015 +0200

----------------------------------------------------------------------
 docs/doc/src/main/resources/RELEASE-NOTES.txt | 1 +
 1 file changed, 1 insertion(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/a6d153a2/docs/doc/src/main/resources/RELEASE-NOTES.txt
----------------------------------------------------------------------
diff --git a/docs/doc/src/main/resources/RELEASE-NOTES.txt b/docs/doc/src/main/resources/RELEASE-NOTES.txt
index 46dd250..2d270aa 100644
--- a/docs/doc/src/main/resources/RELEASE-NOTES.txt
+++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt
@@ -31,6 +31,7 @@ CAY-1994 Modeler Migration Tool Shows No Changes
 CAY-1997 Difference in NULL handling inside the path between PropertyUtils and DataObject.readNestedProperty
 CAY-1998 Speeding up PropertyUtils
 CAY-1999 Unneeded Property import for superclasses with no properties
+CAY-2003 cdbimport doesn't work properly with several includeTable tags
 
 ----------------------------------
 Release: 4.0.M2