cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ntimof...@apache.org
Subject [2/2] cayenne git commit: CAY-2193 HAVING clause - extracted abstract class FluentSelect from ObjectSelect - created new class ColumnSelect (extends FluentSelect)
Date Thu, 12 Jan 2017 13:41:55 GMT
CAY-2193 HAVING clause
 - extracted abstract class FluentSelect from ObjectSelect
 - created new class ColumnSelect (extends FluentSelect)


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

Branch: refs/heads/master
Commit: 4f860180dbcea9ea9cf04edd3da1e4166568c97e
Parents: ba64168
Author: Nikita Timofeev <stariy95@gmail.com>
Authored: Thu Jan 12 15:46:21 2017 +0300
Committer: Nikita Timofeev <stariy95@gmail.com>
Committed: Thu Jan 12 15:46:21 2017 +0300

----------------------------------------------------------------------
 .../select/DefaultSelectTranslator.java         | 109 +++-
 .../translator/select/QualifierTranslator.java  |  64 +++
 .../translator/select/QueryAssembler.java       |  27 +
 .../dba/oracle/OracleSelectTranslator.java      |  10 +-
 .../java/org/apache/cayenne/exp/Property.java   |  23 +-
 .../org/apache/cayenne/query/ColumnSelect.java  | 276 ++++++++++
 .../org/apache/cayenne/query/FluentSelect.java  | 543 +++++++++++++++++++
 .../org/apache/cayenne/query/ObjectSelect.java  | 527 +-----------------
 .../org/apache/cayenne/query/SelectQuery.java   |  39 +-
 .../cayenne/query/SelectQueryMetadata.java      |   3 +-
 .../cayenne/access/ReturnTypesMappingIT.java    |   2 +-
 .../translator/select/TstQueryAssembler.java    |   6 +
 .../apache/cayenne/query/ColumnSelectIT.java    | 242 +++++++++
 .../apache/cayenne/query/ColumnSelectTest.java  | 199 +++++++
 .../apache/cayenne/query/ObjectSelectTest.java  |  49 --
 .../cayenne/query/ObjectSelect_AggregateIT.java |  22 +-
 .../cayenne/query/ObjectSelect_CompileIT.java   |   4 +-
 .../cayenne/query/ObjectSelect_RunIT.java       |  13 +-
 .../apache/cayenne/unit/DB2UnitDbAdapter.java   |   5 +
 .../apache/cayenne/unit/DerbyUnitDbAdapter.java |   5 +
 .../cayenne/unit/OracleUnitDbAdapter.java       |   5 +
 .../cayenne/unit/SQLServerUnitDbAdapter.java    |   5 +
 .../org/apache/cayenne/unit/UnitDbAdapter.java  |   4 +
 23 files changed, 1568 insertions(+), 614 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/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 1ea302d..d21e643 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
@@ -20,6 +20,7 @@ package org.apache.cayenne.access.translator.select;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.dba.TypesMapping;
@@ -56,6 +57,7 @@ import org.apache.cayenne.util.HashCodeBuilder;
 
 import java.sql.Types;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -100,7 +102,7 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 	 * Does this SQL have any aggregate function
 	 */
 	boolean haveAggregate;
-	List<ColumnDescriptor> groupByColumns;
+	Map<ColumnDescriptor, List<DbAttributeBinding>> groupByColumns;
 
 
 	public DefaultSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
@@ -132,7 +134,7 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 
 		// build qualifier
 		QualifierTranslator qualifierTranslator = adapter.getQualifierTranslator(this);
-		StringBuilder qualifierBuffer = qualifierTranslator.appendPart(new StringBuilder());
+		StringBuilder whereQualifierBuffer = qualifierTranslator.appendPart(new StringBuilder());
 
 		// build ORDER BY
 		OrderingTranslator orderingTranslator = new OrderingTranslator(this);
@@ -187,12 +189,12 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 		joins.appendRootWithQuoteSqlIdentifiers(queryBuf, getQueryMetadata().getDbEntity());
 
 		joins.appendJoins(queryBuf);
-		joins.appendQualifier(qualifierBuffer, qualifierBuffer.length() == 0);
+		joins.appendQualifier(whereQualifierBuffer, whereQualifierBuffer.length() == 0);
 
 		// append qualifier
-		if (qualifierBuffer.length() > 0) {
+		if (whereQualifierBuffer.length() > 0) {
 			queryBuf.append(" WHERE ");
-			queryBuf.append(qualifierBuffer);
+			queryBuf.append(whereQualifierBuffer);
 		}
 
 		if(haveAggregate && !groupByColumns.isEmpty()) {
@@ -200,6 +202,18 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 			appendGroupByColumns(queryBuf, groupByColumns);
 		}
 
+		// append HAVING qualifier
+		QualifierTranslator havingQualifierTranslator = adapter.getQualifierTranslator(this);
+		Expression havingQualifier = ((SelectQuery)query).getHavingQualifier();
+		if(havingQualifier != null) {
+			havingQualifierTranslator.setQualifier(havingQualifier);
+			StringBuilder havingQualifierBuffer = havingQualifierTranslator.appendPart(new StringBuilder());
+			if(havingQualifierBuffer.length() > 0) {
+				queryBuf.append(" HAVING ");
+				queryBuf.append(havingQualifierBuffer);
+			}
+		}
+
 		// append prebuilt ordering
 		if (orderingBuffer.length() > 0) {
 			queryBuf.append(" ORDER BY ").append(orderingBuffer);
@@ -241,22 +255,35 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 
 	/**
 	 * Append columns to GROUP BY clause
-	 * use distinct from appendSelectColumns() method
-	 * as it has some incompatible overridden versions (i.e. in Oracle translator)
-	 *
 	 * @since 4.0
 	 */
-	protected void appendGroupByColumns(StringBuilder buffer, List<ColumnDescriptor> groupByColumns) {
-		int columnCount = groupByColumns.size();
-		buffer.append(groupByColumns.get(0).getName());
-
-		for (int i = 1; i < columnCount; i++) {
+	protected void appendGroupByColumns(StringBuilder buffer, Map<ColumnDescriptor, List<DbAttributeBinding>>  groupByColumns) {
+		Iterator<Map.Entry<ColumnDescriptor, List<DbAttributeBinding>>> it = groupByColumns.entrySet().iterator();
+		Map.Entry<ColumnDescriptor, List<DbAttributeBinding>> entry = it.next();
+		appendGroupByColumn(buffer, entry);
+		while(it.hasNext()) {
+			entry = it.next();
 			buffer.append(", ");
-			buffer.append(groupByColumns.get(i).getName());
+			appendGroupByColumn(buffer, entry);
 		}
 	}
 
 	/**
+	 * Append single column to GROUP BY clause
+	 * @since 4.0
+	 */
+	protected void appendGroupByColumn(StringBuilder buffer, Map.Entry<ColumnDescriptor, List<DbAttributeBinding>> entry) {
+		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());
+        }
+	}
+
+	/**
 	 * Handles appending optional limit and offset clauses. This implementation
 	 * does nothing, deferring to subclasses to define the LIMIT/OFFSET clause
 	 * syntax.
@@ -340,25 +367,36 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 	 * If query contains explicit column list, use only them
 	 */
 	<T> List<ColumnDescriptor> appendOverridedColumns(List<ColumnDescriptor> columns, SelectQuery<T> query) {
+		groupByColumns = new HashMap<>();
+
 		QualifierTranslator qualifierTranslator = adapter.getQualifierTranslator(this);
-		groupByColumns = new ArrayList<>();
+		AccumulatingBindingListener bindingListener = new AccumulatingBindingListener();
+		setAddBindingListener(bindingListener);
+
 		for(Property<?> property : query.getColumns()) {
-			StringBuilder builder = new StringBuilder();
-			qualifierTranslator.setOut(builder);
-			qualifierTranslator.doAppendPart(property.getExpression());
+			qualifierTranslator.setQualifier(property.getExpression());
+			StringBuilder builder = qualifierTranslator.appendPart(new StringBuilder());
 
 			int type = TypesMapping.getSqlTypeByJava(property.getType());
+
+			String alias = property.getAlias();
+			if(alias != null) {
+				builder.append(" AS ").append(alias);
+			}
 			ColumnDescriptor descriptor = new ColumnDescriptor(builder.toString(), type);
-			descriptor.setDataRowKey(property.getName());
+			descriptor.setDataRowKey(alias);
 			columns.add(descriptor);
 
 			if(isAggregate(property)) {
 				haveAggregate = true;
 			} else {
-				groupByColumns.add(descriptor);
+				groupByColumns.put(descriptor, bindingListener.getBindings());
 			}
+			bindingListener.reset();
 		}
 
+		setAddBindingListener(null);
+
 		if(!haveAggregate) {
 			// if no expression with aggregation function found, we don't need this information
 			groupByColumns.clear();
@@ -678,6 +716,21 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 		return true;
 	}
 
+	@Override
+	public String getAliasForExpression(Expression exp) {
+		Collection<Property<?>> columns = ((SelectQuery<?>)query).getColumns();
+		if(columns == null) {
+			return null;
+		}
+		for(Property<?> property : columns) {
+			if(property.getExpression().equals(exp)) {
+				return property.getAlias();
+			}
+		}
+
+		return null;
+	}
+
 	static final class ColumnTracker {
 
 		private DbAttribute attribute;
@@ -705,4 +758,20 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 
 	}
 
+	static final class AccumulatingBindingListener implements AddBindingListener {
+		private List<DbAttributeBinding> bindings = new ArrayList<>();
+
+		@Override
+		public void onAdd(DbAttributeBinding binding) {
+			bindings.add(binding);
+		}
+
+		public void reset() {
+			bindings.clear();
+		}
+
+		public List<DbAttributeBinding> getBindings() {
+			return new ArrayList<>(bindings);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/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 3998c00..b758b23 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
@@ -54,6 +54,21 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 	protected boolean matchingObject;
 	protected boolean caseInsensitive;
 
+	/**
+	 * @since 4.0
+	 */
+	protected boolean useAliasForExpressions;
+
+	/**
+	 * @since 4.0
+	 */
+	protected Expression waitingForEndNode;
+
+	/**
+	 * @since 4.0
+	 */
+	protected Expression qualifier;
+
 	public QualifierTranslator(QueryAssembler queryAssembler) {
 		super(queryAssembler);
 
@@ -76,6 +91,22 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 	}
 
 	/**
+	 * Explicitly set qualifier.
+	 * It will be used instead of extracting qualifier from the query itself.
+	 * @since 4.0
+	 */
+	public void setQualifier(Expression qualifier) {
+		this.qualifier = qualifier;
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	public void setUseAliasForExpressions(boolean useAliasForExpressions) {
+		this.useAliasForExpressions = useAliasForExpressions;
+	}
+
+	/**
 	 * Translates query qualifier to SQL WHERE clause. Qualifier is a method
 	 * parameter.
 	 * 
@@ -89,6 +120,11 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 	}
 
 	protected Expression extractQualifier() {
+		// if additional qualifier is set, use it
+		if(this.qualifier != null) {
+			return this.qualifier;
+		}
+
 		Query q = queryAssembler.getQuery();
 
 		Expression qualifier = ((SelectQuery<?>) q).getQualifier();
@@ -194,6 +230,10 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 	@Override
 	public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
 
+		if(waitingForEndNode != null) {
+			return;
+		}
+
 		if (!hasMoreChildren) {
 			return;
 		}
@@ -356,6 +396,20 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 
 	@Override
 	public void startNode(Expression node, Expression parentNode) {
+
+		if(waitingForEndNode != null) {
+			return;
+		}
+
+		if(useAliasForExpressions) {
+			String alias = queryAssembler.getAliasForExpression(node);
+			if(alias != null) {
+				out.append(alias);
+				waitingForEndNode = node;
+				return;
+			}
+		}
+
 		boolean parenthesisNeeded = parenthesisNeeded(node, parentNode);
 
 		if(node.getType() == Expression.FUNCTION_CALL) {
@@ -409,6 +463,13 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 	@Override
 	public void endNode(Expression node, Expression parentNode) {
 
+		if(waitingForEndNode != null) {
+			if(node == waitingForEndNode) {
+				waitingForEndNode = null;
+			}
+			return;
+		}
+
 		try {
 			// check if we need to use objectMatchTranslator to finish building the expression
 			if (node.getOperandCount() == 2 && matchingObject) {
@@ -449,6 +510,9 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 
 	@Override
 	public void objectNode(Object leaf, Expression parentNode) {
+		if(waitingForEndNode != null) {
+			return;
+		}
 
 		try {
 			switch (parentNode.getType()) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
index f026823..1128e22 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
@@ -23,6 +23,7 @@ import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.EntityResolver;
@@ -46,6 +47,10 @@ public abstract class QueryAssembler {
 	protected DbAdapter adapter;
 	protected EntityResolver entityResolver;
 	protected List<DbAttributeBinding> bindings;
+	/**
+	 * @since 4.0
+	 */
+	protected AddBindingListener addBindingListener;
 
 	/**
 	 * @since 4.0
@@ -158,6 +163,9 @@ public abstract class QueryAssembler {
 		binding.setValue(anObject);
 		binding.setStatementPosition(bindings.size() + 1);
 		bindings.add(binding);
+		if(addBindingListener != null) {
+			addBindingListener.onAdd(binding);
+		}
 	}
 
 	/**
@@ -166,4 +174,23 @@ public abstract class QueryAssembler {
 	public DbAttributeBinding[] getBindings() {
 		return bindings.toArray(new DbAttributeBinding[bindings.size()]);
 	}
+
+    /**
+     * @since 4.0
+     */
+	public abstract String getAliasForExpression(Expression exp);
+
+	/**
+	 * @since 4.0
+	 */
+	public void setAddBindingListener(AddBindingListener addBindingListener) {
+		this.addBindingListener = addBindingListener;
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	protected interface AddBindingListener {
+		void onAdd(DbAttributeBinding binding);
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java
index fa6e95b..c98aad9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java
@@ -61,12 +61,18 @@ class OracleSelectTranslator extends DefaultSelectTranslator {
 
 		// append columns (unroll the loop's first element)
 		int columnCount = selectColumnExpList.size();
-		buffer.append(selectColumnExpList.get(0)).append(" AS c0");
+		buffer.append(selectColumnExpList.get(0));
+		if(!selectColumnExpList.get(0).contains(" AS ")) {
+			buffer.append(" AS c0");
+		}
 
 		// assume there is at least 1 element
 		for (int i = 1; i < columnCount; i++) {
 			buffer.append(", ");
-			buffer.append(selectColumnExpList.get(i)).append(" AS c" + i);
+			buffer.append(selectColumnExpList.get(i));
+			if(!selectColumnExpList.get(i).contains(" AS ")) {
+				buffer.append(" AS c").append(i);
+			}
 		}
 	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/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 9c13244..47ac8f0 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,7 @@
  ****************************************************************/
 package org.apache.cayenne.exp;
 
+import org.apache.cayenne.exp.parser.ASTPath;
 import org.apache.cayenne.query.Ordering;
 import org.apache.cayenne.query.PrefetchTreeNode;
 import org.apache.cayenne.query.SortOrder;
@@ -127,6 +128,26 @@ public class Property<E> {
 
     /**
      * @since 4.0
+     * @return alias for this property
+     */
+    public String getAlias() {
+        if(getName() == null) {
+            return null;
+        }
+
+        // check if default name for Path expression is overridden
+        Expression exp = getExpression();
+        if(exp instanceof ASTPath) {
+            if(((ASTPath) exp).getPath().equals(getName())) {
+                return null;
+            }
+        }
+
+        return getName();
+    }
+
+    /**
+     * @since 4.0
      */
     public Expression getExpression() {
         return expressionProvider.get();
@@ -615,7 +636,7 @@ public class Property<E> {
     }
 
     public static <T> Property<T> create(Expression expression, Class<? super T> type) {
-        return new Property<>(expression.expName().toLowerCase(), expression, type);
+        return new Property<>(null, expression, type);
     }
 
     public static <T> Property<T> create(String name, Expression expression, Class<? super T> type) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelect.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelect.java b/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelect.java
new file mode 100644
index 0000000..9aa2667
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelect.java
@@ -0,0 +1,276 @@
+/*****************************************************************
+ *   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.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.map.EntityResolver;
+
+/**
+ * <p>
+ *     A selecting query providing individual properties based on the root object.
+ *
+ * </p>
+ * <p>
+ *     It can be properties of the object itself or some function calls (including aggregate functions)
+ * </p>
+ * <p>
+ * Usage examples:
+ * <pre>
+ *      // selecting list of names:
+ *      List&lt;String&gt; names = ColumnSelect.query(Artist.class, Artist.ARTIST_NAME).select(context);
+ *
+ *      // selecting count:
+ *      long count = ColumnSelect.query(Artist.class, Property.COUNT).selectOne();
+ * </pre>
+ * </p>
+ * @since 4.0
+ */
+public class ColumnSelect<T> extends FluentSelect<T, ColumnSelect<T>> {
+
+    private Collection<Property<?>> columns;
+    private boolean havingExpressionIsActive = false;
+    private Expression having;
+
+    /**
+     *
+     * @param entityType base persistent class that will be used as a root for this query
+     */
+    public static <T> ColumnSelect<T> query(Class<T> entityType) {
+        return new ColumnSelect<T>().entityType(entityType);
+    }
+
+    /**
+     *
+     * @param entityType base persistent class that will be used as a root for this query
+     * @param column single column to select
+     */
+    public static <E> ColumnSelect<E> query(Class<?> entityType, Property<E> column) {
+        return new ColumnSelect<>().entityType(entityType).column(column);
+    }
+
+    /**
+     *
+     * @param entityType base persistent class that will be used as a root for this query
+     * @param columns columns to select
+     */
+    public static ColumnSelect<Object[]> query(Class<?> entityType, Property<?>... columns) {
+        return new ColumnSelect<Object[]>().entityType(entityType).columns(columns);
+    }
+
+    protected ColumnSelect() {
+        super();
+    }
+
+    protected ColumnSelect(ObjectSelect<T> select) {
+        super();
+        this.entityType = select.entityType;
+        this.entityName = select.entityName;
+        this.dbEntityName = select.dbEntityName;
+        this.where = select.where;
+        this.orderings = select.orderings;
+        this.prefetches = select.prefetches;
+        this.limit = select.limit;
+        this.offset = select.offset;
+        this.pageSize = select.pageSize;
+        this.statementFetchSize = select.statementFetchSize;
+        this.cacheStrategy = select.cacheStrategy;
+        this.cacheGroups = select.cacheGroups;
+    }
+
+    @Override
+    protected Query createReplacementQuery(EntityResolver resolver) {
+        SelectQuery<?> replacement = (SelectQuery)super.createReplacementQuery(resolver);
+        replacement.setColumns(columns);
+        replacement.setHavingQualifier(having);
+        return replacement;
+    }
+
+    /**
+     * <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 = ColumnSelect.query(Artist.class)
+     *                                    .columns(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH)
+     *                                    .select(context);
+     * </pre>
+     *
+     * @param properties array of properties to select
+     * @see ColumnSelect#column(Property)
+     */
+    @SuppressWarnings("unchecked")
+    public ColumnSelect<Object[]> columns(Property<?>... properties) {
+        if (properties == null || properties.length == 0) {
+            return (ColumnSelect<Object[]>)this;
+        }
+
+        return columns(Arrays.asList(properties));
+    }
+
+    @SuppressWarnings("unchecked")
+    public ColumnSelect<Object[]> columns(Collection<Property<?>> properties) {
+        if (properties == null || properties.isEmpty()) {
+            return (ColumnSelect<Object[]>)this;
+        }
+
+        if (this.columns == null) {
+            this.columns = new ArrayList<>(properties.size());
+        }
+
+        columns.addAll(properties);
+        return (ColumnSelect<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 ColumnSelect#columns(Property[])} method as subsequent
+     * call to this method will override previous columns set via this or
+     * {@link ColumnSelect#columns(Property[])} method.</p>
+     * <p>
+     * <pre>
+     * List&lt;String&gt; names = ColumnSelect.query(Artist.class, Artist.ARTIST_NAME).select(context);
+     * </pre>
+     *
+     * @param property single property to select
+     * @see ColumnSelect#columns(Property[])
+     */
+    @SuppressWarnings("unchecked")
+    protected  <E> ColumnSelect<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 (ColumnSelect<E>) this;
+    }
+
+    /**
+     * Appends a having qualifier expression of this query. An equivalent to
+     * {@link #and(Expression...)} that can be used a syntactic sugar.
+     *
+     * @return this object
+     */
+    public ColumnSelect<T> having(Expression expression) {
+        havingExpressionIsActive = true;
+        return and(expression);
+    }
+
+    /**
+     * Appends a having 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 ColumnSelect<T> having(String expressionString, Object... parameters) {
+        havingExpressionIsActive = true;
+        return and(ExpressionFactory.exp(expressionString, parameters));
+    }
+
+    /**
+     * AND's provided expressions to the existing WHERE or HAVING clause expression.
+     *
+     * @return this object
+     */
+    @SuppressWarnings("unchecked")
+    public ColumnSelect<T> and(Collection<Expression> expressions) {
+
+        if (expressions == null || expressions.isEmpty()) {
+            return this;
+        }
+
+        Collection<Expression> all;
+        Expression activeExpression = getActiveExpression();
+
+        if (activeExpression != null) {
+            all = new ArrayList<>(expressions.size() + 1);
+            all.add(activeExpression);
+            all.addAll(expressions);
+        } else {
+            all = expressions;
+        }
+
+        setActiveExpression(ExpressionFactory.and(all));
+        return this;
+    }
+
+    /**
+     * OR's provided expressions to the existing WHERE or HAVING clause expression.
+     *
+     * @return this object
+     */
+    @SuppressWarnings("unchecked")
+    public ColumnSelect<T> or(Collection<Expression> expressions) {
+        if (expressions == null || expressions.isEmpty()) {
+            return this;
+        }
+
+        Collection<Expression> all;
+        Expression activeExpression = getActiveExpression();
+
+        if (activeExpression != null) {
+            all = new ArrayList<>(expressions.size() + 1);
+            all.add(activeExpression);
+            all.addAll(expressions);
+        } else {
+            all = expressions;
+        }
+
+        setActiveExpression(ExpressionFactory.or(all));
+        return this;
+    }
+
+    private void setActiveExpression(Expression exp) {
+        if(havingExpressionIsActive) {
+            having = exp;
+        } else {
+            where = exp;
+        }
+    }
+
+    private Expression getActiveExpression() {
+        if(havingExpressionIsActive) {
+            return having;
+        } else {
+            return where;
+        }
+    }
+
+    public Collection<Property<?>> getColumns() {
+        return columns;
+    }
+
+    /**
+     * Returns a HAVING clause Expression of this query.
+     */
+    public Expression getHaving() {
+        return having;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java b/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java
new file mode 100644
index 0000000..de772bc
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java
@@ -0,0 +1,543 @@
+/*****************************************************************
+ *   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.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.cayenne.CayenneRuntimeException;
+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.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * Base class for ObjectSelect and ColumnSelect
+ *
+ * @since 4.0
+ */
+public abstract class FluentSelect<T, S extends FluentSelect<T, S>> extends IndirectQuery implements Select<T> {
+
+    protected Class<?> entityType;
+    protected String entityName;
+    protected String dbEntityName;
+    protected Expression where;
+    protected Collection<Ordering> orderings;
+    protected PrefetchTreeNode prefetches;
+    protected int limit;
+    protected int offset;
+    protected int pageSize;
+    protected int statementFetchSize;
+    protected QueryCacheStrategy cacheStrategy;
+    protected String[] cacheGroups;
+
+    protected FluentSelect() {
+    }
+
+    /**
+     * 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.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 S 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 S 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 S dbEntityName(String dbEntityName) {
+        return resetEntity(null, null, dbEntityName);
+    }
+
+    @SuppressWarnings("unchecked")
+    private S resetEntity(Class<?> entityType, String entityName, String dbEntityName) {
+        this.entityType = entityType;
+        this.entityName = entityName;
+        this.dbEntityName = dbEntityName;
+        return (S)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 S where(Expression expression) {
+        return and(expression);
+    }
+
+    /**
+     * 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 S where(String expressionString, Object... parameters) {
+        return and(ExpressionFactory.exp(expressionString, parameters));
+    }
+
+    /**
+     * AND's provided expressions to the existing WHERE clause expression.
+     *
+     * @return this object
+     */
+    @SuppressWarnings("unchecked")
+    public S and(Expression... expressions) {
+        if (expressions == null || expressions.length == 0) {
+            return (S)this;
+        }
+
+        return and(Arrays.asList(expressions));
+    }
+
+    /**
+     * AND's provided expressions to the existing WHERE clause expression.
+     *
+     * @return this object
+     */
+    @SuppressWarnings("unchecked")
+    public S and(Collection<Expression> expressions) {
+
+        if (expressions == null || expressions.isEmpty()) {
+            return (S)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 (S)this;
+    }
+
+    /**
+     * OR's provided expressions to the existing WHERE clause expression.
+     *
+     * @return this object
+     */
+    @SuppressWarnings("unchecked")
+    public S or(Expression... expressions) {
+        if (expressions == null || expressions.length == 0) {
+            return (S)this;
+        }
+
+        return or(Arrays.asList(expressions));
+    }
+
+    /**
+     * OR's provided expressions to the existing WHERE clause expression.
+     *
+     * @return this object
+     */
+    @SuppressWarnings("unchecked")
+    public S or(Collection<Expression> expressions) {
+        if (expressions == null || expressions.isEmpty()) {
+            return (S)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 (S)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 S 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 S orderBy(String property, SortOrder sortOrder) {
+        return orderBy(new Ordering(property, sortOrder));
+    }
+
+    /**
+     * Add one or more orderings to this query.
+     *
+     * @return this object
+     */
+    @SuppressWarnings("unchecked")
+    public S orderBy(Ordering... orderings) {
+
+        if (orderings == null) {
+            return (S)this;
+        }
+
+        if (this.orderings == null) {
+            this.orderings = new ArrayList<>(orderings.length);
+        }
+
+        Collections.addAll(this.orderings, orderings);
+
+        return (S)this;
+    }
+
+    /**
+     * Adds a list of orderings to this query.
+     *
+     * @return this object
+     */
+    @SuppressWarnings("unchecked")
+    public S orderBy(Collection<Ordering> orderings) {
+
+        if (orderings == null) {
+            return (S)this;
+        }
+
+        if (this.orderings == null) {
+            this.orderings = new ArrayList<>(orderings.size());
+        }
+
+        this.orderings.addAll(orderings);
+
+        return (S)this;
+    }
+
+    /**
+     * Merges prefetch into the query prefetch tree.
+     *
+     * @return this object
+     */
+    @SuppressWarnings("unchecked")
+    public S prefetch(PrefetchTreeNode prefetch) {
+
+        if (prefetch == null) {
+            return (S)this;
+        }
+
+        if (prefetches == null) {
+            prefetches = new PrefetchTreeNode();
+        }
+
+        prefetches.merge(prefetch);
+        return (S)this;
+    }
+
+    /**
+     * Merges a prefetch path with specified semantics into the query prefetch
+     * tree.
+     *
+     * @return this object
+     */
+    @SuppressWarnings("unchecked")
+    public S prefetch(String path, int semantics) {
+
+        if (path == null) {
+            return (S)this;
+        }
+
+        if (prefetches == null) {
+            prefetches = new PrefetchTreeNode();
+        }
+
+        prefetches.addPath(path).setSemantics(semantics);
+        return (S)this;
+    }
+
+    /**
+     * Resets query fetch limit - a parameter that defines max number of objects
+     * that should be ever be fetched from the database.
+     */
+    @SuppressWarnings("unchecked")
+    public S limit(int fetchLimit) {
+        if (this.limit != fetchLimit) {
+            this.limit = fetchLimit;
+            this.replacementQuery = null;
+        }
+
+        return (S)this;
+    }
+
+    /**
+     * Resets query fetch offset - a parameter that defines how many objects
+     * should be skipped when reading data from the database.
+     */
+    @SuppressWarnings("unchecked")
+    public S offset(int fetchOffset) {
+        if (this.offset != fetchOffset) {
+            this.offset = fetchOffset;
+            this.replacementQuery = null;
+        }
+
+        return (S)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.
+     */
+    @SuppressWarnings("unchecked")
+    public S pageSize(int pageSize) {
+        if (this.pageSize != pageSize) {
+            this.pageSize = pageSize;
+            this.replacementQuery = null;
+        }
+
+        return (S)this;
+    }
+
+    /**
+     * Sets fetch size of the PreparedStatement generated for this query. Only
+     * non-negative values would change the default size.
+     *
+     * @see Statement#setFetchSize(int)
+     */
+    @SuppressWarnings("unchecked")
+    public S statementFetchSize(int size) {
+        if (this.statementFetchSize != size) {
+            this.statementFetchSize = size;
+            this.replacementQuery = null;
+        }
+
+        return (S)this;
+    }
+
+    public S cacheStrategy(QueryCacheStrategy strategy, String... cacheGroups) {
+        if (this.cacheStrategy != strategy) {
+            this.cacheStrategy = strategy;
+            this.replacementQuery = null;
+        }
+
+        return cacheGroups(cacheGroups);
+    }
+
+    @SuppressWarnings("unchecked")
+    public S cacheGroups(String... cacheGroups) {
+        this.cacheGroups = cacheGroups != null && cacheGroups.length > 0 ? cacheGroups : null;
+        this.replacementQuery = null;
+        return (S)this;
+    }
+
+    public S 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 S 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 S 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 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(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/4f860180/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 e11589c..7f6066e 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,24 +18,13 @@
  ****************************************************************/
 package org.apache.cayenne.query;
 
-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;
 
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 
 /**
@@ -53,25 +42,11 @@ import java.util.List;
  *
  * @since 4.0
  */
-public class ObjectSelect<T> extends IndirectQuery implements Select<T> {
+public class ObjectSelect<T> extends FluentSelect<T, ObjectSelect<T>> {
 
     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;
+    protected boolean fetchingDataRows;
 
     /**
      * Creates a ObjectSelect that selects objects of a given persistent class.
@@ -151,87 +126,12 @@ public class ObjectSelect<T> extends IndirectQuery implements Select<T> {
     @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");
-        }
-
+        SelectQuery<?> replacement = (SelectQuery<?>) super.createReplacementQuery(resolver);
         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".
      *
@@ -244,448 +144,45 @@ public class ObjectSelect<T> extends IndirectQuery implements Select<T> {
     }
 
     /**
-     * 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)
+     * List&lt;Object[]&gt; columns = ColumnSelect.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)
+     * @see ColumnSelect#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;
+    public ColumnSelect<Object[]> columns(Property<?>... properties) {
+        return new ColumnSelect<>(this).columns(properties);
     }
 
     /**
      * <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
+     * <p>If you need several columns use {@link ColumnSelect#columns(Property[])} method as subsequent
      * call to this method will override previous columns set via this or
-     * {@link ObjectSelect#columns(Property[])} method.</p>
+     * {@link ColumnSelect#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[])
+     * @see ColumnSelect#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;
+    protected <E> ColumnSelect<E> column(Property<E> property) {
+        return new ColumnSelect<>(this).column(property);
     }
 
     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/4f860180/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 773d97d..9c06d05 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
@@ -64,6 +64,11 @@ public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery,
 	 */
 	protected Collection<Property<?>> columns;
 
+	/**
+	 * @since 4.0
+	 */
+	protected Expression havingQualifier;
+
 	SelectQueryMetadata metaData = new SelectQueryMetadata();
 
 	/**
@@ -875,7 +880,7 @@ public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery,
 		if(columns == null || columns.length == 0) {
 			return;
 		}
-		this.columns = Arrays.asList(columns);
+		setColumns(Arrays.asList(columns));
 	}
 
 	/**
@@ -884,4 +889,36 @@ public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery,
 	public Collection<Property<?>> getColumns() {
 		return columns;
 	}
+
+	/**
+	 * Sets new query HAVING qualifier.
+	 * @since 4.0
+	 */
+	public void setHavingQualifier(Expression qualifier) {
+		this.havingQualifier = qualifier;
+	}
+
+	/**
+	 * Returns query HAVING qualifier.
+	 * @since 4.0
+	 */
+	public Expression getHavingQualifier() {
+		return havingQualifier;
+	}
+
+	/**
+	 * Adds specified HAVING qualifier to the existing HAVING qualifier joining it using "AND".
+	 * @since 4.0
+	 */
+	public void andHavingQualifier(Expression e) {
+		havingQualifier = (havingQualifier != null) ? havingQualifier.andExp(e) : e;
+	}
+
+	/**
+	 * Adds specified HAVING qualifier to the existing HAVING qualifier joining it using "OR".
+	 * @since 4.0
+	 */
+	public void orHavingQualifier(Expression e) {
+		havingQualifier = (havingQualifier != null) ? havingQualifier.orExp(e) : e;
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/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 95562b1..741190e 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
@@ -181,7 +181,8 @@ class SelectQueryMetadata extends BaseQueryMetadata {
 		
 		SQLResult result = new SQLResult();
 		for(Property<?> column : query.getColumns()) {
-			result.addColumnResult(column.getName());
+			String name = column.getName() == null ? column.getExpression().expName() : column.getName();
+			result.addColumnResult(name);
 		}
 		resultSetMapping = result.getResolvedComponents(resolver);
 	}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/cayenne-server/src/test/java/org/apache/cayenne/access/ReturnTypesMappingIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/ReturnTypesMappingIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/ReturnTypesMappingIT.java
index 9bb40ff..f06d146 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/ReturnTypesMappingIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/ReturnTypesMappingIT.java
@@ -821,7 +821,7 @@ public class    ReturnTypesMappingIT extends ServerCase {
         DataRow testRead = (DataRow) context.performQuery(MappedSelect.query("SelectReturnTypesMap1")).get(0);
         Object columnValue = testRead.get(columnName);
         assertNotNull(columnValue);
-        assertTrue(Short.class.equals(columnValue.getClass()));
+        assertEquals(Short.class, columnValue.getClass());
         assertEquals(tinyintValue.intValue(), ((Number)columnValue).intValue());
     }
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java
index dfc7495..72a3803 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.JoinType;
@@ -58,6 +59,11 @@ public class TstQueryAssembler extends QueryAssembler {
 	}
 
 	@Override
+	public String getAliasForExpression(Expression exp) {
+		return null;
+	}
+
+	@Override
 	protected void doTranslate() {
 		this.sql = "SELECT * FROM ARTIST";
 	}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/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
new file mode 100644
index 0000000..a4dcada
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
@@ -0,0 +1,242 @@
+/*****************************************************************
+ *   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.sql.Types;
+import java.text.DateFormat;
+import java.util.Arrays;
+import java.util.Locale;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.di.Inject;
+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.unit.UnitDbAdapter;
+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.Ignore;
+import org.junit.Test;
+
+import static org.apache.cayenne.exp.FunctionExpressionFactory.countExp;
+import static org.apache.cayenne.exp.FunctionExpressionFactory.substringExp;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @since 4.0
+ */
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class ColumnSelectIT extends ServerCase {
+
+    @Inject
+    private DataContext context;
+
+    @Inject
+    private DBHelper dbHelper;
+
+    @Inject
+    private UnitDbAdapter unitDbAdapter;
+
+    // Format: d/m/YY
+    private DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US);
+
+
+    @Before
+    public void createArtistsDataSet() throws Exception {
+        TableHelper tArtist = new TableHelper(dbHelper, "ARTIST");
+        tArtist.setColumns("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH");
+        tArtist.setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.DATE);
+
+        java.sql.Date[] dates = new java.sql.Date[5];
+        for(int i=1; i<=5; i++) {
+            dates[i-1] = new java.sql.Date(dateFormat.parse("1/" + i + "/17").getTime());
+        }
+        for (int i = 1; i <= 20; i++) {
+            tArtist.insert(i, "artist" + i, dates[i % 5]);
+        }
+
+        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);
+        }
+        tPaintings.insert(21, "painting21", 2, 1);
+    }
+
+    @After
+    public void clearArtistsDataSet() throws Exception {
+        for(String table : Arrays.asList("PAINTING", "ARTIST", "GALLERY")) {
+            TableHelper tHelper = new TableHelper(dbHelper, table);
+            tHelper.deleteAll();
+        }
+    }
+
+    @Test
+    public void testSelectGroupBy() throws Exception {
+        Property<Long> count = Property.create(countExp(), Long.class);
+
+        Object[] result = ColumnSelect.query(Artist.class)
+                .columns(Artist.DATE_OF_BIRTH, count)
+                .orderBy(Artist.DATE_OF_BIRTH.asc())
+                .selectFirst(context);
+
+        assertEquals(dateFormat.parse("1/1/17"), result[0]);
+        assertEquals(4L, result[1]);
+    }
+
+    @Test
+    public void testSelectSimpleHaving() throws Exception {
+        Property<Long> count = Property.create(countExp(), Long.class);
+
+        Object[] result = ColumnSelect.query(Artist.class)
+                .columns(Artist.DATE_OF_BIRTH, count)
+                .orderBy(Artist.DATE_OF_BIRTH.asc())
+                .having(Artist.DATE_OF_BIRTH.eq(dateFormat.parse("1/2/17")))
+                .selectOne(context);
+
+        assertEquals(dateFormat.parse("1/2/17"), result[0]);
+        assertEquals(4L, result[1]);
+    }
+
+    @Test(expected = Exception.class)
+    public void testHavingOnNonGroupByColumn() throws Exception {
+        Property<String> nameSubstr = Property.create(substringExp(Artist.ARTIST_NAME.path(), 1, 6), String.class);
+        Property<Long> count = Property.create(countExp(), Long.class);
+
+        Object[] q = ColumnSelect.query(Artist.class, nameSubstr, count)
+                .having(Artist.ARTIST_NAME.like("artist%"))
+                .selectOne(context);
+        assertEquals("artist", q[0]);
+        assertEquals(20L, q[1]);
+    }
+
+    @Test
+    public void testSelectRelationshipCount() throws Exception {
+        Property<Long> paintingCount = Property.create(countExp(Artist.PAINTING_ARRAY.path()), Long.class);
+
+        Object[] result = ColumnSelect.query(Artist.class)
+                .columns(Artist.DATE_OF_BIRTH, paintingCount)
+                .orderBy(Artist.DATE_OF_BIRTH.asc())
+                .selectFirst(context);
+        assertEquals(dateFormat.parse("1/1/17"), result[0]);
+        assertEquals(4L, result[1]);
+    }
+
+    @Test
+    public void testSelectHavingWithExpressionAlias() throws Exception {
+
+        Property<String> nameSubstr = Property.create("name_substr", substringExp(Artist.ARTIST_NAME.path(), 1, 6), String.class);
+        Property<Long> count = Property.create(countExp(), Long.class);
+
+        Object[] q = null;
+        try {
+            q = ColumnSelect.query(Artist.class, nameSubstr, count)
+                    .having(count.gt(10L))
+                    .selectOne(context);
+        } catch (CayenneRuntimeException ex) {
+            if(unitDbAdapter.supportsExpressionInHaving()) {
+                fail();
+            } else {
+                return;
+            }
+        }
+        assertEquals("artist", q[0]);
+        assertEquals(20L, q[1]);
+    }
+
+    @Ignore("Need to figure out a better way to handle alias / no alias case for expression in having")
+    @Test
+    public void testSelectHavingWithExpressionNoAlias() throws Exception {
+
+        Property<String> nameSubstr = Property.create(substringExp(Artist.ARTIST_NAME.path(), 1, 6), String.class);
+        Property<Long> count = Property.create(countExp(), Long.class);
+
+        Object[] q = null;
+        try {
+            q = ColumnSelect.query(Artist.class, nameSubstr, count)
+                    .having(count.gt(10L))
+                    .selectOne(context);
+        } catch (CayenneRuntimeException ex) {
+            if(unitDbAdapter.supportsExpressionInHaving()) {
+                fail();
+            } else {
+                return;
+            }
+        }
+        assertEquals("artist", q[0]);
+        assertEquals(20L, q[1]);
+    }
+
+    @Test
+    public void testSelectWhereAndHaving() throws Exception {
+        Property<String> nameFirstLetter = Property.create(substringExp(Artist.ARTIST_NAME.path(), 1, 1), String.class);
+        Property<String> nameSubstr = Property.create("name_substr", substringExp(Artist.ARTIST_NAME.path(), 1, 6), String.class);
+        Property<Long> count = Property.create(countExp(), Long.class);
+
+        Object[] q = null;
+        try {
+            q = ColumnSelect.query(Artist.class, nameSubstr, count)
+                    .where(nameFirstLetter.eq("a"))
+                    .having(count.gt(10L))
+                    .selectOne(context);
+        } catch (CayenneRuntimeException ex) {
+            if(unitDbAdapter.supportsExpressionInHaving()) {
+                fail();
+            } else {
+                return;
+            }
+        }
+        assertEquals("artist", q[0]);
+        assertEquals(20L, q[1]);
+    }
+
+    @Test
+    public void testSelectRelationshipCountHaving() throws Exception {
+        Property<Long> paintingCount = Property.create(countExp(Artist.PAINTING_ARRAY.path()), Long.class);
+
+        Object[] result = null;
+        try {
+            result = ColumnSelect.query(Artist.class)
+                .columns(Artist.ARTIST_NAME, paintingCount)
+                .having(paintingCount.gt(4L))
+                .selectOne(context);
+        } catch (CayenneRuntimeException ex) {
+            if(unitDbAdapter.supportsExpressionInHaving()) {
+                fail();
+            } else {
+                return;
+            }
+        }
+        assertEquals("artist2", result[0]);
+        assertEquals(5L, result[1]);
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4f860180/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectTest.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectTest.java
new file mode 100644
index 0000000..c058916
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectTest.java
@@ -0,0 +1,199 @@
+/*****************************************************************
+ *   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.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+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;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.0
+ */
+public class ColumnSelectTest {
+
+    @Test
+    public void query() throws Exception {
+        ColumnSelect<Artist> q = ColumnSelect.query(Artist.class);
+        assertNull(q.getColumns());
+        assertNull(q.getHaving());
+    }
+
+    @Test
+    public void queryWithColumn() throws Exception {
+        ColumnSelect<String> q = ColumnSelect.query(Artist.class, Artist.ARTIST_NAME);
+        assertEquals(Arrays.asList(Artist.ARTIST_NAME), q.getColumns());
+        assertNull(q.getHaving());
+    }
+
+    @Test
+    public void queryWithColumns() throws Exception {
+        ColumnSelect<Object[]> q = ColumnSelect.query(Artist.class, Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH);
+        assertEquals(Arrays.asList(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH), q.getColumns());
+        assertNull(q.getHaving());
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void columns() throws Exception {
+        ColumnSelect q = ColumnSelect.query(Artist.class);
+        assertNull(q.getColumns());
+        q.columns();
+        assertNull(q.getColumns());
+        q.columns(Artist.ARTIST_NAME, Artist.PAINTING_ARRAY);
+        assertEquals(Arrays.asList(Artist.ARTIST_NAME, Artist.PAINTING_ARRAY), q.getColumns());
+
+        q = ColumnSelect.query(Artist.class, Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH);
+        assertEquals(Arrays.asList(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH), q.getColumns());
+        q.columns(Artist.PAINTING_ARRAY);
+        assertEquals(Arrays.asList(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH, Artist.PAINTING_ARRAY), q.getColumns());
+    }
+
+
+    @Test
+    public void havingExpression() throws Exception {
+        ColumnSelect q = ColumnSelect.query(Artist.class);
+        assertNull(q.getHaving());
+        assertNull(q.getWhere());
+
+        Expression expTrue = ExpressionFactory.expTrue();
+        q.where(expTrue);
+        assertNull(q.getHaving());
+        assertEquals(expTrue, q.getWhere());
+
+        Expression expFalse = ExpressionFactory.expFalse();
+        q.having(expFalse);
+        assertEquals(expFalse, q.getHaving());
+        assertEquals(expTrue, q.getWhere());
+    }
+
+    @Test
+    public void havingString() throws Exception {
+        ColumnSelect q = ColumnSelect.query(Artist.class);
+        assertNull(q.getHaving());
+        assertNull(q.getWhere());
+
+        Expression expTrue = ExpressionFactory.expTrue();
+        q.where(expTrue);
+        assertNull(q.getHaving());
+        assertEquals(expTrue, q.getWhere());
+
+        Expression expFalse = ExpressionFactory.expFalse();
+        q.having("false");
+        assertEquals(expFalse, q.getHaving());
+        assertEquals(expTrue, q.getWhere());
+    }
+
+    @Test
+    public void and() throws Exception {
+        ColumnSelect q = ColumnSelect.query(Artist.class);
+        assertNull(q.getHaving());
+        assertNull(q.getWhere());
+
+        Expression expTrue = ExpressionFactory.expTrue();
+        q.where(expTrue);
+        q.and(expTrue);
+        assertNull(q.getHaving());
+        assertEquals(ExpressionFactory.exp("true and true"), q.getWhere());
+
+        Expression expFalse = ExpressionFactory.expFalse();
+        q.having("false");
+        q.and(expFalse);
+        assertEquals(ExpressionFactory.exp("false and false"), q.getHaving());
+        assertEquals(ExpressionFactory.exp("true and true"), q.getWhere());
+    }
+
+    @Test
+    public void or() throws Exception {
+        ColumnSelect q = ColumnSelect.query(Artist.class);
+        assertNull(q.getHaving());
+        assertNull(q.getWhere());
+
+        Expression expTrue = ExpressionFactory.expTrue();
+        q.where(expTrue);
+        q.or(expTrue);
+        assertNull(q.getHaving());
+        assertEquals(ExpressionFactory.exp("true or true"), q.getWhere());
+
+        Expression expFalse = ExpressionFactory.expFalse();
+        q.having("false");
+        q.or(expFalse);
+        assertEquals(ExpressionFactory.exp("false or false"), q.getHaving());
+        assertEquals(ExpressionFactory.exp("true or true"), q.getWhere());
+    }
+
+
+    @Test
+    public void testColumnsAddByOne() {
+        ColumnSelect<Artist> q = ColumnSelect.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() {
+        ColumnSelect<Artist> q = ColumnSelect.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() {
+        ColumnSelect<Artist> q = ColumnSelect.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());
+    }
+
+}
\ No newline at end of file


Mime
View raw message