This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git


The following commit(s) were added to refs/heads/master by this push:
     new da793eb  CAY-2522 Make ObjectSelect a direct query
da793eb is described below

commit da793eb55b8278764f4e8fe6d9e8f84d3a8ed9f8
Author: Nikita Timofeev <[email protected]>
AuthorDate: Wed Feb 20 11:42:40 2019 +0300

    CAY-2522 Make ObjectSelect a direct query
---
 RELEASE-NOTES.txt                                  |   1 +
 .../java/org/apache/cayenne/access/DataNode.java   |   4 +-
 .../apache/cayenne/access/jdbc/SelectAction.java   |  12 +-
 .../translator/select/ColumnSelectWrapper.java     |  84 --------
 .../translator/select/DefaultSelectTranslator.java |  13 +-
 .../select/DefaultSelectTranslatorFactory.java     |  12 +-
 ...SelectWrapper.java => FluentSelectWrapper.java} |  33 ++--
 .../translator/select/PrefetchNodeStage.java       |   6 +-
 .../translator/select/SelectQueryWrapper.java      |   9 +-
 .../translator/select/SelectTranslatorFactory.java |   4 +-
 .../select/TranslatableQueryWrapper.java           |   2 -
 .../java/org/apache/cayenne/dba/AutoAdapter.java   |   9 +
 .../java/org/apache/cayenne/dba/DbAdapter.java     |   6 +
 .../org/apache/cayenne/dba/JdbcActionBuilder.java  |   9 +
 .../java/org/apache/cayenne/dba/JdbcAdapter.java   |   6 +
 .../apache/cayenne/dba/db2/DB2ActionBuilder.java   |   9 +
 .../apache/cayenne/dba/db2/DB2SelectAction.java    |   4 +-
 .../cayenne/dba/derby/DerbyActionBuilder.java      |   9 +
 .../cayenne/dba/derby/DerbySelectAction.java       |   4 +-
 .../dba/firebird/FirebirdActionBuilder.java        |   9 +
 .../cayenne/dba/firebird/FirebirdSelectAction.java |   5 +-
 .../org/apache/cayenne/dba/h2/H2ActionBuilder.java |   9 +
 .../org/apache/cayenne/dba/h2/H2SelectAction.java  |   4 +-
 .../cayenne/dba/hsqldb/HSQLActionBuilder.java      |   9 +
 .../cayenne/dba/hsqldb/HSQLSelectAction.java       |   4 +-
 .../cayenne/dba/ingres/IngresActionBuilder.java    |   9 +
 .../cayenne/dba/ingres/IngresSelectAction.java     |   4 +-
 .../cayenne/dba/mysql/MySQLActionBuilder.java      |   9 +
 .../cayenne/dba/mysql/MySQLSelectAction.java       |   4 +-
 .../cayenne/dba/oracle/OracleActionBuilder.java    |   9 +
 .../cayenne/dba/oracle/OracleSelectAction.java     |   4 +-
 .../dba/postgres/PostgresActionBuilder.java        |   9 +
 .../cayenne/dba/postgres/PostgresSelectAction.java |   4 +-
 .../cayenne/dba/sqlite/SQLiteActionBuilder.java    |   9 +
 .../cayenne/dba/sqlite/SQLiteSelectAction.java     |   9 +-
 .../org/apache/cayenne/exp/ExpressionFactory.java  |  11 +-
 .../org/apache/cayenne/exp/parser/ASTSubquery.java |  18 +-
 .../org/apache/cayenne/query/ColumnSelect.java     | 114 +++--------
 ...ueryMetadata.java => ColumnSelectMetadata.java} | 219 +++------------------
 .../org/apache/cayenne/query/FluentSelect.java     | 104 +++++-----
 .../query/FluentSelectPrefetchRouterAction.java    | 141 +++++++++++++
 .../org/apache/cayenne/query/ObjectSelect.java     |  96 +++------
 .../apache/cayenne/query/ObjectSelectMetadata.java | 171 ++++++++++++++++
 .../org/apache/cayenne/query/SQLActionVisitor.java |   6 +
 .../apache/cayenne/query/SelectQueryMetadata.java  | 121 ------------
 .../cayenne/query/ToCacheKeyPrefetchProcessor.java |  66 +++++++
 .../cayenne/query/ToCacheKeyTraversalHandler.java  | 103 ++++++++++
 .../translator/select/MockQueryWrapperBuilder.java |   5 -
 .../org/apache/cayenne/query/ColumnSelectTest.java |   6 +-
 .../org/apache/cayenne/query/ObjectSelectTest.java |  10 +-
 .../cayenne/query/ObjectSelect_CompileIT.java      | 188 ------------------
 .../query/SelectQueryMetadataCacheKeyTest.java     |   4 +-
 52 files changed, 827 insertions(+), 902 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 4e52647..8d553b1 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -21,6 +21,7 @@ CAY-2511 Contribute custom properties for attributes
 CAY-2517 EventManager: optimization of adding listeners
 CAY-2518 Add method to append having qualifier expression to ObjectSelect
 CAY-2520 Split ObjectId into several specialized variants
+CAY-2522 Make ObjectSelect a direct query
 CAY-2540 Modeler: redesign dbRelationship editor dialog
 
 Bug Fixes:
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/DataNode.java 
b/cayenne-server/src/main/java/org/apache/cayenne/access/DataNode.java
index 1211bdc..7609c95 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataNode.java
@@ -40,7 +40,7 @@ import org.apache.cayenne.map.ObjAttribute;
 import org.apache.cayenne.query.BatchQuery;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.QueryMetadata;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 import org.apache.cayenne.tx.BaseTransaction;
 import org.apache.cayenne.tx.Transaction;
 import org.apache.cayenne.util.ToStringBuilder;
@@ -348,7 +348,7 @@ public class DataNode implements QueryEngine {
        /**
         * @since 4.0
         */
-       public SelectTranslator selectTranslator(SelectQuery<?> query) {
+       public SelectTranslator selectTranslator(Select<?> query) {
                return selectTranslatorFactory.translator(query, getAdapter(), 
getEntityResolver());
        }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java 
b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
index ea632e6..691dbed 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
@@ -30,7 +30,7 @@ import org.apache.cayenne.log.JdbcEventLogger;
 import org.apache.cayenne.query.PrefetchProcessor;
 import org.apache.cayenne.query.PrefetchTreeNode;
 import org.apache.cayenne.query.QueryMetadata;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 
 import java.sql.Connection;
 import java.sql.PreparedStatement;
@@ -65,13 +65,13 @@ public class SelectAction extends BaseSQLAction {
 
        }
 
-       protected SelectQuery<?> query;
+       protected Select<?> query;
        protected QueryMetadata queryMetadata;
 
        /**
         * @since 4.0
         */
-       public SelectAction(SelectQuery<?> query, DataNode dataNode) {
+       public SelectAction(Select<?> query, DataNode dataNode) {
                super(dataNode);
                this.query = query;
                this.queryMetadata = 
query.getMetaData(dataNode.getEntityResolver());
@@ -163,10 +163,10 @@ public class SelectAction extends BaseSQLAction {
                // needed, as the SQL result count does not directly correspond 
to the
                // number of objects returned from Cayenne.
 
-               int fetchLimit = query.getFetchLimit();
+               int fetchLimit = queryMetadata.getFetchLimit();
                int offset = translator.isSuppressingDistinct()
-                               ? query.getFetchOffset()
-                               : getInMemoryOffset(query.getFetchOffset());
+                               ? queryMetadata.getFetchOffset()
+                               : 
getInMemoryOffset(queryMetadata.getFetchOffset());
 
                if (fetchLimit > 0 || offset > 0) {
                        return new LimitResultIterator<>(iterator, offset, 
fetchLimit);
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnSelectWrapper.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnSelectWrapper.java
deleted file mode 100644
index 21a0069..0000000
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnSelectWrapper.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-
-package org.apache.cayenne.access.translator.select;
-
-import java.util.Collection;
-import java.util.Objects;
-
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.property.BaseProperty;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.query.ColumnSelect;
-import org.apache.cayenne.query.Ordering;
-import org.apache.cayenne.query.PrefetchTreeNode;
-import org.apache.cayenne.query.QueryMetadata;
-import org.apache.cayenne.query.Select;
-
-/**
- * @since 4.2
- */
-public class ColumnSelectWrapper implements TranslatableQueryWrapper {
-
-    private final ColumnSelect<?> columnSelect;
-
-    public ColumnSelectWrapper(ColumnSelect<?> columnSelect) {
-        this.columnSelect = Objects.requireNonNull(columnSelect);
-    }
-
-    @Override
-    public boolean isDistinct() {
-        return columnSelect.isDistinct();
-    }
-
-    @Override
-    public QueryMetadata getMetaData(EntityResolver resolver) {
-        return columnSelect.getMetaData(resolver);
-    }
-
-    @Override
-    public PrefetchTreeNode getPrefetchTree() {
-        return columnSelect.getPrefetches();
-    }
-
-    @Override
-    public Expression getQualifier() {
-        return columnSelect.getWhere();
-    }
-
-    @Override
-    public Collection<Ordering> getOrderings() {
-        return columnSelect.getOrderings();
-    }
-
-    @Override
-    public Collection<BaseProperty<?>> getColumns() {
-        return columnSelect.getColumns();
-    }
-
-    @Override
-    public Expression getHavingQualifier() {
-        return columnSelect.getHaving();
-    }
-
-    @Override
-    public Select<?> unwrap() {
-        return columnSelect;
-    }
-}
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
index 4107dbe..7cc62bc 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
@@ -28,12 +28,11 @@ import 
org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.ObjAttribute;
-import org.apache.cayenne.query.ColumnSelect;
-import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.SelectQuery;
 
 /**
- * Default translator of select queries ({@link SelectQuery}, {@link 
ObjectSelect} and {@link ColumnSelect}).
+ * Default translator of select queries ({@link SelectQuery} or {@link 
FluentSelect}).
  *
  * @since 4.2
  */
@@ -80,12 +79,8 @@ public class DefaultSelectTranslator implements 
SelectTranslator {
         this(new SelectQueryWrapper(query), adapter, entityResolver);
     }
 
-    public DefaultSelectTranslator(ObjectSelect<?> query, DbAdapter adapter, 
EntityResolver entityResolver) {
-        this(new ObjectSelectWrapper(query), adapter, entityResolver);
-    }
-
-    public DefaultSelectTranslator(ColumnSelect<?> query, DbAdapter adapter, 
EntityResolver entityResolver) {
-        this(new ColumnSelectWrapper(query), adapter, entityResolver);
+    public DefaultSelectTranslator(FluentSelect<?> query, DbAdapter adapter, 
EntityResolver entityResolver) {
+        this(new FluentSelectWrapper(query), adapter, entityResolver);
     }
 
     TranslatorContext getContext() {
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorFactory.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorFactory.java
index eec4e1c..9fc437a 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorFactory.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorFactory.java
@@ -18,8 +18,11 @@
  ****************************************************************/
 package org.apache.cayenne.access.translator.select;
 
+import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.FluentSelect;
+import org.apache.cayenne.query.Select;
 import org.apache.cayenne.query.SelectQuery;
 
 /**
@@ -31,7 +34,12 @@ import org.apache.cayenne.query.SelectQuery;
 public class DefaultSelectTranslatorFactory implements SelectTranslatorFactory 
{
 
        @Override
-       public SelectTranslator translator(SelectQuery<?> query, DbAdapter 
adapter, EntityResolver entityResolver) {
-               return adapter.getSelectTranslator(query, entityResolver);
+       public SelectTranslator translator(Select<?> query, DbAdapter adapter, 
EntityResolver entityResolver) {
+               if(query instanceof SelectQuery) {
+                       return 
adapter.getSelectTranslator((SelectQuery<?>)query, entityResolver);
+               } else if(query instanceof FluentSelect) {
+                       return 
adapter.getSelectTranslator((FluentSelect<?>)query, entityResolver);
+               }
+               throw new CayenneRuntimeException("Unsupported type of Select 
query %s", query);
        }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjectSelectWrapper.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/FluentSelectWrapper.java
similarity index 69%
rename from 
cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjectSelectWrapper.java
rename to 
cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/FluentSelectWrapper.java
index 684245e..dc59c2b 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjectSelectWrapper.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/FluentSelectWrapper.java
@@ -25,60 +25,53 @@ import java.util.Objects;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.Ordering;
-import org.apache.cayenne.query.PrefetchTreeNode;
 import org.apache.cayenne.query.QueryMetadata;
-import org.apache.cayenne.query.Select;
 
 /**
  * @since 4.2
  */
-public class ObjectSelectWrapper implements TranslatableQueryWrapper {
+public class FluentSelectWrapper implements TranslatableQueryWrapper {
 
-    private final ObjectSelect<?> selectQuery;
+    private final FluentSelect<?> select;
 
-    public ObjectSelectWrapper(ObjectSelect<?> selectQuery) {
-        this.selectQuery = Objects.requireNonNull(selectQuery);
+    public FluentSelectWrapper(FluentSelect<?> select) {
+        this.select = Objects.requireNonNull(select);
     }
 
     @Override
     public boolean isDistinct() {
-        return false;
+        return select.isDistinct();
     }
 
     @Override
     public QueryMetadata getMetaData(EntityResolver resolver) {
-        return selectQuery.getMetaData(resolver);
-    }
-
-    @Override
-    public PrefetchTreeNode getPrefetchTree() {
-        return selectQuery.getPrefetches();
+        return select.getMetaData(resolver);
     }
 
     @Override
     public Expression getQualifier() {
-        return selectQuery.getWhere();
+        return select.getWhere();
     }
 
     @Override
     public Collection<Ordering> getOrderings() {
-        return selectQuery.getOrderings();
+        return select.getOrderings();
     }
 
     @Override
     public Collection<BaseProperty<?>> getColumns() {
-        return null;
+        return select.getColumns();
     }
 
     @Override
     public Expression getHavingQualifier() {
-        return selectQuery.getHaving();
+        return select.getHaving();
     }
 
     @Override
-    public Select<?> unwrap() {
-        return selectQuery;
+    public FluentSelect<?> unwrap() {
+        return select;
     }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
index 90c0f3d..51006ac 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
@@ -50,19 +50,19 @@ class PrefetchNodeStage implements TranslationStage {
     }
 
     private void updatePrefetchNodes(TranslatorContext context) {
-        if(context.getQuery().getPrefetchTree() == null) {
+        if(context.getMetadata().getPrefetchTree() == null) {
             return;
         }
         // Set entity name, in case MixedConversionStrategy will be used to 
select objects from this query
         // Note: all prefetch nodes will point to query root, it is not a 
problem until select query can't
         // perform some sort of union or sub-queries.
-        for(PrefetchTreeNode prefetch : 
context.getQuery().getPrefetchTree().getChildren()) {
+        for(PrefetchTreeNode prefetch : 
context.getMetadata().getPrefetchTree().getChildren()) {
             
prefetch.setEntityName(context.getMetadata().getObjEntity().getName());
         }
     }
 
     private void processJoint(TranslatorContext context) {
-        PrefetchTreeNode prefetch = context.getQuery().getPrefetchTree();
+        PrefetchTreeNode prefetch = context.getMetadata().getPrefetchTree();
         if(prefetch == null) {
             return;
         }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectQueryWrapper.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectQueryWrapper.java
index 576bb8d..c927107 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectQueryWrapper.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectQueryWrapper.java
@@ -26,9 +26,7 @@ import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Ordering;
-import org.apache.cayenne.query.PrefetchTreeNode;
 import org.apache.cayenne.query.QueryMetadata;
-import org.apache.cayenne.query.Select;
 import org.apache.cayenne.query.SelectQuery;
 
 /**
@@ -53,11 +51,6 @@ public class SelectQueryWrapper implements 
TranslatableQueryWrapper {
     }
 
     @Override
-    public PrefetchTreeNode getPrefetchTree() {
-        return selectQuery.getPrefetchTree();
-    }
-
-    @Override
     public Expression getQualifier() {
         return selectQuery.getQualifier();
     }
@@ -78,7 +71,7 @@ public class SelectQueryWrapper implements 
TranslatableQueryWrapper {
     }
 
     @Override
-    public Select<?> unwrap() {
+    public SelectQuery<?> unwrap() {
         return selectQuery;
     }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslatorFactory.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslatorFactory.java
index 4c306b5..c71e5ea 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslatorFactory.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslatorFactory.java
@@ -20,7 +20,7 @@ package org.apache.cayenne.access.translator.select;
 
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 
 /**
  * A factory for {@link SelectTranslator} objects.
@@ -32,6 +32,6 @@ public interface SelectTranslatorFactory {
        /**
         * Creates a proper translator for a BatchQuery
         */
-       SelectTranslator translator(SelectQuery<?> query, DbAdapter adapter, 
EntityResolver entityResolver);
+       SelectTranslator translator(Select<?> query, DbAdapter adapter, 
EntityResolver entityResolver);
 
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatableQueryWrapper.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatableQueryWrapper.java
index 6bd2c41..5322847 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatableQueryWrapper.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatableQueryWrapper.java
@@ -42,8 +42,6 @@ public interface TranslatableQueryWrapper {
 
     QueryMetadata getMetaData(EntityResolver resolver);
 
-    PrefetchTreeNode getPrefetchTree();
-
     Expression getQualifier();
 
     Collection<Ordering> getOrderings();
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
index fffd1c1..e8eac29 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
@@ -32,6 +32,7 @@ import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
@@ -105,6 +106,14 @@ public class AutoAdapter implements DbAdapter {
                return getAdapter().getSelectTranslator(query, entityResolver);
        }
 
+       /**
+        * @since 4.2
+        */
+       @Override
+       public SelectTranslator getSelectTranslator(FluentSelect<?> query, 
EntityResolver entityResolver) {
+               return getAdapter().getSelectTranslator(query, entityResolver);
+       }
+
        @Override
        public String getBatchTerminator() {
                return getAdapter().getBatchTerminator();
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java
index 629ef13..7f97640 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java
@@ -28,6 +28,7 @@ import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
@@ -62,6 +63,11 @@ public interface DbAdapter {
 
        /**
         * @since 4.2
+        */
+       SelectTranslator getSelectTranslator(FluentSelect<?> query, 
EntityResolver entityResolver);
+
+       /**
+        * @since 4.2
         * @return {@link Function} that can adjust SQL tree to specific 
database flavour
         */
        Function<Node, Node> getSqlTreeProcessor();
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcActionBuilder.java 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcActionBuilder.java
index e255abf..63544cb 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcActionBuilder.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcActionBuilder.java
@@ -27,6 +27,7 @@ import org.apache.cayenne.access.jdbc.SQLTemplateAction;
 import org.apache.cayenne.access.jdbc.SelectAction;
 import org.apache.cayenne.query.BatchQuery;
 import org.apache.cayenne.query.EJBQLQuery;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SQLActionVisitor;
@@ -71,6 +72,14 @@ public class JdbcActionBuilder implements SQLActionVisitor {
         return new SelectAction(query, dataNode);
     }
 
+    /**
+     * @since 4.2
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(FluentSelect<T> query) {
+        return new SelectAction(query, dataNode);
+    }
+
     @Override
     public SQLAction sqlAction(SQLTemplate query) {
         return new SQLTemplateAction(query, dataNode);
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
index db3c5ad..8da76e0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
@@ -42,6 +42,7 @@ import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbJoin;
 import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
@@ -530,6 +531,11 @@ public class JdbcAdapter implements DbAdapter {
     }
 
     @Override
+    public SelectTranslator getSelectTranslator(FluentSelect<?> query, 
EntityResolver entityResolver) {
+        return new DefaultSelectTranslator(query, this, entityResolver);
+    }
+
+    @Override
     public Function<Node, Node> getSqlTreeProcessor() {
         return Function.identity();
     }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2ActionBuilder.java 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2ActionBuilder.java
index f22b8f2..f9a5392 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2ActionBuilder.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2ActionBuilder.java
@@ -20,6 +20,7 @@ package org.apache.cayenne.dba.db2;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.dba.JdbcActionBuilder;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
@@ -42,4 +43,12 @@ public class DB2ActionBuilder extends JdbcActionBuilder {
     public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
         return new DB2SelectAction(query, dataNode);
     }
+
+    /**
+     * @since 4.2
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(FluentSelect<T> query) {
+        return new DB2SelectAction(query, dataNode);
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2SelectAction.java 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2SelectAction.java
index d0f8fd1..64e1e6a 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2SelectAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2SelectAction.java
@@ -21,14 +21,14 @@ package org.apache.cayenne.dba.db2;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 
 /**
  * @since 4.1
  */
 public class DB2SelectAction extends SelectAction {
 
-    public DB2SelectAction(SelectQuery<?> query, DataNode dataNode) {
+    public DB2SelectAction(Select<?> query, DataNode dataNode) {
         super(query, dataNode);
     }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyActionBuilder.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyActionBuilder.java
index b0b67ff..a95a9dc 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyActionBuilder.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyActionBuilder.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.dba.derby;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.dba.JdbcActionBuilder;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
 
@@ -37,4 +38,12 @@ public class DerbyActionBuilder extends JdbcActionBuilder {
     public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
         return new DerbySelectAction(query, dataNode);
     }
+
+    /**
+     * @since 4.2
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(FluentSelect<T> query) {
+        return new DerbySelectAction(query, dataNode);
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbySelectAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbySelectAction.java
index 6a1c921..2f2d2b1 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbySelectAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbySelectAction.java
@@ -21,14 +21,14 @@ package org.apache.cayenne.dba.derby;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 
 /**
  * @since 4.1
  */
 public class DerbySelectAction extends SelectAction {
 
-    public DerbySelectAction(SelectQuery<?> query, DataNode dataNode) {
+    public DerbySelectAction(Select<?> query, DataNode dataNode) {
         super(query, dataNode);
     }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdActionBuilder.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdActionBuilder.java
index 92f606c..290a7b2 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdActionBuilder.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdActionBuilder.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.dba.firebird;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.dba.JdbcActionBuilder;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
 
@@ -38,4 +39,12 @@ public class FirebirdActionBuilder extends JdbcActionBuilder 
{
     public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
         return new FirebirdSelectAction(query, dataNode);
     }
+
+    /**
+     * @since 4.2
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(FluentSelect<T> query) {
+        return new FirebirdSelectAction(query, dataNode);
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdSelectAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdSelectAction.java
index 184a909..d23372c 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdSelectAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdSelectAction.java
@@ -21,15 +21,14 @@ package org.apache.cayenne.dba.firebird;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
-import org.apache.cayenne.query.SQLAction;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 
 /**
  * @since 4.1
  */
 public class FirebirdSelectAction extends SelectAction {
 
-    public FirebirdSelectAction(SelectQuery<?> query, DataNode dataNode) {
+    public FirebirdSelectAction(Select<?> query, DataNode dataNode) {
         super(query, dataNode);
     }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2ActionBuilder.java 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2ActionBuilder.java
index 2cf2a10..81a9651 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2ActionBuilder.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2ActionBuilder.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.dba.h2;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.dba.JdbcActionBuilder;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
 
@@ -36,4 +37,12 @@ public class H2ActionBuilder extends JdbcActionBuilder {
     public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
         return new H2SelectAction(query, dataNode);
     }
+
+    /**
+     * @since 4.2
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(FluentSelect<T> query) {
+        return new H2SelectAction(query, dataNode);
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2SelectAction.java 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2SelectAction.java
index df7380d..f3a8b19 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2SelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2SelectAction.java
@@ -21,14 +21,14 @@ package org.apache.cayenne.dba.h2;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 
 /**
  * @since 4.1
  */
 public class H2SelectAction extends SelectAction {
 
-    public H2SelectAction(SelectQuery<?> query, DataNode dataNode) {
+    public H2SelectAction(Select<?> query, DataNode dataNode) {
         super(query, dataNode);
     }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLActionBuilder.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLActionBuilder.java
index 010315a..9296862 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLActionBuilder.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLActionBuilder.java
@@ -25,6 +25,7 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.ProcedureAction;
 import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import org.apache.cayenne.dba.JdbcActionBuilder;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
@@ -40,6 +41,14 @@ class HSQLActionBuilder extends JdbcActionBuilder {
         return new HSQLSelectAction(query, dataNode);
     }
 
+    /**
+     * @since 4.2
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(FluentSelect<T> query) {
+        return new HSQLSelectAction(query, dataNode);
+    }
+
     @Override
     public SQLAction procedureAction(ProcedureQuery query) {
         return new ProcedureAction(query, dataNode) {
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectAction.java
index 21f4e11..236823e 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectAction.java
@@ -20,14 +20,14 @@ package org.apache.cayenne.dba.hsqldb;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 
 /**
  * @since 3.0
  */
 class HSQLSelectAction extends SelectAction {
 
-       <T> HSQLSelectAction(SelectQuery<T> query, DataNode dataNode) {
+       <T> HSQLSelectAction(Select<T> query, DataNode dataNode) {
                super(query, dataNode);
        }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresActionBuilder.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresActionBuilder.java
index 17f8283..45cd495 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresActionBuilder.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresActionBuilder.java
@@ -20,6 +20,7 @@ package org.apache.cayenne.dba.ingres;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.dba.JdbcActionBuilder;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
 
@@ -36,4 +37,12 @@ public class IngresActionBuilder extends JdbcActionBuilder {
     public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
         return new IngresSelectAction(query, dataNode);
     }
+
+    /**
+     * @since 4.2
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(FluentSelect<T> query) {
+        return new IngresSelectAction(query, dataNode);
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectAction.java
index 3c6e1cb..b723391 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectAction.java
@@ -20,11 +20,11 @@ package org.apache.cayenne.dba.ingres;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 
 public class IngresSelectAction extends SelectAction {
 
-       public <T> IngresSelectAction(SelectQuery<T> query, DataNode dataNode) {
+       public <T> IngresSelectAction(Select<T> query, DataNode dataNode) {
                super(query, dataNode);
        }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLActionBuilder.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLActionBuilder.java
index de9cf7d..23b87cb 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLActionBuilder.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLActionBuilder.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.dba.mysql;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.dba.JdbcActionBuilder;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
@@ -39,6 +40,14 @@ class MySQLActionBuilder extends JdbcActionBuilder {
         return new MySQLSelectAction(query, dataNode);
     }
 
+    /**
+     * @since 4.2
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(FluentSelect<T> query) {
+        return new MySQLSelectAction(query, dataNode);
+    }
+
     @Override
     public SQLAction procedureAction(ProcedureQuery query) {
         return new MySQLProcedureAction(query, dataNode);
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectAction.java
index 0b522d3..82faa67 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectAction.java
@@ -20,14 +20,14 @@ package org.apache.cayenne.dba.mysql;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 
 /**
  * @since 3.0
  */
 class MySQLSelectAction extends SelectAction {
 
-       <T> MySQLSelectAction(SelectQuery<T> query, DataNode dataNode) {
+       <T> MySQLSelectAction(Select<T> query, DataNode dataNode) {
                super(query, dataNode);
        }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleActionBuilder.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleActionBuilder.java
index 1b2be24..bec3d40 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleActionBuilder.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleActionBuilder.java
@@ -22,6 +22,7 @@ package org.apache.cayenne.dba.oracle;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.dba.JdbcActionBuilder;
 import org.apache.cayenne.query.BatchQuery;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SQLTemplate;
@@ -62,4 +63,12 @@ class OracleActionBuilder extends JdbcActionBuilder {
     public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
         return new OracleSelectAction(query, dataNode);
     }
+
+    /**
+     * @since 4.2
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(FluentSelect<T> query) {
+        return new OracleSelectAction(query, dataNode);
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectAction.java
index 60e8e5a..03eba27 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectAction.java
@@ -21,14 +21,14 @@ package org.apache.cayenne.dba.oracle;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 
 /**
  * @since 1.2
  */
 class OracleSelectAction extends SelectAction {
 
-       public <T> OracleSelectAction(SelectQuery<T> query, DataNode dataNode) {
+       public <T> OracleSelectAction(Select<T> query, DataNode dataNode) {
                super(query, dataNode);
        }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresActionBuilder.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresActionBuilder.java
index c583eca..d5f72e3 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresActionBuilder.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresActionBuilder.java
@@ -22,6 +22,7 @@ package org.apache.cayenne.dba.postgres;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.dba.JdbcActionBuilder;
 import org.apache.cayenne.query.BatchQuery;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SelectQuery;
@@ -55,4 +56,12 @@ class PostgresActionBuilder extends JdbcActionBuilder {
     public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
         return new PostgresSelectAction(query, dataNode);
     }
+
+    /**
+     * @since 4.2
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(FluentSelect<T> query) {
+        return new PostgresSelectAction(query, dataNode);
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
index c20e746..a4d5022 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
@@ -20,14 +20,14 @@ package org.apache.cayenne.dba.postgres;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SelectAction;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.Select;
 
 /**
  * @since 3.0
  */
 class PostgresSelectAction extends SelectAction {
 
-       <T> PostgresSelectAction(SelectQuery<T> query, DataNode dataNode) {
+       <T> PostgresSelectAction(Select<T> query, DataNode dataNode) {
                super(query, dataNode);
        }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteActionBuilder.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteActionBuilder.java
index c4cc53c..6d42536 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteActionBuilder.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteActionBuilder.java
@@ -20,6 +20,7 @@ package org.apache.cayenne.dba.sqlite;
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.dba.JdbcActionBuilder;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SQLTemplate;
 import org.apache.cayenne.query.SelectQuery;
@@ -45,4 +46,12 @@ class SQLiteActionBuilder extends JdbcActionBuilder {
     public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
         return new SQLiteSelectAction(query, dataNode);
     }
+
+    /**
+     * @since 4.2
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(FluentSelect<T> query) {
+        return new SQLiteSelectAction(query, dataNode);
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteSelectAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteSelectAction.java
index a578c23..2b9bdca 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteSelectAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteSelectAction.java
@@ -19,20 +19,15 @@
 package org.apache.cayenne.dba.sqlite;
 
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.SelectAction;
-import org.apache.cayenne.query.SQLAction;
-import org.apache.cayenne.query.SelectQuery;
-
-import java.sql.Connection;
-import java.sql.SQLException;
+import org.apache.cayenne.query.Select;
 
 /**
  * @since 4.1
  */
 public class SQLiteSelectAction extends SelectAction {
 
-    public SQLiteSelectAction(SelectQuery<?> query, DataNode dataNode) {
+    public SQLiteSelectAction(Select<?> query, DataNode dataNode) {
         super(query, dataNode);
     }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java 
b/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
index ad9775e..2e10cb6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
@@ -65,7 +65,7 @@ import org.apache.cayenne.exp.parser.JavaCharStream;
 import org.apache.cayenne.exp.parser.SimpleNode;
 import org.apache.cayenne.map.Entity;
 import org.apache.cayenne.query.ColumnSelect;
-import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.FluentSelect;
 import org.apache.cayenne.query.SelectQuery;
 
 import java.io.Reader;
@@ -1367,14 +1367,7 @@ public class ExpressionFactory {
        /**
         * @since 4.2
         */
-       public static Expression exists(ObjectSelect<?> subQuery) {
-               return new ASTExists(new ASTSubquery(subQuery));
-       }
-
-       /**
-        * @since 4.2
-        */
-       public static Expression exists(ColumnSelect<?> subQuery) {
+       public static Expression exists(FluentSelect<?> subQuery) {
                return new ASTExists(new ASTSubquery(subQuery));
        }
 
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSubquery.java 
b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSubquery.java
index f157501..70cde59 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSubquery.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSubquery.java
@@ -23,14 +23,12 @@ import java.io.IOException;
 
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.Persistent;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.query.ColumnSelect;
-import org.apache.cayenne.access.translator.select.ColumnSelectWrapper;
-import org.apache.cayenne.query.ObjectSelect;
-import org.apache.cayenne.access.translator.select.ObjectSelectWrapper;
-import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.access.translator.select.FluentSelectWrapper;
 import org.apache.cayenne.access.translator.select.SelectQueryWrapper;
 import org.apache.cayenne.access.translator.select.TranslatableQueryWrapper;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.query.FluentSelect;
+import org.apache.cayenne.query.SelectQuery;
 
 /**
  * @since 4.2
@@ -43,12 +41,8 @@ public class ASTSubquery extends SimpleNode {
         this(new SelectQueryWrapper(query));
     }
 
-    public ASTSubquery(ObjectSelect<?> query) {
-        this(new ObjectSelectWrapper(query));
-    }
-
-    public ASTSubquery(ColumnSelect<?> query) {
-        this(new ColumnSelectWrapper(query));
+    public ASTSubquery(FluentSelect<?> query) {
+        this(new FluentSelectWrapper(query));
     }
 
     public ASTSubquery(TranslatableQueryWrapper query) {
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelect.java 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelect.java
index b96b6bf..5e7333a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelect.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelect.java
@@ -70,41 +70,23 @@ public class ColumnSelect<T> extends FluentSelect<T> {
     // package private for tests
     boolean singleColumn = true;
     boolean distinct;
-    boolean suppressDistinct;
+
+    ColumnSelectMetadata metaData = new ColumnSelectMetadata();
 
     protected ColumnSelect() {
-        super();
     }
 
     /**
      * Copy constructor to convert ObjectSelect to ColumnSelect
      */
     protected ColumnSelect(ObjectSelect<T> select) {
-        super();
-        this.name = select.name;
         this.entityType = select.entityType;
         this.entityName = select.entityName;
         this.dbEntityName = select.dbEntityName;
         this.where = select.where;
         this.having = select.having;
         this.orderings = select.orderings;
-        this.prefetches = select.prefetches;
-        this.limit = select.limit;
-        this.offset = select.offset;
-        this.pageSize = select.pageSize;
-        this.statementFetchSize = select.statementFetchSize;
-        this.cacheStrategy = select.cacheStrategy;
-        this.cacheGroup = select.cacheGroup;
-    }
-
-    @Override
-    protected Query createReplacementQuery(EntityResolver resolver) {
-        SelectQuery<?> replacement = 
(SelectQuery)super.createReplacementQuery(resolver);
-        replacement.setColumns(columns);
-        replacement.setCanReturnScalarValue(singleColumn);
-        replacement.setDistinct(distinct);
-        replacement.setSuppressDistinct(suppressDistinct);
-        return replacement;
+        this.metaData.copyFromInfo(select.metaData);
     }
 
     /**
@@ -143,7 +125,6 @@ public class ColumnSelect<T> extends FluentSelect<T> {
         this.entityType = entityType;
         this.entityName = entityName;
         this.dbEntityName = dbEntityName;
-        this.replacementQuery = null;
         return this;
     }
 
@@ -233,7 +214,6 @@ public class ColumnSelect<T> extends FluentSelect<T> {
         }
 
         Collections.addAll(this.orderings, orderings);
-        replacementQuery = null;
         return this;
     }
 
@@ -253,7 +233,6 @@ public class ColumnSelect<T> extends FluentSelect<T> {
         }
 
         this.orderings.addAll(orderings);
-        replacementQuery = null;
         return this;
     }
 
@@ -263,17 +242,7 @@ public class ColumnSelect<T> extends FluentSelect<T> {
      * @return this object
      */
     public ColumnSelect<T> prefetch(PrefetchTreeNode prefetch) {
-
-        if (prefetch == null) {
-            return this;
-        }
-
-        if (prefetches == null) {
-            prefetches = new PrefetchTreeNode();
-        }
-
-        prefetches.merge(prefetch);
-        replacementQuery = null;
+        metaData.mergePrefetch(prefetch);
         return this;
     }
 
@@ -284,17 +253,10 @@ public class ColumnSelect<T> extends FluentSelect<T> {
      * @return this object
      */
     public ColumnSelect<T> prefetch(String path, int semantics) {
-
         if (path == null) {
             return this;
         }
-
-        if (prefetches == null) {
-            prefetches = new PrefetchTreeNode();
-        }
-
-        prefetches.addPath(path).setSemantics(semantics);
-        replacementQuery = null;
+        metaData.addPrefetch(path, semantics);
         return this;
     }
 
@@ -303,11 +265,7 @@ public class ColumnSelect<T> extends FluentSelect<T> {
      * that should be ever be fetched from the database.
      */
     public ColumnSelect<T> limit(int fetchLimit) {
-        if (this.limit != fetchLimit) {
-            this.limit = fetchLimit;
-            this.replacementQuery = null;
-        }
-
+        this.metaData.setFetchLimit(fetchLimit);
         return this;
     }
 
@@ -316,11 +274,7 @@ public class ColumnSelect<T> extends FluentSelect<T> {
      * should be skipped when reading data from the database.
      */
     public ColumnSelect<T> offset(int fetchOffset) {
-        if (this.offset != fetchOffset) {
-            this.offset = fetchOffset;
-            this.replacementQuery = null;
-        }
-
+        this.metaData.setFetchOffset(fetchOffset);
         return this;
     }
 
@@ -330,11 +284,7 @@ public class ColumnSelect<T> extends FluentSelect<T> {
      * parts of the result are ever going to be accessed.
      */
     public ColumnSelect<T> pageSize(int pageSize) {
-        if (this.pageSize != pageSize) {
-            this.pageSize = pageSize;
-            this.replacementQuery = null;
-        }
-
+        this.metaData.setPageSize(pageSize);
         return this;
     }
 
@@ -345,25 +295,13 @@ public class ColumnSelect<T> extends FluentSelect<T> {
      * @see Statement#setFetchSize(int)
      */
     public ColumnSelect<T> statementFetchSize(int size) {
-        if (this.statementFetchSize != size) {
-            this.statementFetchSize = size;
-            this.replacementQuery = null;
-        }
-
+        this.metaData.setStatementFetchSize(size);
         return this;
     }
 
     public ColumnSelect<T> cacheStrategy(QueryCacheStrategy strategy) {
-        if (this.cacheStrategy != strategy) {
-            this.cacheStrategy = strategy;
-            this.replacementQuery = null;
-        }
-
-        if(this.cacheGroup != null) {
-            this.cacheGroup = null;
-            this.replacementQuery = null;
-        }
-
+        setCacheStrategy(strategy);
+        setCacheGroup(null);
         return this;
     }
 
@@ -372,8 +310,7 @@ public class ColumnSelect<T> extends FluentSelect<T> {
     }
 
     public ColumnSelect<T> cacheGroup(String cacheGroup) {
-        this.cacheGroup = cacheGroup;
-        this.replacementQuery = null;
+        setCacheGroup(cacheGroup);
         return this;
     }
 
@@ -451,7 +388,6 @@ public class ColumnSelect<T> extends FluentSelect<T> {
         columns.add(firstProperty);
         Collections.addAll(columns, otherProperties);
         singleColumn = false;
-        replacementQuery = null;
         return (ColumnSelect<Object[]>)this;
     }
 
@@ -478,7 +414,6 @@ public class ColumnSelect<T> extends FluentSelect<T> {
 
         columns.addAll(properties);
         singleColumn = false;
-        replacementQuery = null;
         return (ColumnSelect<Object[]>)this;
     }
 
@@ -490,7 +425,6 @@ public class ColumnSelect<T> extends FluentSelect<T> {
             this.columns.clear(); // if we don't clear then return type will 
be incorrect
         }
         this.columns.add(property);
-        this.replacementQuery = null;
         return (ColumnSelect<E>) this;
     }
 
@@ -640,9 +574,8 @@ public class ColumnSelect<T> extends FluentSelect<T> {
      * Explicitly request distinct in query.
      */
     public ColumnSelect<T> distinct() {
-        this.suppressDistinct = false;
+        metaData.setSuppressingDistinct(false);
         this.distinct = true;
-        this.replacementQuery = null;
         return this;
     }
 
@@ -650,12 +583,12 @@ public class ColumnSelect<T> extends FluentSelect<T> {
      * Explicitly suppress distinct in query.
      */
     public ColumnSelect<T> suppressDistinct() {
-        this.suppressDistinct = true;
+        metaData.setSuppressingDistinct(true);
         this.distinct = false;
-        this.replacementQuery = null;
         return this;
     }
 
+    @Override
     public Collection<BaseProperty<?>> getColumns() {
         return columns;
     }
@@ -663,6 +596,7 @@ public class ColumnSelect<T> extends FluentSelect<T> {
     /**
      * @since 4.2
      */
+    @Override
     public boolean isDistinct() {
         return distinct;
     }
@@ -671,4 +605,20 @@ public class ColumnSelect<T> extends FluentSelect<T> {
     public T selectFirst(ObjectContext context) {
         return context.selectFirst(limit(1));
     }
+
+    boolean isSingleColumn() {
+        return singleColumn;
+    }
+
+    @Override
+    public QueryMetadata getMetaData(EntityResolver resolver) {
+        Object root = resolveRoot(resolver);
+        metaData.resolve(root, resolver, this);
+        return metaData;
+    }
+
+    @Override
+    protected BaseQueryMetadata getBaseMetaData() {
+        return metaData;
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelectMetadata.java
similarity index 68%
copy from 
cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
copy to 
cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelectMetadata.java
index 9ee83bf..eb9a389 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelectMetadata.java
@@ -28,16 +28,10 @@ import java.util.Map;
 import java.util.Set;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.ObjectId;
-import org.apache.cayenne.Persistent;
-import org.apache.cayenne.access.types.ValueObjectType;
-import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.TraversalHandler;
 import org.apache.cayenne.exp.parser.ASTDbPath;
-import org.apache.cayenne.exp.parser.ASTFunctionCall;
-import org.apache.cayenne.exp.parser.ASTScalar;
 import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
@@ -58,12 +52,12 @@ import org.apache.cayenne.reflect.ToOneProperty;
 import org.apache.cayenne.util.CayenneMapEntry;
 
 /**
- * @since 3.0
+ * @since 4.2
  */
-class SelectQueryMetadata extends BaseQueryMetadata {
+class ColumnSelectMetadata extends BaseQueryMetadata {
+
+       private static final long serialVersionUID = -3622675304651257963L;
 
-       private static final long serialVersionUID = 7465922769303943945L;
-       
        private Map<String, String> pathSplitAliases;
        private boolean isSingleResultSetMapping;
        private boolean suppressingDistinct;
@@ -74,7 +68,7 @@ class SelectQueryMetadata extends BaseQueryMetadata {
                this.pathSplitAliases = new 
HashMap<>(info.getPathSplitAliases());
        }
 
-       boolean resolve(Object root, EntityResolver resolver, SelectQuery<?> 
query) {
+       boolean resolve(Object root, EntityResolver resolver, ColumnSelect<?> 
query) {
 
                if (super.resolve(root, resolver)) {
                        // generate unique cache key, but only if we are 
caching..
@@ -84,7 +78,7 @@ class SelectQueryMetadata extends BaseQueryMetadata {
 
                        resolveAutoAliases(query);
                        buildResultSetMappingForColumns(query, resolver);
-                       isSingleResultSetMapping = query.canReturnScalarValue() 
&& super.isSingleResultSetMapping();
+                       isSingleResultSetMapping = query.isSingleColumn() && 
super.isSingleResultSetMapping();
 
                        return true;
                }
@@ -92,7 +86,7 @@ class SelectQueryMetadata extends BaseQueryMetadata {
                return false;
        }
 
-       private String makeCacheKey(SelectQuery<?> query, EntityResolver 
resolver) {
+       private String makeCacheKey(ColumnSelect<?> query, EntityResolver 
resolver) {
 
                // create a unique key based on entity or columns, qualifier, 
ordering, fetch offset and limit
 
@@ -115,15 +109,15 @@ class SelectQueryMetadata extends BaseQueryMetadata {
                        }
                }
 
-               if (query.getQualifier() != null) {
+               if (query.getWhere() != null) {
                        key.append('/');
                        if(traversalHandler == null) {
                                traversalHandler = new 
ToCacheKeyTraversalHandler(resolver.getValueObjectTypeRegistry(), key);
                        }
-                       query.getQualifier().traverse(traversalHandler);
+                       query.getWhere().traverse(traversalHandler);
                }
 
-               if (!query.getOrderings().isEmpty()) {
+               if (query.getOrderings() != null && 
!query.getOrderings().isEmpty()) {
                        for (Ordering o : query.getOrderings()) {
                                key.append('/').append(o.getSortSpecString());
                                if (!o.isAscending()) {
@@ -136,25 +130,25 @@ class SelectQueryMetadata extends BaseQueryMetadata {
                        }
                }
 
-               if (query.getFetchOffset() > 0 || query.getFetchLimit() > 0) {
+               if (fetchOffset > 0 || fetchLimit > 0) {
                        key.append('/');
-                       if (query.getFetchOffset() > 0) {
-                               key.append('o').append(query.getFetchOffset());
+                       if (fetchOffset > 0) {
+                               key.append('o').append(fetchOffset);
                        }
-                       if (query.getFetchLimit() > 0) {
-                               key.append('l').append(query.getFetchLimit());
+                       if (fetchLimit > 0) {
+                               key.append('l').append(fetchLimit);
                        }
                }
 
                // add prefetch to cache key per CAY-2349
-               if(query.getPrefetchTree() != null) {
-                       query.getPrefetchTree().traverse(new 
ToCacheKeyPrefetchProcessor(key));
+               if(prefetchTree != null) {
+                       prefetchTree.traverse(new 
ToCacheKeyPrefetchProcessor(key));
                }
 
                return key.toString();
        }
 
-       private void resolveAutoAliases(SelectQuery<?> query) {
+       private void resolveAutoAliases(ColumnSelect<?> query) {
                resolveQualifierAliases(query);
                resolveColumnsAliases(query);
         resolveOrderingAliases(query);
@@ -162,14 +156,14 @@ class SelectQueryMetadata extends BaseQueryMetadata {
                // TODO: include aliases in prefetches? flattened attributes?
        }
 
-       private void resolveQualifierAliases(SelectQuery<?> query) {
-               Expression qualifier = query.getQualifier();
+       private void resolveQualifierAliases(ColumnSelect<?> query) {
+               Expression qualifier = query.getWhere();
                if (qualifier != null) {
                        resolveAutoAliases(qualifier);
                }
        }
 
-       private void resolveColumnsAliases(SelectQuery<?> query) {
+       private void resolveColumnsAliases(ColumnSelect<?> query) {
         Collection<BaseProperty<?>> columns = query.getColumns();
         if(columns != null) {
             for(BaseProperty<?> property : columns) {
@@ -181,8 +175,8 @@ class SelectQueryMetadata extends BaseQueryMetadata {
         }
     }
 
-    private void resolveOrderingAliases(SelectQuery<?> query) {
-        List<Ordering> orderings = query.getOrderings();
+    private void resolveOrderingAliases(ColumnSelect<?> query) {
+        Collection<Ordering> orderings = query.getOrderings();
         if(orderings != null) {
             for(Ordering ordering : orderings) {
                 Expression sortSpec = ordering.getSortSpec();
@@ -193,8 +187,8 @@ class SelectQueryMetadata extends BaseQueryMetadata {
         }
     }
 
-    private void resolveHavingQualifierAliases(SelectQuery<?> query) {
-        Expression havingQualifier = query.getHavingQualifier();
+    private void resolveHavingQualifierAliases(ColumnSelect<?> query) {
+        Expression havingQualifier = query.getHaving();
         if(havingQualifier != null) {
             resolveAutoAliases(havingQualifier);
         }
@@ -227,40 +221,15 @@ class SelectQueryMetadata extends BaseQueryMetadata {
                }
        }
 
-       /**
-        * @since 3.0
-        */
        @Override
        public Map<String, String> getPathSplitAliases() {
                return pathSplitAliases != null ? pathSplitAliases : 
Collections.emptyMap();
        }
 
        /**
-        * @since 3.0
-        */
-       public void addPathSplitAliases(String path, String... aliases) {
-               if (aliases == null) {
-                       throw new NullPointerException("Null aliases");
-               }
-
-               if (aliases.length == 0) {
-                       throw new IllegalArgumentException("No aliases 
specified");
-               }
-
-               if (pathSplitAliases == null) {
-                       pathSplitAliases = new HashMap<>();
-               }
-
-               for (String alias : aliases) {
-                       pathSplitAliases.put(alias, path);
-               }
-       }
-
-       /**
         * Build DB result descriptor, that will be used to read and convert 
raw result of ColumnSelect
-        * @since 4.0
         */
-       private void buildResultSetMappingForColumns(SelectQuery<?> query, 
EntityResolver resolver) {
+       private void buildResultSetMappingForColumns(ColumnSelect<?> query, 
EntityResolver resolver) {
                if(query.getColumns() == null || query.getColumns().isEmpty()) {
                        return;
                }
@@ -333,7 +302,7 @@ class SelectQueryMetadata extends BaseQueryMetadata {
         * @param resolver entity resolver to get ObjEntity and ClassDescriptor
         * @return Entity result
         */
-       private EntityResult buildEntityResultForColumn(SelectQuery<?> query, 
BaseProperty<?> column, EntityResolver resolver) {
+       private EntityResult buildEntityResultForColumn(ColumnSelect<?> query, 
BaseProperty<?> column, EntityResolver resolver) {
                // This method is actually repeating logic of 
DescriptorColumnExtractor.
                // Here we don't care about intermediate joins and few other 
things so it's shorter.
                // Logic of these methods should be unified and simplified, 
possibly to a single source of metadata,
@@ -383,8 +352,8 @@ class SelectQueryMetadata extends BaseQueryMetadata {
                descriptor.visitAllProperties(visitor);
 
                // Collection columns for joint prefetch
-               if(query.getPrefetchTree() != null) {
-                       for (PrefetchTreeNode prefetch : 
query.getPrefetchTree().adjacentJointNodes()) {
+               if(prefetchTree != null) {
+                       for (PrefetchTreeNode prefetch : 
prefetchTree.adjacentJointNodes()) {
                                // for each prefetch add columns from the 
target entity
                                Expression prefetchExp = 
ExpressionFactory.exp(prefetch.getPath());
                                ASTDbPath dbPrefetch = (ASTDbPath) 
oe.translateToDbPath(prefetchExp);
@@ -427,147 +396,17 @@ class SelectQueryMetadata extends BaseQueryMetadata {
                return result;
        }
 
-       /**
-        * @since 4.0
-        */
        @Override
        public boolean isSingleResultSetMapping() {
                return isSingleResultSetMapping;
        }
 
-       /**
-        * @since 4.0
-        */
        @Override
        public boolean isSuppressingDistinct() {
                return suppressingDistinct;
        }
 
-       /**
-        * @since 4.0
-        */
        public void setSuppressingDistinct(boolean suppressingDistinct) {
                this.suppressingDistinct = suppressingDistinct;
        }
-
-       /**
-        * Expression traverse handler to create cache key string out of 
Expression.
-        * {@link Expression#appendAsString(Appendable)} where previously used 
for that,
-        * but it can't handle custom value objects properly (see CAY-2210).
-        *
-        * @see ValueObjectTypeRegistry
-        *
-        * @since 4.0
-        */
-       static class ToCacheKeyTraversalHandler implements TraversalHandler {
-
-               private ValueObjectTypeRegistry registry;
-               private StringBuilder out;
-
-               ToCacheKeyTraversalHandler(ValueObjectTypeRegistry registry, 
StringBuilder out) {
-                       this.registry = registry;
-                       this.out = out;
-               }
-
-               @Override
-               public void finishedChild(Expression node, int childIndex, 
boolean hasMoreChildren) {
-                       out.append(',');
-               }
-
-               @Override
-               public void startNode(Expression node, Expression parentNode) {
-                       if(node.getType() == Expression.FUNCTION_CALL) {
-                               
out.append(((ASTFunctionCall)node).getFunctionName()).append('(');
-                       } else {
-                               out.append(node.getType()).append('(');
-                       }
-               }
-
-               @Override
-               public void endNode(Expression node, Expression parentNode) {
-                       out.append(')');
-               }
-
-               @Override
-               public void objectNode(Object leaf, Expression parentNode) {
-                       if(leaf == null) {
-                               out.append("null");
-                               return;
-                       }
-
-                       if(leaf instanceof ASTScalar) {
-                               leaf = ((ASTScalar) leaf).getValue();
-                       } else if(leaf instanceof Object[]) {
-                               for(Object value : (Object[])leaf) {
-                                       objectNode(value, parentNode);
-                                       out.append(',');
-                               }
-                               return;
-                       }
-
-                       if (leaf instanceof Persistent) {
-                               ObjectId id = ((Persistent) leaf).getObjectId();
-                               Object encode = (id != null) ? id : leaf;
-                               out.append(encode);
-                       } else if (leaf instanceof Enum<?>) {
-                               Enum<?> e = (Enum<?>) leaf;
-                               
out.append("e:").append(leaf.getClass().getName()).append(':').append(e.ordinal());
-                       } else {
-                               ValueObjectType<Object, ?> valueObjectType;
-                               if (registry == null || (valueObjectType = 
registry.getValueType(leaf.getClass())) == null) {
-                                       // Registry will be null in 
cayenne-client context.
-                                       // Maybe we shouldn't create cache key 
at all in that case...
-                                       out.append(leaf);
-                               } else {
-                                       
out.append(valueObjectType.toCacheKey(leaf));
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Prefetch processor that append prefetch tree into cache key.
-        * @since 4.0
-        */
-       static class ToCacheKeyPrefetchProcessor implements PrefetchProcessor {
-
-               StringBuilder out;
-
-               ToCacheKeyPrefetchProcessor(StringBuilder out) {
-                       this.out = out;
-               }
-
-               @Override
-               public boolean startPhantomPrefetch(PrefetchTreeNode node) {
-                       return true;
-               }
-
-               @Override
-               public boolean startDisjointPrefetch(PrefetchTreeNode node) {
-                       out.append("/pd:").append(node.getPath());
-                       return true;
-               }
-
-               @Override
-               public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) 
{
-                       out.append("/pi:").append(node.getPath());
-                       return true;
-               }
-
-               @Override
-               public boolean startJointPrefetch(PrefetchTreeNode node) {
-                       out.append("/pj:").append(node.getPath());
-                       return true;
-               }
-
-               @Override
-               public boolean startUnknownPrefetch(PrefetchTreeNode node) {
-                       out.append("/pu:").append(node.getPath());
-                       return true;
-               }
-
-               @Override
-               public void finishPrefetch(PrefetchTreeNode node) {
-               }
-       }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java 
b/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java
index 2d9d1a3..a650e75 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java
@@ -28,6 +28,7 @@ import org.apache.cayenne.ResultBatchIterator;
 import org.apache.cayenne.ResultIterator;
 import org.apache.cayenne.ResultIteratorCallback;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.ObjEntity;
@@ -37,39 +38,26 @@ import org.apache.cayenne.map.ObjEntity;
  *
  * @since 4.0
  */
-public abstract class FluentSelect<T> extends IndirectQuery implements 
Select<T> {
+public abstract class FluentSelect<T> extends AbstractQuery implements 
Select<T> {
 
+    // root
     protected Class<?> entityType;
     protected String entityName;
     protected String dbEntityName;
+
     protected Expression where;
     protected Expression having;
-    protected Collection<Ordering> orderings;
-    protected PrefetchTreeNode prefetches;
-    protected int limit;
-    protected int offset;
-    protected int pageSize;
-    protected int statementFetchSize;
-    protected QueryCacheStrategy cacheStrategy;
-    protected String cacheGroup;
-
     boolean havingExpressionIsActive = false;
 
+    protected Collection<Ordering> orderings;
+
     protected FluentSelect() {
     }
 
-    /**
-     * Translates self to a SelectQuery.
-     */
-    @SuppressWarnings("unchecked")
-    @Override
-    protected Query createReplacementQuery(EntityResolver resolver) {
-
-        @SuppressWarnings("rawtypes")
-        SelectQuery replacement = new SelectQuery();
-
+    protected Object resolveRoot(EntityResolver resolver) {
+        Object root;
         if (entityType != null) {
-            replacement.setRoot(entityType);
+            root = entityType;
         } else if (entityName != null) {
 
             ObjEntity entity = resolver.getObjEntity(entityName);
@@ -77,7 +65,7 @@ public abstract class FluentSelect<T> extends IndirectQuery 
implements Select<T>
                 throw new CayenneRuntimeException("Unrecognized ObjEntity 
name: %s", entityName);
             }
 
-            replacement.setRoot(entity);
+            root = entity;
         } else if (dbEntityName != null) {
 
             DbEntity entity = resolver.getDbEntity(dbEntityName);
@@ -85,47 +73,27 @@ public abstract class FluentSelect<T> extends IndirectQuery 
implements Select<T>
                 throw new CayenneRuntimeException("Unrecognized DbEntity name: 
%s", dbEntityName);
             }
 
-            replacement.setRoot(entity);
+            root = entity;
         } else {
             throw new CayenneRuntimeException("Undefined root entity of the 
query");
         }
-
-        replacement.setQualifier(where);
-        replacement.setHavingQualifier(having);
-        replacement.addOrderings(orderings);
-        replacement.setPrefetchTree(prefetches);
-        replacement.setCacheStrategy(cacheStrategy);
-        replacement.setCacheGroup(cacheGroup);
-        replacement.setFetchLimit(limit);
-        replacement.setFetchOffset(offset);
-        replacement.setPageSize(pageSize);
-        replacement.setStatementFetchSize(statementFetchSize);
-
-        return replacement;
-    }
-
-    public String getCacheGroup() {
-        return cacheGroup;
-    }
-
-    public QueryCacheStrategy getCacheStrategy() {
-        return cacheStrategy;
+        return root;
     }
 
     public int getStatementFetchSize() {
-        return statementFetchSize;
+        return getBaseMetaData().getStatementFetchSize();
     }
 
     public int getPageSize() {
-        return pageSize;
+        return getBaseMetaData().getPageSize();
     }
 
     public int getLimit() {
-        return limit;
+        return getBaseMetaData().getFetchLimit();
     }
 
     public int getOffset() {
-        return offset;
+        return getBaseMetaData().getFetchOffset();
     }
 
     public Class<?> getEntityType() {
@@ -159,7 +127,7 @@ public abstract class FluentSelect<T> extends IndirectQuery 
implements Select<T>
     }
 
     public PrefetchTreeNode getPrefetches() {
-        return prefetches;
+        return getBaseMetaData().getPrefetchTree();
     }
 
     void setActiveExpression(Expression exp) {
@@ -168,7 +136,6 @@ public abstract class FluentSelect<T> extends IndirectQuery 
implements Select<T>
         } else {
             where = exp;
         }
-        replacementQuery = null;
     }
 
     Expression getActiveExpression() {
@@ -203,4 +170,41 @@ public abstract class FluentSelect<T> extends 
IndirectQuery implements Select<T>
     public ResultBatchIterator<T> batchIterator(ObjectContext context, int 
size) {
         return context.batchIterator(this, size);
     }
+
+    @Override
+    public SQLAction createSQLAction(SQLActionVisitor visitor) {
+        return visitor.objectSelectAction(this);
+    }
+
+    @Override
+    public void route(QueryRouter router, EntityResolver resolver, Query 
substitutedQuery) {
+        super.route(router, resolver, substitutedQuery);
+
+        // suppress prefetches for paginated queries.. instead prefetches will 
be resolved per row...
+        if (getPageSize() <= 0) {
+            routePrefetches(router, resolver);
+        }
+    }
+
+    public boolean isFetchingDataRows() {
+        return false;
+    }
+
+    private void routePrefetches(QueryRouter router, EntityResolver resolver) {
+        new FluentSelectPrefetchRouterAction().route(this, router, resolver);
+    }
+
+    /**
+     * @since 4.2
+     */
+    public Collection<BaseProperty<?>> getColumns() {
+        return null;
+    }
+
+    /**
+     * @since 4.2
+     */
+    public boolean isDistinct() {
+        return false;
+    }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelectPrefetchRouterAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelectPrefetchRouterAction.java
new file mode 100644
index 0000000..a67bf66
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelectPrefetchRouterAction.java
@@ -0,0 +1,141 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.query;
+
+import java.util.Iterator;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.util.CayenneMapEntry;
+
+/**
+ * Preprocessor and router of SelectQuery prefetches.
+ * Copy of {@link SelectQueryPrefetchRouterAction}.
+ *
+ * @since 4.2
+ */
+class FluentSelectPrefetchRouterAction implements PrefetchProcessor {
+
+    FluentSelect<?> query;
+    QueryRouter router;
+    EntityResolver resolver;
+    ClassDescriptor classDescriptor;
+
+    /**
+     * Routes query prefetches, but not the query itself.
+     */
+    void route(FluentSelect<?> query, QueryRouter router, EntityResolver 
resolver) {
+        if (!query.isFetchingDataRows() && query.getPrefetches() != null) {
+
+            this.query = query;
+            this.router = router;
+            this.resolver = resolver;
+            this.classDescriptor = 
query.getMetaData(resolver).getClassDescriptor();
+
+            query.getPrefetches().traverse(this);
+        }
+    }
+
+    public boolean startPhantomPrefetch(PrefetchTreeNode node) {
+        return true;
+    }
+
+    public boolean startDisjointPrefetch(PrefetchTreeNode node) {
+        // don't do anything to root
+        if (node == query.getPrefetches()) {
+            return true;
+        }
+
+        String prefetchPath = node.getPath();
+
+        // find last relationship
+        Iterator<CayenneMapEntry> it = 
classDescriptor.getEntity().resolvePathComponents(
+                prefetchPath);
+
+        ObjRelationship relationship = null;
+        while (it.hasNext()) {
+            relationship = (ObjRelationship) it.next();
+        }
+
+        if (relationship == null) {
+            throw new CayenneRuntimeException("Invalid prefetch '%s' for 
entity '%s'"
+                    , prefetchPath, classDescriptor.getEntity().getName());
+        }
+
+        // chain query and entity qualifiers
+        Expression queryQualifier = query.getWhere();
+
+        Expression entityQualifier = classDescriptor
+                .getEntityInheritanceTree()
+                .qualifierForEntityAndSubclasses();
+
+        if (entityQualifier != null) {
+            queryQualifier = (queryQualifier != null)
+                    ? queryQualifier.andExp(entityQualifier)
+                    : entityQualifier;
+        }
+
+        // create and configure PrefetchSelectQuery
+        PrefetchSelectQuery<?> prefetchQuery = new 
PrefetchSelectQuery<>(prefetchPath, relationship);
+        prefetchQuery.setStatementFetchSize(query.getStatementFetchSize());
+
+        prefetchQuery.setQualifier(classDescriptor.getEntity()
+                .translateToRelatedEntity(queryQualifier, prefetchPath));
+
+        if (relationship.isSourceIndependentFromTargetChange()) {
+            // setup extra result columns to be able to relate result rows to 
the parent
+            // result objects.
+            prefetchQuery.addResultPath("db:" + 
relationship.getReverseDbRelationshipPath());
+        }
+
+        // pass prefetch subtree to enable joint prefetches...
+        prefetchQuery.setPrefetchTree(node);
+
+        // route...
+        prefetchQuery.route(router, resolver, null);
+        return true;
+    }
+
+    public boolean startDisjointByIdPrefetch(PrefetchTreeNode 
prefetchTreeNode) {
+        // simply pass through
+        return true;
+    }
+
+    public boolean startJointPrefetch(PrefetchTreeNode node) {
+        // simply pass through
+        return true;
+    }
+
+    public boolean startUnknownPrefetch(PrefetchTreeNode node) {
+        // don't do anything to root
+        if (node == query.getPrefetches()) {
+            return true;
+        }
+
+        // route unknown as disjoint...
+        return startDisjointPrefetch(node);
+    }
+
+    public void finishPrefetch(PrefetchTreeNode node) {
+    }
+}
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java
index f633372..2ce2919 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java
@@ -57,7 +57,7 @@ public class ObjectSelect<T> extends FluentSelect<T> {
 
     private static final long serialVersionUID = -156124021150949227L;
 
-    protected boolean fetchingDataRows;
+    protected ObjectSelectMetadata metaData = new ObjectSelectMetadata();
 
     /**
      * Creates a ObjectSelect that selects objects of a given persistent class.
@@ -154,17 +154,6 @@ public class ObjectSelect<T> extends FluentSelect<T> {
     }
 
     /**
-     * Translates self to a SelectQuery.
-     */
-    @SuppressWarnings({"deprecation", "unchecked"})
-    @Override
-    protected Query createReplacementQuery(EntityResolver resolver) {
-        SelectQuery<?> replacement = (SelectQuery<?>) 
super.createReplacementQuery(resolver);
-        replacement.setFetchingDataRows(fetchingDataRows);
-        return replacement;
-    }
-
-    /**
      * Sets the type of the entity to fetch without changing the return type of
      * the query.
      *
@@ -365,7 +354,6 @@ public class ObjectSelect<T> extends FluentSelect<T> {
         }
 
         Collections.addAll(this.orderings, orderings);
-        replacementQuery = null;
 
         return this;
     }
@@ -386,7 +374,6 @@ public class ObjectSelect<T> extends FluentSelect<T> {
         }
 
         this.orderings.addAll(orderings);
-        replacementQuery = null;
 
         return this;
     }
@@ -397,17 +384,7 @@ public class ObjectSelect<T> extends FluentSelect<T> {
      * @return this object
      */
     public ObjectSelect<T> prefetch(PrefetchTreeNode prefetch) {
-
-        if (prefetch == null) {
-            return this;
-        }
-
-        if (prefetches == null) {
-            prefetches = new PrefetchTreeNode();
-        }
-
-        prefetches.merge(prefetch);
-        replacementQuery = null;
+        metaData.mergePrefetch(prefetch);
         return this;
     }
 
@@ -418,17 +395,10 @@ public class ObjectSelect<T> extends FluentSelect<T> {
      * @return this object
      */
     public ObjectSelect<T> prefetch(String path, int semantics) {
-
         if (path == null) {
             return this;
         }
-
-        if (prefetches == null) {
-            prefetches = new PrefetchTreeNode();
-        }
-
-        prefetches.addPath(path).setSemantics(semantics);
-        replacementQuery = null;
+        metaData.addPrefetch(path, semantics);
         return this;
     }
 
@@ -436,13 +406,8 @@ public class ObjectSelect<T> extends FluentSelect<T> {
      * Resets query fetch limit - a parameter that defines max number of 
objects
      * that should be ever be fetched from the database.
      */
-    @SuppressWarnings("unchecked")
     public ObjectSelect<T> limit(int fetchLimit) {
-        if (this.limit != fetchLimit) {
-            this.limit = fetchLimit;
-            this.replacementQuery = null;
-        }
-
+        this.metaData.setFetchLimit(fetchLimit);
         return this;
     }
 
@@ -451,11 +416,7 @@ public class ObjectSelect<T> extends FluentSelect<T> {
      * 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;
-        }
-
+        this.metaData.setFetchOffset(fetchOffset);
         return this;
     }
 
@@ -465,11 +426,7 @@ public class ObjectSelect<T> extends FluentSelect<T> {
      * 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;
-        }
-
+        this.metaData.setPageSize(pageSize);
         return this;
     }
 
@@ -480,25 +437,13 @@ public class ObjectSelect<T> extends FluentSelect<T> {
      * @see Statement#setFetchSize(int)
      */
     public ObjectSelect<T> statementFetchSize(int size) {
-        if (this.statementFetchSize != size) {
-            this.statementFetchSize = size;
-            this.replacementQuery = null;
-        }
-
+        this.metaData.setStatementFetchSize(size);
         return this;
     }
 
     public ObjectSelect<T> cacheStrategy(QueryCacheStrategy strategy) {
-        if (this.cacheStrategy != strategy) {
-            this.cacheStrategy = strategy;
-            this.replacementQuery = null;
-        }
-
-        if(this.cacheGroup != null) {
-            this.cacheGroup = null;
-            this.replacementQuery = null;
-        }
-
+        setCacheStrategy(strategy);
+        setCacheGroup(null);
         return this;
     }
 
@@ -507,8 +452,7 @@ public class ObjectSelect<T> extends FluentSelect<T> {
     }
 
     public ObjectSelect<T> cacheGroup(String cacheGroup) {
-        this.cacheGroup = cacheGroup;
-        this.replacementQuery = null;
+        setCacheGroup(cacheGroup);
         return this;
     }
 
@@ -568,10 +512,7 @@ public class ObjectSelect<T> extends FluentSelect<T> {
      */
     @SuppressWarnings("unchecked")
     public ObjectSelect<DataRow> fetchDataRows() {
-        if(!fetchingDataRows) {
-            fetchingDataRows = true;
-            replacementQuery = null;
-        }
+        metaData.setFetchingDataRows(true);
         return (ObjectSelect<DataRow>) this;
     }
 
@@ -725,7 +666,20 @@ public class ObjectSelect<T> extends FluentSelect<T> {
         return context.selectFirst(limit(1));
     }
 
+    @Override
     public boolean isFetchingDataRows() {
-        return fetchingDataRows;
+        return metaData.isFetchingDataRows();
+    }
+
+    @Override
+    public QueryMetadata getMetaData(EntityResolver resolver) {
+        Object root = resolveRoot(resolver);
+        metaData.resolve(root, resolver, this);
+        return metaData;
+    }
+
+    @Override
+    protected BaseQueryMetadata getBaseMetaData() {
+        return metaData;
     }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelectMetadata.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelectMetadata.java
new file mode 100644
index 0000000..b23a4af
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelectMetadata.java
@@ -0,0 +1,171 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.query;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.TraversalHandler;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * @since 4.2
+ */
+class ObjectSelectMetadata extends BaseQueryMetadata {
+
+       private static final long serialVersionUID = -4936484509363047672L;
+
+       private Map<String, String> pathSplitAliases;
+
+       @Override
+       void copyFromInfo(QueryMetadata info) {
+               super.copyFromInfo(info);
+               this.pathSplitAliases = new 
HashMap<>(info.getPathSplitAliases());
+       }
+
+       boolean resolve(Object root, EntityResolver resolver, ObjectSelect<?> 
query) {
+
+               if (super.resolve(root, resolver)) {
+                       // generate unique cache key, but only if we are 
caching..
+                       if (cacheStrategy != null && cacheStrategy != 
QueryCacheStrategy.NO_CACHE) {
+                               this.cacheKey = makeCacheKey(query, resolver);
+                       }
+
+                       resolveAutoAliases(query);
+                       return true;
+               }
+
+               return false;
+       }
+
+       private String makeCacheKey(FluentSelect<?> query, EntityResolver 
resolver) {
+
+               // create a unique key based on entity or columns, qualifier, 
ordering, fetch offset and limit
+
+               StringBuilder key = new StringBuilder();
+               // handler to create string out of expressions, created lazily
+               TraversalHandler traversalHandler = null;
+
+               ObjEntity entity = getObjEntity();
+               if (entity != null) {
+                       key.append(entity.getName());
+               } else if (dbEntity != null) {
+                       key.append("db:").append(dbEntity.getName());
+               }
+
+               if (query.getWhere() != null) {
+                       key.append('/');
+            traversalHandler = new 
ToCacheKeyTraversalHandler(resolver.getValueObjectTypeRegistry(), key);
+                       query.getWhere().traverse(traversalHandler);
+               }
+
+               if (query.getOrderings() != null && 
!query.getOrderings().isEmpty()) {
+                       for (Ordering o : query.getOrderings()) {
+                               key.append('/').append(o.getSortSpecString());
+                               if (!o.isAscending()) {
+                                       key.append(":d");
+                               }
+
+                               if (o.isCaseInsensitive()) {
+                                       key.append(":i");
+                               }
+                       }
+               }
+
+               if (fetchLimit > 0 || fetchOffset > 0) {
+                       key.append('/');
+                       if (fetchOffset > 0) {
+                               key.append('o').append(fetchOffset);
+                       }
+                       if (fetchLimit > 0) {
+                               key.append('l').append(fetchLimit);
+                       }
+               }
+
+               // add prefetch to cache key per CAY-2349
+               if(prefetchTree != null) {
+                       prefetchTree.traverse(new 
ToCacheKeyPrefetchProcessor(key));
+               }
+
+               return key.toString();
+       }
+
+       private void resolveAutoAliases(ObjectSelect<?> query) {
+               resolveQualifierAliases(query);
+        resolveOrderingAliases(query);
+       }
+
+       private void resolveQualifierAliases(ObjectSelect<?> query) {
+               Expression qualifier = query.getWhere();
+               if (qualifier != null) {
+                       resolveAutoAliases(qualifier);
+               }
+       }
+
+
+       private void resolveOrderingAliases(ObjectSelect<?> query) {
+        Collection<Ordering> orderings = query.getOrderings();
+        if(orderings != null) {
+            for(Ordering ordering : orderings) {
+                Expression sortSpec = ordering.getSortSpec();
+                if(sortSpec != null) {
+                    resolveAutoAliases(sortSpec);
+                }
+            }
+        }
+    }
+
+       private void resolveAutoAliases(Expression expression) {
+               Map<String, String> aliases = expression.getPathAliases();
+               if (!aliases.isEmpty()) {
+                       if (pathSplitAliases == null) {
+                               pathSplitAliases = new HashMap<>();
+                       }
+
+                       for(Map.Entry<String, String> entry : 
aliases.entrySet()) {
+                               pathSplitAliases.compute(entry.getKey(), (key, 
value) -> {
+                                       if(value != null && 
!value.equals(entry.getValue())){
+                                               throw new 
CayenneRuntimeException("Can't add the same alias to different path segments.");
+                                       } else {
+                                               return entry.getValue();
+                                       }
+                               });
+                       }
+               }
+
+               int len = expression.getOperandCount();
+               for (int i = 0; i < len; i++) {
+                       Object operand = expression.getOperand(i);
+                       if (operand instanceof Expression) {
+                               resolveAutoAliases((Expression) operand);
+                       }
+               }
+       }
+
+       @Override
+       public Map<String, String> getPathSplitAliases() {
+               return pathSplitAliases != null ? pathSplitAliases : 
Collections.emptyMap();
+       }
+
+}
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/SQLActionVisitor.java 
b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLActionVisitor.java
index fd968e3..baf682f 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/query/SQLActionVisitor.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLActionVisitor.java
@@ -43,6 +43,12 @@ public interface SQLActionVisitor {
     <T> SQLAction objectSelectAction(SelectQuery<T> query);
 
     /**
+     * Creates an action to execute a FluentSelect.
+     * @since 4.2
+     */
+    <T> SQLAction objectSelectAction(FluentSelect<T> query);
+
+    /**
      * Creates an action to execute a SQLTemplate.
      */
     SQLAction sqlAction(SQLTemplate query);
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
index 9ee83bf..2dc74a9 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
@@ -449,125 +449,4 @@ class SelectQueryMetadata extends BaseQueryMetadata {
        public void setSuppressingDistinct(boolean suppressingDistinct) {
                this.suppressingDistinct = suppressingDistinct;
        }
-
-       /**
-        * Expression traverse handler to create cache key string out of 
Expression.
-        * {@link Expression#appendAsString(Appendable)} where previously used 
for that,
-        * but it can't handle custom value objects properly (see CAY-2210).
-        *
-        * @see ValueObjectTypeRegistry
-        *
-        * @since 4.0
-        */
-       static class ToCacheKeyTraversalHandler implements TraversalHandler {
-
-               private ValueObjectTypeRegistry registry;
-               private StringBuilder out;
-
-               ToCacheKeyTraversalHandler(ValueObjectTypeRegistry registry, 
StringBuilder out) {
-                       this.registry = registry;
-                       this.out = out;
-               }
-
-               @Override
-               public void finishedChild(Expression node, int childIndex, 
boolean hasMoreChildren) {
-                       out.append(',');
-               }
-
-               @Override
-               public void startNode(Expression node, Expression parentNode) {
-                       if(node.getType() == Expression.FUNCTION_CALL) {
-                               
out.append(((ASTFunctionCall)node).getFunctionName()).append('(');
-                       } else {
-                               out.append(node.getType()).append('(');
-                       }
-               }
-
-               @Override
-               public void endNode(Expression node, Expression parentNode) {
-                       out.append(')');
-               }
-
-               @Override
-               public void objectNode(Object leaf, Expression parentNode) {
-                       if(leaf == null) {
-                               out.append("null");
-                               return;
-                       }
-
-                       if(leaf instanceof ASTScalar) {
-                               leaf = ((ASTScalar) leaf).getValue();
-                       } else if(leaf instanceof Object[]) {
-                               for(Object value : (Object[])leaf) {
-                                       objectNode(value, parentNode);
-                                       out.append(',');
-                               }
-                               return;
-                       }
-
-                       if (leaf instanceof Persistent) {
-                               ObjectId id = ((Persistent) leaf).getObjectId();
-                               Object encode = (id != null) ? id : leaf;
-                               out.append(encode);
-                       } else if (leaf instanceof Enum<?>) {
-                               Enum<?> e = (Enum<?>) leaf;
-                               
out.append("e:").append(leaf.getClass().getName()).append(':').append(e.ordinal());
-                       } else {
-                               ValueObjectType<Object, ?> valueObjectType;
-                               if (registry == null || (valueObjectType = 
registry.getValueType(leaf.getClass())) == null) {
-                                       // Registry will be null in 
cayenne-client context.
-                                       // Maybe we shouldn't create cache key 
at all in that case...
-                                       out.append(leaf);
-                               } else {
-                                       
out.append(valueObjectType.toCacheKey(leaf));
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Prefetch processor that append prefetch tree into cache key.
-        * @since 4.0
-        */
-       static class ToCacheKeyPrefetchProcessor implements PrefetchProcessor {
-
-               StringBuilder out;
-
-               ToCacheKeyPrefetchProcessor(StringBuilder out) {
-                       this.out = out;
-               }
-
-               @Override
-               public boolean startPhantomPrefetch(PrefetchTreeNode node) {
-                       return true;
-               }
-
-               @Override
-               public boolean startDisjointPrefetch(PrefetchTreeNode node) {
-                       out.append("/pd:").append(node.getPath());
-                       return true;
-               }
-
-               @Override
-               public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) 
{
-                       out.append("/pi:").append(node.getPath());
-                       return true;
-               }
-
-               @Override
-               public boolean startJointPrefetch(PrefetchTreeNode node) {
-                       out.append("/pj:").append(node.getPath());
-                       return true;
-               }
-
-               @Override
-               public boolean startUnknownPrefetch(PrefetchTreeNode node) {
-                       out.append("/pu:").append(node.getPath());
-                       return true;
-               }
-
-               @Override
-               public void finishPrefetch(PrefetchTreeNode node) {
-               }
-       }
 }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/ToCacheKeyPrefetchProcessor.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ToCacheKeyPrefetchProcessor.java
new file mode 100644
index 0000000..9363985
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ToCacheKeyPrefetchProcessor.java
@@ -0,0 +1,66 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * Prefetch processor that append prefetch tree into cache key.
+ * @since 4.2
+ */
+class ToCacheKeyPrefetchProcessor implements PrefetchProcessor {
+
+    private final StringBuilder out;
+
+    ToCacheKeyPrefetchProcessor(StringBuilder out) {
+        this.out = out;
+    }
+
+    @Override
+    public boolean startPhantomPrefetch(PrefetchTreeNode node) {
+        return true;
+    }
+
+    @Override
+    public boolean startDisjointPrefetch(PrefetchTreeNode node) {
+        out.append("/pd:").append(node.getPath());
+        return true;
+    }
+
+    @Override
+    public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
+        out.append("/pi:").append(node.getPath());
+        return true;
+    }
+
+    @Override
+    public boolean startJointPrefetch(PrefetchTreeNode node) {
+        out.append("/pj:").append(node.getPath());
+        return true;
+    }
+
+    @Override
+    public boolean startUnknownPrefetch(PrefetchTreeNode node) {
+        out.append("/pu:").append(node.getPath());
+        return true;
+    }
+
+    @Override
+    public void finishPrefetch(PrefetchTreeNode node) {
+    }
+}
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/ToCacheKeyTraversalHandler.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ToCacheKeyTraversalHandler.java
new file mode 100644
index 0000000..a8fb613
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ToCacheKeyTraversalHandler.java
@@ -0,0 +1,103 @@
+/*****************************************************************
+ *   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 org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.types.ValueObjectType;
+import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.TraversalHandler;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
+import org.apache.cayenne.exp.parser.ASTScalar;
+
+/**
+ * Expression traverse handler to create cache key string out of Expression.
+ * {@link Expression#appendAsString(Appendable)} where previously used for 
that,
+ * but it can't handle custom value objects properly (see CAY-2210).
+ *
+ * @see ValueObjectTypeRegistry
+ * @since 4.2
+ */
+class ToCacheKeyTraversalHandler implements TraversalHandler {
+
+    private ValueObjectTypeRegistry registry;
+    private StringBuilder out;
+
+    ToCacheKeyTraversalHandler(ValueObjectTypeRegistry registry, StringBuilder 
out) {
+        this.registry = registry;
+        this.out = out;
+    }
+
+    @Override
+    public void finishedChild(Expression node, int childIndex, boolean 
hasMoreChildren) {
+        out.append(',');
+    }
+
+    @Override
+    public void startNode(Expression node, Expression parentNode) {
+        if(node.getType() == Expression.FUNCTION_CALL) {
+            out.append(((ASTFunctionCall)node).getFunctionName()).append('(');
+        } else {
+            out.append(node.getType()).append('(');
+        }
+    }
+
+    @Override
+    public void endNode(Expression node, Expression parentNode) {
+        out.append(')');
+    }
+
+    @Override
+    public void objectNode(Object leaf, Expression parentNode) {
+        if(leaf == null) {
+            out.append("null");
+            return;
+        }
+
+        if(leaf instanceof ASTScalar) {
+            leaf = ((ASTScalar) leaf).getValue();
+        } else if(leaf instanceof Object[]) {
+            for(Object value : (Object[])leaf) {
+                objectNode(value, parentNode);
+                out.append(',');
+            }
+            return;
+        }
+
+        if (leaf instanceof Persistent) {
+            ObjectId id = ((Persistent) leaf).getObjectId();
+            Object encode = (id != null) ? id : leaf;
+            out.append(encode);
+        } else if (leaf instanceof Enum<?>) {
+            Enum<?> e = (Enum<?>) leaf;
+            
out.append("e:").append(leaf.getClass().getName()).append(':').append(e.ordinal());
+        } else {
+            ValueObjectType<Object, ?> valueObjectType;
+            if (registry == null || (valueObjectType = 
registry.getValueType(leaf.getClass())) == null) {
+                // Registry will be null in cayenne-client context.
+                // Maybe we shouldn't create cache key at all in that case...
+                out.append(leaf);
+            } else {
+                out.append(valueObjectType.toCacheKey(leaf));
+            }
+        }
+    }
+}
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryWrapperBuilder.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryWrapperBuilder.java
index 3c43204..4361150 100644
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryWrapperBuilder.java
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryWrapperBuilder.java
@@ -104,11 +104,6 @@ class MockQueryWrapperBuilder {
             }
 
             @Override
-            public PrefetchTreeNode getPrefetchTree() {
-                return prefetchTreeNode;
-            }
-
-            @Override
             public Expression getQualifier() {
                 return qualifier;
             }
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectTest.java 
b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectTest.java
index 96c4017..ef0a0c9 100644
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectTest.java
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectTest.java
@@ -243,17 +243,17 @@ public class ColumnSelectTest {
         ColumnSelect<Artist> q = new ColumnSelect<>();
 
         assertFalse(q.distinct);
-        assertFalse(q.suppressDistinct);
+        assertFalse(q.metaData.isSuppressingDistinct());
 
         q.distinct();
 
         assertTrue(q.distinct);
-        assertFalse(q.suppressDistinct);
+        assertFalse(q.metaData.isSuppressingDistinct());
 
         q.suppressDistinct();
 
         assertFalse(q.distinct);
-        assertTrue(q.suppressDistinct);
+        assertTrue(q.metaData.isSuppressingDistinct());
     }
 
 }
\ No newline at end of file
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java 
b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
index dd9e1f2..ee7681b 100644
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
@@ -372,11 +372,11 @@ public class ObjectSelectTest {
        public void testCacheGroups_Collection() {
                ObjectSelect<DataRow> q = 
ObjectSelect.dataRowQuery(Artist.class);
 
-               assertNull(q.getCacheStrategy());
+               assertEquals(QueryCacheStrategy.NO_CACHE, q.getCacheStrategy());
                assertNull(q.getCacheGroup());
 
                q.cacheGroup("a");
-               assertNull(q.getCacheStrategy());
+               assertEquals(QueryCacheStrategy.NO_CACHE, q.getCacheStrategy());
                assertEquals("a", q.getCacheGroup());
        }
 
@@ -384,7 +384,7 @@ public class ObjectSelectTest {
        public void testCacheStrategy() {
                ObjectSelect<DataRow> q = 
ObjectSelect.dataRowQuery(Artist.class);
 
-               assertNull(q.getCacheStrategy());
+               assertEquals(QueryCacheStrategy.NO_CACHE, q.getCacheStrategy());
                assertNull(q.getCacheGroup());
 
                q.cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, "b");
@@ -400,7 +400,7 @@ public class ObjectSelectTest {
        public void testLocalCache() {
                ObjectSelect<DataRow> q = 
ObjectSelect.dataRowQuery(Artist.class);
 
-               assertNull(q.getCacheStrategy());
+               assertEquals(QueryCacheStrategy.NO_CACHE, q.getCacheStrategy());
                assertNull(q.getCacheGroup());
 
                q.localCache("a");
@@ -416,7 +416,7 @@ public class ObjectSelectTest {
        public void testSharedCache() {
                ObjectSelect<DataRow> q = 
ObjectSelect.dataRowQuery(Artist.class);
 
-               assertNull(q.getCacheStrategy());
+               assertEquals(QueryCacheStrategy.NO_CACHE, q.getCacheStrategy());
                assertNull(q.getCacheGroup());
 
                q.sharedCache("b");
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
deleted file mode 100644
index caecb15..0000000
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_CompileIT.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-package org.apache.cayenne.query;
-
-import java.util.Arrays;
-import java.util.Collection;
-
-import org.apache.cayenne.CayenneDataObject;
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.DataRow;
-import org.apache.cayenne.di.Inject;
-import org.apache.cayenne.exp.property.BaseProperty;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.testdo.testmap.Artist;
-import org.apache.cayenne.unit.di.server.CayenneProjects;
-import org.apache.cayenne.unit.di.server.ServerCase;
-import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.junit.Test;
-
-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;
-
-@UseServerRuntime(CayenneProjects.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.getCacheGroup());
-               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).where(Artist.ARTIST_NAME.eq("me"))
-                               .orderBy(Artist.DATE_OF_BIRTH.asc(), 
Artist.ARTIST_NAME.desc()).prefetch(Artist.PAINTING_ARRAY.joint())
-                               
.localCache("cg2").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());
-               assertEquals("cg2", selectQuery.getCacheGroup());
-               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());
-       }
-
-       @Test
-       public void testCreateReplacementQuery_Columns() {
-               ObjectSelect<Artist> q = ObjectSelect.query(Artist.class);
-
-               SelectQuery selectQuery1 = (SelectQuery) 
q.createReplacementQuery(resolver);
-               assertNull(selectQuery1.getColumns());
-
-               ColumnSelect<Object[]> newQ = q.columns(Artist.ARTIST_NAME, 
Artist.DATE_OF_BIRTH);
-
-               SelectQuery selectQuery2 = (SelectQuery) 
newQ.createReplacementQuery(resolver);
-               assertNotNull(selectQuery2.getColumns());
-
-               Collection<BaseProperty<?>> properties = 
Arrays.asList(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH);
-               assertEquals(properties, selectQuery2.getColumns());
-       }
-
-}
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/query/SelectQueryMetadataCacheKeyTest.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/query/SelectQueryMetadataCacheKeyTest.java
index 03675d6..a3352ef 100644
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/query/SelectQueryMetadataCacheKeyTest.java
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/query/SelectQueryMetadataCacheKeyTest.java
@@ -329,11 +329,11 @@ public class SelectQueryMetadataCacheKeyTest {
     }
 
     private TraversalHandler newHandler() {
-        return new SelectQueryMetadata.ToCacheKeyTraversalHandler(registry, 
cacheKey = new StringBuilder());
+        return new ToCacheKeyTraversalHandler(registry, cacheKey = new 
StringBuilder());
     }
 
     private PrefetchProcessor newPrefetchProcessor() {
-        return new SelectQueryMetadata.ToCacheKeyPrefetchProcessor(cacheKey = 
new StringBuilder());
+        return new ToCacheKeyPrefetchProcessor(cacheKey = new StringBuilder());
     }
 
     /* ************* Test types *************** */

Reply via email to