You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2017/03/21 09:16:31 UTC

[2/2] cayenne git commit: CAY-2255 ObjectSelect: columns as full entities CAY-2271 ColumnSelect: support for prefetch and limit

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 <st...@gmail.com>
Authored: Tue Mar 21 11:58:35 2017 +0300
Committer: Nikita Timofeev <st...@gmail.com>
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());
+    }
+
 }