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:10 UTC

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

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);
+	}
 }