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 <aadamc...@apache.org>
Authored: Sat Nov 15 13:08:20 2014 +0300
Committer: aadamchik <aadamc...@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

Reply via email to