This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git


The following commit(s) were added to refs/heads/master by this push:
     new efa1255  CAY-2543 Move ResultSetMapping generation from metadata to 
translator
efa1255 is described below

commit efa12550f597c5b728ad8bb1d7c8720ed94f1391
Author: Nikita Timofeev <[email protected]>
AuthorDate: Thu Feb 21 14:24:02 2019 +0300

    CAY-2543 Move ResultSetMapping generation from metadata to translator
---
 RELEASE-NOTES.txt                                  |   1 +
 .../cayenne/access/IncrementalFaultList.java       |  23 +-
 .../access/MixedResultIncrementalFaultList.java    |   4 +-
 .../select/CustomColumnSetExtractor.java           |   6 +-
 .../translator/select/DefaultSelectTranslator.java |   1 +
 .../select/DescriptorColumnExtractor.java          |  45 ++-
 .../translator/select/IdColumnExtractor.java       |  12 +-
 ...{IdColumnExtractor.java => SQLResultStage.java} |  28 +-
 .../select/TranslatableQueryWrapper.java           |   3 +
 .../translator/select/TranslatorContext.java       |  20 ++
 .../org/apache/cayenne/exp/parser/ASTDbPath.java   |  11 +
 .../org/apache/cayenne/exp/parser/ASTPath.java     |  16 +-
 .../cayenne/map/DefaultEntityResultSegment.java    |   4 +-
 .../apache/cayenne/query/BaseQueryMetadata.java    |   9 +
 .../apache/cayenne/query/ColumnSelectMetadata.java | 336 ++-------------------
 .../apache/cayenne/query/ObjectSelectMetadata.java |  28 +-
 .../org/apache/cayenne/query/QueryMetadata.java    |   6 +
 .../apache/cayenne/query/SelectQueryMetadata.java  | 189 +-----------
 .../select/CustomColumnSetExtractorTest.java       |   1 +
 .../translator/select/MockQueryWrapperBuilder.java |  11 +
 20 files changed, 227 insertions(+), 527 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 8d553b1..22c3cf3 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -23,6 +23,7 @@ CAY-2518 Add method to append having qualifier expression to 
ObjectSelect
 CAY-2520 Split ObjectId into several specialized variants
 CAY-2522 Make ObjectSelect a direct query
 CAY-2540 Modeler: redesign dbRelationship editor dialog
+CAY-2543 Move ResultSetMapping generation from metadata to translator
 
 Bug Fixes:
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/IncrementalFaultList.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/IncrementalFaultList.java
index 1c83d3c..f2cb27b 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/IncrementalFaultList.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/IncrementalFaultList.java
@@ -72,6 +72,7 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
        protected int idWidth;
 
        IncrementalListHelper helper;
+       protected QueryMetadata metadata;
 
        /**
         * Defines the upper limit on the size of fetches. This is needed to 
avoid
@@ -102,7 +103,7 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
         *            maximum number of fetches in one query
         */
        public IncrementalFaultList(DataContext dataContext, Query query, int 
maxFetchSize) {
-               QueryMetadata metadata = 
query.getMetaData(dataContext.getEntityResolver());
+               this.metadata = 
query.getMetaData(dataContext.getEntityResolver());
                if (metadata.getPageSize() <= 0) {
                        throw new CayenneRuntimeException("Not a paginated 
query; page size: " + metadata.getPageSize());
                }
@@ -122,7 +123,6 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
                
this.internalQuery.setFetchingDataRows(metadata.isFetchingDataRows());
                this.internalQuery.setPrefetchTree(metadata.getPrefetchTree());
 
-               this.helper = createHelper(metadata);
                this.idWidth = metadata.getDbEntity().getPrimaryKeys().size();
 
                List<Object> elementsUnsynced = new ArrayList<>();
@@ -143,6 +143,13 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
                }
        }
 
+       IncrementalListHelper getHelper() {
+               if(helper == null) {
+                       helper = createHelper(metadata);
+               }
+               return helper;
+       }
+
        /**
         * @since 1.2
         */
@@ -223,7 +230,7 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
                        List<Object> ids = new ArrayList<>(pageSize);
                        for (int i = fromIndex; i < toIndex; i++) {
                                Object object = elements.get(i);
-                               if (helper.unresolvedSuspect(object)) {
+                               if (getHelper().unresolvedSuspect(object)) {
                                        quals.add(buildIdQualifier(object));
                                        ids.add(object);
                                }
@@ -258,7 +265,7 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
 
        void updatePageWithResults(List<Object> objects, int fromIndex, int 
toIndex) {
                for (Object object : objects) {
-                       helper.updateWithResolvedObjectInRange(object, 
fromIndex, toIndex);
+                       getHelper().updateWithResolvedObjectInRange(object, 
fromIndex, toIndex);
                }
 
                unfetchedObjects -= objects.size();
@@ -307,7 +314,7 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
 
                                for (Object object : objects) {
 
-                                       if (helper.replacesObject(object, id)) {
+                                       if (getHelper().replacesObject(object, 
id)) {
                                                found = true;
                                                break;
                                        }
@@ -506,7 +513,7 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
                synchronized (elements) {
                        Object o = elements.get(index);
 
-                       if (helper.unresolvedSuspect(o)) {
+                       if (getHelper().unresolvedSuspect(o)) {
                                // read this page
                                int pageStart = pageIndex(index) * pageSize;
                                resolveInterval(pageStart, pageStart + 
pageSize);
@@ -522,7 +529,7 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
         * @see java.util.List#indexOf(Object)
         */
        public int indexOf(Object o) {
-               return helper.indexOfObject(o);
+               return getHelper().indexOfObject(o);
        }
 
        /**
@@ -535,7 +542,7 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
        }
 
        public int lastIndexOf(Object o) {
-               return helper.lastIndexOfObject(o);
+               return getHelper().lastIndexOfObject(o);
        }
 
        public E remove(int index) {
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java
index f8c02b8..65ae966 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java
@@ -140,7 +140,7 @@ class MixedResultIncrementalFaultList<E> extends 
IncrementalFaultList<E> {
                 int dataIdx = entry.getKey();
                 for (int i = fromIndex; i < toIndex; i++) {
                     Object[] object = (Object[])elements.get(i);
-                    if (helper.unresolvedSuspect(object[dataIdx])) {
+                    if (getHelper().unresolvedSuspect(object[dataIdx])) {
                         Expression nextQualifier = buildIdQualifier(dataIdx, 
object);
                         if(nextQualifier != null) {
                             quals.add(nextQualifier);
@@ -173,7 +173,7 @@ class MixedResultIncrementalFaultList<E> extends 
IncrementalFaultList<E> {
     }
 
     void updatePageWithResults(List<Persistent> objects, int dataIndex) {
-        MixedArrayListHelper helper = (MixedArrayListHelper)this.helper;
+        MixedArrayListHelper helper = (MixedArrayListHelper)getHelper();
         for (Persistent object : objects) {
             helper.updateWithResolvedObject(object, dataIndex);
         }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java
index 534fd3a..ed2c06f 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java
@@ -60,6 +60,8 @@ class CustomColumnSetExtractor implements ColumnExtractor {
     private void extractSimpleProperty(BaseProperty<?> property) {
         Node sqlNode = context.getQualifierTranslator().translate(property);
         context.addResultNode(sqlNode, true, property, property.getAlias());
+        String name = property.getName() == null ? 
property.getExpression().expName() : property.getName();
+        context.getSqlResult().addColumnResult(name);
     }
 
     private boolean isFullObjectProp(BaseProperty<?> property) {
@@ -108,14 +110,14 @@ class CustomColumnSetExtractor implements ColumnExtractor 
{
 
     private String calculatePrefix(String prefix, BaseProperty<?> property) {
         Expression propertyExpression = property.getExpression();
-        int expressionType = property.getExpression().getType();
+        int expressionType = propertyExpression.getType();
 
         if(expressionType == Expression.FULL_OBJECT && 
propertyExpression.getOperandCount() > 0) {
             Object op = propertyExpression.getOperand(0);
             if(op instanceof ASTPath) {
                 prefix = ((ASTPath) op).getPath();
             }
-        } else if(propertyExpression instanceof ASTPath) {
+        } else if(expressionType == Expression.DB_PATH || expressionType == 
Expression.OBJ_PATH) {
             prefix = ((ASTPath) propertyExpression).getPath();
         }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
index 7cc62bc..1213c38 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
@@ -49,6 +49,7 @@ public class DefaultSelectTranslator implements 
SelectTranslator {
             new LimitOffsetStage(),
             new ColumnDescriptorStage(),
             new TableTreeStage(),
+            new SQLResultStage(),
             new SQLGenerationStage()
     };
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
index 95a26ea..93da03b 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
@@ -22,9 +22,11 @@ package org.apache.cayenne.access.translator.select;
 import java.util.HashSet;
 import java.util.Set;
 
+import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResult;
 import org.apache.cayenne.map.ObjAttribute;
 import org.apache.cayenne.map.ObjRelationship;
 import org.apache.cayenne.reflect.AttributeProperty;
@@ -44,9 +46,11 @@ class DescriptorColumnExtractor extends BaseColumnExtractor 
implements PropertyV
 
     private final ClassDescriptor descriptor;
     private final PathTranslator pathTranslator;
+    private final Set<String> columnTracker = new HashSet<>();
 
+    private EntityResult entityResult;
     private String prefix;
-    private Set<String> columnTracker = new HashSet<>();
+    private String labelPrefix;
 
     DescriptorColumnExtractor(TranslatorContext context, ClassDescriptor 
descriptor) {
         super(context);
@@ -56,14 +60,31 @@ class DescriptorColumnExtractor extends BaseColumnExtractor 
implements PropertyV
 
     public void extract(String prefix) {
         this.prefix = prefix;
-        String labelPrefix = prefix;
+        boolean newEntityResult = false;
+        this.labelPrefix = null;
         TranslatorContext.DescriptorType type = 
TranslatorContext.DescriptorType.OTHER;
-        if(prefix != null && prefix.length() > 2 && 
prefix.startsWith(PREFETCH_PREFIX)) {
+
+        if(prefix != null && prefix.startsWith(PREFETCH_PREFIX)) {
             type = TranslatorContext.DescriptorType.PREFETCH;
             labelPrefix = prefix.substring(2);
-        } else if(descriptor.getEntity().getDbEntity() == 
context.getRootDbEntity()) {
-            type = TranslatorContext.DescriptorType.ROOT;
+            if(context.getQuery().needsResultSetMapping()) {
+                entityResult = context.getRootEntityResult();
+                if (entityResult == null) {
+                    throw new CayenneRuntimeException("Can't process prefetch 
descriptor without root.");
+                }
+                newEntityResult = false;
+            }
+        } else {
+            if(context.getQuery().needsResultSetMapping()) {
+                entityResult = new EntityResult(descriptor.getObjectClass());
+                newEntityResult = true;
+            }
+            if(descriptor.getEntity().getDbEntity() == 
context.getRootDbEntity()){
+                type = TranslatorContext.DescriptorType.ROOT;
+                context.setRootEntityResult(entityResult);
+            }
         }
+
         context.markDescriptorStart(type);
 
         descriptor.visitAllProperties(this);
@@ -75,8 +96,13 @@ class DescriptorColumnExtractor extends BaseColumnExtractor 
implements PropertyV
             String columnUniqueName = alias + '.' + dba.getName();
             if(columnTracker.add(columnUniqueName)) {
                 addDbAttribute(prefix, labelPrefix, dba);
+                addEntityResultField(dba);
             }
         }
+
+        if(newEntityResult) {
+            context.getSqlResult().addEntityResult(entityResult);
+        }
         context.markDescriptorEnd(type);
     }
 
@@ -90,6 +116,7 @@ class DescriptorColumnExtractor extends BaseColumnExtractor 
implements PropertyV
             ResultNodeDescriptor resultNodeDescriptor = 
processTranslationResult(result, i);
             if(resultNodeDescriptor != null && i == count - 1) {
                 resultNodeDescriptor.setJavaType(oa.getType());
+                addEntityResultField(oa.getDbAttribute());
             }
         }
 
@@ -109,6 +136,7 @@ class DescriptorColumnExtractor extends BaseColumnExtractor 
implements PropertyV
         int count = result.getDbAttributes().size();
         for(int i=0; i<count; i++) {
             processTranslationResult(result, i);
+            addEntityResultField(result.getDbAttributes().get(i));
         }
 
         return true;
@@ -138,6 +166,13 @@ class DescriptorColumnExtractor extends 
BaseColumnExtractor implements PropertyV
         return null;
     }
 
+    private void addEntityResultField(DbAttribute attribute) {
+        String name = labelPrefix == null ? attribute.getName() : labelPrefix 
+ '.' + attribute.getName();
+        if(context.getQuery().needsResultSetMapping()) {
+            entityResult.addDbField(name, name);
+        }
+    }
+
     @Override
     public boolean visitToMany(ToManyProperty property) {
         return true;
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/IdColumnExtractor.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/IdColumnExtractor.java
index a718a04..46feccd 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/IdColumnExtractor.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/IdColumnExtractor.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.access.translator.select;
 
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResult;
 import org.apache.cayenne.map.ObjEntity;
 
 /**
@@ -29,6 +30,7 @@ import org.apache.cayenne.map.ObjEntity;
 class IdColumnExtractor extends BaseColumnExtractor {
 
     private final DbEntity dbEntity;
+    private EntityResult result;
 
     IdColumnExtractor(TranslatorContext context, DbEntity dbEntity) {
         super(context);
@@ -37,13 +39,21 @@ class IdColumnExtractor extends BaseColumnExtractor {
 
     IdColumnExtractor(TranslatorContext context, ObjEntity objEntity) {
         this(context, objEntity.getDbEntity());
+        if(context.getQuery().needsResultSetMapping()) {
+            this.result = new EntityResult(objEntity.getName());
+        }
     }
 
     @Override
     public void extract(String prefix) {
         for (DbAttribute dba : dbEntity.getPrimaryKeys()) {
             addDbAttribute(prefix, prefix, dba);
+            if(result != null) {
+                result.addDbField(dba.getName(), prefix + dba.getName());
+            }
+        }
+        if(result != null) {
+            context.getSqlResult().addEntityResult(result);
         }
     }
-
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/IdColumnExtractor.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLResultStage.java
similarity index 62%
copy from 
cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/IdColumnExtractor.java
copy to 
cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLResultStage.java
index a718a04..b4656d0 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/IdColumnExtractor.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLResultStage.java
@@ -19,31 +19,21 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.ObjEntity;
+import java.util.List;
 
 /**
  * @since 4.2
  */
-class IdColumnExtractor extends BaseColumnExtractor {
-
-    private final DbEntity dbEntity;
-
-    IdColumnExtractor(TranslatorContext context, DbEntity dbEntity) {
-        super(context);
-        this.dbEntity = dbEntity;
-    }
-
-    IdColumnExtractor(TranslatorContext context, ObjEntity objEntity) {
-        this(context, objEntity.getDbEntity());
-    }
+public class SQLResultStage implements TranslationStage {
 
     @Override
-    public void extract(String prefix) {
-        for (DbAttribute dba : dbEntity.getPrimaryKeys()) {
-            addDbAttribute(prefix, prefix, dba);
+    public void perform(TranslatorContext context) {
+        if(context.getParentContext() != null || 
!context.getQuery().needsResultSetMapping()) {
+            return;
         }
-    }
 
+        // optimization, resolve metadata result components here too, as it 
have same logic as this extractor...
+        List<Object> resultSetMapping = 
context.getSqlResult().getResolvedComponents(context.getResolver());
+        context.getMetadata().setResultSetMapping(resultSetMapping);
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatableQueryWrapper.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatableQueryWrapper.java
index 5322847..d04031c 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatableQueryWrapper.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatableQueryWrapper.java
@@ -52,4 +52,7 @@ public interface TranslatableQueryWrapper {
 
     Select<?> unwrap();
 
+    default boolean needsResultSetMapping() {
+        return getColumns() != null && !getColumns().isEmpty();
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
index ba03b72..bf89ceb 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
@@ -35,6 +35,8 @@ import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.EntityResult;
+import org.apache.cayenne.map.SQLResult;
 import org.apache.cayenne.query.QueryMetadata;
 
 /**
@@ -99,6 +101,9 @@ public class TranslatorContext implements 
SQLGenerationContext {
     // this flag can be removed if logic that converts result row into an 
object tree allow random order of columns if a row.
     private boolean appendResultToRoot;
 
+    private SQLResult sqlResult;
+    private EntityResult rootEntityResult;
+
     TranslatorContext(TranslatableQueryWrapper query, DbAdapter adapter, 
EntityResolver resolver, TranslatorContext parentContext) {
         this.query = query;
         this.adapter = adapter;
@@ -113,6 +118,9 @@ public class TranslatorContext implements 
SQLGenerationContext {
         this.qualifierTranslator = new QualifierTranslator(this);
         this.quotingStrategy = adapter.getQuotingStrategy();
         this.resultNodeList = new LinkedList<>();
+        if(query.needsResultSetMapping()) {
+            this.sqlResult = new SQLResult();
+        }
     }
 
     /**
@@ -236,6 +244,18 @@ public class TranslatorContext implements 
SQLGenerationContext {
         return skipSQLGeneration;
     }
 
+    SQLResult getSqlResult() {
+        return sqlResult;
+    }
+
+    void setRootEntityResult(EntityResult rootEntityResult) {
+        this.rootEntityResult = rootEntityResult;
+    }
+
+    EntityResult getRootEntityResult() {
+        return rootEntityResult;
+    }
+
     enum DescriptorType {
         ROOT,
         PREFETCH,
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTDbPath.java 
b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTDbPath.java
index 6503bf2..48484aa 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTDbPath.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTDbPath.java
@@ -35,6 +35,7 @@ import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.Entity;
+import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.query.SelectById;
 import org.apache.cayenne.util.CayenneMapEntry;
@@ -194,4 +195,14 @@ public class ASTDbPath extends ASTPath {
        public int getType() {
                return Expression.DB_PATH;
        }
+
+       /**
+        * Helper method to evaluate path expression with Cayenne Entity.
+        */
+       protected CayenneMapEntry evaluateEntityNode(Entity entity) {
+               if(entity instanceof ObjEntity) {
+                       entity = ((ObjEntity) entity).getDbEntity();
+               }
+               return super.evaluateEntityNode(entity);
+       }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTPath.java 
b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTPath.java
index 8ab1a39..bae0533 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTPath.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTPath.java
@@ -22,7 +22,10 @@ package org.apache.cayenne.exp.parser;
 import java.util.Iterator;
 import java.util.Map;
 
+import org.apache.cayenne.map.Attribute;
 import org.apache.cayenne.map.Entity;
+import org.apache.cayenne.map.PathComponent;
+import org.apache.cayenne.map.Relationship;
 import org.apache.cayenne.util.CayenneMapEntry;
 
 /**
@@ -91,13 +94,20 @@ public abstract class ASTPath extends SimpleNode {
         * Helper method to evaluate path expression with Cayenne Entity.
         */
        protected CayenneMapEntry evaluateEntityNode(Entity entity) {
-               Iterator<CayenneMapEntry> path = 
entity.resolvePathComponents(this);
-               CayenneMapEntry next = null;
+               Iterator<PathComponent<Attribute, Relationship>> path = 
entity.resolvePath(this, getPathAliases()).iterator();
+               PathComponent<Attribute, Relationship> next = null;
                while (path.hasNext()) {
                        next = path.next();
                }
 
-               return next;
+               if(next == null) {
+                       return null;
+               }
+
+               if(next.getRelationship() != null) {
+                       return next.getRelationship();
+               }
+               return next.getAttribute();
        }
 
        @Override
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/map/DefaultEntityResultSegment.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/map/DefaultEntityResultSegment.java
index a9bb0f0..49fbfba 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/map/DefaultEntityResultSegment.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/map/DefaultEntityResultSegment.java
@@ -26,13 +26,13 @@ import org.apache.cayenne.reflect.ClassDescriptor;
 /**
  * @since 3.0
  */
-class DefaultEntityResultSegment implements EntityResultSegment {
+public class DefaultEntityResultSegment implements EntityResultSegment {
 
     private ClassDescriptor classDescriptor;
     private Map<String, String> fields;
     private int offset;
 
-    DefaultEntityResultSegment(ClassDescriptor classDescriptor,
+    public DefaultEntityResultSegment(ClassDescriptor classDescriptor,
             Map<String, String> fields, int offset) {
         this.classDescriptor = classDescriptor;
         this.fields = fields;
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java 
b/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java
index e1cd167..40c24bc 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java
@@ -251,6 +251,15 @@ class BaseQueryMetadata implements QueryMetadata, 
Serializable {
        }
 
        /**
+        * used by select translator
+        * @since 4.2
+        */
+       @Override
+       public void setResultSetMapping(List<Object> resultSetMapping) {
+               this.resultSetMapping = resultSetMapping;
+       }
+
+       /**
         * @since 4.0
         */
        @Override
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelectMetadata.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelectMetadata.java
index eb9a389..65f113c 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelectMetadata.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelectMetadata.java
@@ -18,56 +18,33 @@
  ****************************************************************/
 package org.apache.cayenne.query;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
-import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.ExpressionFactory;
-import org.apache.cayenne.exp.TraversalHandler;
-import org.apache.cayenne.exp.parser.ASTDbPath;
 import org.apache.cayenne.exp.property.BaseProperty;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbJoin;
-import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.DefaultEntityResultSegment;
+import org.apache.cayenne.map.DefaultScalarResultSegment;
 import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.map.EntityResult;
-import org.apache.cayenne.map.ObjAttribute;
-import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.map.ObjRelationship;
-import org.apache.cayenne.map.PathComponent;
-import org.apache.cayenne.map.SQLResult;
-import org.apache.cayenne.reflect.AttributeProperty;
-import org.apache.cayenne.reflect.ClassDescriptor;
-import org.apache.cayenne.reflect.PropertyVisitor;
-import org.apache.cayenne.reflect.ToManyProperty;
-import org.apache.cayenne.reflect.ToOneProperty;
-import org.apache.cayenne.util.CayenneMapEntry;
 
 /**
  * @since 4.2
  */
-class ColumnSelectMetadata extends BaseQueryMetadata {
+class ColumnSelectMetadata extends ObjectSelectMetadata {
 
        private static final long serialVersionUID = -3622675304651257963L;
 
-       private Map<String, String> pathSplitAliases;
+       private static final ScalarResultSegment SCALAR_RESULT_SEGMENT
+                       = new DefaultScalarResultSegment(null, -1);
+       private static final EntityResultSegment ENTITY_RESULT_SEGMENT
+                       = new DefaultEntityResultSegment(null, null, -1);
+
        private boolean isSingleResultSetMapping;
        private boolean suppressingDistinct;
 
-       @Override
-       void copyFromInfo(QueryMetadata info) {
-               super.copyFromInfo(info);
-               this.pathSplitAliases = new 
HashMap<>(info.getPathSplitAliases());
-       }
-
        boolean resolve(Object root, EntityResolver resolver, ColumnSelect<?> 
query) {
 
                if (super.resolve(root, resolver)) {
@@ -77,93 +54,21 @@ class ColumnSelectMetadata extends BaseQueryMetadata {
                        }
 
                        resolveAutoAliases(query);
-                       buildResultSetMappingForColumns(query, resolver);
-                       isSingleResultSetMapping = query.isSingleColumn() && 
super.isSingleResultSetMapping();
-
+                       buildResultSetMappingForColumns(query);
+                       isSingleResultSetMapping = query.isSingleColumn();
                        return true;
                }
 
                return false;
        }
 
-       private String makeCacheKey(ColumnSelect<?> query, EntityResolver 
resolver) {
-
-               // create a unique key based on entity or columns, qualifier, 
ordering, fetch offset and limit
-
-               StringBuilder key = new StringBuilder();
-               // handler to create string out of expressions, created lazily
-               TraversalHandler traversalHandler = null;
-
-               ObjEntity entity = getObjEntity();
-               if (entity != null) {
-                       key.append(entity.getName());
-               } else if (dbEntity != null) {
-                       key.append("db:").append(dbEntity.getName());
-               }
-
-               if(query.getColumns() != null && !query.getColumns().isEmpty()) 
{
-                       traversalHandler = new 
ToCacheKeyTraversalHandler(resolver.getValueObjectTypeRegistry(), key);
-                       for(BaseProperty<?> property : query.getColumns()) {
-                               key.append("/c:");
-                               
property.getExpression().traverse(traversalHandler);
-                       }
-               }
-
-               if (query.getWhere() != null) {
-                       key.append('/');
-                       if(traversalHandler == null) {
-                               traversalHandler = new 
ToCacheKeyTraversalHandler(resolver.getValueObjectTypeRegistry(), key);
-                       }
-                       query.getWhere().traverse(traversalHandler);
-               }
-
-               if (query.getOrderings() != null && 
!query.getOrderings().isEmpty()) {
-                       for (Ordering o : query.getOrderings()) {
-                               key.append('/').append(o.getSortSpecString());
-                               if (!o.isAscending()) {
-                                       key.append(":d");
-                               }
-
-                               if (o.isCaseInsensitive()) {
-                                       key.append(":i");
-                               }
-                       }
-               }
-
-               if (fetchOffset > 0 || fetchLimit > 0) {
-                       key.append('/');
-                       if (fetchOffset > 0) {
-                               key.append('o').append(fetchOffset);
-                       }
-                       if (fetchLimit > 0) {
-                               key.append('l').append(fetchLimit);
-                       }
-               }
-
-               // add prefetch to cache key per CAY-2349
-               if(prefetchTree != null) {
-                       prefetchTree.traverse(new 
ToCacheKeyPrefetchProcessor(key));
-               }
-
-               return key.toString();
-       }
-
-       private void resolveAutoAliases(ColumnSelect<?> query) {
-               resolveQualifierAliases(query);
+       @Override
+       protected void resolveAutoAliases(FluentSelect<?> query) {
+               super.resolveAutoAliases(query);
                resolveColumnsAliases(query);
-        resolveOrderingAliases(query);
-               resolveHavingQualifierAliases(query);
-               // TODO: include aliases in prefetches? flattened attributes?
-       }
-
-       private void resolveQualifierAliases(ColumnSelect<?> query) {
-               Expression qualifier = query.getWhere();
-               if (qualifier != null) {
-                       resolveAutoAliases(qualifier);
-               }
        }
 
-       private void resolveColumnsAliases(ColumnSelect<?> query) {
+       protected void resolveColumnsAliases(FluentSelect<?> query) {
         Collection<BaseProperty<?>> columns = query.getColumns();
         if(columns != null) {
             for(BaseProperty<?> property : columns) {
@@ -175,225 +80,44 @@ class ColumnSelectMetadata extends BaseQueryMetadata {
         }
     }
 
-    private void resolveOrderingAliases(ColumnSelect<?> query) {
-        Collection<Ordering> orderings = query.getOrderings();
-        if(orderings != null) {
-            for(Ordering ordering : orderings) {
-                Expression sortSpec = ordering.getSortSpec();
-                if(sortSpec != null) {
-                    resolveAutoAliases(sortSpec);
-                }
-            }
-        }
-    }
-
-    private void resolveHavingQualifierAliases(ColumnSelect<?> query) {
-        Expression havingQualifier = query.getHaving();
-        if(havingQualifier != null) {
-            resolveAutoAliases(havingQualifier);
-        }
-    }
-
-       private void resolveAutoAliases(Expression expression) {
-               Map<String, String> aliases = expression.getPathAliases();
-               if (!aliases.isEmpty()) {
-                       if (pathSplitAliases == null) {
-                               pathSplitAliases = new HashMap<>();
-                       }
-
-                       for(Map.Entry<String, String> entry : 
aliases.entrySet()) {
-                               pathSplitAliases.compute(entry.getKey(), (key, 
value) -> {
-                                       if(value != null && 
!value.equals(entry.getValue())){
-                                               throw new 
CayenneRuntimeException("Can't add the same alias to different path segments.");
-                                       } else {
-                                               return entry.getValue();
-                                       }
-                               });
-                       }
-               }
-
-               int len = expression.getOperandCount();
-               for (int i = 0; i < len; i++) {
-                       Object operand = expression.getOperand(i);
-                       if (operand instanceof Expression) {
-                               resolveAutoAliases((Expression) operand);
-                       }
-               }
-       }
-
        @Override
        public Map<String, String> getPathSplitAliases() {
                return pathSplitAliases != null ? pathSplitAliases : 
Collections.emptyMap();
        }
 
        /**
-        * Build DB result descriptor, that will be used to read and convert 
raw result of ColumnSelect
+        * NOTE: this is a dirty logic, we calculate hollow resultSetMapping 
here and later in translator
+        * (see ColumnExtractorStage and extractors) discard this and calculate 
it with full info.
+        *
+        * This result set mapping required by paginated queries that need only 
result type (entity/scalar) not
+        * full info. So we can optimize this a bit and pair calculation with 
translation that do same thing to provide
+        * result column descriptors.
         */
-       private void buildResultSetMappingForColumns(ColumnSelect<?> query, 
EntityResolver resolver) {
+       private void buildResultSetMappingForColumns(ColumnSelect<?> query) {
                if(query.getColumns() == null || query.getColumns().isEmpty()) {
                        return;
                }
-               
-               SQLResult result = new SQLResult();
+
+               resultSetMapping = new ArrayList<>(query.getColumns().size());
                for(BaseProperty<?> column : query.getColumns()) {
+                       // for each column we need only to know if it's entity 
or scalar
                        Expression exp = column.getExpression();
-                       String name = column.getName() == null ? exp.expName() 
: column.getName();
                        boolean fullObject = false;
                        if(exp.getType() == Expression.OBJ_PATH) {
                                // check if this is toOne relation
-                               Expression dbPath = 
this.getObjEntity().translateToDbPath(exp);
-                               DbRelationship rel = 
findRelationByPath(dbEntity, dbPath);
-                               if(rel != null && !rel.isToMany()) {
-                                       // it this path is toOne relation, than 
select full object for it
-                                       fullObject = true;
-                               }
+                               Object rel = exp.evaluate(getObjEntity());
+                               // it this path is toOne relation, than select 
full object for it
+                               fullObject = rel instanceof ObjRelationship && 
!((ObjRelationship) rel).isToMany();
                        } else if(exp.getType() == Expression.FULL_OBJECT) {
                                fullObject = true;
                        }
 
                        if(fullObject) {
-                               // detected full object column
-                               if(getPageSize() > 0) {
-                                       // for paginated queries keep only IDs
-                                       
result.addEntityResult(buildEntityIdResultForColumn(column, resolver));
-                               } else {
-                                       // will unwrap to full set of 
db-columns (with join prefetch optionally)
-                                       
result.addEntityResult(buildEntityResultForColumn(query, column, resolver));
-                               }
+                               resultSetMapping.add(ENTITY_RESULT_SEGMENT);
                        } else {
-                               // scalar column
-                               result.addColumnResult(name);
+                               resultSetMapping.add(SCALAR_RESULT_SEGMENT);
                        }
                }
-               resultSetMapping = result.getResolvedComponents(resolver);
-       }
-
-       /**
-        * Collect metadata for result with ObjectId (used for paginated 
queries with FullObject columns)
-        *
-        * @param column full object column
-        * @param resolver entity resolver
-        * @return Entity result
-        */
-       private EntityResult buildEntityIdResultForColumn(BaseProperty<?> 
column, EntityResolver resolver) {
-               EntityResult result = new EntityResult(column.getType());
-               DbEntity entity = 
resolver.getObjEntity(column.getType()).getDbEntity();
-               for(DbAttribute attribute : entity.getPrimaryKeys()) {
-                       result.addDbField(attribute.getName(), 
attribute.getName());
-               }
-               return result;
-       }
-
-       private DbRelationship findRelationByPath(DbEntity entity, Expression 
exp) {
-               DbRelationship r = null;
-               for (PathComponent<DbAttribute, DbRelationship> component : 
entity.resolvePath(exp, getPathSplitAliases())) {
-                       r = component.getRelationship();
-               }
-               return r;
-       }
-
-       /**
-        * Collect metadata for column that will be unwrapped to full entity in 
the final SQL
-        * (possibly including joint prefetch).
-        * This information will be used to correctly create Persistent object 
back from raw result.
-        *
-        * @param query original query
-        * @param column full object column
-        * @param resolver entity resolver to get ObjEntity and ClassDescriptor
-        * @return Entity result
-        */
-       private EntityResult buildEntityResultForColumn(ColumnSelect<?> query, 
BaseProperty<?> column, EntityResolver resolver) {
-               // This method is actually repeating logic of 
DescriptorColumnExtractor.
-               // Here we don't care about intermediate joins and few other 
things so it's shorter.
-               // Logic of these methods should be unified and simplified, 
possibly to a single source of metadata,
-               // generated only once and used everywhere.
-
-               final EntityResult result = new EntityResult(column.getType());
-
-               // Collecting visitor for ObjAttributes and toOne relationships
-               PropertyVisitor visitor = new PropertyVisitor() {
-                       public boolean visitAttribute(AttributeProperty 
property) {
-                               ObjAttribute oa = property.getAttribute();
-                               Iterator<CayenneMapEntry> dbPathIterator = 
oa.getDbPathIterator();
-                               while (dbPathIterator.hasNext()) {
-                                       CayenneMapEntry pathPart = 
dbPathIterator.next();
-                                       if (pathPart instanceof DbAttribute) {
-                                               
result.addDbField(pathPart.getName(), pathPart.getName());
-                                       }
-                               }
-                               return true;
-                       }
-
-                       public boolean visitToMany(ToManyProperty property) {
-                               return true;
-                       }
-
-                       public boolean visitToOne(ToOneProperty property) {
-                               DbRelationship dbRel = 
property.getRelationship().getDbRelationships().get(0);
-                               List<DbJoin> joins = dbRel.getJoins();
-                               for (DbJoin join : joins) {
-                                       if(!join.getSource().isPrimaryKey()) {
-                                               
result.addDbField(join.getSource().getName(), join.getSource().getName());
-                                       }
-                               }
-                               return true;
-                       }
-               };
-
-               ObjEntity oe = resolver.getObjEntity(column.getType());
-               DbEntity table = oe.getDbEntity();
-
-               // Additionally collect PKs
-               for (DbAttribute dba : table.getPrimaryKeys()) {
-                       result.addDbField(dba.getName(), dba.getName());
-               }
-
-               ClassDescriptor descriptor = 
resolver.getClassDescriptor(oe.getName());
-               descriptor.visitAllProperties(visitor);
-
-               // Collection columns for joint prefetch
-               if(prefetchTree != null) {
-                       for (PrefetchTreeNode prefetch : 
prefetchTree.adjacentJointNodes()) {
-                               // for each prefetch add columns from the 
target entity
-                               Expression prefetchExp = 
ExpressionFactory.exp(prefetch.getPath());
-                               ASTDbPath dbPrefetch = (ASTDbPath) 
oe.translateToDbPath(prefetchExp);
-                               DbRelationship r = findRelationByPath(table, 
dbPrefetch);
-                               if (r == null) {
-                                       throw new 
CayenneRuntimeException("Invalid joint prefetch '%s' for entity: %s"
-                                                       , prefetch, 
oe.getName());
-                               }
-
-                               // go via target OE to make sure that Java 
types are mapped correctly...
-                               ObjRelationship targetRel = (ObjRelationship) 
prefetchExp.evaluate(oe);
-                               ObjEntity targetEntity = 
targetRel.getTargetEntity();
-                               
prefetch.setEntityName(targetRel.getSourceEntity().getName());
-
-                               String labelPrefix = dbPrefetch.getPath();
-                               Set<String> seenNames = new HashSet<>();
-                               for (ObjAttribute oa : 
targetEntity.getAttributes()) {
-                                       Iterator<CayenneMapEntry> 
dbPathIterator = oa.getDbPathIterator();
-                                       while (dbPathIterator.hasNext()) {
-                                               Object pathPart = 
dbPathIterator.next();
-                                               if (pathPart instanceof 
DbAttribute) {
-                                                       DbAttribute attribute = 
(DbAttribute) pathPart;
-                                                       
if(seenNames.add(attribute.getName())) {
-                                                               
result.addDbField(labelPrefix + '.' + attribute.getName(), labelPrefix + '.' + 
attribute.getName());
-                                                       }
-                                               }
-                                       }
-                               }
-
-                               // append remaining target attributes such as 
keys
-                               DbEntity targetDbEntity = r.getTargetEntity();
-                               for (DbAttribute attribute : 
targetDbEntity.getAttributes()) {
-                                       if(seenNames.add(attribute.getName())) {
-                                               result.addDbField(labelPrefix + 
'.' + attribute.getName(), labelPrefix + '.' + attribute.getName());
-                                       }
-                               }
-                       }
-               }
-
-               return result;
        }
 
        @Override
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelectMetadata.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelectMetadata.java
index b23a4af..51a0576 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelectMetadata.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelectMetadata.java
@@ -26,6 +26,7 @@ import java.util.Map;
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.TraversalHandler;
+import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.ObjEntity;
 
@@ -36,7 +37,7 @@ class ObjectSelectMetadata extends BaseQueryMetadata {
 
        private static final long serialVersionUID = -4936484509363047672L;
 
-       private Map<String, String> pathSplitAliases;
+       protected Map<String, String> pathSplitAliases;
 
        @Override
        void copyFromInfo(QueryMetadata info) {
@@ -59,7 +60,7 @@ class ObjectSelectMetadata extends BaseQueryMetadata {
                return false;
        }
 
-       private String makeCacheKey(FluentSelect<?> query, EntityResolver 
resolver) {
+       protected String makeCacheKey(FluentSelect<?> query, EntityResolver 
resolver) {
 
                // create a unique key based on entity or columns, qualifier, 
ordering, fetch offset and limit
 
@@ -74,6 +75,14 @@ class ObjectSelectMetadata extends BaseQueryMetadata {
                        key.append("db:").append(dbEntity.getName());
                }
 
+               if (query.getColumns() != null && 
!query.getColumns().isEmpty()) {
+                       traversalHandler = new 
ToCacheKeyTraversalHandler(resolver.getValueObjectTypeRegistry(), key);
+                       for (BaseProperty<?> property : query.getColumns()) {
+                               key.append("/c:");
+                               
property.getExpression().traverse(traversalHandler);
+                       }
+               }
+
                if (query.getWhere() != null) {
                        key.append('/');
             traversalHandler = new 
ToCacheKeyTraversalHandler(resolver.getValueObjectTypeRegistry(), key);
@@ -111,20 +120,27 @@ class ObjectSelectMetadata extends BaseQueryMetadata {
                return key.toString();
        }
 
-       private void resolveAutoAliases(ObjectSelect<?> query) {
+       protected void resolveAutoAliases(FluentSelect<?> query) {
                resolveQualifierAliases(query);
         resolveOrderingAliases(query);
+               resolveHavingQualifierAliases(query);
        }
 
-       private void resolveQualifierAliases(ObjectSelect<?> query) {
+       protected void resolveQualifierAliases(FluentSelect<?> query) {
                Expression qualifier = query.getWhere();
                if (qualifier != null) {
                        resolveAutoAliases(qualifier);
                }
        }
 
+       protected void resolveHavingQualifierAliases(FluentSelect<?> query) {
+               Expression havingQualifier = query.getHaving();
+               if(havingQualifier != null) {
+                       resolveAutoAliases(havingQualifier);
+               }
+       }
 
-       private void resolveOrderingAliases(ObjectSelect<?> query) {
+       protected void resolveOrderingAliases(FluentSelect<?> query) {
         Collection<Ordering> orderings = query.getOrderings();
         if(orderings != null) {
             for(Ordering ordering : orderings) {
@@ -136,7 +152,7 @@ class ObjectSelectMetadata extends BaseQueryMetadata {
         }
     }
 
-       private void resolveAutoAliases(Expression expression) {
+       protected void resolveAutoAliases(Expression expression) {
                Map<String, String> aliases = expression.getPathAliases();
                if (!aliases.isEmpty()) {
                        if (pathSplitAliases == null) {
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/QueryMetadata.java 
b/cayenne-server/src/main/java/org/apache/cayenne/query/QueryMetadata.java
index edb1118..603e8c6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/QueryMetadata.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/QueryMetadata.java
@@ -226,6 +226,12 @@ public interface QueryMetadata {
     List<Object> getResultSetMapping();
 
     /**
+     * @since 4.2
+     */
+    default void setResultSetMapping(List<Object> resultSetMapping) {
+    }
+
+    /**
      * @return should the result be mapped to single object (scalar or entity)
      * @see QueryMetadata#getResultSetMapping()
      * @since 4.0
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
index 2dc74a9..1624eae 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
@@ -18,44 +18,22 @@
  ****************************************************************/
 package org.apache.cayenne.query;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.ObjectId;
-import org.apache.cayenne.Persistent;
-import org.apache.cayenne.access.types.ValueObjectType;
-import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.TraversalHandler;
-import org.apache.cayenne.exp.parser.ASTDbPath;
-import org.apache.cayenne.exp.parser.ASTFunctionCall;
-import org.apache.cayenne.exp.parser.ASTScalar;
 import org.apache.cayenne.exp.property.BaseProperty;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbJoin;
-import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.DefaultEntityResultSegment;
+import org.apache.cayenne.map.DefaultScalarResultSegment;
 import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.map.EntityResult;
-import org.apache.cayenne.map.ObjAttribute;
 import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.map.ObjRelationship;
-import org.apache.cayenne.map.PathComponent;
-import org.apache.cayenne.map.SQLResult;
-import org.apache.cayenne.reflect.AttributeProperty;
-import org.apache.cayenne.reflect.ClassDescriptor;
-import org.apache.cayenne.reflect.PropertyVisitor;
-import org.apache.cayenne.reflect.ToManyProperty;
-import org.apache.cayenne.reflect.ToOneProperty;
-import org.apache.cayenne.util.CayenneMapEntry;
 
 /**
  * @since 3.0
@@ -63,6 +41,11 @@ import org.apache.cayenne.util.CayenneMapEntry;
 class SelectQueryMetadata extends BaseQueryMetadata {
 
        private static final long serialVersionUID = 7465922769303943945L;
+
+       private static final ScalarResultSegment SCALAR_RESULT_SEGMENT
+                       = new DefaultScalarResultSegment(null, -1);
+       private static final EntityResultSegment ENTITY_RESULT_SEGMENT
+                       = new DefaultEntityResultSegment(null, null, -1);
        
        private Map<String, String> pathSplitAliases;
        private boolean isSingleResultSetMapping;
@@ -264,167 +247,27 @@ class SelectQueryMetadata extends BaseQueryMetadata {
                if(query.getColumns() == null || query.getColumns().isEmpty()) {
                        return;
                }
-               
-               SQLResult result = new SQLResult();
+
+               resultSetMapping = new ArrayList<>(query.getColumns().size());
                for(BaseProperty<?> column : query.getColumns()) {
+                       // for each column we need only to know if it's entity 
or scalar
                        Expression exp = column.getExpression();
-                       String name = column.getName() == null ? exp.expName() 
: column.getName();
                        boolean fullObject = false;
                        if(exp.getType() == Expression.OBJ_PATH) {
                                // check if this is toOne relation
-                               Expression dbPath = 
this.getObjEntity().translateToDbPath(exp);
-                               DbRelationship rel = 
findRelationByPath(dbEntity, dbPath);
-                               if(rel != null && !rel.isToMany()) {
-                                       // it this path is toOne relation, than 
select full object for it
-                                       fullObject = true;
-                               }
+                               Object rel = exp.evaluate(getObjEntity());
+                               // it this path is toOne relation, than select 
full object for it
+                               fullObject = rel instanceof ObjRelationship && 
!((ObjRelationship) rel).isToMany();
                        } else if(exp.getType() == Expression.FULL_OBJECT) {
                                fullObject = true;
                        }
 
                        if(fullObject) {
-                               // detected full object column
-                               if(getPageSize() > 0) {
-                                       // for paginated queries keep only IDs
-                                       
result.addEntityResult(buildEntityIdResultForColumn(column, resolver));
-                               } else {
-                                       // will unwrap to full set of 
db-columns (with join prefetch optionally)
-                                       
result.addEntityResult(buildEntityResultForColumn(query, column, resolver));
-                               }
+                               resultSetMapping.add(ENTITY_RESULT_SEGMENT);
                        } else {
-                               // scalar column
-                               result.addColumnResult(name);
+                               resultSetMapping.add(SCALAR_RESULT_SEGMENT);
                        }
                }
-               resultSetMapping = result.getResolvedComponents(resolver);
-       }
-
-       /**
-        * Collect metadata for result with ObjectId (used for paginated 
queries with FullObject columns)
-        *
-        * @param column full object column
-        * @param resolver entity resolver
-        * @return Entity result
-        */
-       private EntityResult buildEntityIdResultForColumn(BaseProperty<?> 
column, EntityResolver resolver) {
-               EntityResult result = new EntityResult(column.getType());
-               DbEntity entity = 
resolver.getObjEntity(column.getType()).getDbEntity();
-               for(DbAttribute attribute : entity.getPrimaryKeys()) {
-                       result.addDbField(attribute.getName(), 
attribute.getName());
-               }
-               return result;
-       }
-
-       private DbRelationship findRelationByPath(DbEntity entity, Expression 
exp) {
-               DbRelationship r = null;
-               for (PathComponent<DbAttribute, DbRelationship> component : 
entity.resolvePath(exp, getPathSplitAliases())) {
-                       r = component.getRelationship();
-               }
-               return r;
-       }
-
-       /**
-        * Collect metadata for column that will be unwrapped to full entity in 
the final SQL
-        * (possibly including joint prefetch).
-        * This information will be used to correctly create Persistent object 
back from raw result.
-        *
-        * @param query original query
-        * @param column full object column
-        * @param resolver entity resolver to get ObjEntity and ClassDescriptor
-        * @return Entity result
-        */
-       private EntityResult buildEntityResultForColumn(SelectQuery<?> query, 
BaseProperty<?> column, EntityResolver resolver) {
-               // This method is actually repeating logic of 
DescriptorColumnExtractor.
-               // Here we don't care about intermediate joins and few other 
things so it's shorter.
-               // Logic of these methods should be unified and simplified, 
possibly to a single source of metadata,
-               // generated only once and used everywhere.
-
-               final EntityResult result = new EntityResult(column.getType());
-
-               // Collecting visitor for ObjAttributes and toOne relationships
-               PropertyVisitor visitor = new PropertyVisitor() {
-                       public boolean visitAttribute(AttributeProperty 
property) {
-                               ObjAttribute oa = property.getAttribute();
-                               Iterator<CayenneMapEntry> dbPathIterator = 
oa.getDbPathIterator();
-                               while (dbPathIterator.hasNext()) {
-                                       CayenneMapEntry pathPart = 
dbPathIterator.next();
-                                       if (pathPart instanceof DbAttribute) {
-                                               
result.addDbField(pathPart.getName(), pathPart.getName());
-                                       }
-                               }
-                               return true;
-                       }
-
-                       public boolean visitToMany(ToManyProperty property) {
-                               return true;
-                       }
-
-                       public boolean visitToOne(ToOneProperty property) {
-                               DbRelationship dbRel = 
property.getRelationship().getDbRelationships().get(0);
-                               List<DbJoin> joins = dbRel.getJoins();
-                               for (DbJoin join : joins) {
-                                       if(!join.getSource().isPrimaryKey()) {
-                                               
result.addDbField(join.getSource().getName(), join.getSource().getName());
-                                       }
-                               }
-                               return true;
-                       }
-               };
-
-               ObjEntity oe = resolver.getObjEntity(column.getType());
-               DbEntity table = oe.getDbEntity();
-
-               // Additionally collect PKs
-               for (DbAttribute dba : table.getPrimaryKeys()) {
-                       result.addDbField(dba.getName(), dba.getName());
-               }
-
-               ClassDescriptor descriptor = 
resolver.getClassDescriptor(oe.getName());
-               descriptor.visitAllProperties(visitor);
-
-               // Collection columns for joint prefetch
-               if(query.getPrefetchTree() != null) {
-                       for (PrefetchTreeNode prefetch : 
query.getPrefetchTree().adjacentJointNodes()) {
-                               // for each prefetch add columns from the 
target entity
-                               Expression prefetchExp = 
ExpressionFactory.exp(prefetch.getPath());
-                               ASTDbPath dbPrefetch = (ASTDbPath) 
oe.translateToDbPath(prefetchExp);
-                               DbRelationship r = findRelationByPath(table, 
dbPrefetch);
-                               if (r == null) {
-                                       throw new 
CayenneRuntimeException("Invalid joint prefetch '%s' for entity: %s"
-                                                       , prefetch, 
oe.getName());
-                               }
-
-                               // go via target OE to make sure that Java 
types are mapped correctly...
-                               ObjRelationship targetRel = (ObjRelationship) 
prefetchExp.evaluate(oe);
-                               ObjEntity targetEntity = 
targetRel.getTargetEntity();
-                               
prefetch.setEntityName(targetRel.getSourceEntity().getName());
-
-                               String labelPrefix = dbPrefetch.getPath();
-                               Set<String> seenNames = new HashSet<>();
-                               for (ObjAttribute oa : 
targetEntity.getAttributes()) {
-                                       Iterator<CayenneMapEntry> 
dbPathIterator = oa.getDbPathIterator();
-                                       while (dbPathIterator.hasNext()) {
-                                               Object pathPart = 
dbPathIterator.next();
-                                               if (pathPart instanceof 
DbAttribute) {
-                                                       DbAttribute attribute = 
(DbAttribute) pathPart;
-                                                       
if(seenNames.add(attribute.getName())) {
-                                                               
result.addDbField(labelPrefix + '.' + attribute.getName(), labelPrefix + '.' + 
attribute.getName());
-                                                       }
-                                               }
-                                       }
-                               }
-
-                               // append remaining target attributes such as 
keys
-                               DbEntity targetDbEntity = r.getTargetEntity();
-                               for (DbAttribute attribute : 
targetDbEntity.getAttributes()) {
-                                       if(seenNames.add(attribute.getName())) {
-                                               result.addDbField(labelPrefix + 
'.' + attribute.getName(), labelPrefix + '.' + attribute.getName());
-                                       }
-                               }
-                       }
-               }
-
-               return result;
        }
 
        /**
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractorTest.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractorTest.java
index 2836ab8..18d706a 100644
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractorTest.java
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractorTest.java
@@ -46,6 +46,7 @@ public class CustomColumnSetExtractorTest extends 
BaseColumnExtractorTest {
     public void testExtractWithoutPrefix() {
         DbEntity mockDbEntity = createMockDbEntity("mock");
         TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withNeedsResultSetMapping(true)
                 .withMetaData(new MockQueryMetadataBuilder()
                         .withDbEntity(mockDbEntity)
                         .build())
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryWrapperBuilder.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryWrapperBuilder.java
index 4361150..e1bcc5c 100644
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryWrapperBuilder.java
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryWrapperBuilder.java
@@ -50,6 +50,7 @@ class MockQueryWrapperBuilder {
     private Expression havingQualifier;
 
     private Select<?> mockSelect;
+    private boolean needsResultSetMapping;
 
     MockQueryWrapperBuilder withDistinct(boolean distinct) {
         this.distinct = distinct;
@@ -91,6 +92,11 @@ class MockQueryWrapperBuilder {
         return this;
     }
 
+    MockQueryWrapperBuilder withNeedsResultSetMapping(boolean 
needsResultSetMapping) {
+        this.needsResultSetMapping = needsResultSetMapping;
+        return this;
+    }
+
     TranslatableQueryWrapper build() {
         return new TranslatableQueryWrapper() {
             @Override
@@ -127,6 +133,11 @@ class MockQueryWrapperBuilder {
             public Select<?> unwrap() {
                 return mockSelect;
             }
+
+            @Override
+            public boolean needsResultSetMapping() {
+                return needsResultSetMapping;
+            }
         };
     }
 }

Reply via email to