http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java
new file mode 100644
index 0000000..b330608
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java
@@ -0,0 +1,129 @@
+/*****************************************************************
+ *   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.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.Entity;
+
+/**
+ * @since 4.2
+ */
+abstract class PathProcessor<T extends Entity> implements 
PathTranslationResult {
+
+    public static final char OUTER_JOIN_INDICATOR = '+';
+    public static final char SPLIT_PATH_INDICATOR = '#';
+
+    protected final Map<String, String> pathSplitAliases;
+    protected final TranslatorContext context;
+    protected final List<String> attributePaths;
+    protected final List<DbAttribute> attributes;
+    protected final StringBuilder currentDbPath;
+
+    protected boolean lastComponent;
+    protected boolean isOuterJoin;
+    protected T entity;
+    protected DbRelationship relationship;
+    protected String currentAlias;
+
+    public PathProcessor(TranslatorContext context, T entity) {
+        this.context = Objects.requireNonNull(context);
+        this.entity = Objects.requireNonNull(entity);
+        this.pathSplitAliases = context.getMetadata().getPathSplitAliases();
+        this.currentDbPath = new StringBuilder();
+        this.attributes = new ArrayList<>(1);
+        this.attributePaths = new ArrayList<>(1);
+    }
+
+    public PathTranslationResult process(String path) {
+        PathComponents components = new PathComponents(path);
+        String[] rawComponents = components.getAll();
+        for(int i=0; i<rawComponents.length; i++) {
+            String next = rawComponents[i];
+            isOuterJoin = false;
+            lastComponent = i == rawComponents.length - 1;
+            String alias = pathSplitAliases.get(next);
+            if(alias != null) {
+                currentAlias = next;
+                processAliasedAttribute(next, alias);
+                currentAlias = null;
+            } else {
+                if(next.charAt(next.length() - 1) == OUTER_JOIN_INDICATOR) {
+                    isOuterJoin = true;
+                    next = next.substring(0, next.length() - 1);
+                }
+                processNormalAttribute(next);
+            }
+        }
+
+        return this;
+    }
+
+    protected void addAttribute(String path, DbAttribute attribute) {
+        attributePaths.add(path);
+        attributes.add(attribute);
+    }
+
+    abstract protected void processAliasedAttribute(String next, String alias);
+
+    abstract protected void processNormalAttribute(String next);
+
+    @Override
+    public List<DbAttribute> getDbAttributes() {
+        return attributes;
+    }
+
+    @Override
+    public List<String> getAttributePaths() {
+        return attributePaths;
+    }
+
+    @Override
+    public Optional<DbRelationship> getDbRelationship() {
+        if(relationship == null) {
+            return Optional.empty();
+        }
+        return Optional.of(relationship);
+    }
+
+    @Override
+    public String getFinalPath() {
+        return currentDbPath.toString();
+    }
+
+    protected void appendCurrentPath(String nextSegment) {
+        if(currentDbPath.length() > 0) {
+            currentDbPath.append('.');
+        }
+        currentDbPath.append(nextSegment);
+        if(currentAlias != null) {
+            currentDbPath.append(SPLIT_PATH_INDICATOR).append(currentAlias);
+        }
+        if(isOuterJoin) {
+            currentDbPath.append(OUTER_JOIN_INDICATOR);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
new file mode 100644
index 0000000..f4158de
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   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.util.List;
+import java.util.Optional;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbRelationship;
+
+/**
+ * Interface that describes result of path translation
+ *
+ * @since 4.2
+ */
+interface PathTranslationResult {
+
+    String getFinalPath();
+
+    Optional<DbRelationship> getDbRelationship();
+
+    List<DbAttribute> getDbAttributes();
+
+    List<String> getAttributePaths();
+
+    default DbAttribute getLastAttribute() {
+        return getDbAttributes().get(getDbAttributes().size() - 1);
+    }
+
+    default String getLastAttributePath() {
+        return getAttributePaths().get(getAttributePaths().size() - 1);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslator.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslator.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslator.java
new file mode 100644
index 0000000..c3dd57f
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslator.java
@@ -0,0 +1,60 @@
+/*****************************************************************
+ *   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.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * @since 4.2
+ */
+class PathTranslator {
+
+    private final Map<String, PathTranslationResult> objResultCache = new 
ConcurrentHashMap<>();
+    private final Map<String, PathTranslationResult> dbResultCache = new 
ConcurrentHashMap<>();
+
+    private final TranslatorContext context;
+
+    PathTranslator(TranslatorContext context) {
+        this.context = context;
+    }
+
+    PathTranslationResult translatePath(ObjEntity entity, String path, String 
parentPath) {
+        return objResultCache.computeIfAbsent(parentPath + '.' + 
entity.getName() + '.' + path,
+                (k) -> new ObjPathProcessor(context, entity, 
parentPath).process(path));
+    }
+
+    PathTranslationResult translatePath(ObjEntity entity, String path) {
+        return translatePath(entity, path, null);
+    }
+
+    PathTranslationResult translatePath(DbEntity entity, String path, String 
parentPath) {
+        return dbResultCache.computeIfAbsent(parentPath + '.' + 
entity.getName() + '.' + path,
+                (k) -> new DbPathProcessor(context, entity, 
parentPath).process(path));
+    }
+
+    PathTranslationResult translatePath(DbEntity entity, String path) {
+        return translatePath(entity, path, null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
new file mode 100644
index 0000000..90c0f3d
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
@@ -0,0 +1,128 @@
+/*****************************************************************
+ *   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.CayenneRuntimeException;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.JoinType;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.query.PrefetchSelectQuery;
+import org.apache.cayenne.query.PrefetchTreeNode;
+import org.apache.cayenne.query.Select;
+import org.apache.cayenne.reflect.ClassDescriptor;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.table;
+
+/**
+ * @since 4.2
+ */
+class PrefetchNodeStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        updatePrefetchNodes(context);
+        processJoint(context);
+        processPrefetchQuery(context);
+    }
+
+    private void updatePrefetchNodes(TranslatorContext context) {
+        if(context.getQuery().getPrefetchTree() == null) {
+            return;
+        }
+        // Set entity name, in case MixedConversionStrategy will be used to 
select objects from this query
+        // Note: all prefetch nodes will point to query root, it is not a 
problem until select query can't
+        // perform some sort of union or sub-queries.
+        for(PrefetchTreeNode prefetch : 
context.getQuery().getPrefetchTree().getChildren()) {
+            
prefetch.setEntityName(context.getMetadata().getObjEntity().getName());
+        }
+    }
+
+    private void processJoint(TranslatorContext context) {
+        PrefetchTreeNode prefetch = context.getQuery().getPrefetchTree();
+        if(prefetch == null) {
+            return;
+        }
+
+        ObjEntity objEntity = context.getMetadata().getObjEntity();
+
+        for(PrefetchTreeNode node : prefetch.adjacentJointNodes()) {
+            Expression prefetchExp = ExpressionFactory.exp(node.getPath());
+            ASTDbPath dbPrefetch = (ASTDbPath) 
objEntity.translateToDbPath(prefetchExp);
+            final String dbPath = dbPrefetch.getPath();
+            DbEntity dbEntity = objEntity.getDbEntity();
+
+            PathComponents components = new PathComponents(dbPath);
+            StringBuilder fullPath = new StringBuilder();
+            for(String c : components.getAll()) {
+                DbRelationship rel = dbEntity.getRelationship(c);
+                if(rel == null) {
+                    throw new CayenneRuntimeException("Unable to resolve path 
%s for entity %s", dbPath, objEntity.getName());
+                }
+                if(fullPath.length() > 0) {
+                    fullPath.append('.');
+                }
+                context.getTableTree().addJoinTable("p:" + 
fullPath.append(c).toString(), rel, JoinType.LEFT_OUTER);
+                dbEntity = rel.getTargetEntity();
+            }
+
+            ObjRelationship targetRel = (ObjRelationship) 
prefetchExp.evaluate(objEntity);
+            ClassDescriptor prefetchClassDescriptor = 
context.getResolver().getClassDescriptor(targetRel.getTargetEntityName());
+
+            DescriptorColumnExtractor columnExtractor = new 
DescriptorColumnExtractor(context, prefetchClassDescriptor);
+            columnExtractor.extract("p:" + dbPath);
+        }
+    }
+
+    private void processPrefetchQuery(TranslatorContext context) {
+        Select<?> select = context.getQuery().unwrap();
+        if(!(select instanceof PrefetchSelectQuery)) {
+            return;
+        }
+
+        PathTranslator pathTranslator = context.getPathTranslator();
+        PrefetchSelectQuery<?> prefetchSelectQuery = (PrefetchSelectQuery<?>) 
select;
+        for(String prefetchPath: prefetchSelectQuery.getResultPaths()) {
+            ASTDbPath pathExp = (ASTDbPath) 
context.getMetadata().getClassDescriptor().getEntity()
+                    .translateToDbPath(ExpressionFactory.exp(prefetchPath));
+
+            String path = pathExp.getPath();
+            PathTranslationResult result = pathTranslator
+                    .translatePath(context.getMetadata().getDbEntity(), path);
+            result.getDbRelationship().ifPresent(r -> {
+                DbEntity targetEntity = r.getTargetEntity();
+                context.getTableTree().addJoinTable(path, r, JoinType.INNER);
+                for (DbAttribute pk : targetEntity.getPrimaryKeys()) {
+                    // note that we may select a source attribute, but label 
it as target for simplified snapshot processing
+                    String finalPath = path + '.' + pk.getName();
+                    String alias = context.getTableTree().aliasForPath(path);
+                    Node columnNode = table(alias).column(pk).build();
+                    context.addResultNode(columnNode, 
finalPath).setDbAttribute(pk);
+                }
+            });
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java
new file mode 100644
index 0000000..c4d2ba1
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java
@@ -0,0 +1,74 @@
+/*****************************************************************
+ *   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.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.exp.parser.ASTObjPath;
+import org.apache.cayenne.exp.parser.SimpleNode;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.reflect.ClassDescriptor;
+
+/**
+ * @since 4.2
+ */
+class QualifierTranslationStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        QualifierTranslator translator = context.getQualifierTranslator();
+
+        Expression expression = context.getQuery().getQualifier();
+
+        // Attaching Obj entity's qualifier
+        ObjEntity entity = context.getMetadata().getObjEntity();
+        if (entity != null) {
+            ClassDescriptor descriptor = 
context.getMetadata().getClassDescriptor();
+            Expression entityQualifier = 
descriptor.getEntityInheritanceTree().qualifierForEntityAndSubclasses();
+            if (entityQualifier != null) {
+                expression = expression == null ? entityQualifier : 
expression.andExp(entityQualifier);
+            }
+        }
+
+        // Attaching root Db entity's qualifier
+        DbEntity dbEntity = context.getMetadata().getDbEntity();
+        if (dbEntity != null) {
+            Expression dbQualifier = dbEntity.getQualifier();
+            if (dbQualifier != null) {
+                dbQualifier = dbQualifier.transform(node -> {
+                    if (node instanceof ASTObjPath) {
+                        return new ASTDbPath(((SimpleNode) 
node).getOperand(0));
+                    }
+                    return node;
+                });
+
+                expression = expression == null ? dbQualifier : 
expression.andExp(dbQualifier);
+            }
+        }
+
+        Node qualifierNode = translator.translate(expression);
+
+        if(qualifierNode != null) {
+            context.getSelectBuilder().where(qualifierNode);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/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 3795976..d48a53f 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
@@ -19,672 +19,394 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import java.io.IOException;
-import java.util.Arrays;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
-import java.util.function.Function;
+import java.util.Set;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
-import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.access.sqlbuilder.ExpressionNodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.ValueNodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.sqltree.BetweenNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.BitwiseNotNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.EmptyNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.EqualNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.InNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NotEqualNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NotNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TextNode;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.TraversalHandler;
 import org.apache.cayenne.exp.parser.ASTDbPath;
-import org.apache.cayenne.exp.parser.ASTExtract;
+import org.apache.cayenne.exp.parser.ASTFullObject;
 import org.apache.cayenne.exp.parser.ASTFunctionCall;
 import org.apache.cayenne.exp.parser.ASTObjPath;
+import org.apache.cayenne.exp.parser.ASTSubquery;
 import org.apache.cayenne.exp.parser.PatternMatchNode;
 import org.apache.cayenne.exp.parser.SimpleNode;
+import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.JoinType;
-import org.apache.cayenne.map.ObjEntity;
-import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.SelectQuery;
-import org.apache.cayenne.reflect.ClassDescriptor;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
+import static org.apache.cayenne.exp.Expression.*;
 
 /**
- * Translates query qualifier to SQL. Used as a helper class by query
- * translators.
+ * @since 4.2
  */
-public class QualifierTranslator extends QueryAssemblerHelper implements 
TraversalHandler {
-
-       protected DataObjectMatchTranslator objectMatchTranslator;
-       protected boolean matchingObject;
-       protected boolean caseInsensitive;
-
-       /**
-        * @since 4.0
-        */
-       protected boolean useAliasForExpressions;
-
-       /**
-        * @since 4.0
-        */
-       protected Expression waitingForEndNode;
-
-       /**
-        * @since 4.0
-        */
-       protected Expression qualifier;
-
-       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;
-       }
-
-       /**
-        * Explicitly set qualifier.
-        * It will be used instead of extracting qualifier from the query 
itself.
-        * @since 4.0
-        */
-       public void setQualifier(Expression qualifier) {
-               this.qualifier = qualifier;
-       }
-
-       /**
-        * @since 4.0
-        */
-       public void setUseAliasForExpressions(boolean useAliasForExpressions) {
-               this.useAliasForExpressions = useAliasForExpressions;
-       }
-
-       /**
-        * 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() {
-               // if additional qualifier is set, use it
-               if(this.qualifier != null) {
-                       return this.qualifier;
-               }
-
-               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();
-       }
-
-       @Override
-       public void finishedChild(Expression node, int childIndex, boolean 
hasMoreChildren) {
-
-               if(waitingForEndNode != null) {
-                       return;
-               }
-
-               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 ">>";
-       }
-
-       @Override
-       public void startNode(Expression node, Expression parentNode) {
-
-               if(waitingForEndNode != null) {
-                       return;
-               }
-
-               if(useAliasForExpressions) {
-                       String alias = 
queryAssembler.getAliasForExpression(node);
-                       if(alias != null) {
-                               out.append(alias);
-                               waitingForEndNode = node;
-                               return;
-                       }
-               }
-
-               boolean parenthesisNeeded = parenthesisNeeded(node, parentNode);
-
-               if(node.getType() == Expression.FUNCTION_CALL) {
-                       if(node instanceof ASTExtract) {
-                               appendExtractFunction((ASTExtract) node);
-                       } else {
-                               appendFunction((ASTFunctionCall) node);
-                       }
-                       if(parenthesisNeeded) {
-                               out.append("(");
-                       }
-                       return;
-               }
-
-               if(node.getType() == Expression.FULL_OBJECT && parentNode != 
null) {
-                       throw new CayenneRuntimeException("Expression is not 
supported in where clause.");
-               }
-
-               int count = node.getOperandCount();
-
-               if (count == 2) {
-                       // binary nodes are the only ones that currently 
require this
-                       detectObjectMatch(node);
-               }
-
-               if (parenthesisNeeded) {
-                       out.append('(');
-               }
-
-               if (count == 0) {
-                       // not all databases handle true/false
-                       if (node.getType() == Expression.TRUE) {
-                               out.append("1 = 1");
-                       } else if (node.getType() == Expression.FALSE) {
-                               out.append("1 = 0");
-                       } else if (node.getType() == Expression.ASTERISK) {
-                               out.append("*");
-                       }
-               }
-
-               if (count == 1) {
-                       if (node.getType() == Expression.NEGATIVE) {
-                               out.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
-        */
-       @Override
-       public void endNode(Expression node, Expression parentNode) {
-
-               if(waitingForEndNode != null) {
-                       if(node == waitingForEndNode) {
-                               waitingForEndNode = null;
-                       }
-                       return;
-               }
-
-               try {
-                       // check if we need to use objectMatchTranslator to 
finish building the expression
-                       if (node.getOperandCount() == 2 && matchingObject) {
-                               appendObjectMatch();
-                       }
-
-                       boolean parenthesisNeeded = parenthesisNeeded(node, 
parentNode);
-                       boolean likeIgnoreCase = (node.getType() == 
Expression.LIKE_IGNORE_CASE || node.getType() == 
Expression.NOT_LIKE_IGNORE_CASE);
-                       boolean isPatternMatchNode = 
PatternMatchNode.class.isAssignableFrom(node.getClass());
-
-                       // closing UPPER parenthesis
-                       if (likeIgnoreCase && !caseInsensitive) {
-                               out.append(')');
-                       }
-
-                       if (isPatternMatchNode) {
-                               appendLikeEscapeCharacter((PatternMatchNode) 
node);
-                       }
-
-                       // clean up trailing comma in function argument list
-                       if(node.getType() == Expression.FUNCTION_CALL) {
-                               
clearLastFunctionArgDivider((ASTFunctionCall)node);
-                       }
-
-                       // closing LIKE parenthesis
-                       if (parenthesisNeeded) {
-                               out.append(')');
-                       }
-
-                       // if inside function call, put comma between arguments
-                       if(parentNode != null && parentNode.getType() == 
Expression.FUNCTION_CALL) {
-                               appendFunctionArgDivider((ASTFunctionCall) 
parentNode);
-                       }
-               } catch (IOException ioex) {
-                       throw new CayenneRuntimeException("Error appending 
content", ioex);
-               }
-       }
-
-       @Override
-       public void objectNode(Object leaf, Expression parentNode) {
-               if(waitingForEndNode != null) {
-                       return;
-               }
-
-               try {
-                       switch (parentNode.getType()) {
-                               case Expression.OBJ_PATH:
-                                       appendObjPath(parentNode);
-                                       break;
-                               case Expression.DB_PATH:
-                                       appendDbPath(parentNode);
-                                       break;
-                               case Expression.LIST:
-                                       appendList(parentNode, 
paramsDbType(parentNode));
-                                       break;
-                               case Expression.FUNCTION_CALL:
-                                       appendFunctionArg(leaf, 
(ASTFunctionCall)parentNode);
-                                       break;
-                               default:
-                                       appendLiteral(leaf, 
paramsDbType(parentNode), parentNode);
-                       }
-               } catch (IOException ioex) {
-                       throw new CayenneRuntimeException("Error appending 
content", ioex);
-               }
-       }
-
-       protected boolean parenthesisNeeded(Expression node, Expression 
parentNode) {
-               if (node.getType() == Expression.FUNCTION_CALL) {
-                       return ((ASTFunctionCall)node).needParenthesis();
-               }
-
-               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
-                               || node.getType() == Expression.DB_PATH
-                               || node.getType() == Expression.ASTERISK) {
-                       return false;
-               }
-
-               return true;
-       }
-
-       private final void appendList(Expression listExpr, DbAttribute 
paramDesc) throws IOException {
-               Iterator<?> it;
-               Object list = listExpr.getOperand(0);
-               if (list instanceof List) {
-                       it = ((List<?>) list).iterator();
-               } else if (list instanceof Object[]) {
-                       it = Arrays.asList((Object[]) list).iterator();
-               } 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);
-               }
-       }
-
-       /**
-        * Append function name to result SQL
-        * Override this method to rename or skip function if generic name 
isn't supported on target DB.
-        * @since 4.0
-        */
-       protected void appendFunction(ASTFunctionCall functionExpression) {
-               out.append(functionExpression.getFunctionName());
-       }
-
-       /**
-        * Special case for extract date/time parts functions as they have many 
variants
-        * @since 4.0
-        */
-       protected void appendExtractFunction(ASTExtract functionExpression) {
-               appendFunction(functionExpression);
-       }
-
-       /**
-        * Append scalar argument of a function call
-        * Used only for values stored in ASTScalar other
-        * expressions appended in objectNode() method
-        *
-        * @since 4.0
-        */
-       protected void appendFunctionArg(Object value, ASTFunctionCall 
functionExpression) throws IOException {
-               // Create fake DbAttribute to pass argument info down to bind 
it to SQL prepared statement
-               DbAttribute dbAttrForArg = new DbAttribute();
-               
dbAttrForArg.setType(TypesMapping.getSqlTypeByJava(value.getClass()));
-               super.appendLiteral(value, dbAttrForArg, functionExpression);
-               appendFunctionArgDivider(functionExpression);
-       }
-
-       /**
-        * Append divider between function arguments.
-        * In overriding methods can be replaced e.g. for " || " for CONCAT 
operation
-        * @since 4.0
-        */
-       protected void appendFunctionArgDivider(ASTFunctionCall 
functionExpression) {
-               out.append(", ");
-       }
-
-       /**
-        * Clear last divider as we currently don't now position of argument 
until parent element is ended.
-        * @since 4.0
-        */
-       protected void clearLastFunctionArgDivider(ASTFunctionCall 
functionExpression) {
-               if(functionExpression.getOperandCount() > 0) {
-                       out.delete(out.length() - 2, out.length());
-               }
-       }
-
-       /**
-        * 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 Function<Object, Object> {
-
-               public Object apply(Object input) {
-                       if (input instanceof ASTObjPath) {
-                               return new ASTDbPath(((SimpleNode) 
input).getOperand(0));
-                       }
-                       return input;
-               }
-       }
+class QualifierTranslator implements TraversalHandler {
+
+    private final TranslatorContext context;
+    private final PathTranslator pathTranslator;
+    private final Set<Object> expressionsToSkip;
+    private final Deque<Node> nodeStack;
+
+    private Node currentNode;
+
+    QualifierTranslator(TranslatorContext context) {
+        this.context = context;
+        this.pathTranslator = context.getPathTranslator();
+        this.expressionsToSkip = new HashSet<>();
+        this.nodeStack = new ArrayDeque<>();
+    }
+
+    Node translate(BaseProperty<?> property) {
+        if(property == null) {
+            return null;
+        }
+
+        Node result = translate(property.getExpression());
+        if(property.getAlias() != null) {
+            return aliased(result, property.getAlias()).build();
+        }
+        return result;
+    }
+
+    Node translate(Expression qualifier) {
+        if(qualifier == null) {
+            return null;
+        }
+
+        Node rootNode = new EmptyNode();
+        expressionsToSkip.clear();
+        boolean hasCurrentNode = currentNode != null;
+        if(hasCurrentNode) {
+            nodeStack.push(currentNode);
+        }
+
+        currentNode = rootNode;
+        qualifier.traverse(this);
+
+        if(hasCurrentNode) {
+            currentNode = nodeStack.pop();
+        } else {
+            currentNode = null;
+        }
+
+        if(rootNode.getChildrenCount() == 1) {
+            // trim empty node
+            Node child = rootNode.getChild(0);
+            child.setParent(null);
+            return child;
+        }
+        return rootNode;
+    }
+
+    @Override
+    public void startNode(Expression node, Expression parentNode) {
+        if(expressionsToSkip.contains(node) || 
expressionsToSkip.contains(parentNode)) {
+            return;
+        }
+        Node nextNode = expressionNodeToSqlNode(node, parentNode);
+        if(nextNode == null) {
+            return;
+        }
+        currentNode.addChild(nextNode);
+        nextNode.setParent(currentNode);
+        currentNode = nextNode;
+    }
+
+    private Node expressionNodeToSqlNode(Expression node, Expression 
parentNode) {
+        switch (node.getType()) {
+            case NOT_IN:
+                return new InNode(true);
+            case IN:
+                return new InNode(false);
+            case NOT_BETWEEN:
+            case BETWEEN:
+                return new BetweenNode(node.getType() == NOT_BETWEEN);
+            case NOT:
+                return new NotNode();
+            case BITWISE_NOT:
+                return new BitwiseNotNode();
+            case EQUAL_TO:
+                return new EqualNode();
+            case NOT_EQUAL_TO:
+                return new NotEqualNode();
+
+            case LIKE:
+            case NOT_LIKE:
+            case LIKE_IGNORE_CASE:
+            case NOT_LIKE_IGNORE_CASE:
+                PatternMatchNode patternMatchNode = (PatternMatchNode)node;
+                boolean not = node.getType() == NOT_LIKE || node.getType() == 
NOT_LIKE_IGNORE_CASE;
+                return new LikeNode(patternMatchNode.isIgnoringCase(), not, 
patternMatchNode.getEscapeChar());
+
+            case OBJ_PATH:
+                String path = (String)node.getOperand(0);
+                PathTranslationResult result = 
pathTranslator.translatePath(context.getMetadata().getObjEntity(), path);
+                return processPathTranslationResult(node, parentNode, result);
+
+            case DB_PATH:
+                String dbPath = (String)node.getOperand(0);
+                PathTranslationResult dbResult = 
pathTranslator.translatePath(context.getMetadata().getDbEntity(), dbPath);
+                return processPathTranslationResult(node, parentNode, 
dbResult);
+
+            case FUNCTION_CALL:
+                ASTFunctionCall functionCall = (ASTFunctionCall)node;
+                return function(functionCall.getFunctionName()).build();
+
+            case ADD:
+            case SUBTRACT:
+            case MULTIPLY:
+            case DIVIDE:
+            case NEGATIVE:
+            case BITWISE_AND:
+            case BITWISE_LEFT_SHIFT:
+            case BITWISE_OR:
+            case BITWISE_RIGHT_SHIFT:
+            case BITWISE_XOR:
+            case OR:
+            case AND:
+            case LESS_THAN:
+            case LESS_THAN_EQUAL_TO:
+            case GREATER_THAN:
+            case GREATER_THAN_EQUAL_TO:
+                return new OpExpressionNode(expToStr(node.getType()));
+
+            case TRUE:
+            case FALSE:
+            case ASTERISK:
+                return new TextNode(' ' + expToStr(node.getType()));
+
+            case EXISTS:
+                return new FunctionNode("EXISTS", null, false);
+
+            case SUBQUERY:
+                ASTSubquery subquery = (ASTSubquery)node;
+                DefaultSelectTranslator translator = new 
DefaultSelectTranslator(subquery.getQuery(), context);
+                translator.translate();
+                return translator.getContext().getSelectBuilder().build();
+
+            case ENCLOSING_OBJECT:
+                // Translate via parent context's translator
+                Expression expression = (Expression) node.getOperand(0);
+                if(context.getParentContext() == null) {
+                    throw new CayenneRuntimeException("Unable to translate 
qualifier, no parent context to use for expression " + node);
+                }
+                expressionsToSkip.add(expression);
+                return 
context.getParentContext().getQualifierTranslator().translate(expression);
+
+            case FULL_OBJECT:
+                ASTFullObject fullObject = (ASTFullObject)node;
+                if(fullObject.getOperandCount() == 0) {
+                    Collection<DbAttribute> dbAttributes = 
context.getMetadata().getDbEntity().getPrimaryKeys();
+                    if(dbAttributes.size() > 1) {
+                        throw new CayenneRuntimeException("Unable to translate 
reference on entity with more than one PK.");
+                    }
+                    DbAttribute attribute = dbAttributes.iterator().next();
+                    String alias = 
context.getTableTree().aliasForAttributePath(attribute.getName());
+                    return table(alias).column(attribute).build();
+                } else {
+                    return null;
+                }
+        }
+
+        return null;
+    }
+
+    private Node processPathTranslationResult(Expression node, Expression 
parentNode, PathTranslationResult result) {
+        if(result.getDbRelationship().isPresent()
+                && result.getDbAttributes().size() > 1
+                && 
result.getDbRelationship().get().getTargetEntity().getPrimaryKeys().size() > 1) 
{
+            return createMultiAttributeMatch(node, parentNode, result);
+        } else if(result.getDbAttributes().isEmpty()) {
+            return new EmptyNode();
+        } else {
+            String alias = 
context.getTableTree().aliasForPath(result.getLastAttributePath());
+            return table(alias).column(result.getLastAttribute()).build();
+        }
+    }
+
+    private Node createMultiAttributeMatch(Expression node, Expression 
parentNode, PathTranslationResult result) {
+        ObjectId objectId = null;
+        expressionsToSkip.add(node);
+        expressionsToSkip.add(parentNode);
+
+        int siblings = parentNode.getOperandCount();
+        for(int i=0; i<siblings; i++) {
+            Object operand = parentNode.getOperand(i);
+            if(node == operand) {
+                continue;
+            }
+
+            if(operand instanceof Persistent) {
+                objectId = ((Persistent) operand).getObjectId();
+                break;
+            } else if(operand instanceof ObjectId) {
+                objectId = (ObjectId)operand;
+                break;
+            } else if(operand instanceof ASTObjPath) {
+                // TODO: support comparision of multi attribute ObjPath with 
other multi attribute ObjPath
+                throw new UnsupportedOperationException("Comparision of 
multiple attributes not supported for ObjPath");
+            }
+        }
+
+        if(objectId == null) {
+            throw new CayenneRuntimeException("Multi attribute ObjPath isn't 
matched with valid value. " +
+                    "List or Persistent object required.");
+        }
+
+        ExpressionNodeBuilder expressionNodeBuilder = null;
+        ExpressionNodeBuilder eq;
+
+        Iterator<DbAttribute> pkIt = 
result.getDbRelationship().get().getTargetEntity().getPrimaryKeys().iterator();
+        int count = result.getDbAttributes().size();
+
+        for(int i=0; i<count; i++) {
+            String path = result.getAttributePaths().get(i);
+            DbAttribute attribute = result.getDbAttributes().get(i);
+            DbAttribute pk = pkIt.next();
+            String alias = context.getTableTree().aliasForPath(path);
+            Object nextValue = objectId.getIdSnapshot().get(pk.getName());
+            eq = table(alias).column(attribute).eq(value(nextValue));
+            if (expressionNodeBuilder == null) {
+                expressionNodeBuilder = eq;
+            } else {
+                expressionNodeBuilder = expressionNodeBuilder.and(eq);
+            }
+        }
+
+        return expressionNodeBuilder.build();
+    }
+
+    @Override
+    public void endNode(Expression node, Expression parentNode) {
+        if(expressionsToSkip.contains(node) || 
expressionsToSkip.contains(parentNode)) {
+            return;
+        }
+        if(currentNode.getParent() != null) {
+            currentNode = currentNode.getParent();
+        }
+    }
+
+    @Override
+    public void objectNode(Object leaf, Expression parentNode) {
+        if(expressionsToSkip.contains(parentNode)) {
+            return;
+        }
+        if(parentNode.getType() == Expression.OBJ_PATH || parentNode.getType() 
== Expression.DB_PATH) {
+            return;
+        }
+
+        ValueNodeBuilder valueNodeBuilder = 
value(leaf).attribute(findDbAttribute(parentNode));
+        if(parentNode.getType() == Expression.LIST) {
+            valueNodeBuilder.array(true);
+        }
+        Node nextNode = valueNodeBuilder.build();
+
+        currentNode.addChild(nextNode);
+        nextNode.setParent(currentNode);
+    }
+
+    protected DbAttribute findDbAttribute(Expression node) {
+        int len = node.getOperandCount();
+        if (len != 2) {
+            if (node instanceof SimpleNode) {
+                Expression parent = (Expression) ((SimpleNode) 
node).jjtGetParent();
+                if (parent != null) {
+                    node = parent;
+                } else {
+                    return null;
+                }
+            }
+        }
+
+        PathTranslationResult result = null;
+        for(int i=0; i<node.getOperandCount(); i++) {
+            Object op = node.getOperand(i);
+            if(op instanceof ASTObjPath) {
+                result = 
pathTranslator.translatePath(context.getMetadata().getObjEntity(), 
((ASTObjPath) op).getPath());
+                break;
+            } else if(op instanceof ASTDbPath) {
+                result = 
pathTranslator.translatePath(context.getMetadata().getDbEntity(), ((ASTDbPath) 
op).getPath());
+                break;
+            }
+        }
+
+        if(result == null) {
+            return null;
+        }
+
+        return result.getLastAttribute();
+    }
+
+    @Override
+    public void finishedChild(Expression node, int childIndex, boolean 
hasMoreChildren) {
+    }
+
+    private String expToStr(int type) {
+        switch (type) {
+            case AND:
+                return "AND";
+            case OR:
+                return "OR";
+            case LESS_THAN:
+                return "<";
+            case LESS_THAN_EQUAL_TO:
+                return "<=";
+            case GREATER_THAN:
+                return ">";
+            case GREATER_THAN_EQUAL_TO:
+                return ">=";
+            case ADD:
+                return "+";
+            case SUBTRACT:
+                return "-";
+            case MULTIPLY:
+                return "*";
+            case DIVIDE:
+                return "/";
+            case NEGATIVE:
+                return "-";
+            case BITWISE_AND:
+                return "&";
+            case BITWISE_OR:
+                return "|";
+            case BITWISE_XOR:
+                return "^";
+            case BITWISE_NOT:
+                return "!";
+            case BITWISE_LEFT_SHIFT:
+                return "<<";
+            case BITWISE_RIGHT_SHIFT:
+                return ">>";
+            case TRUE:
+                return "1=1";
+            case FALSE:
+                return "1=0";
+            case ASTERISK:
+                return "*";
+            default:
+                return "{other}";
+        }
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/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
deleted file mode 100644
index 3293168..0000000
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
+++ /dev/null
@@ -1,197 +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
- * <p/>
- * http://www.apache.org/licenses/LICENSE-2.0
- * <p/>
- * 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.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.cayenne.access.translator.DbAttributeBinding;
-import org.apache.cayenne.access.types.ExtendedType;
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.map.JoinType;
-import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.QueryMetadata;
-
-/**
- * Abstract superclass of Query translators.
- */
-public abstract class QueryAssembler {
-
-       protected Query query;
-       protected QueryMetadata queryMetadata;
-       protected boolean translated;
-       protected String sql;
-       protected DbAdapter adapter;
-       protected EntityResolver entityResolver;
-       protected List<DbAttributeBinding> bindings;
-       /**
-        * @since 4.0
-        */
-       protected AddBindingListener addBindingListener;
-
-       /**
-        * @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<>();
-       }
-
-       /**
-        * 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) {
-               ExtendedType extendedType = anObject != null
-                               ? 
adapter.getExtendedTypes().getRegisteredType(anObject.getClass())
-                               : adapter.getExtendedTypes().getDefaultType();
-
-               DbAttributeBinding binding = new DbAttributeBinding(dbAttr);
-               binding.setStatementPosition(bindings.size() + 1);
-               binding.setValue(anObject);
-               binding.setExtendedType(extendedType);
-
-               bindings.add(binding);
-               if(addBindingListener != null) {
-                       addBindingListener.onAdd(binding);
-               }
-       }
-
-       /**
-        * @since 4.0
-        */
-       public DbAttributeBinding[] getBindings() {
-               return bindings.toArray(new 
DbAttributeBinding[bindings.size()]);
-       }
-
-    /**
-     * @since 4.0
-     */
-       public abstract String getAliasForExpression(Expression exp);
-
-       /**
-        * @since 4.0
-        */
-       public void setAddBindingListener(AddBindingListener 
addBindingListener) {
-               this.addBindingListener = addBindingListener;
-       }
-
-       /**
-        * @since 4.0
-        */
-       protected interface AddBindingListener {
-               void onAdd(DbAttributeBinding binding);
-       }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/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
deleted file mode 100644
index fcdad65..0000000
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
+++ /dev/null
@@ -1,488 +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.CayenneRuntimeException;
-import org.apache.cayenne.ObjectId;
-import org.apache.cayenne.Persistent;
-import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.parser.PatternMatchNode;
-import org.apache.cayenne.exp.parser.SimpleNode;
-import org.apache.cayenne.map.*;
-import org.apache.cayenne.util.CayenneMapEntry;
-
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Translates parts of the query to SQL. Always works in the context of parent
- * Translator.
- */
-public abstract class QueryAssemblerHelper {
-
-       protected QueryAssembler queryAssembler;
-       protected StringBuilder out;
-       protected QuotingStrategy strategy;
-
-       /**
-        * Force joining tables for all relations, not only for toMany
-        * @since 4.0
-        */
-       private boolean forceJoinForRelations;
-
-       /**
-        * 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: %s", 
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) {
-                               throw new CayenneRuntimeException("Object must 
have a single primary key column to serve " +
-                                               "as a query parameter. This 
object has %s: %s", snap.size(), snap);
-                       }
-
-                       // 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) {
-                               throw new CayenneRuntimeException("Object must 
have a single primary key column to serve " +
-                                               "as a query parameter. This 
object has %s: %s", snap.size(), snap);
-                       }
-
-                       // 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 (forceJoinForRelations || rel.isToMany()) {
-                       // append joins
-                       queryAssembler.dbRelationshipAdded(rel, joinType, 
joinSplitAlias);
-               }
-
-               // get last DbRelationship on the list
-               List<DbJoin> joins = rel.getJoins();
-               if (joins.size() != 1) {
-                       String msg = "OBJ_PATH expressions are only supported 
for a single-join relationships. " +
-                                       "This relationship has %s joins.";
-                       throw new CayenneRuntimeException(msg, joins.size());
-               }
-
-               DbJoin join = joins.get(0);
-
-               DbAttribute attribute;
-
-               if (rel.isToMany()) {
-                       DbEntity ent = join.getRelationship().getTargetEntity();
-                       Collection<DbAttribute> pk = ent.getPrimaryKeys();
-                       if (pk.size() != 1) {
-                               String msg = "DB_NAME expressions can only 
support targets with a single column PK. " +
-                                               "This entity has %d columns in 
primary key.";
-                               throw new CayenneRuntimeException(msg, 
pk.size());
-                       }
-
-                       attribute = pk.iterator().next();
-               } else {
-                       attribute = forceJoinForRelations ? join.getTarget() : 
join.getSource();
-               }
-
-               processColumn(attribute);
-       }
-
-       /**
-        * Force joining tables for all relations, not only for toMany
-        * @since 4.0
-        */
-       protected void setForceJoinForRelations(boolean forceJoinForRelations) {
-               this.forceJoinForRelations = forceJoinForRelations;
-       }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java
new file mode 100644
index 0000000..1488c6a
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java
@@ -0,0 +1,138 @@
+/*****************************************************************
+ *   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.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NodeType;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SimpleNodeTreeVisitor;
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.exp.parser.ASTAggregateFunctionCall;
+import org.apache.cayenne.exp.property.BaseProperty;
+import org.apache.cayenne.map.DbAttribute;
+
+/**
+ * @since 4.2
+ */
+class ResultNodeDescriptor {
+    private final Node node;
+    private final boolean inDataRow;
+    private final boolean isAggregate;
+    private final BaseProperty<?> property;
+
+    private String dataRowKey;
+    private DbAttribute dbAttribute;
+    private String javaType;
+
+    ResultNodeDescriptor(Node node, boolean inDataRow, BaseProperty<?> 
property, String dataRowKey) {
+        this.node = node;
+        this.inDataRow = inDataRow;
+        this.property = property;
+        this.dataRowKey = dataRowKey;
+        this.isAggregate = property != null
+                && property.getExpression() instanceof 
ASTAggregateFunctionCall;
+    }
+
+    public boolean isAggregate() {
+        return isAggregate;
+    }
+
+    public boolean isInDataRow() {
+        return inDataRow;
+    }
+
+    public BaseProperty<?> getProperty() {
+        return property;
+    }
+
+    public Node getNode() {
+        return node;
+    }
+
+    public String getDataRowKey() {
+        if (dataRowKey != null) {
+            return dataRowKey;
+        }
+        if (property != null) {
+            return property.getAlias();
+        }
+        if (getDbAttribute() != null) {
+            return getDbAttribute().getName();
+        }
+        return null;
+    }
+
+    public void setDataRowKey(String dataRowKey) {
+        this.dataRowKey = dataRowKey;
+    }
+
+    public ResultNodeDescriptor setJavaType(String javaType) {
+        this.javaType = javaType;
+        return this;
+    }
+
+    public ResultNodeDescriptor setDbAttribute(DbAttribute dbAttribute) {
+        this.dbAttribute = dbAttribute;
+        return this;
+    }
+
+    public String getJavaType() {
+        if (javaType != null) {
+            return javaType;
+        }
+        if (property != null) {
+            return property.getType().getCanonicalName();
+        }
+        if (getDbAttribute() != null) {
+            return TypesMapping.getJavaBySqlType(getDbAttribute().getType());
+        }
+        return null;
+    }
+
+    public int getJdbcType() {
+        if (getDbAttribute() != null) {
+            return getDbAttribute().getType();
+        }
+
+        if (getProperty() != null) {
+            return TypesMapping.getSqlTypeByJava(getProperty().getType());
+        }
+
+        return TypesMapping.NOT_DEFINED;
+    }
+
+    public DbAttribute getDbAttribute() {
+        if (this.dbAttribute != null) {
+            return this.dbAttribute;
+        }
+        DbAttribute[] dbAttribute = {null};
+        node.visit(new SimpleNodeTreeVisitor() {
+            @Override
+            public boolean onNodeStart(Node node) {
+                if (node.getType() == NodeType.COLUMN) {
+                    dbAttribute[0] = ((ColumnNode) node).getAttribute();
+                    return false;
+                }
+                return true;
+            }
+        });
+        return this.dbAttribute = dbAttribute[0];
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java
new file mode 100644
index 0000000..9551035
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java
@@ -0,0 +1,44 @@
+/*****************************************************************
+ *   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.sqlbuilder.SQLGenerationVisitor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+
+/**
+ * @since 4.2
+ */
+class SQLGenerationStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        if(context.isSkipSQLGeneration()) {
+            return;
+        }
+        // Build final SQL tree
+        Node node = context.getSelectBuilder().build();
+        // convert to database flavour
+        node = context.getAdapter().getSqlTreeProcessor().apply(node);
+        // generate SQL
+        SQLGenerationVisitor visitor = new SQLGenerationVisitor(new 
DefaultQuotingAppendable(context));
+        node.visit(visitor);
+        context.setFinalSQL(visitor.getSQLString());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectQueryWrapper.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectQueryWrapper.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectQueryWrapper.java
new file mode 100644
index 0000000..576bb8d
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectQueryWrapper.java
@@ -0,0 +1,84 @@
+/*****************************************************************
+ *   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.util.Collection;
+import java.util.Objects;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.property.BaseProperty;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.Ordering;
+import org.apache.cayenne.query.PrefetchTreeNode;
+import org.apache.cayenne.query.QueryMetadata;
+import org.apache.cayenne.query.Select;
+import org.apache.cayenne.query.SelectQuery;
+
+/**
+ * @since 4.2
+ */
+public class SelectQueryWrapper implements TranslatableQueryWrapper {
+
+    private final SelectQuery<?> selectQuery;
+
+    public SelectQueryWrapper(SelectQuery<?> selectQuery) {
+        this.selectQuery = Objects.requireNonNull(selectQuery);
+    }
+
+    @Override
+    public boolean isDistinct() {
+        return selectQuery.isDistinct();
+    }
+
+    @Override
+    public QueryMetadata getMetaData(EntityResolver resolver) {
+        return selectQuery.getMetaData(resolver);
+    }
+
+    @Override
+    public PrefetchTreeNode getPrefetchTree() {
+        return selectQuery.getPrefetchTree();
+    }
+
+    @Override
+    public Expression getQualifier() {
+        return selectQuery.getQualifier();
+    }
+
+    @Override
+    public Collection<Ordering> getOrderings() {
+        return selectQuery.getOrderings();
+    }
+
+    @Override
+    public Collection<BaseProperty<?>> getColumns() {
+        return selectQuery.getColumns();
+    }
+
+    @Override
+    public Expression getHavingQualifier() {
+        return selectQuery.getHavingQualifier();
+    }
+
+    @Override
+    public Select<?> unwrap() {
+        return selectQuery;
+    }
+}

Reply via email to