CAY-2255 ObjectSelect: columns as full entities
CAY-2271 ColumnSelect: support for prefetch and limit


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

Branch: refs/heads/master
Commit: e098f236032ee8cdd695993f7b71d1b44388318f
Parents: 05a7725
Author: Nikita Timofeev <[email protected]>
Authored: Tue Mar 21 11:58:35 2017 +0300
Committer: Nikita Timofeev <[email protected]>
Committed: Tue Mar 21 11:58:35 2017 +0300

----------------------------------------------------------------------
 .../cayenne/access/DataContextQueryAction.java  |  38 +-
 .../cayenne/access/DataDomainQueryAction.java   |  13 +-
 .../cayenne/access/IncrementalFaultList.java    |  45 +-
 .../access/MixedResultIncrementalFaultList.java | 279 ++++++++++
 .../jdbc/reader/DefaultRowReaderFactory.java    |   4 +-
 .../cayenne/access/jdbc/reader/IdRowReader.java |  13 +-
 .../select/DefaultSelectTranslator.java         | 131 ++++-
 .../translator/select/QualifierTranslator.java  |   8 +-
 .../translator/select/QueryAssemblerHelper.java |  36 +-
 .../java/org/apache/cayenne/exp/Expression.java |   5 +
 .../apache/cayenne/exp/ExpressionFactory.java   |   9 +
 .../java/org/apache/cayenne/exp/Property.java   |  42 ++
 .../cayenne/exp/parser/ASTFullObject.java       |  63 +++
 .../org/apache/cayenne/query/ObjectSelect.java  |   4 +-
 .../cayenne/query/SelectQueryMetadata.java      | 180 ++++++-
 .../org/apache/cayenne/CayenneCompoundIT.java   |  24 +
 .../apache/cayenne/query/ColumnSelectIT.java    | 508 ++++++++++++++++++-
 .../cayenne/query/ObjectSelect_RunIT.java       | 141 -----
 docs/doc/src/main/resources/RELEASE-NOTES.txt   |   2 +
 19 files changed, 1311 insertions(+), 234 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
index 948671f..0ef2c0c 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
@@ -28,6 +28,7 @@ import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.PersistenceState;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.query.EntityResultSegment;
 import org.apache.cayenne.query.ObjectIdQuery;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.RefreshQuery;
@@ -75,8 +76,7 @@ class DataContextQueryAction extends ObjectContextQueryAction 
{
             ObjectIdQuery oidQuery = (ObjectIdQuery) query;
 
             if (!oidQuery.isFetchMandatory()) {
-                Object object = polymorphicObjectFromCache(
-                        oidQuery.getObjectId());
+                Object object = 
polymorphicObjectFromCache(oidQuery.getObjectId());
                 if (object != null) {
 
                     // TODO: andrus, 10/14/2006 - obtaining a row from an 
object is the
@@ -104,23 +104,27 @@ class DataContextQueryAction extends 
ObjectContextQueryAction {
     @Override
     protected boolean interceptPaginatedQuery() {
         if (metadata.getPageSize() > 0) {
-
-            DbEntity dbEntity = metadata.getDbEntity();
-            Integer maxIdQualifierSize = actingDataContext
-                    .getParentDataDomain()
-                    .getMaxIdQualifierSize();
+            Integer maxIdQualifierSize = 
actingDataContext.getParentDataDomain().getMaxIdQualifierSize();
             List<?> paginatedList;
-            if (dbEntity != null && dbEntity.getPrimaryKeys().size() == 1) {
-                paginatedList = new SimpleIdIncrementalFaultList<Object>(
-                        actingDataContext,
-                        query,
-                        maxIdQualifierSize);
+            List<Object> rsMapping = metadata.getResultSetMapping();
+            boolean mixedResults = false;
+            if(rsMapping != null) {
+                if(rsMapping.size() > 1) {
+                    mixedResults = true;
+                } else if(rsMapping.size() == 1) {
+                    mixedResults = !(rsMapping.get(0) instanceof 
EntityResultSegment);
+                }
             }
-            else {
-                paginatedList = new IncrementalFaultList<Object>(
-                        actingDataContext,
-                        query,
-                        maxIdQualifierSize);
+
+            if(mixedResults) {
+                paginatedList = new 
MixedResultIncrementalFaultList<>(actingDataContext, query, maxIdQualifierSize);
+            } else {
+                DbEntity dbEntity = metadata.getDbEntity();
+                if (dbEntity != null && dbEntity.getPrimaryKeys().size() == 1) 
{
+                    paginatedList = new 
SimpleIdIncrementalFaultList<Object>(actingDataContext, query, 
maxIdQualifierSize);
+                } else {
+                    paginatedList = new 
IncrementalFaultList<Object>(actingDataContext, query, maxIdQualifierSize);
+                }
             }
 
             response = new ListResponse(paginatedList);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
index 933d616..6e9b9f8 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
@@ -672,9 +672,18 @@ class DataDomainQueryAction implements QueryRouter, 
OperationObserver {
         @Override
         void convert(List<DataRow> mainRows) {
 
-            ClassDescriptor descriptor = metadata.getClassDescriptor();
             PrefetchTreeNode prefetchTree = metadata.getPrefetchTree();
 
+            List<Object> rsMapping = metadata.getResultSetMapping();
+            EntityResultSegment resultSegment = null;
+            if(rsMapping != null && !rsMapping.isEmpty()) {
+                resultSegment = (EntityResultSegment)rsMapping.get(0);
+            }
+
+            ClassDescriptor descriptor = resultSegment == null
+                    ? metadata.getClassDescriptor()
+                    : resultSegment.getClassDescriptor();
+
             PrefetchProcessorNode node = toResultsTree(descriptor, 
prefetchTree, mainRows);
             List<Persistent> objects = node.getObjects();
             updateResponse(mainRows, objects != null ? objects : new 
ArrayList<>(1));
@@ -714,7 +723,7 @@ class DataDomainQueryAction implements QueryRouter, 
OperationObserver {
                             prefetchTreeNode = new PrefetchTreeNode();
                         }
                         PrefetchTreeNode addPath = 
prefetchTreeNode.addPath(prefetch.getPath());
-                        
addPath.setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+                        addPath.setSemantics(prefetch.getSemantics());
                         addPath.setPhantom(false);
                     }
                 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/IncrementalFaultList.java
----------------------------------------------------------------------
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 1e13ea8..bdfce69 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
@@ -59,7 +59,7 @@ import java.util.NoSuchElementException;
 public class IncrementalFaultList<E> implements List<E>, Serializable {
 
        protected int pageSize;
-       protected List elements;
+       protected final List elements;
        protected DataContext dataContext;
        protected ObjEntity rootEntity;
        protected SelectQuery<?> internalQuery;
@@ -71,7 +71,7 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
         */
        protected int idWidth;
 
-       private IncrementalListHelper helper;
+       IncrementalListHelper helper;
 
        /**
         * Defines the upper limit on the size of fetches. This is needed to 
avoid
@@ -156,12 +156,11 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
         * 
         * @since 3.0
         */
-       protected void fillIn(final Query query, List elementsList) {
+       protected void fillIn(final Query query, List<Object> elementsList) {
 
                elementsList.clear();
 
-               try (ResultIterator it = 
dataContext.performIteratedQuery(query);) {
-
+               try (ResultIterator it = 
dataContext.performIteratedQuery(query)) {
                        while (it.hasNextRow()) {
                                elementsList.add(it.nextRow());
                        }
@@ -236,7 +235,6 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
                        }
 
                        // fetch the range of objects in fetchSize chunks
-                       boolean fetchesDataRows = 
internalQuery.isFetchingDataRows();
                        List<Object> objects = new ArrayList<>(qualsSize);
 
                        int fetchSize = maxFetchSize > 0 ? maxFetchSize : 
Integer.MAX_VALUE;
@@ -244,15 +242,7 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
                        int fetchEnd = Math.min(qualsSize, fetchSize);
                        int fetchBegin = 0;
                        while (fetchBegin < qualsSize) {
-                               SelectQuery<Object> query = new 
SelectQuery<>(rootEntity, ExpressionFactory.joinExp(
-                                               Expression.OR, 
quals.subList(fetchBegin, fetchEnd)));
-
-                               query.setFetchingDataRows(fetchesDataRows);
-
-                               if (!query.isFetchingDataRows()) {
-                                       
query.setPrefetchTree(internalQuery.getPrefetchTree());
-                               }
-
+                               SelectQuery<Object> query = 
createSelectQuery(quals.subList(fetchBegin, fetchEnd));
                                objects.addAll(dataContext.performQuery(query));
                                fetchBegin = fetchEnd;
                                fetchEnd += Math.min(fetchSize, qualsSize - 
fetchEnd);
@@ -262,13 +252,28 @@ public class IncrementalFaultList<E> implements List<E>, 
Serializable {
                        checkPageResultConsistency(objects, ids);
 
                        // replace ids in the list with objects
-                       Iterator it = objects.iterator();
-                       while (it.hasNext()) {
-                               
helper.updateWithResolvedObjectInRange(it.next(), fromIndex, toIndex);
-                       }
+                       updatePageWithResults(objects, fromIndex, toIndex);
+               }
+       }
 
-                       unfetchedObjects -= objects.size();
+       void updatePageWithResults(List<Object> objects, int fromIndex, int 
toIndex) {
+               for (Object object : objects) {
+                       helper.updateWithResolvedObjectInRange(object, 
fromIndex, toIndex);
                }
+
+               unfetchedObjects -= objects.size();
+       }
+
+       SelectQuery<Object> createSelectQuery(List<Expression> expressions) {
+               SelectQuery<Object> query = new SelectQuery<>(rootEntity,
+                               ExpressionFactory.joinExp(Expression.OR, 
expressions));
+
+               query.setFetchingDataRows(internalQuery.isFetchingDataRows());
+               if (!query.isFetchingDataRows()) {
+                       query.setPrefetchTree(internalQuery.getPrefetchTree());
+               }
+
+               return query;
        }
 
        /**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..9ca2136
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java
@@ -0,0 +1,279 @@
+/*****************************************************************
+ *   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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.ResultIterator;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.query.ColumnSelect;
+import org.apache.cayenne.query.EntityResultSegment;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.QueryMetadata;
+import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.util.Util;
+
+/**
+ * FaultList that is used for paginated {@link ColumnSelect} queries.
+ * It expects data as Object[] where ids are stored instead of Persistent 
objects (as raw value for single PK
+ * or Map for compound PKs).
+ * Scalar values that were fetched from ColumnSelect not processed in any way,
+ * if there is no Persistent objects in the result Collection it will be 
iterated as is, without faulting anything.
+ *
+ * @see QueryMetadata#getPageSize()
+ * @see org.apache.cayenne.access.translator.select.DefaultSelectTranslator
+ * @see org.apache.cayenne.query.SelectQueryMetadata
+ *
+ * @since 4.0
+ */
+class MixedResultIncrementalFaultList<E> extends IncrementalFaultList<E> {
+
+    /**
+     * Cached positions for entity results in elements array
+     */
+    private Map<Integer, ObjEntity> indexToEntity;
+
+    /**
+     * Whether result contains only scalars
+     */
+    private boolean scalarResult;
+
+    /**
+     * Creates a new IncrementalFaultList using a given DataContext and query.
+     *
+     * @param dataContext  DataContext used by IncrementalFaultList to fill 
itself with
+     *                     objects.
+     * @param query        Main query used to retrieve data. Must have 
"pageSize"
+     *                     property set to a value greater than zero.
+     */
+    MixedResultIncrementalFaultList(DataContext dataContext, Query query, int 
maxFetchSize) {
+        super(dataContext, query, maxFetchSize);
+
+        // this should generally be true, and may be it worth to do something 
if it's not
+        if(query instanceof ColumnSelect) {
+            this.internalQuery.setColumns(((ColumnSelect<?>) 
query).getColumns());
+        }
+    }
+
+    @Override
+    IncrementalListHelper createHelper(QueryMetadata metadata) {
+        // first compile some meta data about results
+        indexToEntity = new HashMap<>();
+        scalarResult = true;
+        for(Object next : metadata.getResultSetMapping()) {
+            if(next instanceof EntityResultSegment) {
+                EntityResultSegment resultSegment = (EntityResultSegment)next;
+                ObjEntity entity = 
resultSegment.getClassDescriptor().getEntity();
+                // store entity's PK position in result
+                indexToEntity.put(resultSegment.getColumnOffset(), entity);
+                scalarResult = false;
+            }
+        }
+
+        // if there is no entities in this results,
+        // than all data is already there and we don't need to resolve any 
objects
+        if(indexToEntity.isEmpty()) {
+            return new ScalarArrayListHelper();
+        } else {
+            return new MixedArrayListHelper();
+        }
+    }
+
+    @Override
+    protected void fillIn(final Query query, List<Object> elementsList) {
+        elementsList.clear();
+        try (ResultIterator it = dataContext.performIteratedQuery(query)) {
+            while (it.hasNextRow()) {
+                elementsList.add(it.nextRow());
+            }
+        }
+
+        unfetchedObjects = elementsList.size();
+    }
+
+    @Override
+    protected void resolveInterval(int fromIndex, int toIndex) {
+        if (fromIndex >= toIndex || scalarResult) {
+            return;
+        }
+
+        synchronized (elements) {
+            if (elements.size() == 0) {
+                return;
+            }
+
+            // perform bound checking
+            if (fromIndex < 0) {
+                fromIndex = 0;
+            }
+
+            if (toIndex > elements.size()) {
+                toIndex = elements.size();
+            }
+
+            for(Map.Entry<Integer, ObjEntity> entry : 
indexToEntity.entrySet()) {
+                List<Expression> quals = new ArrayList<>(pageSize);
+                int dataIdx = entry.getKey();
+                for (int i = fromIndex; i < toIndex; i++) {
+                    Object[] object = (Object[])elements.get(i);
+                    if (helper.unresolvedSuspect(object[dataIdx])) {
+                        quals.add(buildIdQualifier(dataIdx, object));
+                    }
+                }
+
+                int qualsSize = quals.size();
+                if (qualsSize == 0) {
+                    continue;
+                }
+
+                // fetch the range of objects in fetchSize chunks
+                List<Persistent> objects = new ArrayList<>(qualsSize);
+
+                int fetchSize = maxFetchSize > 0 ? maxFetchSize : 
Integer.MAX_VALUE;
+                int fetchEnd = Math.min(qualsSize, fetchSize);
+                int fetchBegin = 0;
+                while (fetchBegin < qualsSize) {
+                    SelectQuery<Persistent> query = 
createSelectQuery(entry.getValue(), quals.subList(fetchBegin, fetchEnd));
+                    objects.addAll(dataContext.performQuery(query));
+                    fetchBegin = fetchEnd;
+                    fetchEnd += Math.min(fetchSize, qualsSize - fetchEnd);
+                }
+
+                // replace ids in the list with objects
+                updatePageWithResults(objects, dataIdx);
+            }
+        }
+    }
+
+    void updatePageWithResults(List<Persistent> objects, int dataIndex) {
+        MixedArrayListHelper helper = (MixedArrayListHelper)this.helper;
+        for (Persistent object : objects) {
+            helper.updateWithResolvedObject(object, dataIndex);
+        }
+    }
+
+    SelectQuery<Persistent> createSelectQuery(ObjEntity entity, 
List<Expression> expressions) {
+        SelectQuery<Persistent> query = new SelectQuery<>(entity, 
ExpressionFactory.joinExp(Expression.OR, expressions));
+        if (entity.equals(rootEntity)) {
+            query.setPrefetchTree(internalQuery.getPrefetchTree());
+        }
+        return query;
+    }
+
+    Expression buildIdQualifier(int index, Object[] data) {
+        Map<String, Object> map;
+        if(data[index] instanceof Map) {
+            map = (Map<String, Object>)data[index];
+        } else {
+            map = new HashMap<>();
+            int i = 0;
+            for (ObjAttribute attribute : 
indexToEntity.get(index).getPrimaryKeys()) {
+                map.put(attribute.getDbAttributeName(), data[index + i++]);
+            }
+        }
+        return ExpressionFactory.matchAllDbExp(map, Expression.EQUAL_TO);
+    }
+
+    /**
+     * Helper that operates on Object[] and checks for Persistent objects' 
presence in it.
+     */
+    class MixedArrayListHelper extends IncrementalListHelper {
+        @Override
+        boolean unresolvedSuspect(Object object) {
+            return !(object instanceof Persistent);
+        }
+
+        @Override
+        boolean objectsAreEqual(Object object, Object objectInTheList) {
+            if(!(object instanceof Object[])){
+                return false;
+            }
+            return Arrays.equals((Object[])object, (Object[])objectInTheList);
+        }
+
+        @Override
+        boolean replacesObject(Object object, Object objectInTheList) {
+            throw new UnsupportedOperationException();
+        }
+
+        boolean replacesObject(Persistent object, Object[] dataInTheList, int 
dataIdx) {
+            Map<?, ?> map = object.getObjectId().getIdSnapshot();
+
+            if(dataInTheList[dataIdx] instanceof Map) {
+                Map<?, ?> id = (Map<?, ?>) dataInTheList[dataIdx];
+                if (id.size() != map.size()) {
+                    return false;
+                }
+
+                for (Map.Entry<?, ?> entry : id.entrySet()) {
+                    if (!Util.nullSafeEquals(entry.getValue(), 
map.get(entry.getKey()))) {
+                        return false;
+                    }
+                }
+            } else {
+                for(Object id : map.values()) {
+                    if (!dataInTheList[dataIdx++].equals(id)) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+
+        void updateWithResolvedObject(Persistent object, int dataIdx) {
+            synchronized (elements) {
+                for (Object element : elements) {
+                    Object[] data = (Object[]) element;
+                    if (replacesObject(object, data, dataIdx)) {
+                        data[dataIdx] = object;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Helper that actually does nothing
+     */
+    class ScalarArrayListHelper extends IncrementalListHelper {
+        @Override
+        boolean unresolvedSuspect(Object object) {
+            return false;
+        }
+
+        @Override
+        boolean objectsAreEqual(Object object, Object objectInTheList) {
+            return objectInTheList.equals(object);
+        }
+
+        @Override
+        boolean replacesObject(Object object, Object objectInTheList) {
+            return false;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
index b9cb766..73a29e7 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
@@ -96,7 +96,7 @@ public class DefaultRowReaderFactory implements 
RowReaderFactory {
                        EntityResultSegment resultMetadata, 
PostprocessorFactory postProcessorFactory) {
 
                if (queryMetadata.getPageSize() > 0) {
-                       return new IdRowReader<Object>(descriptor, 
queryMetadata, postProcessorFactory.get());
+                       return new IdRowReader<Object>(descriptor, 
queryMetadata, resultMetadata, postProcessorFactory.get());
                } else if (resultMetadata.getClassDescriptor() != null && 
resultMetadata.getClassDescriptor().hasSubclasses()) {
                        return new InheritanceAwareEntityRowReader(descriptor, 
resultMetadata, postProcessorFactory.get());
                } else {
@@ -108,7 +108,7 @@ public class DefaultRowReaderFactory implements 
RowReaderFactory {
                        PostprocessorFactory postProcessorFactory) {
 
                if (queryMetadata.getPageSize() > 0) {
-                       return new IdRowReader<Object>(descriptor, 
queryMetadata, postProcessorFactory.get());
+                       return new IdRowReader<Object>(descriptor, 
queryMetadata, null, postProcessorFactory.get());
                } else if (queryMetadata.getClassDescriptor() != null && 
queryMetadata.getClassDescriptor().hasSubclasses()) {
                        return new InheritanceAwareRowReader(descriptor, 
queryMetadata, postProcessorFactory.get());
                } else {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java
index a108dd7..7ad539d 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java
@@ -26,6 +26,7 @@ import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.jdbc.RowDescriptor;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.query.EntityResultSegment;
 import org.apache.cayenne.query.QueryMetadata;
 import org.apache.cayenne.util.Util;
 
@@ -36,10 +37,12 @@ class IdRowReader<T> extends BaseRowReader<T> {
 
     protected int[] pkIndices;
 
-    public IdRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, 
DataRowPostProcessor postProcessor) {
+    public IdRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, 
EntityResultSegment resultMetadata, DataRowPostProcessor postProcessor) {
         super(descriptor, queryMetadata, postProcessor);
 
-        DbEntity dbEntity = queryMetadata.getDbEntity();
+        DbEntity dbEntity = resultMetadata == null
+                ? queryMetadata.getDbEntity()
+                : 
resultMetadata.getClassDescriptor().getEntity().getDbEntity();
         if (dbEntity == null) {
             throw new CayenneRuntimeException("Null root DbEntity, can't index 
PK");
         }
@@ -99,13 +102,9 @@ class IdRowReader<T> extends BaseRowReader<T> {
 
         DataRow idRow = new DataRow(2);
         idRow.setEntityName(entityName);
-        int len = pkIndices.length;
-
-        for (int i = 0; i < len; i++) {
 
+        for (int index : pkIndices) {
             // dereference column index
-            int index = pkIndices[i];
-
             // note: jdbc column indexes start from 1, not 0 as in arrays
             Object val = converters[index].materializeObject(resultSet, index 
+ 1, types[index]);
             idRow.put(labels[index], val);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
index affbe7a..42ac0b8 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
@@ -19,6 +19,7 @@
 package org.apache.cayenne.access.translator.select;
 
 import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.Persistent;
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.dba.DbAdapter;
@@ -104,6 +105,11 @@ public class DefaultSelectTranslator extends 
QueryAssembler implements SelectTra
        boolean haveAggregate;
        Map<ColumnDescriptor, List<DbAttributeBinding>> groupByColumns;
 
+       /**
+        * Callback for joins creation
+        */
+       AddJoinListener joinListener;
+
 
        public DefaultSelectTranslator(Query query, DbAdapter adapter, 
EntityResolver entityResolver) {
                super(query, adapter, entityResolver);
@@ -278,14 +284,22 @@ public class DefaultSelectTranslator extends 
QueryAssembler implements SelectTra
         * @since 4.0
         */
        protected void appendGroupByColumn(StringBuilder buffer, 
Map.Entry<ColumnDescriptor, List<DbAttributeBinding>> entry) {
+               String fullName;
+               if(entry.getKey().isExpression()) {
+                       fullName = entry.getKey().getDataRowKey();
+               } else {
+                       QuotingStrategy strategy = 
getAdapter().getQuotingStrategy();
+                       fullName = 
strategy.quotedIdentifier(queryMetadata.getDataMap(),
+                                       entry.getKey().getNamePrefix(), 
entry.getKey().getName());
+               }
+
+               buffer.append(fullName);
+
                
if(entry.getKey().getDataRowKey().equals(entry.getKey().getName())) {
-                       buffer.append(entry.getKey().getName());
-            for (DbAttributeBinding binding : entry.getValue()) {
-                addToParamList(binding.getAttribute(), binding.getValue());
-            }
-        } else {
-            buffer.append(entry.getKey().getDataRowKey());
-        }
+                       for (DbAttributeBinding binding : entry.getValue()) {
+                               addToParamList(binding.getAttribute(), 
binding.getValue());
+                       }
+               }
        }
 
        /**
@@ -360,9 +374,9 @@ public class DefaultSelectTranslator extends QueryAssembler 
implements SelectTra
                } else if (query.getRoot() instanceof DbEntity) {
                        appendDbEntityColumns(columns, query);
                } else if (getQueryMetadata().getPageSize() > 0) {
-                       appendIdColumns(columns, query);
+                       appendIdColumns(columns, 
queryMetadata.getClassDescriptor().getEntity());
                } else {
-                       appendQueryColumns(columns, query);
+                       appendQueryColumns(columns, query, 
queryMetadata.getClassDescriptor(), null);
                }
 
                return columns;
@@ -376,12 +390,60 @@ public class DefaultSelectTranslator extends 
QueryAssembler implements SelectTra
 
                QualifierTranslator qualifierTranslator = 
adapter.getQualifierTranslator(this);
                AccumulatingBindingListener bindingListener = new 
AccumulatingBindingListener();
+               final String[] joinTableAliasForProperty = {null};
+               joinListener = new AddJoinListener() {
+                       @Override
+                       public void joinAdded() {
+                               // capture last alias for joined table, will 
use it to resolve object columns
+                               joinTableAliasForProperty[0] = 
getCurrentAlias();
+                       }
+               };
                setAddBindingListener(bindingListener);
 
                for(Property<?> property : query.getColumns()) {
+                       int expressionType = property.getExpression().getType();
+                       boolean objectProperty = expressionType == 
Expression.FULL_OBJECT;
+                       // evaluate ObjPath with Persistent type as toOne 
relations and use it as full object
+                       
if(Persistent.class.isAssignableFrom(property.getType())) {
+                               if(expressionType == Expression.OBJ_PATH) {
+                                       objectProperty = true;
+                               } else {
+                                       // should we warn or throw an error?
+                               }
+                       }
+
+                       // forbid direct selection of toMany relationships 
columns
+                       if((expressionType == Expression.OBJ_PATH || 
expressionType == Expression.DB_PATH) &&
+                                       
(Collection.class.isAssignableFrom(property.getType()) ||
+                                                       
Map.class.isAssignableFrom(property.getType()))) {
+                               throw new CayenneRuntimeException("Can't 
directly select toMany relationship columns. " +
+                                               "Either select it with 
aggregate functions like count() or with flat() function to select full related 
objects.");
+                       }
+
+                       // Qualifier Translator in case of Object Columns have 
side effect -
+                       // it will create required joins, that we catch with 
listener above.
+                       // And we force created join alias for all columns of 
Object we select.
                        
qualifierTranslator.setQualifier(property.getExpression());
+                       
qualifierTranslator.setForceJoinForRelations(objectProperty);
                        StringBuilder builder = 
qualifierTranslator.appendPart(new StringBuilder());
 
+                       // If we want full object, use appendQueryColumns 
method, to fully process class descriptor
+                       if(objectProperty) {
+                               List<ColumnDescriptor> classColumns = new 
ArrayList<>();
+                               ObjEntity entity = 
entityResolver.getObjEntity(property.getType());
+                               if(getQueryMetadata().getPageSize() > 0) {
+                                       appendIdColumns(classColumns, entity);
+                               } else {
+                                       ClassDescriptor classDescriptor = 
entityResolver.getClassDescriptor(entity.getName());
+                                       appendQueryColumns(classColumns, query, 
classDescriptor, joinTableAliasForProperty[0]);
+                               }
+                               for(ColumnDescriptor descriptor : classColumns) 
{
+                                       columns.add(descriptor);
+                                       groupByColumns.put(descriptor, 
Collections.<DbAttributeBinding>emptyList());
+                               }
+                               continue;
+                       }
+
                        int type = 
TypesMapping.getSqlTypeByJava(property.getType());
 
                        String alias = property.getAlias();
@@ -402,6 +464,8 @@ public class DefaultSelectTranslator extends QueryAssembler 
implements SelectTra
                }
 
                setAddBindingListener(null);
+               qualifierTranslator.setForceJoinForRelations(false);
+               joinListener = null;
 
                if(!haveAggregate) {
                        // if no expression with aggregation function found, we 
don't need this information
@@ -442,9 +506,9 @@ public class DefaultSelectTranslator extends QueryAssembler 
implements SelectTra
         * Appends columns needed for object SelectQuery to the provided columns
         * list.
         */
-       <T> List<ColumnDescriptor> appendQueryColumns(final 
List<ColumnDescriptor> columns, SelectQuery<T> query) {
+       <T> List<ColumnDescriptor> appendQueryColumns(final 
List<ColumnDescriptor> columns, SelectQuery<T> query, ClassDescriptor 
descriptor, final String tableAlias) {
 
-               final Set<ColumnTracker> attributes = new 
HashSet<ColumnTracker>();
+               final Set<ColumnTracker> attributes = new HashSet<>();
 
                // fetched attributes include attributes that are either:
                //
@@ -452,8 +516,6 @@ public class DefaultSelectTranslator extends QueryAssembler 
implements SelectTra
                // * PK
                // * FK used in relationship
                // * joined prefetch PK
-
-               ClassDescriptor descriptor = queryMetadata.getClassDescriptor();
                ObjEntity oe = descriptor.getEntity();
 
                PropertyVisitor visitor = new PropertyVisitor() {
@@ -474,7 +536,7 @@ public class DefaultSelectTranslator extends QueryAssembler 
implements SelectTra
                                        } else if (pathPart instanceof 
DbAttribute) {
                                                DbAttribute dbAttr = 
(DbAttribute) pathPart;
 
-                                               appendColumn(columns, oa, 
dbAttr, attributes, null);
+                                               appendColumn(columns, oa, 
dbAttr, attributes, null, tableAlias);
                                        }
                                }
                                return true;
@@ -499,7 +561,7 @@ public class DefaultSelectTranslator extends QueryAssembler 
implements SelectTra
                                List<DbJoin> joins = dbRel.getJoins();
                                for (DbJoin join : joins) {
                                        DbAttribute src = join.getSource();
-                                       appendColumn(columns, null, src, 
attributes, null);
+                                       appendColumn(columns, null, src, 
attributes, null, tableAlias);
                                }
                        }
                };
@@ -511,9 +573,9 @@ public class DefaultSelectTranslator extends QueryAssembler 
implements SelectTra
                resetJoinStack();
 
                // add remaining needed attrs from DbEntity
-               DbEntity table = getQueryMetadata().getDbEntity();
+               DbEntity table = oe.getDbEntity();
                for (DbAttribute dba : table.getPrimaryKeys()) {
-                       appendColumn(columns, null, dba, attributes, null);
+                       appendColumn(columns, null, dba, attributes, null, 
tableAlias);
                }
 
                // special handling of a disjoint query...
@@ -571,6 +633,12 @@ public class DefaultSelectTranslator extends 
QueryAssembler implements SelectTra
 
                // handle joint prefetches directly attached to this query...
                if (query.getPrefetchTree() != null) {
+                       // 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 : 
query.getPrefetchTree().getChildren()) {
+                               prefetch.setEntityName(oe.getName());
+                       }
 
                        for (PrefetchTreeNode prefetch : 
query.getPrefetchTree().adjacentJointNodes()) {
 
@@ -633,14 +701,12 @@ public class DefaultSelectTranslator extends 
QueryAssembler implements SelectTra
                return columns;
        }
 
-       <T> List<ColumnDescriptor> appendIdColumns(final List<ColumnDescriptor> 
columns, SelectQuery<T> query) {
+       <T> List<ColumnDescriptor> appendIdColumns(final List<ColumnDescriptor> 
columns, ObjEntity objEntity) {
 
-               Set<ColumnTracker> skipSet = new HashSet<ColumnTracker>();
+               Set<ColumnTracker> skipSet = new HashSet<>();
 
-               ClassDescriptor descriptor = queryMetadata.getClassDescriptor();
-               ObjEntity oe = descriptor.getEntity();
-               DbEntity dbEntity = oe.getDbEntity();
-               for (ObjAttribute attribute : oe.getPrimaryKeys()) {
+               DbEntity dbEntity = objEntity.getDbEntity();
+               for (ObjAttribute attribute : objEntity.getPrimaryKeys()) {
 
                        // synthetic objattributes can't reliably lookup their 
DbAttribute,
                        // so do it manually..
@@ -652,9 +718,17 @@ public class DefaultSelectTranslator extends 
QueryAssembler implements SelectTra
        }
 
        private void appendColumn(List<ColumnDescriptor> columns, ObjAttribute 
objAttribute, DbAttribute attribute,
-                       Set<ColumnTracker> skipSet, String label) {
+                                                         Set<ColumnTracker> 
skipSet, String label) {
+               appendColumn(columns, objAttribute, attribute, skipSet, label, 
null);
+       }
+
+       private void appendColumn(List<ColumnDescriptor> columns, ObjAttribute 
objAttribute, DbAttribute attribute,
+                       Set<ColumnTracker> skipSet, String label, String alias) 
{
+
+               if(alias == null) {
+                       alias = getCurrentAlias();
+               }
 
-               String alias = getCurrentAlias();
                if (skipSet.add(new ColumnTracker(alias, attribute))) {
 
                        ColumnDescriptor column = (objAttribute != null) ? new 
ColumnDescriptor(objAttribute, attribute, alias)
@@ -712,6 +786,9 @@ public class DefaultSelectTranslator extends QueryAssembler 
implements SelectTra
                }
 
                getJoinStack().pushJoin(relationship, joinType, joinSplitAlias);
+               if(joinListener != null) {
+                       joinListener.joinAdded();
+               }
        }
 
        /**
@@ -788,4 +865,8 @@ public class DefaultSelectTranslator extends QueryAssembler 
implements SelectTra
                        return new ArrayList<>(bindings);
                }
        }
+
+       interface AddJoinListener {
+               void joinAdded();
+       }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/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 deb7d52..51ebf52 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
@@ -142,9 +142,7 @@ public class QualifierTranslator extends 
QueryAssemblerHelper implements Travers
                        }
                }
 
-               /**
-                * Attaching root Db entity's qualifier
-                */
+               // Attaching root Db entity's qualifier
                if (getDbEntity() != null) {
                        Expression dbQualifier = getDbEntity().getQualifier();
                        if (dbQualifier != null) {
@@ -425,6 +423,10 @@ public class QualifierTranslator extends 
QueryAssemblerHelper implements Travers
                        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) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
index decdaa7..2fc6078 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
@@ -46,6 +46,12 @@ public abstract class QueryAssemblerHelper {
        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.
         */
@@ -444,7 +450,7 @@ public abstract class QueryAssemblerHelper {
         */
        protected void processRelTermination(DbRelationship rel, JoinType 
joinType, String joinSplitAlias) {
 
-               if (rel.isToMany()) {
+               if (forceJoinForRelations || rel.isToMany()) {
                        // append joins
                        queryAssembler.dbRelationshipAdded(rel, joinType, 
joinSplitAlias);
                }
@@ -452,33 +458,39 @@ public abstract class QueryAssemblerHelper {
                // get last DbRelationship on the list
                List<DbJoin> joins = rel.getJoins();
                if (joins.size() != 1) {
-                       StringBuilder msg = new StringBuilder();
-                       msg.append("OBJ_PATH expressions are only supported 
").append("for a single-join relationships. ")
-                                       .append("This relationship has 
").append(joins.size()).append(" joins.");
+                       String msg = "OBJ_PATH expressions are only supported 
for a single-join relationships. " +
+                                       "This relationship has " + joins.size() 
+ " joins.";
 
-                       throw new CayenneRuntimeException(msg.toString());
+                       throw new CayenneRuntimeException(msg);
                }
 
                DbJoin join = joins.get(0);
 
-               DbAttribute attribute = null;
+               DbAttribute attribute;
 
                if (rel.isToMany()) {
-                       DbEntity ent = (DbEntity) 
join.getRelationship().getTargetEntity();
+                       DbEntity ent = join.getRelationship().getTargetEntity();
                        Collection<DbAttribute> pk = ent.getPrimaryKeys();
                        if (pk.size() != 1) {
-                               StringBuilder msg = new StringBuilder();
-                               msg.append("DB_NAME expressions can only 
support ").append("targets with a single column PK. ")
-                                               .append("This entity has 
").append(pk.size()).append(" columns in primary key.");
+                               String msg = "DB_NAME expressions can only 
support targets with a single column PK. " +
+                                               "This entity has " + pk.size() 
+ " columns in primary key.";
 
-                               throw new 
CayenneRuntimeException(msg.toString());
+                               throw new CayenneRuntimeException(msg);
                        }
 
                        attribute = pk.iterator().next();
                } else {
-                       attribute = join.getSource();
+                       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/e098f236/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java 
b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
index 8491e23..0d35cea 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
@@ -161,6 +161,11 @@ public abstract class Expression implements Serializable, 
XMLSerializable {
         */
        public static final int ASTERISK = 46;
 
+       /**
+        * @since 4.0
+        */
+       public static final int FULL_OBJECT = 47;
+
        protected int type;
 
        /**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java 
b/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
index e9fa6b0..b2770bc 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
@@ -33,6 +33,7 @@ import org.apache.cayenne.exp.parser.ASTDbPath;
 import org.apache.cayenne.exp.parser.ASTDivide;
 import org.apache.cayenne.exp.parser.ASTEqual;
 import org.apache.cayenne.exp.parser.ASTFalse;
+import org.apache.cayenne.exp.parser.ASTFullObject;
 import org.apache.cayenne.exp.parser.ASTGreater;
 import org.apache.cayenne.exp.parser.ASTGreaterOrEqual;
 import org.apache.cayenne.exp.parser.ASTIn;
@@ -1245,6 +1246,14 @@ public class ExpressionFactory {
                return joinExp(Expression.OR, pairs);
        }
 
+       public static Expression fullObjectExp() {
+               return new ASTFullObject();
+       }
+
+       public static Expression fullObjectExp(Expression exp) {
+               return new ASTFullObject(exp);
+       }
+
        /**
         * @since 4.0
         */

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java 
b/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
index 59eb17d..9ceaf6f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.cayenne.exp;
 
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.Persistent;
 import org.apache.cayenne.exp.parser.ASTPath;
 import org.apache.cayenne.query.Ordering;
 import org.apache.cayenne.query.PrefetchTreeNode;
@@ -27,6 +29,7 @@ import org.apache.cayenne.reflect.PropertyUtils;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /**
  * <p>
@@ -788,6 +791,24 @@ public class Property<E> {
         return new Property<>(alias, this.getExpression(), this.getType());
     }
 
+    /**
+     * <p>Create new "flat" property for toMany relationship.</p>
+     * <p>
+     *     Example:
+     *     <pre>{@code
+     *     List<Object[]> result = ObjectSelect
+     *          .columnQuery(Artist.class, Artist.ARTIST_NAME, 
Artist.PAINTING_ARRAY.flat(Painting.class))
+     *          .select(context);
+     *     }</pre>
+     * </p>
+     */
+    public <T extends Persistent> Property<T> flat(Class<? super T> tClass) {
+        if(!Collection.class.isAssignableFrom(type) && 
!Map.class.isAssignableFrom(type)) {
+            throw new CayenneRuntimeException("Can use flat() function only on 
Property mapped on toMany relationship.");
+        }
+        return create(ExpressionFactory.fullObjectExp(getExpression()), 
tClass);
+    }
+
     public Class<? super E> getType() {
         return type;
     }
@@ -820,6 +841,27 @@ public class Property<E> {
     }
 
     /**
+     * <p>
+     * Creates "self" Property for persistent class.
+     * This property can be used to select full object along with some of it 
properties (or
+     * properties that can be resolved against query root)
+     * </p>
+     * <p>
+     *     Here is sample code, that will select all Artists and count of 
their Paintings:
+     *     <pre>{@code
+     *     Property<Artist> artistFull = Property.createSelf(Artist.class);
+     *     List<Object[]> result = ObjectSelect
+     *          .columnQuery(Artist.class, artistFull, 
Artist.PAINTING_ARRAY.count())
+     *          .select(context);
+     *     }
+     *     </pre>
+     * </p>
+     */
+    public static <T extends Persistent> Property<T> createSelf(Class<? super 
T> type) {
+        return new Property<>(null, ExpressionFactory.fullObjectExp(), type);
+    }
+
+    /**
      * Since Expression is mutable we need to provide clean Expression for 
every getter call.
      * So to keep Property itself immutable we use ExpressionProvider.
      * @see Property#Property(String, Class)

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java 
b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java
new file mode 100644
index 0000000..25f6d97
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java
@@ -0,0 +1,63 @@
+/*****************************************************************
+ *   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.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+
+/**
+ * @since 4.0
+ */
+public class ASTFullObject extends SimpleNode {
+
+    public ASTFullObject(Expression expression) {
+        this();
+        Node node = wrapChild(expression);
+        jjtAddChild(node, 0);
+        node.jjtSetParent(this);
+    }
+
+    public ASTFullObject() {
+        this(0);
+    }
+
+    protected ASTFullObject(int i) {
+        super(i);
+    }
+
+    @Override
+    protected String getExpressionOperator(int index) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        return o;
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTFullObject(id);
+    }
+
+    @Override
+    public int getType() {
+        return Expression.FULL_OBJECT;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java
index 262f18c..da03ccb 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java
@@ -125,7 +125,7 @@ public class ObjectSelect<T> extends FluentSelect<T, 
ObjectSelect<T>> {
      * @param entityType base persistent class that will be used as a root for 
this query
      * @param column single column to select
      */
-    protected static <E> ColumnSelect<E> columnQuery(Class<?> entityType, 
Property<E> column) {
+    public static <E> ColumnSelect<E> columnQuery(Class<?> entityType, 
Property<E> column) {
         return new ColumnSelect<>().entityType(entityType).column(column);
     }
 
@@ -136,7 +136,7 @@ public class ObjectSelect<T> extends FluentSelect<T, 
ObjectSelect<T>> {
      * @param firstColumn column to select
      * @param otherColumns columns to select
      */
-    protected static ColumnSelect<Object[]> columnQuery(Class<?> entityType, 
Property<?> firstColumn, Property<?>... otherColumns) {
+    public static ColumnSelect<Object[]> columnQuery(Class<?> entityType, 
Property<?> firstColumn, Property<?>... otherColumns) {
         return new 
ColumnSelect<Object[]>().entityType(entityType).columns(firstColumn, 
otherColumns);
     }
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
----------------------------------------------------------------------
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 aba36c5..158fbea 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
@@ -21,14 +21,34 @@ package org.apache.cayenne.query;
 import java.io.IOException;
 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.Property;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.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
@@ -185,6 +205,7 @@ class SelectQueryMetadata extends BaseQueryMetadata {
        }
 
        /**
+        * Build DB result descriptor, that will be used to read and convert 
raw result of ColumnSelect
         * @since 4.0
         */
        private void buildResultSetMappingForColumns(SelectQuery<?> query, 
EntityResolver resolver) {
@@ -194,13 +215,168 @@ class SelectQueryMetadata extends BaseQueryMetadata {
                
                SQLResult result = new SQLResult();
                for(Property<?> column : query.getColumns()) {
-                       String name = column.getName() == null ? 
column.getExpression().expName() : column.getName();
-                       result.addColumnResult(name);
+                       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;
+                               }
+                       } 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));
+                               }
+                       } else {
+                               // scalar column
+                               result.addColumnResult(name);
+                       }
                }
                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(Property<?> 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.
+        *
+        * This method is actually repeating logic of
+        * {@link 
org.apache.cayenne.access.translator.select.DefaultSelectTranslator#appendQueryColumns}.
+        * 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.
+        *
+        * @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, 
Property<?> column, EntityResolver resolver) {
+               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 '" + prefetch + "' for entity: "
+                                                       + 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;
+       }
+
+       /**
         * @since 4.0
         */
        @Override

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/test/java/org/apache/cayenne/CayenneCompoundIT.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/CayenneCompoundIT.java 
b/cayenne-server/src/test/java/org/apache/cayenne/CayenneCompoundIT.java
index 83913b6..191c725 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/CayenneCompoundIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/CayenneCompoundIT.java
@@ -20,6 +20,8 @@
 package org.apache.cayenne;
 
 import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.test.jdbc.DBHelper;
 import org.apache.cayenne.test.jdbc.TableHelper;
@@ -65,6 +67,12 @@ public class CayenneCompoundIT extends ServerCase {
                tCompoundPKTest.insert("PK1", "PK2", "BBB");
        }
 
+       private void createCompoundPKs(int size) throws Exception {
+               for(int i=0; i<size; i++) {
+                       tCompoundPKTest.insert("PK"+i, "PK"+(2*i), "BBB"+i);
+               }
+       }
+
        private void createOneCharPK() throws Exception {
                tCharPKTest.insert("CPK", "AAAA");
        }
@@ -157,4 +165,20 @@ public class CayenneCompoundIT extends ServerCase {
                assertEquals("CPK", Cayenne.pkForObject(object));
        }
 
+
+       @Test
+       public void testPaginatedColumnSelect() throws Exception {
+               createCompoundPKs(20);
+
+               List<Object[]> result = 
ObjectSelect.query(CompoundPkTestEntity.class)
+                               .columns(CompoundPkTestEntity.NAME, 
Property.createSelf(CompoundPkTestEntity.class))
+                               .pageSize(7)
+                               .select(context);
+               assertEquals(20, result.size());
+               for(Object[] next : result) {
+                       assertEquals(2, next.length);
+                       assertEquals(String.class, next[0].getClass());
+                       assertEquals(CompoundPkTestEntity.class, 
next[1].getClass());
+               }
+       }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java 
b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
index 4319398..02102c5 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
@@ -23,15 +23,24 @@ import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.sql.Types;
 import java.text.DateFormat;
+import java.util.List;
 import java.util.Locale;
 
 import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.ResultBatchIterator;
+import org.apache.cayenne.ResultIteratorCallback;
 import org.apache.cayenne.access.DataContext;
 import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.FunctionExpressionFactory;
 import org.apache.cayenne.exp.Property;
 import org.apache.cayenne.test.jdbc.DBHelper;
 import org.apache.cayenne.test.jdbc.TableHelper;
 import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.testdo.testmap.Gallery;
 import org.apache.cayenne.testdo.testmap.Painting;
 import org.apache.cayenne.unit.PostgresUnitDbAdapter;
 import org.apache.cayenne.unit.UnitDbAdapter;
@@ -42,8 +51,10 @@ import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
 
-import static org.apache.cayenne.exp.FunctionExpressionFactory.substringExp;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 /**
@@ -333,4 +344,499 @@ public class ColumnSelectIT extends ServerCase {
                 .selectOne(context);
         assertEquals(count2, count3);
     }
+
+    @Test
+    public void testSelectFirst_MultiColumns() throws Exception {
+        Object[] a = ObjectSelect.query(Artist.class)
+                .columns(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH)
+                .columns(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH)
+                .columns(Artist.ARTIST_NAME.alias("newName"))
+                .where(Artist.ARTIST_NAME.like("artist%"))
+                .orderBy("db:ARTIST_ID")
+                .selectFirst(context);
+        assertNotNull(a);
+        assertEquals("artist1", a[0]);
+        assertEquals("artist1", a[4]);
+    }
+
+    @Test
+    public void testSelectFirst_SingleValueInColumns() throws Exception {
+        Object[] a = ObjectSelect.query(Artist.class)
+                .columns(Artist.ARTIST_NAME)
+                .where(Artist.ARTIST_NAME.like("artist%"))
+                .orderBy("db:ARTIST_ID")
+                .selectFirst(context);
+        assertNotNull(a);
+        assertEquals("artist1", a[0]);
+    }
+
+    @Test
+    public void testSelectFirst_SubstringName() throws Exception {
+        Expression exp = 
FunctionExpressionFactory.substringExp(Artist.ARTIST_NAME.path(), 5, 3);
+        Property<String> substrName = Property.create("substrName", exp, 
String.class);
+        Object[] a = ObjectSelect.query(Artist.class)
+                .columns(Artist.ARTIST_NAME, substrName)
+                .where(substrName.eq("st3"))
+                .selectFirst(context);
+
+        assertNotNull(a);
+        assertEquals("artist3", a[0]);
+        assertEquals("st3", a[1]);
+    }
+
+    @Test
+    public void testSelectFirst_RelColumns() throws Exception {
+        // set shorter than painting_array.paintingTitle alias as some DBs 
doesn't support dot in alias
+        Property<String> paintingTitle = 
Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).alias("paintingTitle");
+
+        Object[] a = ObjectSelect.query(Artist.class)
+                .columns(Artist.ARTIST_NAME, paintingTitle)
+                .orderBy(paintingTitle.asc())
+                .selectFirst(context);
+        assertNotNull(a);
+        assertEquals("painting1", a[1]);
+    }
+
+    @Test
+    public void testSelectFirst_RelColumn() throws Exception {
+        // set shorter than painting_array.paintingTitle alias as some DBs 
doesn't support dot in alias
+        Property<String> paintingTitle = 
Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).alias("paintingTitle");
+
+        String a = ObjectSelect.query(Artist.class)
+                .column(paintingTitle)
+                .orderBy(paintingTitle.asc())
+                .selectFirst(context);
+        assertNotNull(a);
+        assertEquals("painting1", a);
+    }
+
+    @Test
+    public void testSelectFirst_RelColumnWithFunction() throws Exception {
+        Property<String> altTitle = 
Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE)
+                .substring(7, 3).concat(" ", Artist.ARTIST_NAME)
+                .alias("altTitle");
+
+        String a = ObjectSelect.query(Artist.class)
+                .column(altTitle)
+                .where(altTitle.like("ng1%"))
+                .and(Artist.ARTIST_NAME.like("%ist1"))
+//                             .orderBy(altTitle.asc()) // unsupported for now
+                .selectFirst(context);
+        assertNotNull(a);
+        assertEquals("ng1 artist1", a);
+    }
+
+    /*
+     *  Test iterated select
+     */
+
+    @Test
+    public void testIterationSingleColumn() throws Exception {
+        ColumnSelect<String> columnSelect = 
ObjectSelect.query(Artist.class).column(Artist.ARTIST_NAME);
+
+        final int[] count = new int[1];
+        columnSelect.iterate(context, new ResultIteratorCallback<String>() {
+            @Override
+            public void next(String object) {
+                count[0]++;
+                assertTrue(object.startsWith("artist"));
+            }
+        });
+
+        assertEquals(20, count[0]);
+    }
+
+    @Test
+    public void testBatchIterationSingleColumn() throws Exception {
+        ColumnSelect<String> columnSelect = 
ObjectSelect.query(Artist.class).column(Artist.ARTIST_NAME);
+
+        try(ResultBatchIterator<String> it = 
columnSelect.batchIterator(context, 10)) {
+            List<String> next = it.next();
+            assertEquals(10, next.size());
+            assertTrue(next.get(0).startsWith("artist"));
+        }
+    }
+
+    @Test
+    public void testIterationMultiColumns() throws Exception {
+        ColumnSelect<Object[]> columnSelect = 
ObjectSelect.query(Artist.class).columns(Artist.ARTIST_NAME, 
Artist.DATE_OF_BIRTH);
+
+        final int[] count = new int[1];
+        columnSelect.iterate(context, new ResultIteratorCallback<Object[]>() {
+            @Override
+            public void next(Object[] object) {
+                count[0]++;
+                assertTrue(object[0] instanceof String);
+                assertTrue(object[1] instanceof java.util.Date);
+            }
+        });
+
+        assertEquals(20, count[0]);
+    }
+
+    @Test
+    public void testBatchIterationMultiColumns() throws Exception {
+        ColumnSelect<Object[]> columnSelect = 
ObjectSelect.query(Artist.class).columns(Artist.ARTIST_NAME, 
Artist.DATE_OF_BIRTH);
+
+        try(ResultBatchIterator<Object[]> it = 
columnSelect.batchIterator(context, 10)) {
+            List<Object[]> next = it.next();
+            assertEquals(10, next.size());
+            assertTrue(next.get(0)[0] instanceof String);
+            assertTrue(next.get(0)[1] instanceof java.util.Date);
+        }
+    }
+
+    /*
+     *  Test select with page size
+     */
+
+    @Test
+    public void testPageSizeOneScalar() {
+        List<String> a = ObjectSelect.query(Artist.class)
+                .column(Artist.ARTIST_NAME.trim())
+                .pageSize(10)
+                .select(context);
+        assertNotNull(a);
+        assertEquals(20, a.size());
+        int idx = 0;
+        for(String next : a) {
+            assertNotNull(""+idx, next);
+            idx++;
+        }
+    }
+
+    @Test
+    public void testPageSizeScalars() {
+        List<Object[]> a = ObjectSelect.query(Artist.class)
+                .columns(Artist.ARTIST_NAME.trim(), Artist.DATE_OF_BIRTH, 
Artist.PAINTING_ARRAY.count())
+                .pageSize(10)
+                .select(context);
+        assertNotNull(a);
+        assertEquals(5, a.size());
+        int idx = 0;
+        for(Object[] next : a) {
+            assertNotNull(next);
+            assertTrue("" + idx, next[0] instanceof String);
+            assertTrue("" + idx, next[1] instanceof java.util.Date);
+            assertTrue("" + idx, next[2] instanceof Long);
+            idx++;
+        }
+    }
+
+    @Test
+    public void testPageSizeOneObject() {
+        Property<Artist> artistFull = Property.createSelf(Artist.class);
+        List<Artist> a = ObjectSelect.query(Artist.class)
+                .column(artistFull)
+                .pageSize(10)
+                .select(context);
+        assertNotNull(a);
+        assertEquals(20, a.size());
+        for(Artist next : a){
+            assertNotNull(next);
+        }
+    }
+
+    @Test
+    public void testPageSizeObjectAndScalars() {
+        Property<Artist> artistFull = Property.createSelf(Artist.class);
+        List<Object[]> a = ObjectSelect.query(Artist.class)
+                .columns(Artist.ARTIST_NAME, artistFull, 
Artist.PAINTING_ARRAY.count())
+                .pageSize(10)
+                .select(context);
+        assertNotNull(a);
+        assertEquals(5, a.size());
+        int idx = 0;
+        for(Object[] next : a) {
+            assertNotNull(next);
+            assertEquals("" + idx, String.class, next[0].getClass());
+            assertEquals("" + idx, Artist.class, next[1].getClass());
+            assertEquals("" + idx, Long.class, next[2].getClass());
+            idx++;
+        }
+    }
+
+    @Test
+    public void testPageSizeObjects() {
+        Property<Artist> artistFull = Property.createSelf(Artist.class);
+        List<Object[]> a = ObjectSelect.query(Artist.class)
+                .columns(Artist.ARTIST_NAME, artistFull, 
Artist.PAINTING_ARRAY.flat(Painting.class))
+                .pageSize(10)
+                .select(context);
+        assertNotNull(a);
+        assertEquals(21, a.size());
+        int idx = 0;
+        for(Object[] next : a) {
+            assertNotNull(next);
+            assertEquals("" + idx, String.class, next[0].getClass());
+            assertEquals("" + idx, Artist.class, next[1].getClass());
+            assertEquals("" + idx, Painting.class, next[2].getClass());
+            idx++;
+        }
+    }
+
+    /*
+     *  Test prefetch
+     */
+
+    @Test
+    public void testObjectColumnWithJointPrefetch() {
+        Property<Artist> artistFull = Property.createSelf(Artist.class);
+
+        List<Object[]> result = ObjectSelect.query(Artist.class)
+                .columns(artistFull, Artist.DATE_OF_BIRTH, 
Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE))
+                .prefetch(Artist.PAINTING_ARRAY.joint())
+                .select(context);
+
+        checkPrefetchResults(result);
+    }
+
+    @Test
+    public void testObjectColumnWithDisjointPrefetch() {
+        Property<Artist> artistFull = Property.createSelf(Artist.class);
+
+        List<Object[]> result = ObjectSelect.query(Artist.class)
+                .columns(artistFull, Artist.DATE_OF_BIRTH, 
Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE))
+                .prefetch(Artist.PAINTING_ARRAY.disjoint())
+                .select(context);
+
+        checkPrefetchResults(result);
+    }
+
+    @Test
+    public void testObjectColumnWithDisjointByIdPrefetch() {
+        Property<Artist> artistFull = Property.createSelf(Artist.class);
+
+        List<Object[]> result = ObjectSelect.query(Artist.class)
+                .columns(artistFull, Artist.DATE_OF_BIRTH, 
Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE))
+                .prefetch(Artist.PAINTING_ARRAY.disjointById())
+                .select(context);
+
+        checkPrefetchResults(result);
+    }
+
+    private void checkPrefetchResults(List<Object[]> result) {
+        assertEquals(21, result.size());
+        for(Object[] next : result) {
+            assertTrue(next[0] instanceof Artist);
+            assertTrue(next[1] instanceof java.util.Date);
+            assertTrue(next[2] instanceof String);
+            Artist artist = (Artist)next[0];
+            assertEquals(PersistenceState.COMMITTED, 
artist.getPersistenceState());
+
+            Object paintingsArr = 
artist.readPropertyDirectly(Artist.PAINTING_ARRAY.getName());
+            assertFalse(paintingsArr instanceof Fault);
+            assertTrue(((List)paintingsArr).size() > 0);
+        }
+    }
+
+    @Test
+    public void testAggregateColumnWithJointPrefetch() {
+        Property<Artist> artistFull = Property.createSelf(Artist.class);
+
+        List<Object[]> result = ObjectSelect.query(Artist.class)
+                .columns(artistFull, Artist.PAINTING_ARRAY.count())
+                .prefetch(Artist.PAINTING_ARRAY.joint())
+                .select(context);
+
+        checkAggregatePrefetchResults(result);
+    }
+
+    @Test
+    public void testAggregateColumnWithDisjointPrefetch() {
+        Property<Artist> artistFull = Property.createSelf(Artist.class);
+
+        List<Object[]> result = ObjectSelect.query(Artist.class)
+                .columns(artistFull, Artist.PAINTING_ARRAY.count())
+                .prefetch(Artist.PAINTING_ARRAY.disjoint())
+                .select(context);
+
+        checkAggregatePrefetchResults(result);
+    }
+
+    @Test
+    public void testAggregateColumnWithDisjointByIdPrefetch() {
+        Property<Artist> artistFull = Property.createSelf(Artist.class);
+
+        List<Object[]> result = ObjectSelect.query(Artist.class)
+                .columns(artistFull, Artist.PAINTING_ARRAY.count())
+                .prefetch(Artist.PAINTING_ARRAY.disjointById())
+                .select(context);
+
+        checkAggregatePrefetchResults(result);
+    }
+
+    private void checkAggregatePrefetchResults(List<Object[]> result) {
+        assertEquals(5, result.size());
+        for(Object[] next : result) {
+            assertTrue(next[0] instanceof Artist);
+            assertTrue(next[1] instanceof Long);
+            Artist artist = (Artist)next[0];
+            assertEquals(PersistenceState.COMMITTED, 
artist.getPersistenceState());
+
+            Object paintingsArr = 
artist.readPropertyDirectly(Artist.PAINTING_ARRAY.getName());
+            assertFalse(paintingsArr instanceof Fault);
+            assertTrue(((List)paintingsArr).size() == (long)next[1]);
+        }
+    }
+
+    @Test
+    public void testObjectSelectWithJointPrefetch() {
+        List<Artist> result = ObjectSelect.query(Artist.class)
+                .column(Property.createSelf(Artist.class))
+                .prefetch(Artist.PAINTING_ARRAY.joint())
+                .select(context);
+        assertEquals(20, result.size());
+
+        for(Artist artist : result) {
+            assertEquals(PersistenceState.COMMITTED, 
artist.getPersistenceState());
+
+            Object paintingsArr = 
artist.readPropertyDirectly(Artist.PAINTING_ARRAY.getName());
+            assertFalse(paintingsArr instanceof Fault);
+        }
+    }
+
+    @Test
+    public void testObjectWithDisjointPrefetch() {
+        List<Artist> result = ObjectSelect.query(Artist.class)
+                .column(Property.createSelf(Artist.class))
+                .prefetch(Artist.PAINTING_ARRAY.disjoint())
+                .select(context);
+        assertEquals(20, result.size());
+        for(Artist artist : result) {
+            assertEquals(PersistenceState.COMMITTED, 
artist.getPersistenceState());
+
+            Object paintingsArr = 
artist.readPropertyDirectly(Artist.PAINTING_ARRAY.getName());
+            assertFalse(paintingsArr instanceof Fault);
+        }
+    }
+
+    @Test
+    public void testObjectWithDisjointByIdPrefetch() {
+        List<Artist> result = ObjectSelect.query(Artist.class)
+                .column(Property.createSelf(Artist.class))
+                .prefetch(Artist.PAINTING_ARRAY.disjointById())
+                .select(context);
+        assertEquals(20, result.size());
+        for(Artist artist : result) {
+            assertEquals(PersistenceState.COMMITTED, 
artist.getPersistenceState());
+
+            Object paintingsArr = 
artist.readPropertyDirectly(Artist.PAINTING_ARRAY.getName());
+            assertFalse(paintingsArr instanceof Fault);
+        }
+    }
+
+    /*
+     *  Test Persistent object select
+     */
+
+    @Test
+    public void testObjectColumn() {
+        Property<Artist> artistFull = Property.createSelf(Artist.class);
+
+        List<Object[]> result = ObjectSelect.query(Artist.class)
+                .columns(artistFull, Artist.ARTIST_NAME, 
Artist.PAINTING_ARRAY.count())
+                .select(context);
+        assertEquals(5, result.size());
+
+        for(Object[] next : result) {
+            assertTrue(next[0] instanceof Artist);
+            assertTrue(next[1] instanceof String);
+            assertTrue(next[2] instanceof Long);
+            assertEquals(PersistenceState.COMMITTED, 
((Artist)next[0]).getPersistenceState());
+        }
+    }
+
+    @Test
+    public void testObjectColumnToOne() {
+        Property<Artist> artistFull = 
Property.create(ExpressionFactory.fullObjectExp(Painting.TO_ARTIST.getExpression()),
 Artist.class);
+        Property<Gallery> galleryFull = 
Property.create(ExpressionFactory.fullObjectExp(Painting.TO_GALLERY.getExpression()),
 Gallery.class);
+
+        List<Object[]> result = ObjectSelect.query(Painting.class)
+                .columns(Painting.PAINTING_TITLE, artistFull, galleryFull)
+                .select(context);
+        assertEquals(21, result.size());
+
+        for(Object[] next : result) {
+            assertTrue(next[0] instanceof String);
+            assertTrue(next[1] instanceof Artist);
+            assertTrue(next[2] instanceof Gallery);
+            assertEquals(PersistenceState.COMMITTED, 
((Artist)next[1]).getPersistenceState());
+        }
+    }
+
+    @Test
+    public void testObjectColumnToOneAsObjPath() {
+
+        List<Object[]> result = ObjectSelect.query(Painting.class)
+                .columns(Painting.PAINTING_TITLE, Painting.TO_ARTIST, 
Painting.TO_GALLERY)
+                .select(context);
+        assertEquals(21, result.size());
+
+        for(Object[] next : result) {
+            assertTrue(next[0] instanceof String);
+            assertTrue(next[1] instanceof Artist);
+            assertTrue(next[2] instanceof Gallery);
+            assertEquals(PersistenceState.COMMITTED, 
((Artist)next[1]).getPersistenceState());
+        }
+    }
+
+    @Test
+    public void testObjectColumnToMany() throws Exception {
+        Property<Artist> artist = Property.createSelf(Artist.class);
+
+        List<Object[]> result = ObjectSelect.query(Artist.class)
+                .columns(artist, Artist.PAINTING_ARRAY.flat(Painting.class), 
Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY))
+                .select(context);
+        assertEquals(21, result.size());
+
+        for(Object[] next : result) {
+            assertTrue(next[0] instanceof Artist);
+            assertTrue(next[1] instanceof Painting);
+            assertTrue(next[2] instanceof Gallery);
+            assertEquals(PersistenceState.COMMITTED, 
((Artist)next[0]).getPersistenceState());
+            assertEquals(PersistenceState.COMMITTED, 
((Painting)(next[1])).getPersistenceState());
+            assertEquals(PersistenceState.COMMITTED, 
((Gallery)(next[2])).getPersistenceState());
+        }
+    }
+
+    @Test(expected = CayenneRuntimeException.class)
+    public void testDirectRelationshipSelect() {
+        // We should fail here as actual result will be just distinct 
paintings' ids.
+        List<List<Painting>> result = ObjectSelect.query(Artist.class)
+                .column(Artist.PAINTING_ARRAY).select(context);
+        assertEquals(21, result.size());
+    }
+
+    @Test(expected = CayenneRuntimeException.class)
+    public void testSelfPropertyInOrderBy() {
+        Property<Artist> artistProperty = Property.createSelf(Artist.class);
+        ObjectSelect.query(Artist.class)
+                .column(artistProperty)
+                .orderBy(artistProperty.desc())
+                .select(context);
+    }
+
+    @Test(expected = CayenneRuntimeException.class)
+    public void testSelfPropertyInWhere() {
+        Artist artist = ObjectSelect.query(Artist.class).selectFirst(context);
+        Property<Artist> artistProperty = Property.createSelf(Artist.class);
+        List<Artist> result = ObjectSelect.query(Artist.class)
+                .column(artistProperty)
+                .where(artistProperty.eq(artist))
+                .select(context);
+    }
+
+    @Test
+    public void testObjPropertyInWhere() {
+        Artist artist = ObjectSelect.query(Artist.class, 
Artist.ARTIST_NAME.eq("artist1"))
+                .selectFirst(context);
+        Property<Painting> paintingProperty = 
Property.createSelf(Painting.class);
+        List<Painting> result = ObjectSelect.query(Painting.class)
+                .column(paintingProperty)
+                .where(Painting.TO_ARTIST.eq(artist))
+                .select(context);
+        assertEquals(4, result.size());
+    }
+
 }

Reply via email to