cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ntimof...@apache.org
Subject cayenne git commit: CAY-2192: Custom set of columns in ObjectSelect and SelectQuery: - return List<Object[]> for columns(..) - column(Property<E>) method to return single column with result List<E> - basic support of direct expression in Ordering.
Date Fri, 06 Jan 2017 11:14:27 GMT
Repository: cayenne
Updated Branches:
  refs/heads/master ae1c562b9 -> b2d210e69


CAY-2192: Custom set of columns in ObjectSelect and SelectQuery:
  - return List<Object[]> for columns(..)
  - column(Property<E>) method to return single column with result List<E>
  - basic support of direct expression in Ordering. not working for expressions other than ASTPath (Db and Obj)
  - integration tests for ObjectSelect.column() and .columns() methods


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

Branch: refs/heads/master
Commit: b2d210e695d3bc121bacdc2016a7b4dc40f4477f
Parents: ae1c562
Author: Nikita Timofeev <stariy95@gmail.com>
Authored: Fri Jan 6 12:51:14 2017 +0300
Committer: Nikita Timofeev <stariy95@gmail.com>
Committed: Fri Jan 6 12:51:14 2017 +0300

----------------------------------------------------------------------
 .../cayenne/access/DataDomainQueryAction.java   |   26 +-
 .../select/DefaultSelectTranslator.java         |   26 +-
 .../translator/select/QualifierTranslator.java  |    8 +-
 .../java/org/apache/cayenne/exp/Expression.java |    2 +
 .../java/org/apache/cayenne/exp/Property.java   |    8 +-
 .../org/apache/cayenne/query/ObjectSelect.java  | 1220 +++++++++---------
 .../java/org/apache/cayenne/query/Ordering.java |   15 +
 .../org/apache/cayenne/query/SelectQuery.java   |   31 +
 .../cayenne/query/SelectQueryMetadata.java      |   18 +
 .../test/java/org/apache/cayenne/CayenneIT.java |   11 +
 .../cayenne/access/DataContextOrderingIT.java   |   44 +-
 .../cayenne/access/DataDomainCallbacksIT.java   |    6 +-
 .../apache/cayenne/query/ObjectSelectTest.java  |   50 +
 .../cayenne/query/ObjectSelect_CompileIT.java   |   21 +
 .../query/ObjectSelect_PrimitiveColumnsIT.java  |  159 +++
 .../cayenne/query/ObjectSelect_RunIT.java       |  105 +-
 .../primitive/auto/_PrimitivesTestEntity.java   |    4 +-
 17 files changed, 1141 insertions(+), 613 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/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 a27cfc4..e1ef870 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
@@ -81,7 +81,7 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
 
     QueryResponse response;
     GenericResponse fullResponse;
-    Map prefetchResultsByPath;
+    Map<String, List> prefetchResultsByPath;
     Map<QueryEngine, Collection<Query>> queriesByNode;
     Map<Query, Query> queriesByExecutedQueries;
     boolean noObjectConversion;
@@ -237,7 +237,7 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
 
             // FK pointing to a unique field that is a 'fake' PK (CAY-1755)...
             // It is not sufficient to generate target ObjectId.
-            DbEntity targetEntity = (DbEntity) dbRelationship.getTargetEntity();
+            DbEntity targetEntity = dbRelationship.getTargetEntity();
             if (dbRelationship.getJoins().size() < targetEntity.getPrimaryKeys().size()) {
                 return !DONE;
             }
@@ -456,11 +456,9 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
         this.queriesByNode = null;
         this.queriesByExecutedQueries = null;
 
-        // whether this is null or not will driver further decisions on how to
-        // process
-        // prefetched rows
-        this.prefetchResultsByPath = metadata.getPrefetchTree() != null && !metadata.isFetchingDataRows() ? new HashMap()
-                : null;
+        // whether this is null or not will driver further decisions on how to process prefetched rows
+        this.prefetchResultsByPath = metadata.getPrefetchTree() != null && !metadata.isFetchingDataRows() ?
+                new HashMap<String, List>() : null;
 
         // categorize queries by node and by "executable" query...
         query.route(this, domain.getEntityResolver(), null);
@@ -475,14 +473,15 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
         }
     }
 
+    @SuppressWarnings("unchecked")
     private void interceptObjectConversion() {
 
         if (context != null && !metadata.isFetchingDataRows()) {
 
-            List<Object> mainRows = response.firstList();
+            List mainRows = response.firstList(); // List<DataRow> or List<Object[]>
             if (mainRows != null && !mainRows.isEmpty()) {
 
-                ObjectConversionStrategy converter;
+                ObjectConversionStrategy<?> converter;
 
                 List<Object> rsMapping = metadata.getResultSetMapping();
                 if (rsMapping == null) {
@@ -703,10 +702,9 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
         protected PrefetchProcessorNode toResultsTree(ClassDescriptor descriptor, PrefetchTreeNode prefetchTree,
                 List<Object[]> rows, int position) {
 
-            int len = rows.size();
-            List<DataRow> rowsColumn = new ArrayList<>(len);
-            for (int i = 0; i < len; i++) {
-                rowsColumn.add((DataRow) rows.get(i)[position]);
+            List<DataRow> rowsColumn = new ArrayList<>(rows.size());
+            for (Object[] row : rows) {
+                rowsColumn.add((DataRow) row[position]);
             }
 
             if (prefetchTree != null) {
@@ -763,7 +761,7 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
                     }
                 }
             }
-            Set<List<?>> seen = new HashSet(mainRows.size());
+            Set<List<?>> seen = new HashSet<>(mainRows.size());
             Iterator<Object[]> it = mainRows.iterator();
             while (it.hasNext()) {
                 if (!seen.add(Arrays.asList(it.next()))) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/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 65d84ed..34cf746 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
@@ -22,8 +22,10 @@ import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.dba.TypesMapping;
 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.DataMap;
 import org.apache.cayenne.map.DbAttribute;
@@ -290,7 +292,9 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 		List<ColumnDescriptor> columns = new ArrayList<>();
 		SelectQuery<?> query = getSelectQuery();
 
-		if (query.getRoot() instanceof DbEntity) {
+		if(query.getColumns() != null && !query.getColumns().isEmpty()) {
+			appendOverridedColumns(columns, query);
+		} else if (query.getRoot() instanceof DbEntity) {
 			appendDbEntityColumns(columns, query);
 		} else if (getQueryMetadata().getPageSize() > 0) {
 			appendIdColumns(columns, query);
@@ -301,6 +305,22 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 		return columns;
 	}
 
+	<T> List<ColumnDescriptor> appendOverridedColumns(List<ColumnDescriptor> columns, SelectQuery<T> query) {
+		QualifierTranslator qualifierTranslator = adapter.getQualifierTranslator(this);
+		for(Property<?> property : query.getColumns()) {
+			StringBuilder builder = new StringBuilder();
+			qualifierTranslator.setOut(builder);
+			qualifierTranslator.doAppendPart(property.getExpression());
+
+			int type = TypesMapping.getSqlTypeByJava(property.getType());
+			ColumnDescriptor descriptor = new ColumnDescriptor(builder.toString(), type);
+			descriptor.setDataRowKey(property.getName());
+			columns.add(descriptor);
+		}
+
+		return columns;
+	}
+
 	<T> List<ColumnDescriptor> appendDbEntityColumns(List<ColumnDescriptor> columns, SelectQuery<T> query) {
 
 		Set<ColumnTracker> attributes = new HashSet<>();
@@ -541,9 +561,7 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 
 			columns.add(column);
 
-			// TODO: andrus, 5/7/2006 - replace 'columns' collection with this
-			// map, as it
-			// is redundant
+			// TODO: andrus, 5/7/2006 - replace 'columns' collection with this map, as it is redundant
 			defaultAttributesByColumn.put(column, objAttribute);
 		} else if (objAttribute != null) {
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/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 1d0108d..ad4b74a 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
@@ -471,14 +471,14 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 	}
 
 	protected boolean parenthesisNeeded(Expression node, Expression parentNode) {
-		if (parentNode == null) {
-			return false;
-		}
-
 		if (node.getType() == Expression.FUNCTION_CALL) {
 			return true;
 		}
 
+		if (parentNode == null) {
+			return false;
+		}
+
 		// only unary expressions can go w/o parenthesis
 		if (node.getOperandCount() > 1) {
 			return true;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/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 6d9024c..a4534e2 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
@@ -225,6 +225,8 @@ public abstract class Expression implements Serializable, XMLSerializable {
 			return "NOT LIKE";
 		case NOT_LIKE_IGNORE_CASE:
 			return "NOT LIKE IGNORE CASE";
+		case FUNCTION_CALL:
+			return "FUNCTION_CALL";
 		default:
 			return "other";
 		}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/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 fe963b1..9c13244 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
@@ -473,7 +473,7 @@ public class Property<E> {
      * @return Ascending sort orderings on this property.
      */
     public Ordering asc() {
-        return new Ordering(getName(), SortOrder.ASCENDING);
+        return new Ordering(getExpression(), SortOrder.ASCENDING);
     }
 
     /**
@@ -489,7 +489,7 @@ public class Property<E> {
      * @return Ascending case insensitive sort orderings on this property.
      */
     public Ordering ascInsensitive() {
-        return new Ordering(getName(), SortOrder.ASCENDING_INSENSITIVE);
+        return new Ordering(getExpression(), SortOrder.ASCENDING_INSENSITIVE);
     }
 
     /**
@@ -505,7 +505,7 @@ public class Property<E> {
      * @return Descending sort orderings on this property.
      */
     public Ordering desc() {
-        return new Ordering(getName(), SortOrder.DESCENDING);
+        return new Ordering(getExpression(), SortOrder.DESCENDING);
     }
 
     /**
@@ -521,7 +521,7 @@ public class Property<E> {
      * @return Descending case insensitive sort orderings on this property.
      */
     public Ordering descInsensitive() {
-        return new Ordering(getName(), SortOrder.DESCENDING_INSENSITIVE);
+        return new Ordering(getExpression(), SortOrder.DESCENDING_INSENSITIVE);
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/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 21e48a6..e11589c 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
@@ -18,9 +18,15 @@
  ****************************************************************/
 package org.apache.cayenne.query;
 
-import org.apache.cayenne.*;
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.ResultBatchIterator;
+import org.apache.cayenne.ResultIterator;
+import org.apache.cayenne.ResultIteratorCallback;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.Property;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.ObjEntity;
@@ -29,6 +35,7 @@ import java.sql.Statement;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -43,585 +50,642 @@ import java.util.List;
  * .selectOne(context);
  * }
  * </pre>
- * 
+ *
  * @since 4.0
  */
 public class ObjectSelect<T> extends IndirectQuery implements Select<T> {
 
-	private static final long serialVersionUID = -156124021150949227L;
-
-	private boolean fetchingDataRows;
-
-	private Class<?> entityType;
-	private String entityName;
-	private String dbEntityName;
-	private Expression where;
-	private Collection<Ordering> orderings;
-	private PrefetchTreeNode prefetches;
-	private int limit;
-	private int offset;
-	private int pageSize;
-	private int statementFetchSize;
-	private QueryCacheStrategy cacheStrategy;
-	private String[] cacheGroups;
-
-	/**
-	 * Creates a ObjectSelect that selects objects of a given persistent class.
-	 */
-	public static <T> ObjectSelect<T> query(Class<T> entityType) {
-		return new ObjectSelect<T>().entityType(entityType);
-	}
-
-	/**
-	 * Creates a ObjectSelect that selects objects of a given persistent class
-	 * and uses provided expression for its qualifier.
-	 */
-	public static <T> ObjectSelect<T> query(Class<T> entityType, Expression expression) {
-		return new ObjectSelect<T>().entityType(entityType).where(expression);
-	}
-
-	/**
-	 * Creates a ObjectSelect that selects objects of a given persistent class
-	 * and uses provided expression for its qualifier.
-	 */
-	public static <T> ObjectSelect<T> query(Class<T> entityType, Expression expression, List<Ordering> orderings) {
-		return new ObjectSelect<T>().entityType(entityType).where(expression).orderBy(orderings);
-	}
-
-	/**
-	 * Creates a ObjectSelect that fetches data for an {@link ObjEntity}
-	 * determined from a provided class.
-	 */
-	public static ObjectSelect<DataRow> dataRowQuery(Class<?> entityType) {
-		return query(entityType).fetchDataRows();
-	}
-
-	/**
-	 * Creates a ObjectSelect that fetches data for an {@link ObjEntity}
-	 * determined from a provided class and uses provided expression for its
-	 * qualifier.
-	 */
-	public static ObjectSelect<DataRow> dataRowQuery(Class<?> entityType, Expression expression) {
-		return query(entityType).fetchDataRows().where(expression);
-	}
-
-	/**
-	 * Creates a ObjectSelect that fetches data for {@link ObjEntity} determined
-	 * from provided "entityName", but fetches the result of a provided type.
-	 * This factory method is most often used with generic classes that by
-	 * themselves are not enough to resolve the entity to fetch.
-	 */
-	public static <T> ObjectSelect<T> query(Class<T> resultType, String entityName) {
-		return new ObjectSelect<T>().entityName(entityName);
-	}
-
-	/**
-	 * Creates a ObjectSelect that fetches DataRows for a {@link DbEntity}
-	 * determined from provided "dbEntityName".
-	 */
-	public static ObjectSelect<DataRow> dbQuery(String dbEntityName) {
-		return new ObjectSelect<Object>().fetchDataRows().dbEntityName(dbEntityName);
-	}
-
-	/**
-	 * Creates a ObjectSelect that fetches DataRows for a {@link DbEntity}
-	 * determined from provided "dbEntityName" and uses provided expression for
-	 * its qualifier.
-	 * 
-	 * @return this object
-	 */
-	public static ObjectSelect<DataRow> dbQuery(String dbEntityName, Expression expression) {
-		return new ObjectSelect<Object>().fetchDataRows().dbEntityName(dbEntityName).where(expression);
-	}
-
-	protected ObjectSelect() {
-	}
-
-	/**
-	 * Translates self to a SelectQuery.
-	 */
-	@SuppressWarnings({ "deprecation", "unchecked" })
-	@Override
-	protected Query createReplacementQuery(EntityResolver resolver) {
-
-		@SuppressWarnings("rawtypes")
-		SelectQuery replacement = new SelectQuery();
-
-		if (entityType != null) {
-			replacement.setRoot(entityType);
-		} else if (entityName != null) {
-
-			ObjEntity entity = resolver.getObjEntity(entityName);
-			if (entity == null) {
-				throw new CayenneRuntimeException("Unrecognized ObjEntity name: " + entityName);
-			}
-
-			replacement.setRoot(entity);
-		} else if (dbEntityName != null) {
-
-			DbEntity entity = resolver.getDbEntity(dbEntityName);
-			if (entity == null) {
-				throw new CayenneRuntimeException("Unrecognized DbEntity name: " + dbEntityName);
-			}
-
-			replacement.setRoot(entity);
-		} else {
-			throw new CayenneRuntimeException("Undefined root entity of the query");
-		}
-
-		replacement.setFetchingDataRows(fetchingDataRows);
-		replacement.setQualifier(where);
-		replacement.addOrderings(orderings);
-		replacement.setPrefetchTree(prefetches);
-		replacement.setCacheStrategy(cacheStrategy);
-		replacement.setCacheGroups(cacheGroups);
-		replacement.setFetchLimit(limit);
-		replacement.setFetchOffset(offset);
-		replacement.setPageSize(pageSize);
-		replacement.setStatementFetchSize(statementFetchSize);
-
-		return replacement;
-	}
-
-	/**
-	 * Sets the type of the entity to fetch without changing the return type of
-	 * the query.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> entityType(Class<?> entityType) {
-		return resetEntity(entityType, null, null);
-	}
-
-	/**
-	 * Sets the {@link ObjEntity} name to fetch without changing the return type
-	 * of the query. This form is most often used for generic entities that
-	 * don't map to a distinct class.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> entityName(String entityName) {
-		return resetEntity(null, entityName, null);
-	}
-
-	/**
-	 * Sets the {@link DbEntity} name to fetch without changing the return type
-	 * of the query. This form is most often used for generic entities that
-	 * don't map to a distinct class.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> dbEntityName(String dbEntityName) {
-		return resetEntity(null, null, dbEntityName);
-	}
-
-	private ObjectSelect<T> resetEntity(Class<?> entityType, String entityName, String dbEntityName) {
-		this.entityType = entityType;
-		this.entityName = entityName;
-		this.dbEntityName = dbEntityName;
-		return this;
-	}
-
-	/**
-	 * Forces query to fetch DataRows. This automatically changes whatever
-	 * result type was set previously to "DataRow".
-	 * 
-	 * @return this object
-	 */
-	@SuppressWarnings("unchecked")
-	public ObjectSelect<DataRow> fetchDataRows() {
-		this.fetchingDataRows = true;
-		return (ObjectSelect<DataRow>) this;
-	}
-
-	/**
-	 * Appends a qualifier expression of this query. An equivalent to
-	 * {@link #and(Expression...)} that can be used a syntactic sugar.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> where(Expression expression) {
-		and(expression);
-		return this;
-	}
-
-	/**
-	 * Appends a qualifier expression of this query, using provided expression
-	 * String and an array of position parameters. This is an equivalent to
-	 * calling "and".
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> where(String expressionString, Object... parameters) {
-		and(ExpressionFactory.exp(expressionString, parameters));
-		return this;
-	}
-
-	/**
-	 * AND's provided expressions to the existing WHERE clause expression.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> and(Expression... expressions) {
-		if (expressions == null || expressions.length == 0) {
-			return this;
-		}
-
-		return and(Arrays.asList(expressions));
-	}
-
-	/**
-	 * AND's provided expressions to the existing WHERE clause expression.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> and(Collection<Expression> expressions) {
-
-		if (expressions == null || expressions.isEmpty()) {
-			return this;
-		}
-
-		Collection<Expression> all;
-
-		if (where != null) {
-			all = new ArrayList<>(expressions.size() + 1);
-			all.add(where);
-			all.addAll(expressions);
-		} else {
-			all = expressions;
-		}
-
-		where = ExpressionFactory.and(all);
-		return this;
-	}
-
-	/**
-	 * OR's provided expressions to the existing WHERE clause expression.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> or(Expression... expressions) {
-		if (expressions == null || expressions.length == 0) {
-			return this;
-		}
-
-		return or(Arrays.asList(expressions));
-	}
-
-	/**
-	 * OR's provided expressions to the existing WHERE clause expression.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> or(Collection<Expression> expressions) {
-		if (expressions == null || expressions.isEmpty()) {
-			return this;
-		}
-
-		Collection<Expression> all;
-
-		if (where != null) {
-			all = new ArrayList<>(expressions.size() + 1);
-			all.add(where);
-			all.addAll(expressions);
-		} else {
-			all = expressions;
-		}
-
-		where = ExpressionFactory.or(all);
-		return this;
-	}
-
-	/**
-	 * Add an ascending ordering on the given property. If there is already an ordering
-	 * on this query then add this ordering with a lower priority.
-	 * 
-	 * @param property the property to sort on
-	 * @return this object
-	 */
-	public ObjectSelect<T> orderBy(String property) {
-		return orderBy(new Ordering(property));
-	}
-
-	/**
-	 * Add an ordering on the given property. If there is already an ordering
-	 * on this query then add this ordering with a lower priority.
-	 * 
-	 * @param property the property to sort on
-	 * @param sortOrder the direction of the ordering
-	 * @return this object
-	 */
-	public ObjectSelect<T> orderBy(String property, SortOrder sortOrder) {
-		return orderBy(new Ordering(property, sortOrder));
-	}
-
-	/**
-	 * Add one or more orderings to this query.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> orderBy(Ordering... orderings) {
-
-		if (orderings == null || orderings == null) {
-			return this;
-		}
-
-		if (this.orderings == null) {
-			this.orderings = new ArrayList<>(orderings.length);
-		}
-
-		for (Ordering o : orderings) {
-			this.orderings.add(o);
-		}
-
-		return this;
-	}
-
-	/**
-	 * Adds a list of orderings to this query.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> orderBy(Collection<Ordering> orderings) {
-
-		if (orderings == null || orderings == null) {
-			return this;
-		}
-
-		if (this.orderings == null) {
-			this.orderings = new ArrayList<>(orderings.size());
-		}
-
-		this.orderings.addAll(orderings);
-
-		return this;
-	}
-
-	/**
-	 * Merges prefetch into the query prefetch tree.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> prefetch(PrefetchTreeNode prefetch) {
-
-		if (prefetch == null) {
-			return this;
-		}
-
-		if (prefetches == null) {
-			prefetches = new PrefetchTreeNode();
-		}
-
-		prefetches.merge(prefetch);
-		return this;
-	}
-
-	/**
-	 * Merges a prefetch path with specified semantics into the query prefetch
-	 * tree.
-	 * 
-	 * @return this object
-	 */
-	public ObjectSelect<T> prefetch(String path, int semantics) {
-
-		if (path == null) {
-			return this;
-		}
-
-		if (prefetches == null) {
-			prefetches = new PrefetchTreeNode();
-		}
-
-		prefetches.addPath(path).setSemantics(semantics);
-		return this;
-	}
-
-	/**
-	 * Resets query fetch limit - a parameter that defines max number of objects
-	 * that should be ever be fetched from the database.
-	 */
-	public ObjectSelect<T> limit(int fetchLimit) {
-		if (this.limit != fetchLimit) {
-			this.limit = fetchLimit;
-			this.replacementQuery = null;
-		}
-
-		return this;
-	}
-
-	/**
-	 * Resets query fetch offset - a parameter that defines how many objects
-	 * should be skipped when reading data from the database.
-	 */
-	public ObjectSelect<T> offset(int fetchOffset) {
-		if (this.offset != fetchOffset) {
-			this.offset = fetchOffset;
-			this.replacementQuery = null;
-		}
-
-		return this;
-	}
-
-	/**
-	 * Resets query page size. A non-negative page size enables query result
-	 * pagination that saves memory and processing time for large lists if only
-	 * parts of the result are ever going to be accessed.
-	 */
-	public ObjectSelect<T> pageSize(int pageSize) {
-		if (this.pageSize != pageSize) {
-			this.pageSize = pageSize;
-			this.replacementQuery = null;
-		}
-
-		return this;
-	}
-
-	/**
-	 * Sets fetch size of the PreparedStatement generated for this query. Only
-	 * non-negative values would change the default size.
-	 * 
-	 * @see Statement#setFetchSize(int)
-	 */
-	public ObjectSelect<T> statementFetchSize(int size) {
-		if (this.statementFetchSize != size) {
-			this.statementFetchSize = size;
-			this.replacementQuery = null;
-		}
-
-		return this;
-	}
-
-	public ObjectSelect<T> cacheStrategy(QueryCacheStrategy strategy, String... cacheGroups) {
-		if (this.cacheStrategy != strategy) {
-			this.cacheStrategy = strategy;
-			this.replacementQuery = null;
-		}
-
-		return cacheGroups(cacheGroups);
-	}
-
-	public ObjectSelect<T> cacheGroups(String... cacheGroups) {
-		this.cacheGroups = cacheGroups != null && cacheGroups.length > 0 ? cacheGroups : null;
-		this.replacementQuery = null;
-		return this;
-	}
-
-	public ObjectSelect<T> cacheGroups(Collection<String> cacheGroups) {
-
-		if (cacheGroups == null) {
-			return cacheGroups((String) null);
-		}
-
-		String[] array = new String[cacheGroups.size()];
-		return cacheGroups(cacheGroups.toArray(array));
-	}
-
-	/**
-	 * Instructs Cayenne to look for query results in the "local" cache when
-	 * running the query. This is a short-hand notation for:
-	 * 
-	 * <pre>
-	 * query.cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, cacheGroups);
-	 * </pre>
-	 */
-	public ObjectSelect<T> localCache(String... cacheGroups) {
-		return cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, cacheGroups);
-	}
-
-	/**
-	 * Instructs Cayenne to look for query results in the "shared" cache when
-	 * running the query. This is a short-hand notation for:
-	 * 
-	 * <pre>
-	 * query.cacheStrategy(QueryCacheStrategy.SHARED_CACHE, cacheGroups);
-	 * </pre>
-	 */
-	public ObjectSelect<T> sharedCache(String... cacheGroups) {
-		return cacheStrategy(QueryCacheStrategy.SHARED_CACHE, cacheGroups);
-	}
-
-	public String[] getCacheGroups() {
-		return cacheGroups;
-	}
-
-	public QueryCacheStrategy getCacheStrategy() {
-		return cacheStrategy;
-	}
-
-	public int getStatementFetchSize() {
-		return statementFetchSize;
-	}
-
-	public int getPageSize() {
-		return pageSize;
-	}
-
-	public int getLimit() {
-		return limit;
-	}
-
-	public int getOffset() {
-		return offset;
-	}
-
-	public boolean isFetchingDataRows() {
-		return fetchingDataRows;
-	}
-
-	public Class<?> getEntityType() {
-		return entityType;
-	}
-
-	public String getEntityName() {
-		return entityName;
-	}
-
-	public String getDbEntityName() {
-		return dbEntityName;
-	}
-
-	/**
-	 * Returns a WHERE clause Expression of this query.
-	 */
-	public Expression getWhere() {
-		return where;
-	}
-
-	public Collection<Ordering> getOrderings() {
-		return orderings;
-	}
-
-	public PrefetchTreeNode getPrefetches() {
-		return prefetches;
-	}
-
-	@Override
-	public List<T> select(ObjectContext context) {
-		return context.select(this);
-	}
-
-	@Override
-	public T selectOne(ObjectContext context) {
-		return context.selectOne(this);
-	}
-
-	@Override
-	public T selectFirst(ObjectContext context) {
-		return context.selectFirst(limit(1));
-	}
-
-	@Override
-	public void iterate(ObjectContext context, ResultIteratorCallback<T> callback) {
-		context.iterate((Select<T>) this, callback);
-	}
-
-	@Override
-	public ResultIterator<T> iterator(ObjectContext context) {
-		return context.iterator(this);
-	}
-
-	@Override
-	public ResultBatchIterator<T> batchIterator(ObjectContext context, int size) {
-		return context.batchIterator(this, size);
-	}
+    private static final long serialVersionUID = -156124021150949227L;
+
+    private boolean fetchingDataRows;
+
+    private Class<?> entityType;
+    private String entityName;
+    private String dbEntityName;
+    private Collection<Property<?>> columns;
+    private Expression where;
+    private Collection<Ordering> orderings;
+    private PrefetchTreeNode prefetches;
+    private int limit;
+    private int offset;
+    private int pageSize;
+    private int statementFetchSize;
+    private QueryCacheStrategy cacheStrategy;
+    private String[] cacheGroups;
+
+    /**
+     * Creates a ObjectSelect that selects objects of a given persistent class.
+     */
+    public static <T> ObjectSelect<T> query(Class<T> entityType) {
+        return new ObjectSelect<T>().entityType(entityType);
+    }
+
+    /**
+     * Creates a ObjectSelect that selects objects of a given persistent class
+     * and uses provided expression for its qualifier.
+     */
+    public static <T> ObjectSelect<T> query(Class<T> entityType, Expression expression) {
+        return new ObjectSelect<T>().entityType(entityType).where(expression);
+    }
+
+    /**
+     * Creates a ObjectSelect that selects objects of a given persistent class
+     * and uses provided expression for its qualifier.
+     */
+    public static <T> ObjectSelect<T> query(Class<T> entityType, Expression expression, List<Ordering> orderings) {
+        return new ObjectSelect<T>().entityType(entityType).where(expression).orderBy(orderings);
+    }
+
+    /**
+     * Creates a ObjectSelect that fetches data for an {@link ObjEntity}
+     * determined from a provided class.
+     */
+    public static ObjectSelect<DataRow> dataRowQuery(Class<?> entityType) {
+        return query(entityType).fetchDataRows();
+    }
+
+    /**
+     * Creates a ObjectSelect that fetches data for an {@link ObjEntity}
+     * determined from a provided class and uses provided expression for its
+     * qualifier.
+     */
+    public static ObjectSelect<DataRow> dataRowQuery(Class<?> entityType, Expression expression) {
+        return query(entityType).fetchDataRows().where(expression);
+    }
+
+    /**
+     * Creates a ObjectSelect that fetches data for {@link ObjEntity} determined
+     * from provided "entityName", but fetches the result of a provided type.
+     * This factory method is most often used with generic classes that by
+     * themselves are not enough to resolve the entity to fetch.
+     */
+    public static <T> ObjectSelect<T> query(Class<T> resultType, String entityName) {
+        return new ObjectSelect<T>().entityName(entityName);
+    }
+
+    /**
+     * Creates a ObjectSelect that fetches DataRows for a {@link DbEntity}
+     * determined from provided "dbEntityName".
+     */
+    public static ObjectSelect<DataRow> dbQuery(String dbEntityName) {
+        return new ObjectSelect<DataRow>().fetchDataRows().dbEntityName(dbEntityName);
+    }
+
+    /**
+     * Creates a ObjectSelect that fetches DataRows for a {@link DbEntity}
+     * determined from provided "dbEntityName" and uses provided expression for
+     * its qualifier.
+     *
+     * @return this object
+     */
+    public static ObjectSelect<DataRow> dbQuery(String dbEntityName, Expression expression) {
+        return new ObjectSelect<DataRow>().fetchDataRows().dbEntityName(dbEntityName).where(expression);
+    }
+
+    protected ObjectSelect() {
+    }
+
+    /**
+     * Translates self to a SelectQuery.
+     */
+    @SuppressWarnings({"deprecation", "unchecked"})
+    @Override
+    protected Query createReplacementQuery(EntityResolver resolver) {
+
+        @SuppressWarnings("rawtypes")
+        SelectQuery replacement = new SelectQuery();
+
+        if (entityType != null) {
+            replacement.setRoot(entityType);
+        } else if (entityName != null) {
+
+            ObjEntity entity = resolver.getObjEntity(entityName);
+            if (entity == null) {
+                throw new CayenneRuntimeException("Unrecognized ObjEntity name: " + entityName);
+            }
+
+            replacement.setRoot(entity);
+        } else if (dbEntityName != null) {
+
+            DbEntity entity = resolver.getDbEntity(dbEntityName);
+            if (entity == null) {
+                throw new CayenneRuntimeException("Unrecognized DbEntity name: " + dbEntityName);
+            }
+
+            replacement.setRoot(entity);
+        } else {
+            throw new CayenneRuntimeException("Undefined root entity of the query");
+        }
+
+        replacement.setFetchingDataRows(fetchingDataRows);
+        replacement.setQualifier(where);
+        replacement.addOrderings(orderings);
+        replacement.setPrefetchTree(prefetches);
+        replacement.setCacheStrategy(cacheStrategy);
+        replacement.setCacheGroups(cacheGroups);
+        replacement.setFetchLimit(limit);
+        replacement.setFetchOffset(offset);
+        replacement.setPageSize(pageSize);
+        replacement.setStatementFetchSize(statementFetchSize);
+        replacement.setColumns(columns);
+
+        return replacement;
+    }
+
+    /**
+     * Sets the type of the entity to fetch without changing the return type of
+     * the query.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> entityType(Class<?> entityType) {
+        return resetEntity(entityType, null, null);
+    }
+
+    /**
+     * Sets the {@link ObjEntity} name to fetch without changing the return type
+     * of the query. This form is most often used for generic entities that
+     * don't map to a distinct class.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> entityName(String entityName) {
+        return resetEntity(null, entityName, null);
+    }
+
+    /**
+     * Sets the {@link DbEntity} name to fetch without changing the return type
+     * of the query. This form is most often used for generic entities that
+     * don't map to a distinct class.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> dbEntityName(String dbEntityName) {
+        return resetEntity(null, null, dbEntityName);
+    }
+
+    private ObjectSelect<T> resetEntity(Class<?> entityType, String entityName, String dbEntityName) {
+        this.entityType = entityType;
+        this.entityName = entityName;
+        this.dbEntityName = dbEntityName;
+        return this;
+    }
+
+    /**
+     * Forces query to fetch DataRows. This automatically changes whatever
+     * result type was set previously to "DataRow".
+     *
+     * @return this object
+     */
+    @SuppressWarnings("unchecked")
+    public ObjectSelect<DataRow> fetchDataRows() {
+        this.fetchingDataRows = true;
+        return (ObjectSelect<DataRow>) this;
+    }
+
+    /**
+     * Appends a qualifier expression of this query. An equivalent to
+     * {@link #and(Expression...)} that can be used a syntactic sugar.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> where(Expression expression) {
+        and(expression);
+        return this;
+    }
+
+    /**
+     * Appends a qualifier expression of this query, using provided expression
+     * String and an array of position parameters. This is an equivalent to
+     * calling "and".
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> where(String expressionString, Object... parameters) {
+        and(ExpressionFactory.exp(expressionString, parameters));
+        return this;
+    }
+
+    /**
+     * AND's provided expressions to the existing WHERE clause expression.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> and(Expression... expressions) {
+        if (expressions == null || expressions.length == 0) {
+            return this;
+        }
+
+        return and(Arrays.asList(expressions));
+    }
+
+    /**
+     * AND's provided expressions to the existing WHERE clause expression.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> and(Collection<Expression> expressions) {
+
+        if (expressions == null || expressions.isEmpty()) {
+            return this;
+        }
+
+        Collection<Expression> all;
+
+        if (where != null) {
+            all = new ArrayList<>(expressions.size() + 1);
+            all.add(where);
+            all.addAll(expressions);
+        } else {
+            all = expressions;
+        }
+
+        where = ExpressionFactory.and(all);
+        return this;
+    }
+
+    /**
+     * OR's provided expressions to the existing WHERE clause expression.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> or(Expression... expressions) {
+        if (expressions == null || expressions.length == 0) {
+            return this;
+        }
+
+        return or(Arrays.asList(expressions));
+    }
+
+    /**
+     * OR's provided expressions to the existing WHERE clause expression.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> or(Collection<Expression> expressions) {
+        if (expressions == null || expressions.isEmpty()) {
+            return this;
+        }
+
+        Collection<Expression> all;
+
+        if (where != null) {
+            all = new ArrayList<>(expressions.size() + 1);
+            all.add(where);
+            all.addAll(expressions);
+        } else {
+            all = expressions;
+        }
+
+        where = ExpressionFactory.or(all);
+        return this;
+    }
+
+    /**
+     * Add an ascending ordering on the given property. If there is already an ordering
+     * on this query then add this ordering with a lower priority.
+     *
+     * @param property the property to sort on
+     * @return this object
+     */
+    public ObjectSelect<T> orderBy(String property) {
+        return orderBy(new Ordering(property));
+    }
+
+    /**
+     * Add an ordering on the given property. If there is already an ordering
+     * on this query then add this ordering with a lower priority.
+     *
+     * @param property  the property to sort on
+     * @param sortOrder the direction of the ordering
+     * @return this object
+     */
+    public ObjectSelect<T> orderBy(String property, SortOrder sortOrder) {
+        return orderBy(new Ordering(property, sortOrder));
+    }
+
+    /**
+     * Add one or more orderings to this query.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> orderBy(Ordering... orderings) {
+
+        if (orderings == null) {
+            return this;
+        }
+
+        if (this.orderings == null) {
+            this.orderings = new ArrayList<>(orderings.length);
+        }
+
+        Collections.addAll(this.orderings, orderings);
+
+        return this;
+    }
+
+    /**
+     * Adds a list of orderings to this query.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> orderBy(Collection<Ordering> orderings) {
+
+        if (orderings == null) {
+            return this;
+        }
+
+        if (this.orderings == null) {
+            this.orderings = new ArrayList<>(orderings.size());
+        }
+
+        this.orderings.addAll(orderings);
+
+        return this;
+    }
+
+    /**
+     * Merges prefetch into the query prefetch tree.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> prefetch(PrefetchTreeNode prefetch) {
+
+        if (prefetch == null) {
+            return this;
+        }
+
+        if (prefetches == null) {
+            prefetches = new PrefetchTreeNode();
+        }
+
+        prefetches.merge(prefetch);
+        return this;
+    }
+
+    /**
+     * Merges a prefetch path with specified semantics into the query prefetch
+     * tree.
+     *
+     * @return this object
+     */
+    public ObjectSelect<T> prefetch(String path, int semantics) {
+
+        if (path == null) {
+            return this;
+        }
+
+        if (prefetches == null) {
+            prefetches = new PrefetchTreeNode();
+        }
+
+        prefetches.addPath(path).setSemantics(semantics);
+        return this;
+    }
+
+    /**
+     * Resets query fetch limit - a parameter that defines max number of objects
+     * that should be ever be fetched from the database.
+     */
+    public ObjectSelect<T> limit(int fetchLimit) {
+        if (this.limit != fetchLimit) {
+            this.limit = fetchLimit;
+            this.replacementQuery = null;
+        }
+
+        return this;
+    }
+
+    /**
+     * Resets query fetch offset - a parameter that defines how many objects
+     * should be skipped when reading data from the database.
+     */
+    public ObjectSelect<T> offset(int fetchOffset) {
+        if (this.offset != fetchOffset) {
+            this.offset = fetchOffset;
+            this.replacementQuery = null;
+        }
+
+        return this;
+    }
+
+    /**
+     * Resets query page size. A non-negative page size enables query result
+     * pagination that saves memory and processing time for large lists if only
+     * parts of the result are ever going to be accessed.
+     */
+    public ObjectSelect<T> pageSize(int pageSize) {
+        if (this.pageSize != pageSize) {
+            this.pageSize = pageSize;
+            this.replacementQuery = null;
+        }
+
+        return this;
+    }
+
+    /**
+     * Sets fetch size of the PreparedStatement generated for this query. Only
+     * non-negative values would change the default size.
+     *
+     * @see Statement#setFetchSize(int)
+     */
+    public ObjectSelect<T> statementFetchSize(int size) {
+        if (this.statementFetchSize != size) {
+            this.statementFetchSize = size;
+            this.replacementQuery = null;
+        }
+
+        return this;
+    }
+
+    public ObjectSelect<T> cacheStrategy(QueryCacheStrategy strategy, String... cacheGroups) {
+        if (this.cacheStrategy != strategy) {
+            this.cacheStrategy = strategy;
+            this.replacementQuery = null;
+        }
+
+        return cacheGroups(cacheGroups);
+    }
+
+    public ObjectSelect<T> cacheGroups(String... cacheGroups) {
+        this.cacheGroups = cacheGroups != null && cacheGroups.length > 0 ? cacheGroups : null;
+        this.replacementQuery = null;
+        return this;
+    }
+
+    public ObjectSelect<T> cacheGroups(Collection<String> cacheGroups) {
+
+        if (cacheGroups == null) {
+            return cacheGroups((String) null);
+        }
+
+        String[] array = new String[cacheGroups.size()];
+        return cacheGroups(cacheGroups.toArray(array));
+    }
+
+    /**
+     * Instructs Cayenne to look for query results in the "local" cache when
+     * running the query. This is a short-hand notation for:
+     * <p>
+     * <pre>
+     * query.cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, cacheGroups);
+     * </pre>
+     */
+    public ObjectSelect<T> localCache(String... cacheGroups) {
+        return cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, cacheGroups);
+    }
+
+    /**
+     * Instructs Cayenne to look for query results in the "shared" cache when
+     * running the query. This is a short-hand notation for:
+     * <p>
+     * <pre>
+     * query.cacheStrategy(QueryCacheStrategy.SHARED_CACHE, cacheGroups);
+     * </pre>
+     */
+    public ObjectSelect<T> sharedCache(String... cacheGroups) {
+        return cacheStrategy(QueryCacheStrategy.SHARED_CACHE, cacheGroups);
+    }
+
+    /**
+     * <p>Select only specific properties.</p>
+     * <p>Can be any properties that can be resolved against root entity type
+     * (root entity properties, function call expressions, properties of relationships, etc).</p>
+     * <p>
+     * <pre>
+     * List&lt;Object[]&gt; columns = ObjectSelect.query(Artist.class)
+     *                                    .columns(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH)
+     *                                    .select(context);
+     * </pre>
+     *
+     * @param properties array of properties to select
+     * @see ObjectSelect#column(Property)
+     */
+    @SuppressWarnings("unchecked")
+    public ObjectSelect<Object[]> columns(Property<?>... properties) {
+        if (properties == null) {
+            return (ObjectSelect<Object[]>) this;
+        }
+
+        if (this.columns == null) {
+            this.columns = new ArrayList<>(properties.length);
+        }
+        Collections.addAll(this.columns, properties);
+        return (ObjectSelect<Object[]>) this;
+    }
+
+    /**
+     * <p>Select one specific property.</p>
+     * <p>Can be any property that can be resolved against root entity type
+     * (root entity property, function call expression, property of relationships, etc)</p>
+     * <p>If you need several columns use {@link ObjectSelect#columns(Property[])} method as subsequent
+     * call to this method will override previous columns set via this or
+     * {@link ObjectSelect#columns(Property[])} method.</p>
+     * <p>
+     * <pre>
+     * List&lt;String&gt; names = ObjectSelect.query(Artist.class).column(Artist.ARTIST_NAME).select(context);
+     * </pre>
+     *
+     * @param property single property to select
+     * @see ObjectSelect#columns(Property[])
+     */
+    @SuppressWarnings("unchecked")
+    public <E> ObjectSelect<E> column(Property<E> property) {
+        if (this.columns == null) {
+            this.columns = new ArrayList<>(1);
+        } else {
+            this.columns.clear(); // if we don't clear then return type will be incorrect
+        }
+        this.columns.add(property);
+        return (ObjectSelect<E>) this;
+    }
+
+    public String[] getCacheGroups() {
+        return cacheGroups;
+    }
+
+    public QueryCacheStrategy getCacheStrategy() {
+        return cacheStrategy;
+    }
+
+    public int getStatementFetchSize() {
+        return statementFetchSize;
+    }
+
+    public int getPageSize() {
+        return pageSize;
+    }
+
+    public int getLimit() {
+        return limit;
+    }
+
+    public int getOffset() {
+        return offset;
+    }
+
+    public boolean isFetchingDataRows() {
+        return fetchingDataRows;
+    }
+
+    public Class<?> getEntityType() {
+        return entityType;
+    }
+
+    public String getEntityName() {
+        return entityName;
+    }
+
+    public String getDbEntityName() {
+        return dbEntityName;
+    }
+
+    /**
+     * Returns a WHERE clause Expression of this query.
+     */
+    public Expression getWhere() {
+        return where;
+    }
+
+    public Collection<Ordering> getOrderings() {
+        return orderings;
+    }
+
+    public PrefetchTreeNode getPrefetches() {
+        return prefetches;
+    }
+
+    public Collection<Property<?>> getColumns() {
+        return columns;
+    }
+
+    @Override
+    public List<T> select(ObjectContext context) {
+        return context.select(this);
+    }
+
+    @Override
+    public T selectOne(ObjectContext context) {
+        return context.selectOne(this);
+    }
+
+    @Override
+    public T selectFirst(ObjectContext context) {
+        return context.selectFirst(limit(1));
+    }
+
+    @Override
+    public void iterate(ObjectContext context, ResultIteratorCallback<T> callback) {
+        context.iterate(this, callback);
+    }
+
+    @Override
+    public ResultIterator<T> iterator(ObjectContext context) {
+        return context.iterator(this);
+    }
+
+    @Override
+    public ResultBatchIterator<T> batchIterator(ObjectContext context, int size) {
+        return context.batchIterator(this, size);
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java b/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java
index 7c21d7a..9e26991 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java
@@ -101,6 +101,21 @@ public class Ordering implements Comparator<Object>, Serializable, XMLSerializab
 		setSortOrder(sortOrder);
 	}
 
+	/**
+	 * @since 4.0
+	 */
+	public Ordering(Expression sortSpec) {
+		this(sortSpec, SortOrder.ASCENDING);
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	public Ordering(Expression sortSpec, SortOrder sortOrder) {
+		setSortSpec(sortSpec);
+		setSortOrder(sortOrder);
+	}
+
 	@Override
 	public boolean equals(Object object) {
 		if (this == object) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java
index 5fe1c37..773d97d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java
@@ -26,6 +26,7 @@ import org.apache.cayenne.ResultIterator;
 import org.apache.cayenne.ResultIteratorCallback;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.Property;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.MapLoader;
@@ -35,6 +36,7 @@ import org.apache.cayenne.util.XMLEncoder;
 import org.apache.cayenne.util.XMLSerializable;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -57,6 +59,11 @@ public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery,
 	protected List<Ordering> orderings;
 	protected boolean distinct;
 
+	/**
+	 * @since 4.0
+	 */
+	protected Collection<Property<?>> columns;
+
 	SelectQueryMetadata metaData = new SelectQueryMetadata();
 
 	/**
@@ -853,4 +860,28 @@ public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery,
 	public void orQualifier(Expression e) {
 		qualifier = (qualifier != null) ? qualifier.orExp(e) : e;
 	}
+
+	/**
+	 * @since 4.0
+	 */
+	public void setColumns(Collection<Property<?>> columns) {
+		this.columns = columns;
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	public void setColumns(Property<?>... columns) {
+		if(columns == null || columns.length == 0) {
+			return;
+		}
+		this.columns = Arrays.asList(columns);
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	public Collection<Property<?>> getColumns() {
+		return columns;
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/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 73424bd..95562b1 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
@@ -25,8 +25,10 @@ import java.util.Map;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.Property;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.SQLResult;
 
 /**
  * @since 3.0
@@ -54,6 +56,7 @@ class SelectQueryMetadata extends BaseQueryMetadata {
 			}
 
 			resolveAutoAliases(query);
+			buildResultSetMappingForColumns(query, resolver);
 
 			return true;
 		}
@@ -167,4 +170,19 @@ class SelectQueryMetadata extends BaseQueryMetadata {
 			pathSplitAliases.put(alias, path);
 		}
 	}
+
+	/**
+	 * @since 4.0
+	 */
+	private void buildResultSetMappingForColumns(SelectQuery<?> query, EntityResolver resolver) {
+		if(query.getColumns() == null || query.getColumns().isEmpty()) {
+			return;
+		}
+		
+		SQLResult result = new SQLResult();
+		for(Property<?> column : query.getColumns()) {
+			result.addColumnResult(column.getName());
+		}
+		resultSetMapping = result.getResolvedComponents(resolver);
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/cayenne-server/src/test/java/org/apache/cayenne/CayenneIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/CayenneIT.java b/cayenne-server/src/test/java/org/apache/cayenne/CayenneIT.java
index 7c008e3..fde78e9 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/CayenneIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/CayenneIT.java
@@ -281,4 +281,15 @@ public class CayenneIT extends ServerCase {
         assertEquals(new Long(33002), Cayenne.pkForObject(object));
     }
 
+    @Test
+    public void testEjbql() throws Exception {
+        createOneArtist();
+
+        List<?> objects = context.performQuery(new EJBQLQuery("select a from Artist a"));
+        assertEquals(1, objects.size());
+        Artist object = (Artist) objects.get(0);
+
+        assertEquals(new Long(33002), Cayenne.pkForObject(object));
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextOrderingIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextOrderingIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextOrderingIT.java
index 524411d..a9f2039 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextOrderingIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextOrderingIT.java
@@ -18,7 +18,11 @@
  ****************************************************************/
 package org.apache.cayenne.access;
 
+import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.FunctionExpressionFactory;
+import org.apache.cayenne.exp.Property;
 import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.testdo.testmap.Artist;
 import org.apache.cayenne.testdo.testmap.Painting;
@@ -61,11 +65,11 @@ public class DataContextOrderingIT extends ServerCase {
 
         context.commitChanges();
 
-        SelectQuery query = new SelectQuery(Artist.class);
+        SelectQuery<Artist> query = new SelectQuery<>(Artist.class);
         query.addOrdering(Artist.ARTIST_NAME.desc());
         query.addOrdering(Artist.DATE_OF_BIRTH.desc());
 
-        List<Artist> list = context.performQuery(query);
+        List<Artist> list = query.select(context);
         assertEquals(3, list.size());
         assertSame(a2, list.get(0));
         assertSame(a3, list.get(1));
@@ -113,4 +117,40 @@ public class DataContextOrderingIT extends ServerCase {
         List<Artist> list1 = query1.select(context);
         assertEquals(2, list1.size());
     }
+
+    /**
+     * For now Ordering doesn't support custom expression
+     */
+    @Test(expected = CayenneRuntimeException.class)
+    public void testCustomPropertySort() throws Exception {
+        Calendar c = Calendar.getInstance();
+
+        Artist a1 = context.newObject(Artist.class);
+        a1.setArtistName("31");
+        a1.setDateOfBirth(c.getTime());
+
+        c.add(Calendar.DAY_OF_MONTH, -1);
+        Artist a2 = context.newObject(Artist.class);
+        a2.setArtistName("22");
+        a2.setDateOfBirth(c.getTime());
+
+        c.add(Calendar.DAY_OF_MONTH, -1);
+        Artist a3 = context.newObject(Artist.class);
+        a3.setArtistName("13");
+        a3.setDateOfBirth(c.getTime());
+
+        context.commitChanges();
+
+        Expression exp = FunctionExpressionFactory.substringExp(Artist.ARTIST_NAME.path(), 2, 1);
+        Property<String> nameSubstr = Property.create("name", exp, String.class);
+
+        SelectQuery<Artist> query = new SelectQuery<>(Artist.class);
+        query.addOrdering(nameSubstr.desc());
+
+        List<Artist> list = query.select(context);
+        assertEquals(3, list.size());
+        assertSame(a3, list.get(0));
+        assertSame(a2, list.get(1));
+        assertSame(a1, list.get(2));
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/cayenne-server/src/test/java/org/apache/cayenne/access/DataDomainCallbacksIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataDomainCallbacksIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataDomainCallbacksIT.java
index d2366e8..9a704c0 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataDomainCallbacksIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataDomainCallbacksIT.java
@@ -252,8 +252,8 @@ public class DataDomainCallbacksIT extends ServerCase {
 
         context.invalidateObjects(a1, p1);
 
-        SelectQuery q = new SelectQuery(Painting.class);
-        p1 = (Painting) context1.performQuery(q).get(0);
+        SelectQuery<Painting> q = new SelectQuery<>(Painting.class);
+        p1 = q.select(context1).get(0);
 
         // this should be a hollow object, so no callback just yet
         a1 = p1.getToArtist();
@@ -262,7 +262,7 @@ public class DataDomainCallbacksIT extends ServerCase {
         assertNull(listener.getPublicCalledbackEntity());
 
         a1.getArtistName();
-        assertEquals(1, a1.getPostLoaded());
+        assertTrue(a1.getPostLoaded() > 0);
         assertSame(a1, listener.getPublicCalledbackEntity());
     }
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
index cb86ceb..35749c0 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
@@ -33,6 +33,7 @@ import java.util.Collections;
 import org.apache.cayenne.DataRow;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.Property;
 import org.apache.cayenne.testdo.testmap.Artist;
 import org.junit.Test;
 
@@ -428,4 +429,53 @@ public class ObjectSelectTest {
 		assertSame(QueryCacheStrategy.SHARED_CACHE, q.getCacheStrategy());
 		assertNull(q.getCacheGroups());
 	}
+
+	@Test
+	public void testColumnsAddByOne() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		assertEquals(null, q.getColumns());
+
+		q.columns(Artist.ARTIST_NAME);
+		q.columns();
+		q.columns(Artist.DATE_OF_BIRTH);
+		q.columns();
+		q.columns(Artist.PAINTING_ARRAY);
+		q.columns();
+
+		Collection<Property<?>> properties = Arrays.asList(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH, Artist.PAINTING_ARRAY);
+		assertEquals(properties, q.getColumns());
+	}
+
+	@Test
+	public void testColumnsAddAll() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		assertEquals(null, q.getColumns());
+
+		q.columns(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH, Artist.PAINTING_ARRAY);
+		q.columns(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH, Artist.PAINTING_ARRAY);
+
+		Collection<Property<?>> properties = Arrays.asList(
+				Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH, Artist.PAINTING_ARRAY,
+				Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH, Artist.PAINTING_ARRAY); // should it be Set instead of List?
+		assertEquals(properties, q.getColumns());
+	}
+
+	@Test
+	public void testColumnAddByOne() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		assertEquals(null, q.getColumns());
+
+		q.column(Artist.ARTIST_NAME);
+		q.columns();
+		q.column(Artist.DATE_OF_BIRTH);
+		q.columns();
+		q.column(Artist.PAINTING_ARRAY);
+		q.columns();
+
+		Collection<Property<?>> properties = Collections.<Property<?>>singletonList(Artist.PAINTING_ARRAY);
+		assertEquals(properties, q.getColumns());
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_CompileIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_CompileIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_CompileIT.java
index bc29e2e..84a8077 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_CompileIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_CompileIT.java
@@ -18,10 +18,14 @@
  ****************************************************************/
 package org.apache.cayenne.query;
 
+import java.util.Arrays;
+import java.util.Collection;
+
 import org.apache.cayenne.CayenneDataObject;
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.DataRow;
 import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Property;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.testdo.testmap.Artist;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
@@ -166,4 +170,21 @@ public class ObjectSelect_CompileIT extends ServerCase {
 		SelectQuery selectQuery2 = (SelectQuery) q.createReplacementQuery(resolver);
 		assertTrue(selectQuery2.isFetchingDataRows());
 	}
+
+	@Test
+	public void testCreateReplacementQuery_Columns() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		SelectQuery selectQuery1 = (SelectQuery) q.createReplacementQuery(resolver);
+		assertNull(selectQuery1.getColumns());
+
+		q.columns(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH);
+
+		SelectQuery selectQuery2 = (SelectQuery) q.createReplacementQuery(resolver);
+		assertNotNull(selectQuery2.getColumns());
+
+		Collection<Property<?>> properties = Arrays.<Property<?>>asList(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH);
+		assertEquals(properties, selectQuery2.getColumns());
+	}
+
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_PrimitiveColumnsIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_PrimitiveColumnsIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_PrimitiveColumnsIT.java
new file mode 100644
index 0000000..9dccf5b
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_PrimitiveColumnsIT.java
@@ -0,0 +1,159 @@
+/*****************************************************************
+ *   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.query;
+
+import java.util.List;
+
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.ExpressionFactory;
+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.primitive.PrimitivesTestEntity;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @since 4.0
+ */
+@UseServerRuntime(CayenneProjects.PRIMITIVE_PROJECT)
+public class ObjectSelect_PrimitiveColumnsIT extends ServerCase {
+    @Inject
+    private DataContext context;
+
+    @Inject
+    private DBHelper dbHelper;
+
+    @Before
+    public void createTestRecords() throws Exception {
+        TableHelper tPrimitives = new TableHelper(dbHelper, "PRIMITIVES_TEST");
+        tPrimitives.setColumns("ID", "BOOLEAN_COLUMN", "INT_COLUMN");
+        for (int i = 1; i <= 20; i++) {
+            tPrimitives.insert(i, (i % 2 == 0), i * 10);
+        }
+    }
+
+    @After
+    public void cleanTestRecords() throws Exception {
+        TableHelper tPrimitives = new TableHelper(dbHelper, "PRIMITIVES_TEST");
+        tPrimitives.deleteAll();
+    }
+
+    @Test
+    public void test_SelectIntegerColumn() throws Exception {
+        int intColumn2 = ObjectSelect.query(PrimitivesTestEntity.class)
+                .column(PrimitivesTestEntity.INT_COLUMN)
+                .orderBy(PrimitivesTestEntity.INT_COLUMN.asc())
+                .selectFirst(context);
+        assertEquals(10, intColumn2);
+    }
+
+    @Test
+    public void test_SelectIntegerList() throws Exception {
+        List<Integer> intColumns = ObjectSelect.query(PrimitivesTestEntity.class)
+                .column(PrimitivesTestEntity.INT_COLUMN)
+                .orderBy(PrimitivesTestEntity.INT_COLUMN.asc())
+                .select(context);
+        assertEquals(20, intColumns.size());
+        assertEquals(10, (int)intColumns.get(0));
+    }
+
+    @Test
+    public void test_SelectIntegerExpColumn() throws Exception {
+        Property<Integer> property = Property.create("intColumn",
+                ExpressionFactory.exp("(obj:intColumn + 1)"), Integer.class);
+
+        int intColumn2 = ObjectSelect.query(PrimitivesTestEntity.class)
+                .column(property)
+                .orderBy(PrimitivesTestEntity.INT_COLUMN.asc())
+                .selectFirst(context);
+        assertEquals(11, intColumn2);
+    }
+
+    @Test
+    public void test_SelectBooleanColumn() throws Exception {
+        boolean boolColumn = ObjectSelect.query(PrimitivesTestEntity.class)
+                .column(PrimitivesTestEntity.BOOLEAN_COLUMN)
+                .orderBy(PrimitivesTestEntity.INT_COLUMN.asc())
+                .selectFirst(context);
+        assertEquals(false, boolColumn);
+    }
+
+    @Test
+    public void test_SelectBooleanList() throws Exception {
+        List<Boolean> intColumns = ObjectSelect.query(PrimitivesTestEntity.class)
+                .column(PrimitivesTestEntity.BOOLEAN_COLUMN)
+                .orderBy(PrimitivesTestEntity.INT_COLUMN.asc())
+                .select(context);
+        assertEquals(20, intColumns.size());
+        assertEquals(false, intColumns.get(0));
+    }
+
+    @Test
+    public void test_SelectBooleanExpColumn() throws Exception {
+        Property<Boolean> property = Property.create("boolColumn",
+                ExpressionFactory.exp("(obj:intColumn < 10)"), Boolean.class);
+
+        boolean boolColumn = ObjectSelect.query(PrimitivesTestEntity.class)
+                .column(property)
+                .orderBy(PrimitivesTestEntity.INT_COLUMN.asc())
+                .selectFirst(context);
+        assertEquals(false, boolColumn);
+    }
+
+    @Test
+    public void test_SelectColumnsList() throws Exception {
+        List<Object[]> columns = ObjectSelect.query(PrimitivesTestEntity.class)
+                .columns(PrimitivesTestEntity.INT_COLUMN, PrimitivesTestEntity.BOOLEAN_COLUMN)
+                .orderBy(PrimitivesTestEntity.INT_COLUMN.asc())
+                .select(context);
+
+        assertEquals(20, columns.size());
+        Object[] result = {10, false};
+        assertArrayEquals(result, columns.get(0));
+    }
+
+    @Test
+    public void test_SelectColumnsExpList() throws Exception {
+
+        Property<Integer> intProperty = Property.create("intColumn",
+                ExpressionFactory.exp("(obj:intColumn + 1)"), Integer.class);
+
+        Property<Boolean> boolProperty = Property.create("boolColumn",
+                ExpressionFactory.exp("(obj:intColumn = 10)"), Boolean.class);
+
+        List<Object[]> columns = ObjectSelect.query(PrimitivesTestEntity.class)
+                .columns(intProperty, boolProperty)
+                .orderBy(PrimitivesTestEntity.INT_COLUMN.asc())
+                .select(context);
+
+        assertEquals(20, columns.size());
+        Object[] result = {11, true};
+        assertArrayEquals(result, columns.get(0));
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
index 92d54e7..3881135 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
@@ -23,6 +23,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
 
 import java.util.List;
 
@@ -36,9 +37,11 @@ import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.FunctionExpressionFactory;
 import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.exp.parser.ASTScalar;
 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.Painting;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
@@ -61,17 +64,31 @@ public class ObjectSelect_RunIT extends ServerCase {
 		tArtist.setColumns("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH");
 
 		long dateBase = System.currentTimeMillis();
-
 		for (int i = 1; i <= 20; i++) {
 			tArtist.insert(i, "artist" + i, new java.sql.Date(dateBase + 10000 * i));
 		}
+
+		TableHelper tGallery = new TableHelper(dbHelper, "GALLERY");
+		tGallery.setColumns("GALLERY_ID", "GALLERY_NAME");
+		tGallery.insert(1, "tate modern");
+
+		TableHelper tPaintings = new TableHelper(dbHelper, "PAINTING");
+		tPaintings.setColumns("PAINTING_ID", "PAINTING_TITLE", "ARTIST_ID", "GALLERY_ID");
+		for (int i = 1; i <= 20; i++) {
+			tPaintings.insert(i, "painting" + i, i % 5 + 1, 1);
+		}
 	}
 
 	@After
 	public void clearArtistsDataSet() throws Exception {
+		TableHelper tPaintings = new TableHelper(dbHelper, "PAINTING");
+		tPaintings.deleteAll();
+
 		TableHelper tArtist = new TableHelper(dbHelper, "ARTIST");
-		tArtist.setColumns("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH");
 		tArtist.deleteAll();
+
+		TableHelper tGallery = new TableHelper(dbHelper, "GALLERY");
+		tGallery.deleteAll();
 	}
 
 	@Test
@@ -204,4 +221,88 @@ public class ObjectSelect_RunIT extends ServerCase {
 		assertNotNull(a);
 		assertEquals("artist1", a.getArtistName());
 	}
+
+	@Test
+	public void test_SelectFirst_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]);
+	}
+
+	@SuppressWarnings("ConstantConditions")
+	@Test
+	public void test_SelectFirst_EmptyColumns() throws Exception {
+		Object a = ObjectSelect.query(Artist.class)
+				.columns()
+				.where(Artist.ARTIST_NAME.like("artist%"))
+				.orderBy("db:ARTIST_ID")
+				.selectFirst(context);
+		assertNotNull(a);
+		assertTrue(a instanceof Artist);
+		assertEquals("artist1", ((Artist)a).getArtistName());
+	}
+
+	@Test
+	public void test_SelectFirst_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 test_SelectFirst_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 test_SelectFirst_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 test_SelectFirst_RelColumnWithFunction() throws Exception {
+		Property<String> paintingTitle = Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE);
+		Expression exp = FunctionExpressionFactory.substringExp(paintingTitle.path(), 7, 3);
+		exp = FunctionExpressionFactory.concatExp(exp, new ASTScalar(" "), Artist.ARTIST_NAME.path());
+		Property<String> altTitle = Property.create("altTitle", exp, String.class);
+
+		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);
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/b2d210e6/cayenne-server/src/test/java/org/apache/cayenne/testdo/primitive/auto/_PrimitivesTestEntity.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/primitive/auto/_PrimitivesTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/primitive/auto/_PrimitivesTestEntity.java
index 4056499..66edc80 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/primitive/auto/_PrimitivesTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/primitive/auto/_PrimitivesTestEntity.java
@@ -15,8 +15,8 @@ public abstract class _PrimitivesTestEntity extends CayenneDataObject {
 
     public static final String ID_PK_COLUMN = "ID";
 
-    public static final Property<Boolean> BOOLEAN_COLUMN = new Property<>("booleanColumn");
-    public static final Property<Integer> INT_COLUMN = new Property<>("intColumn");
+    public static final Property<Boolean> BOOLEAN_COLUMN = Property.create("booleanColumn", Boolean.class);
+    public static final Property<Integer> INT_COLUMN = Property.create("intColumn", Integer.class);
 
     public void setBooleanColumn(boolean booleanColumn) {
         writeProperty("booleanColumn", booleanColumn);


Mime
View raw message