cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aadamc...@apache.org
Subject [6/8] cayenne git commit: CAY-1959 Chainable API for SelectQuery
Date Sun, 16 Nov 2014 12:10:05 GMT
CAY-1959 Chainable API for SelectQuery


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

Branch: refs/heads/CAY-1946
Commit: a0f941a02bdc3320e38fd830983b5f034a4a306f
Parents: 5f7b6dd
Author: aadamchik <aadamchik@apache.org>
Authored: Sat Nov 15 13:08:20 2014 +0300
Committer: aadamchik <aadamchik@apache.org>
Committed: Sat Nov 15 13:25:11 2014 +0300

----------------------------------------------------------------------
 .../java/org/apache/cayenne/exp/Property.java   |  19 +-
 .../apache/cayenne/query/BaseQueryMetadata.java |   2 +-
 .../org/apache/cayenne/query/ObjectSelect.java  | 668 ++++++++++++++++++
 .../java/org/apache/cayenne/query/Ordering.java | 696 ++++++++++---------
 .../apache/cayenne/query/PrefetchTreeNode.java  |  15 +
 .../org/apache/cayenne/query/SelectQuery.java   |  10 +-
 .../cayenne/remote/IncrementalSelectQuery.java  |   5 +-
 .../apache/cayenne/query/ObjectSelectTest.java  | 482 +++++++++++++
 .../cayenne/query/ObjectSelect_CompileIT.java   | 168 +++++
 .../cayenne/query/ObjectSelect_RunIT.java       |  94 +++
 docs/doc/src/main/resources/RELEASE-NOTES.txt   |   1 +
 11 files changed, 1805 insertions(+), 355 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/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 48f1549..b42e890 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
@@ -385,9 +385,7 @@ public class Property<E> {
 	 * prefetch semantics.
 	 */
 	public PrefetchTreeNode joint() {
-		PrefetchTreeNode node = prefetch();
-		node.setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
-		return node.getRoot();
+		return PrefetchTreeNode.withPath(name, PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
 	}
 
 	/**
@@ -396,9 +394,7 @@ public class Property<E> {
 	 * "disjoint" prefetch semantics.
 	 */
 	public PrefetchTreeNode disjoint() {
-		PrefetchTreeNode node = prefetch();
-		node.setSemantics(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
-		return node.getRoot();
+		return PrefetchTreeNode.withPath(name, PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
 	}
 
 	/**
@@ -407,16 +403,7 @@ public class Property<E> {
 	 * "disjoint by id" prefetch semantics.
 	 */
 	public PrefetchTreeNode disjointById() {
-		PrefetchTreeNode node = prefetch();
-		node.setSemantics(PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS);
-		return node.getRoot();
-	}
-
-	PrefetchTreeNode prefetch() {
-		PrefetchTreeNode root = new PrefetchTreeNode();
-		PrefetchTreeNode node = root.addPath(name);
-		node.setPhantom(false);
-		return node;
+		return PrefetchTreeNode.withPath(name, PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS);
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java b/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java
index 1f17aac..d85b85a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java
@@ -44,7 +44,7 @@ import org.apache.cayenne.util.XMLSerializable;
 class BaseQueryMetadata implements QueryMetadata, XMLSerializable, Serializable {
 
 	private static final long serialVersionUID = 5129792493303459115L;
-	
+
 	int fetchLimit = QueryMetadata.FETCH_LIMIT_DEFAULT;
 	int fetchOffset = QueryMetadata.FETCH_OFFSET_DEFAULT;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/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
new file mode 100644
index 0000000..7618bcc
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java
@@ -0,0 +1,668 @@
+/*****************************************************************
+ *   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.List;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.ObjectContext;
+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;
+
+/**
+ * A selecting query providing chainable API. Can be viewed as an alternative to
+ * {@link SelectQuery}.
+ * 
+ * @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 exp;
+	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).exp(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).exp(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().exp(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).exp(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(exp);
+		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;
+	}
+
+	/**
+	 * Initializes or resets a qualifier expression of this query.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> exp(Expression expression) {
+		this.exp = expression;
+		return this;
+	}
+
+	/**
+	 * Initializes or resets a qualifier expression of this query, using
+	 * provided expression String and an array of position parameters.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> exp(String expressionString, Object... parameters) {
+		this.exp = ExpressionFactory.exp(expressionString, parameters);
+		return this;
+	}
+
+	/**
+	 * AND's provided expressions to the existing qualifier 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 qualifier expression.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> and(Collection<Expression> expressions) {
+
+		if (expressions == null || expressions.isEmpty()) {
+			return this;
+		}
+
+		Collection<Expression> all;
+
+		if (exp != null) {
+			all = new ArrayList<Expression>(expressions.size() + 1);
+			all.add(exp);
+			all.addAll(expressions);
+		} else {
+			all = expressions;
+		}
+
+		exp = ExpressionFactory.and(all);
+		return this;
+	}
+
+	/**
+	 * OR's provided expressions to the existing qualifier 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 qualifier expression.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> or(Collection<Expression> expressions) {
+		if (expressions == null || expressions.isEmpty()) {
+			return this;
+		}
+
+		Collection<Expression> all;
+
+		if (exp != null) {
+			all = new ArrayList<Expression>(expressions.size() + 1);
+			all.add(exp);
+			all.addAll(expressions);
+		} else {
+			all = expressions;
+		}
+
+		exp = ExpressionFactory.or(all);
+		return this;
+	}
+
+	/**
+	 * Initializes ordering clause of this query with a single ascending
+	 * ordering on a given property.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> orderBy(String property) {
+		return orderBy(new Ordering(property));
+	}
+
+	/**
+	 * Initializes ordering clause of this query with a single ordering on a
+	 * given property.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> orderBy(String property, SortOrder sortOrder) {
+		return orderBy(new Ordering(property, sortOrder));
+	}
+
+	/**
+	 * Initializes or resets a list of orderings of this query.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> orderBy(Ordering... orderings) {
+
+		if (this.orderings != null) {
+			this.orderings.clear();
+		}
+
+		return addOrderBy(orderings);
+	}
+
+	/**
+	 * Initializes or resets a list of orderings of this query.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> orderBy(Collection<Ordering> orderings) {
+
+		if (this.orderings != null) {
+			this.orderings.clear();
+		}
+
+		return addOrderBy(orderings);
+	}
+
+	/**
+	 * Adds a single ascending ordering on a given property to the existing
+	 * ordering clause of this query.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> addOrderBy(String property) {
+		return addOrderBy(new Ordering(property));
+	}
+
+	/**
+	 * Adds a single ordering on a given property to the existing ordering
+	 * clause of this query.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> addOrderBy(String property, SortOrder sortOrder) {
+		return addOrderBy(new Ordering(property, sortOrder));
+	}
+
+	/**
+	 * Adds new orderings to the list of the existing orderings.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> addOrderBy(Ordering... orderings) {
+
+		if (orderings == null || orderings == null) {
+			return this;
+		}
+
+		if (this.orderings == null) {
+			this.orderings = new ArrayList<Ordering>(orderings.length);
+		}
+
+		for (Ordering o : orderings) {
+			this.orderings.add(o);
+		}
+
+		return this;
+	}
+
+	/**
+	 * Adds new orderings to the list of orderings.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> addOrderBy(Collection<Ordering> orderings) {
+
+		if (orderings == null || orderings == null) {
+			return this;
+		}
+
+		if (this.orderings == null) {
+			this.orderings = new ArrayList<Ordering>(orderings.size());
+		}
+
+		this.orderings.addAll(orderings);
+
+		return this;
+	}
+
+	/**
+	 * Resets internal prefetches to the new value, which is a single prefetch
+	 * with specified semantics.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> prefetch(String path, int semantics) {
+		this.prefetches = PrefetchTreeNode.withPath(path, semantics);
+		return this;
+	}
+
+	/**
+	 * Resets internal prefetches to the new value.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> prefetch(PrefetchTreeNode prefetch) {
+		this.prefetches = prefetch;
+		return this;
+	}
+
+	/**
+	 * Merges prefetch into the query prefetch tree.
+	 * 
+	 * @return this object
+	 */
+	public ObjectSelect<T> addPrefetch(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> addPrefetch(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;
+	}
+
+	public Expression getExp() {
+		return exp;
+	}
+
+	public Collection<Ordering> getOrderings() {
+		return orderings;
+	}
+
+	public PrefetchTreeNode getPrefetches() {
+		return prefetches;
+	}
+
+	/**
+	 * Selects objects using provided context. Essentially the inversion of
+	 * "ObjectContext.select(query)".
+	 */
+	public List<T> select(ObjectContext context) {
+		return context.select(this);
+	}
+
+	/**
+	 * Selects a single object using provided context. Essentially the inversion
+	 * of "ObjectContext.selectOne(Select)".
+	 */
+	public T selectOne(ObjectContext context) {
+		return context.selectOne(this);
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/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 08c8544..ccd7a58 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
@@ -37,339 +37,371 @@ import org.apache.cayenne.util.XMLSerializable;
 import org.apache.commons.collections.ComparatorUtils;
 
 /**
- * Defines object sorting criteria, used either for in-memory sorting of object lists or
- * as a specification for building <em>ORDER BY</em> clause of a SelectQuery query. Note
- * that in case of in-memory sorting, Ordering can be used with any JavaBeans, not just
- * DataObjects.
+ * Defines object sorting criteria, used either for in-memory sorting of object
+ * lists or as a specification for building <em>ORDER BY</em> clause of a
+ * SelectQuery query. Note that in case of in-memory sorting, Ordering can be
+ * used with any JavaBeans, not just DataObjects.
  */
 public class Ordering implements Comparator<Object>, Serializable, XMLSerializable {
 
-    protected String sortSpecString;
-    protected transient Expression sortSpec;
-    protected SortOrder sortOrder;
-    protected boolean pathExceptionSuppressed = false;
-    protected boolean nullSortedFirst = true;
-
-    /**
-     * Orders a given list of objects, using a List of Orderings applied according the
-     * default iteration order of the Orderings list. I.e. each Ordering with lower index
-     * is more significant than any other Ordering with higher index. List being ordered
-     * is modified in place.
-     */
-    public static void orderList(List<?> objects, List<? extends Ordering> orderings) {
-        Collections.sort(objects, ComparatorUtils.chainedComparator(orderings));
-    }
-
-    public Ordering() {
-    }
-
-    /**
-     * @since 3.0
-     */
-    public Ordering(String sortPathSpec, SortOrder sortOrder) {
-        setSortSpecString(sortPathSpec);
-        setSortOrder(sortOrder);
-    }
-
-    /**
-     * Sets sortSpec to be an expression represented by string argument.
-     * 
-     * @since 1.1
-     */
-    public void setSortSpecString(String sortSpecString) {
-        if (!Util.nullSafeEquals(this.sortSpecString, sortSpecString)) {
-            this.sortSpecString = sortSpecString;
-            this.sortSpec = null;
-        }
-    }
-
-    /**
-     * Sets sort order for whether nulls are at the top or bottom of the resulting list.
-     * Default is true.
-     * 
-     * @param nullSortedFirst true sorts nulls to the top of the list, false sorts nulls
-     *            to the bottom
-     */
-    public void setNullSortedFirst(boolean nullSortedFirst) {
-        this.nullSortedFirst = nullSortedFirst;
-    }
-
-    /**
-     * Get sort order for nulls.
-     * 
-     * @return true if nulls are sorted to the top of the list, false if sorted to the
-     *         bottom
-     */
-    public boolean isNullSortedFirst() {
-        return nullSortedFirst;
-    }
-
-    /**
-     * Sets whether a path with a null in the middle is ignored. For example, a sort from
-     * <code>painting</code> on <code>artist.name</code> would by default throw an
-     * exception if the artist was null. If set to true, then this is treated just like a
-     * null value. Default is false.
-     * 
-     * @param pathExceptionSuppressed true to suppress exceptions and sort as null
-     */
-    public void setPathExceptionSupressed(boolean pathExceptionSuppressed) {
-        this.pathExceptionSuppressed = pathExceptionSuppressed;
-    }
-
-    /**
-     * Is a path with a null in the middle is ignored.
-     * 
-     * @return true is exception is suppressed and sorted as null
-     */
-    public boolean isPathExceptionSuppressed() {
-        return pathExceptionSuppressed;
-    }
-
-    /**
-     * Returns sortSpec string representation.
-     * 
-     * @since 1.1
-     */
-    public String getSortSpecString() {
-        return sortSpecString;
-    }
-
-    /**
-     * Sets the sort order for this ordering.
-     * 
-     * @since 3.0
-     */
-    public void setSortOrder(SortOrder order) {
-        this.sortOrder = order;
-    }
-
-    /** Returns true if sorting is done in ascending order. */
-    public boolean isAscending() {
-        return sortOrder == null
-                || sortOrder == SortOrder.ASCENDING
-                || sortOrder == SortOrder.ASCENDING_INSENSITIVE;
-    }
-
-    /**
-     * Returns true if the sorting is done in descending order.
-     * 
-     * @since 3.0
-     */
-    public boolean isDescending() {
-        return !isAscending();
-    }
-
-    /**
-     * If the sort order is DESCENDING or DESCENDING_INSENSITIVE, sets the sort order to
-     * ASCENDING or ASCENDING_INSENSITIVE, respectively.
-     * 
-     * @since 3.0
-     */
-    public void setAscending() {
-        if (sortOrder == null || sortOrder == SortOrder.DESCENDING)
-            setSortOrder(SortOrder.ASCENDING);
-        else if (sortOrder == SortOrder.DESCENDING_INSENSITIVE)
-            setSortOrder(SortOrder.ASCENDING_INSENSITIVE);
-    }
-
-    /**
-     * If the sort order is ASCENDING or ASCENDING_INSENSITIVE, sets the sort order to
-     * DESCENDING or DESCENDING_INSENSITIVE, respectively.
-     * 
-     * @since 3.0
-     */
-    public void setDescending() {
-        if (sortOrder == null || sortOrder == SortOrder.ASCENDING)
-            setSortOrder(SortOrder.DESCENDING);
-        else if (sortOrder == SortOrder.ASCENDING_INSENSITIVE)
-            setSortOrder(SortOrder.DESCENDING_INSENSITIVE);
-    }
-
-    /** Returns true if the sorting is case insensitive */
-    public boolean isCaseInsensitive() {
-        return !isCaseSensitive();
-    }
-
-    /**
-     * Returns true if the sorting is case sensitive.
-     * 
-     * @since 3.0
-     */
-    public boolean isCaseSensitive() {
-        return sortOrder == null
-                || sortOrder == SortOrder.ASCENDING
-                || sortOrder == SortOrder.DESCENDING;
-    }
-
-    /**
-     * If the sort order is ASCENDING or DESCENDING, sets the sort order to
-     * ASCENDING_INSENSITIVE or DESCENDING_INSENSITIVE, respectively.
-     * 
-     * @since 3.0
-     */
-    public void setCaseInsensitive() {
-        if (sortOrder == null || sortOrder == SortOrder.ASCENDING)
-            setSortOrder(SortOrder.ASCENDING_INSENSITIVE);
-        else if (sortOrder == SortOrder.DESCENDING)
-            setSortOrder(SortOrder.DESCENDING_INSENSITIVE);
-    }
-
-    /**
-     * If the sort order is ASCENDING_INSENSITIVE or DESCENDING_INSENSITIVE, sets the sort
-     * order to ASCENDING or DESCENDING, respectively.
-     * 
-     * @since 3.0
-     */
-    public void setCaseSensitive() {
-        if (sortOrder == null || sortOrder == SortOrder.ASCENDING_INSENSITIVE)
-            setSortOrder(SortOrder.ASCENDING);
-        else if (sortOrder == SortOrder.DESCENDING_INSENSITIVE)
-            setSortOrder(SortOrder.DESCENDING);
-    }
-
-    /**
-     * Returns the expression defining a ordering Java Bean property.
-     */
-    public Expression getSortSpec() {
-        if (sortSpecString == null) {
-            return null;
-        }
-
-        // compile on demand .. since orderings can only be paths, avoid the overhead of
-        // Expression.fromString, and parse them manually
-        if (sortSpec == null) {
-
-            if (sortSpecString.startsWith(ASTDbPath.DB_PREFIX)) {
-                sortSpec = new ASTDbPath(sortSpecString.substring(ASTDbPath.DB_PREFIX
-                        .length()));
-            }
-            else if (sortSpecString.startsWith(ASTObjPath.OBJ_PREFIX)) {
-                sortSpec = new ASTObjPath(sortSpecString.substring(ASTObjPath.OBJ_PREFIX
-                        .length()));
-            }
-            else {
-                sortSpec = new ASTObjPath(sortSpecString);
-            }
-        }
-
-        return sortSpec;
-    }
-
-    /**
-     * Sets the expression defining a ordering Java Bean property.
-     */
-    public void setSortSpec(Expression sortSpec) {
-        this.sortSpec = sortSpec;
-        this.sortSpecString = (sortSpec != null) ? sortSpec.toString() : null;
-    }
-
-    /**
-     * Orders the given list of objects according to the ordering that this object
-     * specifies. List is modified in-place.
-     * 
-     * @param objects a List of objects to be sorted
-     */
-    public void orderList(List<?> objects) {
-        Collections.sort(objects, this);
-    }
-
-    /**
-     * Comparable interface implementation. Can compare two Java Beans based on the stored
-     * expression.
-     */
-    public int compare(Object o1, Object o2) {
-        Expression exp = getSortSpec();
-        Object value1 = null;
-        Object value2 = null;
-        try {
-            value1 = exp.evaluate(o1);
-        }
-        catch (ExpressionException e) {
-            if (pathExceptionSuppressed
-                    && e.getCause() instanceof org.apache.cayenne.reflect.UnresolvablePathException) {
-                // do nothing, we expect this
-            }
-            else {
-                // re-throw
-                throw e;
-            }
-        }
-
-        try {
-            value2 = exp.evaluate(o2);
-        }
-        catch (ExpressionException e) {
-            if (pathExceptionSuppressed
-                    && e.getCause() instanceof org.apache.cayenne.reflect.UnresolvablePathException) {
-                // do nothing, we expect this
-            }
-            else {
-                // rethrow
-                throw e;
-            }
-        }
-
-        if (value1 == null && value2 == null) {
-            return 0;
-        }
-        else if (value1 == null) {
-            return nullSortedFirst ? -1 : 1;
-        }
-        else if (value2 == null) {
-            return nullSortedFirst ? 1 : -1;
-        }
-
-        if (isCaseInsensitive()) {
-            // TODO: to upper case should probably be defined as a separate expression
-            // type
-            value1 = ConversionUtil.toUpperCase(value1);
-            value2 = ConversionUtil.toUpperCase(value2);
-        }
-
-        int compareResult = ConversionUtil.toComparable(value1).compareTo(
-                ConversionUtil.toComparable(value2));
-        return (isAscending()) ? compareResult : -compareResult;
-    }
-
-    /**
-     * Encodes itself as a query ordering.
-     * 
-     * @since 1.1
-     */
-    public void encodeAsXML(XMLEncoder encoder) {
-        encoder.print("<ordering");
-
-        if (isDescending()) {
-            encoder.print(" descending=\"true\"");
-        }
-
-        if (isCaseInsensitive()) {
-            encoder.print(" ignore-case=\"true\"");
-        }
-
-        encoder.print(">");
-        if (getSortSpec() != null) {
-            getSortSpec().encodeAsXML(encoder);
-        }
-        encoder.println("</ordering>");
-    }
-
-    @Override
-    public String toString() {
-        StringWriter buffer = new StringWriter();
-        PrintWriter pw = new PrintWriter(buffer);
-        XMLEncoder encoder = new XMLEncoder(pw);
-        encodeAsXML(encoder);
-        pw.close();
-        buffer.flush();
-        return buffer.toString();
-    }
-    
-    /**
-     * Returns sort order for this ordering
-     * @since 3.1
-     */
-    public SortOrder getSortOrder() {
-        return sortOrder;
-    }
+	private static final long serialVersionUID = -9167074787055881422L;
+
+	protected String sortSpecString;
+	protected transient Expression sortSpec;
+	protected SortOrder sortOrder;
+	protected boolean pathExceptionSuppressed = false;
+	protected boolean nullSortedFirst = true;
+
+	/**
+	 * Orders a given list of objects, using a List of Orderings applied
+	 * according the default iteration order of the Orderings list. I.e. each
+	 * Ordering with lower index is more significant than any other Ordering
+	 * with higher index. List being ordered is modified in place.
+	 */
+	public static void orderList(List<?> objects, List<? extends Ordering> orderings) {
+		Collections.sort(objects, ComparatorUtils.chainedComparator(orderings));
+	}
+
+	public Ordering() {
+	}
+
+	/**
+	 * Create an ordering instance with a provided path and ascending sorting
+	 * strategy.
+	 * 
+	 * @since 4.0
+	 */
+	public Ordering(String sortPathSpec) {
+		this(sortPathSpec, SortOrder.ASCENDING);
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	public Ordering(String sortPathSpec, SortOrder sortOrder) {
+		setSortSpecString(sortPathSpec);
+		setSortOrder(sortOrder);
+	}
+
+	@Override
+	public boolean equals(Object object) {
+		if (this == object) {
+			return true;
+		}
+
+		if (!(object instanceof Ordering)) {
+			return false;
+		}
+
+		Ordering o = (Ordering) object;
+
+		if (!Util.nullSafeEquals(sortSpecString, o.sortSpecString)) {
+			return false;
+		}
+
+		if (sortOrder != o.sortOrder) {
+			return false;
+		}
+
+		if (pathExceptionSuppressed != o.pathExceptionSuppressed) {
+			return false;
+		}
+
+		if (nullSortedFirst != o.nullSortedFirst) {
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * Sets sortSpec to be an expression represented by string argument.
+	 * 
+	 * @since 1.1
+	 */
+	public void setSortSpecString(String sortSpecString) {
+		if (!Util.nullSafeEquals(this.sortSpecString, sortSpecString)) {
+			this.sortSpecString = sortSpecString;
+			this.sortSpec = null;
+		}
+	}
+
+	/**
+	 * Sets sort order for whether nulls are at the top or bottom of the
+	 * resulting list. Default is true.
+	 * 
+	 * @param nullSortedFirst
+	 *            true sorts nulls to the top of the list, false sorts nulls to
+	 *            the bottom
+	 */
+	public void setNullSortedFirst(boolean nullSortedFirst) {
+		this.nullSortedFirst = nullSortedFirst;
+	}
+
+	/**
+	 * Get sort order for nulls.
+	 * 
+	 * @return true if nulls are sorted to the top of the list, false if sorted
+	 *         to the bottom
+	 */
+	public boolean isNullSortedFirst() {
+		return nullSortedFirst;
+	}
+
+	/**
+	 * Sets whether a path with a null in the middle is ignored. For example, a
+	 * sort from <code>painting</code> on <code>artist.name</code> would by
+	 * default throw an exception if the artist was null. If set to true, then
+	 * this is treated just like a null value. Default is false.
+	 * 
+	 * @param pathExceptionSuppressed
+	 *            true to suppress exceptions and sort as null
+	 */
+	public void setPathExceptionSupressed(boolean pathExceptionSuppressed) {
+		this.pathExceptionSuppressed = pathExceptionSuppressed;
+	}
+
+	/**
+	 * Is a path with a null in the middle is ignored.
+	 * 
+	 * @return true is exception is suppressed and sorted as null
+	 */
+	public boolean isPathExceptionSuppressed() {
+		return pathExceptionSuppressed;
+	}
+
+	/**
+	 * Returns sortSpec string representation.
+	 * 
+	 * @since 1.1
+	 */
+	public String getSortSpecString() {
+		return sortSpecString;
+	}
+
+	/**
+	 * Sets the sort order for this ordering.
+	 * 
+	 * @since 3.0
+	 */
+	public void setSortOrder(SortOrder order) {
+		this.sortOrder = order;
+	}
+
+	/** Returns true if sorting is done in ascending order. */
+	public boolean isAscending() {
+		return sortOrder == null || sortOrder == SortOrder.ASCENDING || sortOrder == SortOrder.ASCENDING_INSENSITIVE;
+	}
+
+	/**
+	 * Returns true if the sorting is done in descending order.
+	 * 
+	 * @since 3.0
+	 */
+	public boolean isDescending() {
+		return !isAscending();
+	}
+
+	/**
+	 * If the sort order is DESCENDING or DESCENDING_INSENSITIVE, sets the sort
+	 * order to ASCENDING or ASCENDING_INSENSITIVE, respectively.
+	 * 
+	 * @since 3.0
+	 */
+	public void setAscending() {
+		if (sortOrder == null || sortOrder == SortOrder.DESCENDING)
+			setSortOrder(SortOrder.ASCENDING);
+		else if (sortOrder == SortOrder.DESCENDING_INSENSITIVE)
+			setSortOrder(SortOrder.ASCENDING_INSENSITIVE);
+	}
+
+	/**
+	 * If the sort order is ASCENDING or ASCENDING_INSENSITIVE, sets the sort
+	 * order to DESCENDING or DESCENDING_INSENSITIVE, respectively.
+	 * 
+	 * @since 3.0
+	 */
+	public void setDescending() {
+		if (sortOrder == null || sortOrder == SortOrder.ASCENDING)
+			setSortOrder(SortOrder.DESCENDING);
+		else if (sortOrder == SortOrder.ASCENDING_INSENSITIVE)
+			setSortOrder(SortOrder.DESCENDING_INSENSITIVE);
+	}
+
+	/** Returns true if the sorting is case insensitive */
+	public boolean isCaseInsensitive() {
+		return !isCaseSensitive();
+	}
+
+	/**
+	 * Returns true if the sorting is case sensitive.
+	 * 
+	 * @since 3.0
+	 */
+	public boolean isCaseSensitive() {
+		return sortOrder == null || sortOrder == SortOrder.ASCENDING || sortOrder == SortOrder.DESCENDING;
+	}
+
+	/**
+	 * If the sort order is ASCENDING or DESCENDING, sets the sort order to
+	 * ASCENDING_INSENSITIVE or DESCENDING_INSENSITIVE, respectively.
+	 * 
+	 * @since 3.0
+	 */
+	public void setCaseInsensitive() {
+		if (sortOrder == null || sortOrder == SortOrder.ASCENDING)
+			setSortOrder(SortOrder.ASCENDING_INSENSITIVE);
+		else if (sortOrder == SortOrder.DESCENDING)
+			setSortOrder(SortOrder.DESCENDING_INSENSITIVE);
+	}
+
+	/**
+	 * If the sort order is ASCENDING_INSENSITIVE or DESCENDING_INSENSITIVE,
+	 * sets the sort order to ASCENDING or DESCENDING, respectively.
+	 * 
+	 * @since 3.0
+	 */
+	public void setCaseSensitive() {
+		if (sortOrder == null || sortOrder == SortOrder.ASCENDING_INSENSITIVE)
+			setSortOrder(SortOrder.ASCENDING);
+		else if (sortOrder == SortOrder.DESCENDING_INSENSITIVE)
+			setSortOrder(SortOrder.DESCENDING);
+	}
+
+	/**
+	 * Returns the expression defining a ordering Java Bean property.
+	 */
+	public Expression getSortSpec() {
+		if (sortSpecString == null) {
+			return null;
+		}
+
+		// compile on demand .. since orderings can only be paths, avoid the
+		// overhead of
+		// Expression.fromString, and parse them manually
+		if (sortSpec == null) {
+
+			if (sortSpecString.startsWith(ASTDbPath.DB_PREFIX)) {
+				sortSpec = new ASTDbPath(sortSpecString.substring(ASTDbPath.DB_PREFIX.length()));
+			} else if (sortSpecString.startsWith(ASTObjPath.OBJ_PREFIX)) {
+				sortSpec = new ASTObjPath(sortSpecString.substring(ASTObjPath.OBJ_PREFIX.length()));
+			} else {
+				sortSpec = new ASTObjPath(sortSpecString);
+			}
+		}
+
+		return sortSpec;
+	}
+
+	/**
+	 * Sets the expression defining a ordering Java Bean property.
+	 */
+	public void setSortSpec(Expression sortSpec) {
+		this.sortSpec = sortSpec;
+		this.sortSpecString = (sortSpec != null) ? sortSpec.toString() : null;
+	}
+
+	/**
+	 * Orders the given list of objects according to the ordering that this
+	 * object specifies. List is modified in-place.
+	 * 
+	 * @param objects
+	 *            a List of objects to be sorted
+	 */
+	public void orderList(List<?> objects) {
+		Collections.sort(objects, this);
+	}
+
+	/**
+	 * Comparable interface implementation. Can compare two Java Beans based on
+	 * the stored expression.
+	 */
+	public int compare(Object o1, Object o2) {
+		Expression exp = getSortSpec();
+		Object value1 = null;
+		Object value2 = null;
+		try {
+			value1 = exp.evaluate(o1);
+		} catch (ExpressionException e) {
+			if (pathExceptionSuppressed && e.getCause() instanceof org.apache.cayenne.reflect.UnresolvablePathException) {
+				// do nothing, we expect this
+			} else {
+				// re-throw
+				throw e;
+			}
+		}
+
+		try {
+			value2 = exp.evaluate(o2);
+		} catch (ExpressionException e) {
+			if (pathExceptionSuppressed && e.getCause() instanceof org.apache.cayenne.reflect.UnresolvablePathException) {
+				// do nothing, we expect this
+			} else {
+				// rethrow
+				throw e;
+			}
+		}
+
+		if (value1 == null && value2 == null) {
+			return 0;
+		} else if (value1 == null) {
+			return nullSortedFirst ? -1 : 1;
+		} else if (value2 == null) {
+			return nullSortedFirst ? 1 : -1;
+		}
+
+		if (isCaseInsensitive()) {
+			// TODO: to upper case should probably be defined as a separate
+			// expression
+			// type
+			value1 = ConversionUtil.toUpperCase(value1);
+			value2 = ConversionUtil.toUpperCase(value2);
+		}
+
+		int compareResult = ConversionUtil.toComparable(value1).compareTo(ConversionUtil.toComparable(value2));
+		return (isAscending()) ? compareResult : -compareResult;
+	}
+
+	/**
+	 * Encodes itself as a query ordering.
+	 * 
+	 * @since 1.1
+	 */
+	public void encodeAsXML(XMLEncoder encoder) {
+		encoder.print("<ordering");
+
+		if (isDescending()) {
+			encoder.print(" descending=\"true\"");
+		}
+
+		if (isCaseInsensitive()) {
+			encoder.print(" ignore-case=\"true\"");
+		}
+
+		encoder.print(">");
+		if (getSortSpec() != null) {
+			getSortSpec().encodeAsXML(encoder);
+		}
+		encoder.println("</ordering>");
+	}
+
+	@Override
+	public String toString() {
+		StringWriter buffer = new StringWriter();
+		PrintWriter pw = new PrintWriter(buffer);
+		XMLEncoder encoder = new XMLEncoder(pw);
+		encodeAsXML(encoder);
+		pw.close();
+		buffer.flush();
+		return buffer.toString();
+	}
+
+	/**
+	 * Returns sort order for this ordering
+	 * 
+	 * @since 3.1
+	 */
+	public SortOrder getSortOrder() {
+		return sortOrder;
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java b/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java
index 11711fc..e44b08d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java
@@ -63,6 +63,21 @@ public class PrefetchTreeNode implements Serializable, XMLSerializable {
 	protected Collection<PrefetchTreeNode> children;
 
 	/**
+	 * Creates and returns a prefetch tree spanning a single path. The tree is
+	 * made of phantom nodes, up to the leaf node, which is non-phantom and has
+	 * specified semantics.
+	 * 
+	 * @since 4.0
+	 */
+	public static PrefetchTreeNode withPath(String path, int semantics) {
+		PrefetchTreeNode root = new PrefetchTreeNode();
+		PrefetchTreeNode node = root.addPath(path);
+		node.setPhantom(false);
+		node.setSemantics(semantics);
+		return root;
+	}
+
+	/**
 	 * Creates a root node of the prefetch tree. Children can be added to the
 	 * parent by calling "addPath".
 	 */

http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/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 80dba3f..8618bed 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
@@ -20,6 +20,7 @@
 package org.apache.cayenne.query;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -44,7 +45,7 @@ import org.apache.cayenne.util.XMLSerializable;
 public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery, XMLSerializable, Select<T> {
 
 	private static final long serialVersionUID = 5486418811888197559L;
-	
+
 	public static final String DISTINCT_PROPERTY = "cayenne.SelectQuery.distinct";
 	public static final boolean DISTINCT_DEFAULT = false;
 
@@ -500,11 +501,12 @@ public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery,
 	/**
 	 * Adds a list of orderings.
 	 */
-	public void addOrderings(List<? extends Ordering> orderings) {
+	public void addOrderings(Collection<? extends Ordering> orderings) {
 		// If the supplied list of orderings is null, do not attempt to add
 		// to the collection (addAll() will NPE otherwise).
-		if (orderings != null)
+		if (orderings != null) {
 			nonNullOrderings().addAll(orderings);
+		}
 	}
 
 	/**
@@ -589,7 +591,7 @@ public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery,
 	 * @since 4.0
 	 */
 	public void addPrefetch(PrefetchTreeNode prefetchElement) {
-		 metaData.mergePrefetch(prefetchElement);
+		metaData.mergePrefetch(prefetchElement);
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/cayenne-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java b/cayenne-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java
index 2e6150b..bf935df 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.remote;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
@@ -156,8 +157,8 @@ class IncrementalSelectQuery<T> extends SelectQuery<T> {
     }
 
     @Override
-    public void addOrderings(List orderings) {
-        query.addOrderings(orderings);
+    public void addOrderings(Collection<? extends Ordering> orderings) {
+    	query.addOrderings(orderings);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/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
new file mode 100644
index 0000000..cd69200
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
@@ -0,0 +1,482 @@
+/*****************************************************************
+ *   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 static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collection;
+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.testdo.testmap.Artist;
+import org.junit.Test;
+
+public class ObjectSelectTest {
+
+	@Test
+	public void testDataRowQuery() {
+		ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class);
+		assertNotNull(q);
+		assertTrue(q.isFetchingDataRows());
+
+		assertEquals(Artist.class, q.getEntityType());
+		assertNull(q.getEntityName());
+		assertNull(q.getDbEntityName());
+	}
+
+	@Test
+	public void testQuery_RootType() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+		assertNotNull(q);
+		assertNull(q.getExp());
+		assertFalse(q.isFetchingDataRows());
+
+		assertEquals(Artist.class, q.getEntityType());
+		assertNull(q.getEntityName());
+		assertNull(q.getDbEntityName());
+	}
+
+	@Test
+	public void testQuery_RootType_WithQualifier() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class, ExpressionFactory.matchExp("a", "A"));
+		assertNotNull(q);
+		assertEquals("a = \"A\"", q.getExp().toString());
+		assertFalse(q.isFetchingDataRows());
+
+		assertEquals(Artist.class, q.getEntityType());
+		assertNull(q.getEntityName());
+		assertNull(q.getDbEntityName());
+	}
+
+	@Test
+	public void testQuery_TypeAndEntity() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class, "Painting");
+		assertNotNull(q);
+		assertFalse(q.isFetchingDataRows());
+
+		assertNull(q.getEntityType());
+		assertEquals("Painting", q.getEntityName());
+		assertNull(q.getDbEntityName());
+	}
+
+	@Test
+	public void testQuery_TypeAndDbEntity() {
+		ObjectSelect<DataRow> q = ObjectSelect.dbQuery("PAINTING");
+		assertNotNull(q);
+		assertTrue(q.isFetchingDataRows());
+
+		assertNull(q.getEntityType());
+		assertNull(q.getEntityName());
+		assertEquals("PAINTING", q.getDbEntityName());
+	}
+
+	@Test
+	public void testExp() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		q.exp(ExpressionFactory.matchExp("a", 3));
+		assertEquals("a = 3", q.getExp().toString());
+
+		q.exp(ExpressionFactory.matchExp("b", 4));
+		assertEquals("b = 4", q.getExp().toString());
+	}
+
+	@Test
+	public void testAnd_Array() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		q.exp(ExpressionFactory.matchExp("a", 3));
+		assertEquals("a = 3", q.getExp().toString());
+
+		q.and(ExpressionFactory.matchExp("b", 4), ExpressionFactory.greaterExp("c", 5));
+		assertEquals("(a = 3) and (b = 4) and (c > 5)", q.getExp().toString());
+	}
+
+	@Test
+	public void testAnd_Collection() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		q.exp(ExpressionFactory.matchExp("a", 3));
+		assertEquals("a = 3", q.getExp().toString());
+
+		Collection<Expression> exps = Arrays.asList(ExpressionFactory.matchExp("b", 4),
+				ExpressionFactory.greaterExp("c", 5));
+
+		q.and(exps);
+		assertEquals("(a = 3) and (b = 4) and (c > 5)", q.getExp().toString());
+	}
+
+	@Test
+	public void testAnd_ArrayNull() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		q.exp(ExpressionFactory.matchExp("a", 3));
+		assertEquals("a = 3", q.getExp().toString());
+
+		q.and();
+		assertEquals("a = 3", q.getExp().toString());
+	}
+
+	@Test
+	public void testAnd_ArrayEmpty() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		q.exp(ExpressionFactory.matchExp("a", 3));
+		assertEquals("a = 3", q.getExp().toString());
+
+		q.and(new Expression[0]);
+		assertEquals("a = 3", q.getExp().toString());
+	}
+
+	@Test
+	public void testAnd_CollectionEmpty() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		q.exp(ExpressionFactory.matchExp("a", 3));
+		assertEquals("a = 3", q.getExp().toString());
+
+		q.and(Collections.<Expression> emptyList());
+		assertEquals("a = 3", q.getExp().toString());
+	}
+
+	@Test
+	public void testOr_Array() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		q.exp(ExpressionFactory.matchExp("a", 3));
+		assertEquals("a = 3", q.getExp().toString());
+
+		q.or(ExpressionFactory.matchExp("b", 4), ExpressionFactory.greaterExp("c", 5));
+		assertEquals("(a = 3) or (b = 4) or (c > 5)", q.getExp().toString());
+	}
+
+	@Test
+	public void testOr_Collection() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		q.exp(ExpressionFactory.matchExp("a", 3));
+		assertEquals("a = 3", q.getExp().toString());
+
+		Collection<Expression> exps = Arrays.asList(ExpressionFactory.matchExp("b", 4),
+				ExpressionFactory.greaterExp("c", 5));
+
+		q.or(exps);
+		assertEquals("(a = 3) or (b = 4) or (c > 5)", q.getExp().toString());
+	}
+
+	@Test
+	public void testOrderBy_Array() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		Ordering o1 = new Ordering("x");
+		q.orderBy(o1);
+
+		Object[] result1 = q.getOrderings().toArray();
+		assertEquals(1, result1.length);
+		assertSame(o1, result1[0]);
+
+		Ordering o2 = new Ordering("y");
+		q.orderBy(o2);
+
+		Object[] result2 = q.getOrderings().toArray();
+		assertEquals(1, result2.length);
+		assertSame(o2, result2[0]);
+	}
+
+	@Test
+	public void testAddOrderBy_Array() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		Ordering o1 = new Ordering("x");
+		q.orderBy(o1);
+
+		Object[] result1 = q.getOrderings().toArray();
+		assertEquals(1, result1.length);
+		assertSame(o1, result1[0]);
+
+		Ordering o2 = new Ordering("y");
+		q.addOrderBy(o2);
+
+		Object[] result2 = q.getOrderings().toArray();
+		assertEquals(2, result2.length);
+		assertSame(o1, result2[0]);
+		assertSame(o2, result2[1]);
+	}
+
+	@Test
+	public void testOrderBy_Collection() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		Ordering o1 = new Ordering("x");
+		q.orderBy(Collections.singletonList(o1));
+
+		Object[] result1 = q.getOrderings().toArray();
+		assertEquals(1, result1.length);
+		assertSame(o1, result1[0]);
+
+		Ordering o2 = new Ordering("y");
+		q.orderBy(Collections.singletonList(o2));
+
+		Object[] result2 = q.getOrderings().toArray();
+		assertEquals(1, result2.length);
+		assertSame(o2, result2[0]);
+	}
+
+	@Test
+	public void testAddOrderBy_Collection() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		Ordering o1 = new Ordering("x");
+		q.orderBy(Collections.singletonList(o1));
+
+		Object[] result1 = q.getOrderings().toArray();
+		assertEquals(1, result1.length);
+		assertSame(o1, result1[0]);
+
+		Ordering o2 = new Ordering("y");
+		q.addOrderBy(Collections.singletonList(o2));
+
+		Object[] result2 = q.getOrderings().toArray();
+		assertEquals(2, result2.length);
+		assertSame(o1, result2[0]);
+		assertSame(o2, result2[1]);
+	}
+
+	@Test
+	public void testOrderBy_Property() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		q.orderBy("x");
+
+		Object[] result1 = q.getOrderings().toArray();
+		assertEquals(1, result1.length);
+		assertEquals(new Ordering("x", SortOrder.ASCENDING), result1[0]);
+
+		q.orderBy("y");
+
+		Object[] result2 = q.getOrderings().toArray();
+		assertEquals(1, result2.length);
+		assertEquals(new Ordering("y", SortOrder.ASCENDING), result2[0]);
+	}
+
+	@Test
+	public void testOrderBy_PropertyStrategy() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		q.orderBy("x", SortOrder.ASCENDING_INSENSITIVE);
+
+		Object[] result1 = q.getOrderings().toArray();
+		assertEquals(1, result1.length);
+		assertEquals(new Ordering("x", SortOrder.ASCENDING_INSENSITIVE), result1[0]);
+
+		q.orderBy("y", SortOrder.DESCENDING);
+
+		Object[] result2 = q.getOrderings().toArray();
+		assertEquals(1, result2.length);
+		assertEquals(new Ordering("y", SortOrder.DESCENDING), result2[0]);
+	}
+
+	@Test
+	public void testAddOrderBy_Property() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		q.addOrderBy("x");
+
+		Object[] result1 = q.getOrderings().toArray();
+		assertEquals(1, result1.length);
+		assertEquals(new Ordering("x", SortOrder.ASCENDING), result1[0]);
+
+		q.addOrderBy("y");
+
+		Object[] result2 = q.getOrderings().toArray();
+		assertEquals(2, result2.length);
+		assertEquals(new Ordering("x", SortOrder.ASCENDING), result2[0]);
+		assertEquals(new Ordering("y", SortOrder.ASCENDING), result2[1]);
+	}
+
+	@Test
+	public void testPrefetch() {
+
+		PrefetchTreeNode root = PrefetchTreeNode.withPath("a.b", PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+		q.prefetch(root);
+
+		assertSame(root, q.getPrefetches());
+	}
+
+	@Test
+	public void testPrefetch_Path() {
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+		q.prefetch("a.b", PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
+		PrefetchTreeNode root1 = q.getPrefetches();
+
+		assertNotNull(root1);
+		assertNotNull(root1.getNode("a.b"));
+
+		q.prefetch("a.c", PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
+		PrefetchTreeNode root2 = q.getPrefetches();
+
+		assertNotNull(root2);
+		assertNotNull(root2.getNode("a.c"));
+		assertNull(root2.getNode("a.b"));
+		assertNotSame(root1, root2);
+	}
+
+	@Test
+	public void testAddPrefetch() {
+
+		PrefetchTreeNode root = PrefetchTreeNode.withPath("a.b", PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+		q.prefetch(root);
+
+		assertSame(root, q.getPrefetches());
+
+		PrefetchTreeNode subRoot = PrefetchTreeNode.withPath("a.b.c", PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+		q.addPrefetch(subRoot);
+
+		assertSame(root, q.getPrefetches());
+
+		assertNotNull(root.getNode("a.b.c"));
+	}
+
+	@Test
+	public void testLimit() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		assertEquals(0, q.getLimit());
+		q.limit(2);
+		assertEquals(2, q.getLimit());
+
+		q.limit(3).limit(5);
+		assertEquals(5, q.getLimit());
+	}
+	
+	@Test
+	public void testOffset() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		assertEquals(0, q.getOffset());
+		q.offset(2);
+		assertEquals(2, q.getOffset());
+
+		q.offset(3).offset(5);
+		assertEquals(5, q.getOffset());
+	}
+	
+	@Test
+	public void testStatementFetchSize() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		assertEquals(0, q.getStatementFetchSize());
+		q.statementFetchSize(2);
+		assertEquals(2, q.getStatementFetchSize());
+
+		q.statementFetchSize(3).statementFetchSize(5);
+		assertEquals(5, q.getStatementFetchSize());
+	}
+	
+	
+	@Test
+	public void testCacheGroups_Collection() {
+		ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class);
+
+		assertNull(q.getCacheStrategy());
+		assertNull(q.getCacheGroups());
+
+		q.cacheGroups(Arrays.asList("a", "b"));
+		assertNull(q.getCacheStrategy());
+		assertArrayEquals(new String[] { "a", "b" }, q.getCacheGroups());
+	}
+
+	@Test
+	public void testCacheStrategy() {
+		ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class);
+
+		assertNull(q.getCacheStrategy());
+		assertNull(q.getCacheGroups());
+
+		q.cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, "a", "b");
+		assertSame(QueryCacheStrategy.LOCAL_CACHE, q.getCacheStrategy());
+		assertArrayEquals(new String[] { "a", "b" }, q.getCacheGroups());
+
+		q.cacheStrategy(QueryCacheStrategy.SHARED_CACHE);
+		assertSame(QueryCacheStrategy.SHARED_CACHE, q.getCacheStrategy());
+		assertNull(q.getCacheGroups());
+	}
+	
+	@Test
+	public void testLocalCache() {
+		ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class);
+
+		assertNull(q.getCacheStrategy());
+		assertNull(q.getCacheGroups());
+
+		q.localCache("a", "b");
+		assertSame(QueryCacheStrategy.LOCAL_CACHE, q.getCacheStrategy());
+		assertArrayEquals(new String[] { "a", "b" }, q.getCacheGroups());
+
+		q.localCache();
+		assertSame(QueryCacheStrategy.LOCAL_CACHE, q.getCacheStrategy());
+		assertNull(q.getCacheGroups());
+	}
+	
+	@Test
+	public void testSharedCache() {
+		ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class);
+
+		assertNull(q.getCacheStrategy());
+		assertNull(q.getCacheGroups());
+
+		q.sharedCache("a", "b");
+		assertSame(QueryCacheStrategy.SHARED_CACHE, q.getCacheStrategy());
+		assertArrayEquals(new String[] { "a", "b" }, q.getCacheGroups());
+
+		q.sharedCache();
+		assertSame(QueryCacheStrategy.SHARED_CACHE, q.getCacheStrategy());
+		assertNull(q.getCacheGroups());
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/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
new file mode 100644
index 0000000..9a7a590
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_CompileIT.java
@@ -0,0 +1,168 @@
+/*****************************************************************
+ *   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 static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+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 org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+@UseServerRuntime(ServerCase.TESTMAP_PROJECT)
+public class ObjectSelect_CompileIT extends ServerCase {
+
+	@Inject
+	private EntityResolver resolver;
+
+	@Test
+	public void testCreateReplacementQuery_Bare() {
+
+		// use only a minimal number of attributes, with null/defaults for
+		// everything else
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		Query replacement = q.createReplacementQuery(resolver);
+		assertThat(replacement, instanceOf(SelectQuery.class));
+
+		@SuppressWarnings("unchecked")
+		SelectQuery<Artist> selectQuery = (SelectQuery<Artist>) replacement;
+		assertNull(selectQuery.getQualifier());
+		assertEquals(Artist.class, selectQuery.getRoot());
+		assertEquals(0, selectQuery.getOrderings().size());
+		assertNull(selectQuery.getPrefetchTree());
+
+		assertEquals(QueryCacheStrategy.NO_CACHE, selectQuery.getCacheStrategy());
+		assertNull(selectQuery.getCacheGroups());
+		assertEquals(0, selectQuery.getFetchLimit());
+		assertEquals(0, selectQuery.getFetchOffset());
+		assertEquals(0, selectQuery.getPageSize());
+		assertEquals(0, selectQuery.getStatementFetchSize());
+	}
+
+	@Test
+	public void testCreateReplacementQuery_Full() {
+
+		// add all possible attributes to the query and make sure they got
+		// propagated
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class).exp(Artist.ARTIST_NAME.eq("me"))
+				.orderBy(Artist.DATE_OF_BIRTH.asc(), Artist.ARTIST_NAME.desc()).prefetch(Artist.PAINTING_ARRAY.joint())
+				.localCache("cg2", "cg1").limit(46).offset(9).pageSize(6).statementFetchSize(789);
+
+		Query replacement = q.createReplacementQuery(resolver);
+		assertThat(replacement, instanceOf(SelectQuery.class));
+
+		@SuppressWarnings("unchecked")
+		SelectQuery<Artist> selectQuery = (SelectQuery<Artist>) replacement;
+		assertEquals("artistName = \"me\"", selectQuery.getQualifier().toString());
+
+		assertEquals(2, selectQuery.getOrderings().size());
+		assertArrayEquals(new Object[] { Artist.DATE_OF_BIRTH.asc(), Artist.ARTIST_NAME.desc() }, selectQuery
+				.getOrderings().toArray());
+
+		PrefetchTreeNode prefetch = selectQuery.getPrefetchTree();
+		assertNotNull(prefetch);
+		assertEquals(1, prefetch.getChildren().size());
+
+		PrefetchTreeNode childPrefetch = prefetch.getNode(Artist.PAINTING_ARRAY.getName());
+		assertEquals(Artist.PAINTING_ARRAY.getName(), childPrefetch.getName());
+		assertEquals(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS, childPrefetch.getSemantics());
+
+		assertEquals(QueryCacheStrategy.LOCAL_CACHE, selectQuery.getCacheStrategy());
+		assertArrayEquals(new Object[] { "cg2", "cg1" }, selectQuery.getCacheGroups());
+
+		assertEquals(46, selectQuery.getFetchLimit());
+		assertEquals(9, selectQuery.getFetchOffset());
+		assertEquals(6, selectQuery.getPageSize());
+		assertEquals(789, selectQuery.getStatementFetchSize());
+	}
+
+	@Test
+	public void testCreateReplacementQuery_RootClass() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		@SuppressWarnings("rawtypes")
+		SelectQuery qr = (SelectQuery) q.createReplacementQuery(resolver);
+		assertEquals(Artist.class, qr.getRoot());
+		assertFalse(qr.isFetchingDataRows());
+	}
+
+	@Test
+	public void testCreateReplacementQuery_RootDataRow() {
+		ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class);
+
+		@SuppressWarnings("rawtypes")
+		SelectQuery qr = (SelectQuery) q.createReplacementQuery(resolver);
+		assertEquals(Artist.class, qr.getRoot());
+		assertTrue(qr.isFetchingDataRows());
+	}
+
+	@Test
+	public void testCreateReplacementQuery_RootDbEntity() {
+		ObjectSelect<DataRow> q = ObjectSelect.dbQuery("ARTIST");
+
+		@SuppressWarnings("rawtypes")
+		SelectQuery qr = (SelectQuery) q.createReplacementQuery(resolver);
+		assertEquals(resolver.getDbEntity("ARTIST"), qr.getRoot());
+		assertTrue(qr.isFetchingDataRows());
+	}
+
+	@Test
+	public void testCreateReplacementQuery_RootObjEntity() {
+		ObjectSelect<CayenneDataObject> q = ObjectSelect.query(CayenneDataObject.class, "Artist");
+
+		@SuppressWarnings("rawtypes")
+		SelectQuery qr = (SelectQuery) q.createReplacementQuery(resolver);
+		assertEquals(resolver.getObjEntity(Artist.class), qr.getRoot());
+		assertFalse(qr.isFetchingDataRows());
+	}
+
+	@Test(expected = CayenneRuntimeException.class)
+	public void testCreateReplacementQuery_RootAbscent() {
+		ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class).entityName(null);
+		q.createReplacementQuery(resolver);
+	}
+
+	@Test
+	public void testCreateReplacementQuery_DataRows() {
+		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
+
+		@SuppressWarnings("rawtypes")
+		SelectQuery selectQuery1 = (SelectQuery) q.createReplacementQuery(resolver);
+		assertFalse(selectQuery1.isFetchingDataRows());
+
+		q.fetchDataRows();
+
+		@SuppressWarnings("rawtypes")
+		SelectQuery selectQuery2 = (SelectQuery) q.createReplacementQuery(resolver);
+		assertTrue(selectQuery2.isFetchingDataRows());
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/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
new file mode 100644
index 0000000..1b921a4
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
@@ -0,0 +1,94 @@
+/*****************************************************************
+ *   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 static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import java.util.List;
+
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.di.Inject;
+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.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+@UseServerRuntime(ServerCase.TESTMAP_PROJECT)
+public class ObjectSelect_RunIT extends ServerCase {
+
+	@Inject
+	private DataContext context;
+
+	@Inject
+	private DBHelper dbHelper;
+
+	@Override
+	protected void setUpAfterInjection() throws Exception {
+		dbHelper.deleteAll("PAINTING_INFO");
+		dbHelper.deleteAll("PAINTING");
+		dbHelper.deleteAll("ARTIST_EXHIBIT");
+		dbHelper.deleteAll("ARTIST_GROUP");
+		dbHelper.deleteAll("ARTIST");
+	}
+
+	protected void createArtistsDataSet() throws Exception {
+		TableHelper tArtist = new TableHelper(dbHelper, "ARTIST");
+		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));
+		}
+	}
+
+	@Test
+	public void test_SelectObjects() throws Exception {
+
+		createArtistsDataSet();
+
+		List<Artist> result = ObjectSelect.query(Artist.class).select(context);
+		assertEquals(20, result.size());
+		assertThat(result.get(0), instanceOf(Artist.class));
+
+		Artist a = ObjectSelect.query(Artist.class).exp(Artist.ARTIST_NAME.eq("artist14")).selectOne(context);
+		assertNotNull(a);
+		assertEquals("artist14", a.getArtistName());
+	}
+
+	@Test
+	public void test_SelectDataRows() throws Exception {
+
+		createArtistsDataSet();
+
+		List<DataRow> result = ObjectSelect.dataRowQuery(Artist.class).select(context);
+		assertEquals(20, result.size());
+		assertThat(result.get(0), instanceOf(DataRow.class));
+
+		DataRow a = ObjectSelect.dataRowQuery(Artist.class).exp(Artist.ARTIST_NAME.eq("artist14")).selectOne(context);
+		assertNotNull(a);
+		assertEquals("artist14", a.get("ARTIST_NAME"));
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/docs/doc/src/main/resources/RELEASE-NOTES.txt
----------------------------------------------------------------------
diff --git a/docs/doc/src/main/resources/RELEASE-NOTES.txt b/docs/doc/src/main/resources/RELEASE-NOTES.txt
index 86ec1c3..7711b36 100644
--- a/docs/doc/src/main/resources/RELEASE-NOTES.txt
+++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt
@@ -66,6 +66,7 @@ CAY-1952 Undeprecate (actually restore) ObjectContext.deleteObject(..)
 CAY-1953 Redo ResultIteratorCallback to handle single row callback instead of iterator
 CAY-1954 Make Cayenne class constructor protected
 CAY-1958 SelectById - a new full-featured select query to get objects by id
+CAY-1959 Chainable API for SelectQuery
 CAY-1960 ExpressionFactory.exp(..) , and(..), or(..)
 CAY-1962 Implement CayenneTable column resize on double-click on the header separator
 CAY-1965 Change version from 3.2 to 4.0


Mime
View raw message