http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteSelectAction.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..a578c23
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteSelectAction.java
@@ -0,0 +1,43 @@
+/*****************************************************************
+ *   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.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;
+
+/**
+ * @since 4.1
+ */
+public class SQLiteSelectAction extends SelectAction {
+
+    public SQLiteSelectAction(SelectQuery<?> query, DataNode dataNode) {
+        super(query, dataNode);
+    }
+
+    @Override
+    protected int getInMemoryOffset(int queryOffset) {
+        return 0;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteTreeProcessor.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteTreeProcessor.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteTreeProcessor.java
new file mode 100644
index 0000000..d20bc66
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteTreeProcessor.java
@@ -0,0 +1,100 @@
+package org.apache.cayenne.dba.sqlite;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TextNode;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+import org.apache.cayenne.dba.mysql.sqltree.MysqlLimitOffsetNode;
+
+/**
+ * @since 4.2
+ */
+public class SQLiteTreeProcessor extends BaseSQLTreeProcessor {
+
+    @Override
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int 
index) {
+        replaceChild(parent, index, new MysqlLimitOffsetNode(child.getLimit(), 
child.getOffset()), false);
+    }
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        String functionName = child.getFunctionName();
+        Node replacement = null;
+        switch (functionName) {
+            case "LOCATE":
+                replacement = new FunctionNode("INSTR", child.getAlias(), 
true);
+                for (int i = 0; i <= 1; i++) {
+                    replacement.addChild(child.getChild(1 - i));
+                }
+                parent.replaceChild(index, replacement);
+                return;
+            case "DAY_OF_YEAR":
+                replaceExtractFunction(parent, child, index, "'%j'");
+                return;
+            case "DAY_OF_WEEK":
+                replaceExtractFunction(parent, child, index, "'%w'");
+                return;
+            case "WEEK":
+                replaceExtractFunction(parent, child, index, "'%W'");
+                return;
+            case "YEAR":
+                replaceExtractFunction(parent, child, index, "'%Y'");
+                return;
+            case "MONTH":
+                replaceExtractFunction(parent, child, index, "'%m'");
+                return;
+            case "DAY":
+            case "DAY_OF_MONTH":
+                replaceExtractFunction(parent, child, index, "'%d'");
+                return;
+            case "HOUR":
+                replaceExtractFunction(parent, child, index, "'%H'");
+                return;
+            case "MINUTE":
+                replaceExtractFunction(parent, child, index, "'%M'");
+                return;
+            case "SECOND":
+                replaceExtractFunction(parent, child, index, "'%S'");
+                return;
+
+            case "SUBSTRING":
+                replacement = new FunctionNode("SUBSTR", child.getAlias(), 
true);
+                break;
+            case "CONCAT":
+                replacement = new OpExpressionNode("||");
+                break;
+            case "MOD":
+                replacement = new OpExpressionNode("%");
+                break;
+            case "CURRENT_DATE":
+            case "CURRENT_TIMESTAMP":
+            case "CURRENT_TIME":
+                replacement = new FunctionNode(functionName, child.getAlias(), 
false);
+                break;
+        }
+
+        if(replacement != null) {
+            replaceChild(parent, index, replacement);
+        }
+    }
+
+    private void replaceExtractFunction(Node parent, FunctionNode original, 
int index, String format) {
+        Node replacement = new FunctionNode("cast", original.getAlias(), true) 
{
+            @Override
+            public void appendChildrenSeparator(QuotingAppendable buffer, int 
childIdx) {
+                buffer.append(" as ");
+            }
+        };
+
+        FunctionNode strftime = new FunctionNode("strftime", null, true);
+        strftime.addChild(new TextNode(format));
+        strftime.addChild(original.getChild(0));
+        replacement.addChild(strftime);
+        replacement.addChild(new TextNode("integer"));
+
+        parent.replaceChild(index, replacement);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
index a94bf62..6a99dcb 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
@@ -20,11 +20,10 @@
 package org.apache.cayenne.dba.sqlserver;
 
 import java.util.List;
+import java.util.function.Function;
 
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.translator.select.QualifierTranslator;
-import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.access.translator.select.SelectTranslator;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
@@ -32,10 +31,8 @@ import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
 import org.apache.cayenne.dba.sybase.SybaseAdapter;
 import org.apache.cayenne.di.Inject;
-import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
-import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.resource.ResourceLocator;
 
 /**
@@ -91,11 +88,11 @@ public class SQLServerAdapter extends SybaseAdapter {
        }
 
        /**
-        * @since 4.0
+        * @since 4.2
         */
        @Override
-       public SelectTranslator getSelectTranslator(SelectQuery<?> query, 
EntityResolver entityResolver) {
-               return new SQLServerSelectTranslator(query, this, 
entityResolver);
+       public Function<Node, Node> getSqlTreeProcessor() {
+               return new SQLServerTreeProcessor();
        }
 
        /**
@@ -108,14 +105,4 @@ public class SQLServerAdapter extends SybaseAdapter {
                return query.createSQLAction(new SQLServerActionBuilder(node));
        }
 
-       /**
-        * Returns a trimming translator.
-        */
-       @Override
-       public QualifierTranslator getQualifierTranslator(QueryAssembler 
queryAssembler) {
-               QualifierTranslator translator = new 
SQLServerTrimmingQualifierTranslator(queryAssembler,
-                               SQLServerAdapter.TRIM_FUNCTION);
-               translator.setCaseInsensitive(caseInsensitiveCollations);
-               return translator;
-       }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectTranslator.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectTranslator.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectTranslator.java
deleted file mode 100644
index b78877c..0000000
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerSelectTranslator.java
+++ /dev/null
@@ -1,54 +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.dba.sqlserver;
-
-import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.query.Query;
-
-public class SQLServerSelectTranslator extends DefaultSelectTranslator {
-
-       /**
-        * @since 4.0
-        */
-       public SQLServerSelectTranslator(Query query, DbAdapter adapter, 
EntityResolver entityResolver) {
-               super(query, adapter, entityResolver);
-       }
-
-       @Override
-       protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-
-               int limit = queryMetadata.getFetchLimit();
-               int offset = queryMetadata.getFetchOffset();
-
-               if (limit > 0) {
-                       String sql = buffer.toString();
-
-                       // If contains distinct insert top limit after
-                       if (sql.startsWith("SELECT DISTINCT ")) {
-                               buffer.replace(0, 15, "SELECT DISTINCT TOP " + 
(offset + limit));
-
-                       } else {
-                               buffer.replace(0, 6, "SELECT TOP " + (offset + 
limit));
-                       }
-               }
-       }
-
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTreeProcessor.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTreeProcessor.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTreeProcessor.java
new file mode 100644
index 0000000..2681a53
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTreeProcessor.java
@@ -0,0 +1,128 @@
+/*****************************************************************
+ *   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.dba.sqlserver;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.EmptyNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NodeType;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TextNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TopNode;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+import org.apache.cayenne.dba.sqlserver.sqltree.SQLServerColumnNode;
+
+/**
+ * @since 4.2
+ */
+public class SQLServerTreeProcessor extends BaseSQLTreeProcessor {
+
+    @Override
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int 
index) {
+        // SQLServer uses "SELECT DISTINCT TOP N" or "SELECT TOP N" instead of 
LIMIT
+        // Offset will be calculated in-memory
+        replaceChild(parent, index, new EmptyNode(), false);
+        if(child.getLimit() > 0) {
+            int limit = child.getLimit() + child.getOffset();
+            // we have root actually as input for processor, but it's better 
to keep processor stateless
+            // root shouldn't be far from limit's parent (likely it will be 
parent itself)
+            Node root = getRoot(parent);
+            int idx = 0;
+            if(root.getChild(0).getType() == NodeType.DISTINCT) {
+                idx = 1;
+            }
+            root.addChild(idx, new TopNode(limit));
+        }
+    }
+
+    private Node getRoot(Node node) {
+        while(node.getParent() != null) {
+            node = node.getParent();
+        }
+        return node;
+    }
+
+    @Override
+    protected void onColumnNode(Node parent, ColumnNode child, int index) {
+        replaceChild(parent, index,  new SQLServerColumnNode(child));
+    }
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        String functionName = child.getFunctionName();
+        Node replacement = null;
+        switch (functionName) {
+            case "LENGTH":
+                replacement = new FunctionNode("LEN", child.getAlias(), true);
+                break;
+            case "LOCATE":
+                replacement = new FunctionNode("CHARINDEX", child.getAlias(), 
true);
+                break;
+            case "MOD":
+                replacement = new OpExpressionNode("%");
+                break;
+            case "TRIM":
+                Node rtrim = new FunctionNode("RTRIM", null, true);
+                replacement = new FunctionNode("LTRIM", child.getAlias(), 
true);
+                for(int i=0; i<child.getChildrenCount(); i++) {
+                    rtrim.addChild(child.getChild(i));
+                }
+                replacement.addChild(rtrim);
+                parent.replaceChild(index, replacement);
+                return;
+            case "CURRENT_DATE":
+                replacement = new FunctionNode("{fn CURDATE()}", 
child.getAlias(), false);
+                break;
+            case "CURRENT_TIME":
+                replacement = new FunctionNode("{fn CURTIME()}", 
child.getAlias(), false);
+                break;
+            case "CURRENT_TIMESTAMP":
+                replacement = new FunctionNode("CURRENT_TIMESTAMP", 
child.getAlias(), false);
+                break;
+
+            case "YEAR":
+            case "MONTH":
+            case "WEEK":
+            case "DAY_OF_YEAR":
+            case "DAY":
+            case "DAY_OF_MONTH":
+            case "DAY_OF_WEEK":
+            case "HOUR":
+            case "MINUTE":
+            case "SECOND":
+                replacement = new FunctionNode("DATEPART", child.getAlias(), 
true);
+                if("DAY_OF_MONTH".equals(functionName)) {
+                    functionName = "DAY";
+                } else if("DAY_OF_WEEK".equals(functionName)) {
+                    functionName = "WEEKDAY";
+                } else if("DAY_OF_YEAR".equals(functionName)) {
+                    functionName = "DAYOFYEAR";
+                }
+                replacement.addChild(new TextNode(functionName));
+                break;
+        }
+
+        if(replacement != null) {
+            replaceChild(parent, index, replacement);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
deleted file mode 100644
index 9847742..0000000
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
+++ /dev/null
@@ -1,207 +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.dba.sqlserver;
-
-import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.access.translator.select.TrimmingQualifierTranslator;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.parser.ASTExtract;
-import org.apache.cayenne.exp.parser.ASTFunctionCall;
-import org.apache.cayenne.map.DbAttribute;
-
-import java.sql.Types;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * @since 3.0
- */
-class SQLServerTrimmingQualifierTranslator extends TrimmingQualifierTranslator 
{
-
-       // since LIKE IGNORE CASE requires more contextual information than the
-       // super
-       // translator can provide, we are using an internal element stack to 
trace
-       // translation
-       // context.. Maybe it is a good idea to introduce it in the superclass?
-       private List<Expression> expressionStack;
-
-       SQLServerTrimmingQualifierTranslator(QueryAssembler queryAssembler, 
String trimFunction) {
-               super(queryAssembler, trimFunction);
-               expressionStack = new ArrayList<>();
-       }
-
-       @Override
-       public void startNode(Expression node, Expression parentNode) {
-               push(node);
-               super.startNode(node, parentNode);
-       }
-
-       @Override
-       protected void processColumn(DbAttribute dbAttr) {
-
-               Expression node = peek(1);
-
-               boolean likeCI = node != null && dbAttr.getType() == Types.CLOB
-                               && (node.getType() == 
Expression.LIKE_IGNORE_CASE || node.getType() == 
Expression.NOT_LIKE_IGNORE_CASE);
-
-               if (likeCI) {
-                       out.append("CAST(");
-               }
-
-               super.processColumn(dbAttr);
-
-               if (likeCI) {
-                       out.append(" AS NVARCHAR(MAX))");
-               }
-       }
-
-       @Override
-       protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, 
Expression pathExp) {
-               Expression node = peek(1);
-
-               boolean likeCI = node != null && dbAttr.getType() == Types.CLOB
-                               && (node.getType() == 
Expression.LIKE_IGNORE_CASE || node.getType() == 
Expression.NOT_LIKE_IGNORE_CASE);
-
-               if (likeCI) {
-                       out.append("CAST(");
-               }
-
-               super.processColumnWithQuoteSqlIdentifiers(dbAttr, node);
-
-               if (likeCI) {
-                       out.append(" AS NVARCHAR(MAX))");
-               }
-       }
-
-       @Override
-       public void endNode(Expression node, Expression parentNode) {
-               super.endNode(node, parentNode);
-               pop();
-       }
-
-       private void push(Expression node) {
-               expressionStack.add(node);
-       }
-
-       private void pop() {
-               int len = expressionStack.size();
-               if (len > 0) {
-                       expressionStack.remove(len - 1);
-               }
-       }
-
-       private Expression peek(int tailIndex) {
-               int index = expressionStack.size() - tailIndex - 1;
-               if (index < 0) {
-                       return null;
-               }
-
-               return expressionStack.get(index);
-       }
-
-    /**
-     * @since 4.0
-     */
-       @Override
-       protected void appendFunction(ASTFunctionCall functionExpression) {
-               switch (functionExpression.getFunctionName()) {
-                       case "LENGTH":
-                               out.append("LEN");
-                               break;
-                       case "LOCATE":
-                               out.append("CHARINDEX");
-                               break;
-                       case "MOD":
-                               // noop
-                               break;
-                       case "TRIM":
-                               out.append("LTRIM(RTRIM");
-                               break;
-                       case "CURRENT_DATE":
-                               out.append("{fn CURDATE()}");
-                               break;
-                       case "CURRENT_TIME":
-                               out.append("{fn CURTIME()}");
-                               break;
-                       default:
-                               super.appendFunction(functionExpression);
-               }
-       }
-
-    /**
-     * @since 4.0
-     */
-       @Override
-       protected void appendFunctionArgDivider(ASTFunctionCall 
functionExpression) {
-               if("MOD".equals(functionExpression.getFunctionName())) {
-                       out.append(" % ");
-               } else {
-                       super.appendFunctionArgDivider(functionExpression);
-               }
-       }
-
-    /**
-     * @since 4.0
-     */
-       @Override
-       protected void clearLastFunctionArgDivider(ASTFunctionCall 
functionExpression) {
-               if("MOD".equals(functionExpression.getFunctionName())) {
-                       out.delete(out.length() - " % ".length(), out.length());
-               } else {
-                       super.clearLastFunctionArgDivider(functionExpression);
-                       if("TRIM".equals(functionExpression.getFunctionName())) 
{
-                               out.append(")");
-                       }
-               }
-
-               if(functionExpression instanceof ASTExtract) {
-                       out.append(")");
-               }
-       }
-
-       @Override
-       protected boolean parenthesisNeeded(Expression node, Expression 
parentNode) {
-               if (node.getType() == Expression.FUNCTION_CALL) {
-                       if (node instanceof ASTExtract) {
-                               return false;
-                       }
-               }
-
-               return super.parenthesisNeeded(node, parentNode);
-       }
-
-       @Override
-       protected void appendExtractFunction(ASTExtract functionExpression) {
-               out.append("DATEPART(");
-               switch (functionExpression.getPart()) {
-                       case DAY_OF_MONTH:
-                               out.append("DAY");
-                               break;
-                       case DAY_OF_WEEK:
-                               out.append("WEEKDAY");
-                               break;
-                       case DAY_OF_YEAR:
-                               out.append("DAYOFYEAR");
-                               break;
-                       default:
-                               out.append(functionExpression.getPart().name());
-               }
-               out.append(" , ");
-       }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/sqltree/SQLServerColumnNode.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/sqltree/SQLServerColumnNode.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/sqltree/SQLServerColumnNode.java
new file mode 100644
index 0000000..b629840
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/sqltree/SQLServerColumnNode.java
@@ -0,0 +1,46 @@
+/*****************************************************************
+ *   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.dba.sqlserver.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TrimmingColumnNode;
+
+/**
+ * @since 4.2
+ */
+public class SQLServerColumnNode extends TrimmingColumnNode {
+
+    public SQLServerColumnNode(ColumnNode columnNode) {
+        super(columnNode);
+    }
+
+    @Override
+    protected void appendClobColumnNode(QuotingAppendable buffer) {
+        buffer.append(" CAST(");
+        appendColumnNode(buffer);
+        buffer.append(" AS NVARCHAR(MAX))");
+    }
+
+    @Override
+    public SQLServerColumnNode copy() {
+        return new SQLServerColumnNode(columnNode.deepCopy());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
index 71fa2d9..2aaff9d 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
@@ -23,12 +23,11 @@ import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.sql.Types;
 import java.util.List;
+import java.util.function.Function;
 
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
-import org.apache.cayenne.access.translator.select.QualifierTranslator;
-import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.access.types.ByteArrayType;
 import org.apache.cayenne.access.types.ByteType;
 import org.apache.cayenne.access.types.CharType;
@@ -43,15 +42,13 @@ import org.apache.cayenne.dba.DefaultQuotingStrategy;
 import org.apache.cayenne.dba.JdbcAdapter;
 import org.apache.cayenne.dba.PkGenerator;
 import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.dba.sqlserver.SQLServerTreeProcessor;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.resource.ResourceLocator;
 
 /**
- * DbAdapter implementation for <a href="http://www.sybase.com";>Sybase
- * RDBMS</a>.
+ * DbAdapter implementation for <a href="http://www.sybase.com";>Sybase 
RDBMS</a>.
  */
 public class SybaseAdapter extends JdbcAdapter {
 
@@ -79,20 +76,12 @@ public class SybaseAdapter extends JdbcAdapter {
         return new SybaseEJBQLTranslatorFactory();
     }
 
-       /**
-        * @since 4.0
-        */
-       @Override
-       public SelectTranslator getSelectTranslator(SelectQuery<?> query, 
EntityResolver entityResolver) {
-               return new SybaseSelectTranslator(query, this, entityResolver);
-       }
-
     /**
-     * @since 4.0
+     * @since 4.2
      */
     @Override
-    public QualifierTranslator getQualifierTranslator(QueryAssembler 
queryAssembler) {
-        return new SybaseQualifierTranslator(queryAssembler);
+    public Function<Node, Node> getSqlTreeProcessor() {
+        return new SQLServerTreeProcessor();
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseQualifierTranslator.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseQualifierTranslator.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseQualifierTranslator.java
deleted file mode 100644
index 0f6e239..0000000
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseQualifierTranslator.java
+++ /dev/null
@@ -1,114 +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.dba.sybase;
-
-import org.apache.cayenne.access.translator.select.QualifierTranslator;
-import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.parser.ASTExtract;
-import org.apache.cayenne.exp.parser.ASTFunctionCall;
-
-/**
- * @since 4.0
- */
-public class SybaseQualifierTranslator extends QualifierTranslator {
-
-    public SybaseQualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler);
-    }
-
-    @Override
-    protected void appendFunction(ASTFunctionCall functionExpression) {
-        switch (functionExpression.getFunctionName()) {
-            case "MOD":
-            case "CONCAT":
-                // noop
-                break;
-            case "LENGTH":
-                out.append("LEN");
-                break;
-            case "LOCATE":
-                out.append("CHARINDEX");
-                break;
-            default:
-                super.appendFunction(functionExpression);
-        }
-    }
-
-    @Override
-    protected void appendFunctionArgDivider(ASTFunctionCall 
functionExpression) {
-        switch (functionExpression.getFunctionName()) {
-            case "MOD":
-                out.append(" % ");
-                break;
-            case "CONCAT":
-                out.append(" + ");
-                break;
-            default:
-                super.appendFunctionArgDivider(functionExpression);
-        }
-    }
-
-    @Override
-    protected void clearLastFunctionArgDivider(ASTFunctionCall 
functionExpression) {
-        switch (functionExpression.getFunctionName()) {
-            case "MOD":
-            case "CONCAT":
-                out.delete(out.length() - 3, out.length());
-                break;
-            default:
-                super.clearLastFunctionArgDivider(functionExpression);
-        }
-
-        if(functionExpression instanceof ASTExtract) {
-            out.append(")");
-        }
-    }
-
-    @Override
-    protected boolean parenthesisNeeded(Expression node, Expression 
parentNode) {
-        if (node.getType() == Expression.FUNCTION_CALL) {
-            if (node instanceof ASTExtract) {
-                return false;
-            }
-        }
-
-        return super.parenthesisNeeded(node, parentNode);
-    }
-
-    @Override
-    protected void appendExtractFunction(ASTExtract functionExpression) {
-        out.append("datepart(");
-        switch (functionExpression.getPart()) {
-            case DAY_OF_MONTH:
-                out.append("day");
-                break;
-            case DAY_OF_WEEK:
-                out.append("weekday");
-                break;
-            case DAY_OF_YEAR:
-                out.append("dayofyear");
-                break;
-            default:
-                out.append(functionExpression.getPart().name().toLowerCase());
-        }
-        out.append(" , ");
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseSelectTranslator.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseSelectTranslator.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseSelectTranslator.java
deleted file mode 100644
index 93af596..0000000
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseSelectTranslator.java
+++ /dev/null
@@ -1,52 +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.dba.sybase;
-
-import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.query.Query;
-
-public class SybaseSelectTranslator extends DefaultSelectTranslator {
-       /**
-        * @since 4.0
-        */
-       public SybaseSelectTranslator(Query query, DbAdapter adapter, 
EntityResolver entityResolver) {
-               super(query, adapter, entityResolver);
-       }
-
-       @Override
-       protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-
-               int limit = queryMetadata.getFetchLimit();
-               int offset = queryMetadata.getFetchOffset();
-
-               if (limit > 0) {
-                       String sql = buffer.toString();
-
-                       // If contains distinct insert top limit after
-                       if (sql.startsWith("SELECT DISTINCT ")) {
-                               buffer.replace(0, 15, "SELECT DISTINCT TOP " + 
(offset + limit));
-
-                       } else {
-                               buffer.replace(0, 6, "SELECT TOP " + (offset + 
limit));
-                       }
-               }
-       }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelect.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelect.java 
b/cayenne-server/src/main/java/org/apache/cayenne/query/ColumnSelect.java
index 35c36f2..fafd258 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
@@ -686,6 +686,13 @@ public class ColumnSelect<T> extends FluentSelect<T> {
         return having;
     }
 
+    /**
+     * @since 4.2
+     */
+    public boolean isDistinct() {
+        return distinct;
+    }
+
     @Override
     public T selectFirst(ObjectContext context) {
         return context.selectFirst(limit(1));

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java
index 8a9e7d9..9ee83bf 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
@@ -232,7 +232,7 @@ class SelectQueryMetadata extends BaseQueryMetadata {
         */
        @Override
        public Map<String, String> getPathSplitAliases() {
-               return pathSplitAliases != null ? pathSplitAliases : 
Collections.<String, String> emptyMap();
+               return pathSplitAliases != null ? pathSplitAliases : 
Collections.emptyMap();
        }
 
        /**
@@ -328,18 +328,17 @@ class SelectQueryMetadata extends BaseQueryMetadata {
         * (possibly including joint prefetch).
         * This information will be used to correctly create Persistent object 
back from raw result.
         *
-        * This method is actually repeating logic of
-        * {@link 
org.apache.cayenne.access.translator.select.DefaultSelectTranslator#appendQueryColumns}.
-        * 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,
-        * generated only once and used everywhere.
-        *
         * @param query original query
         * @param column full object column
         * @param resolver entity resolver to get ObjEntity and ClassDescriptor
         * @return Entity result
         */
        private EntityResult buildEntityResultForColumn(SelectQuery<?> 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,
+               // generated only once and used everywhere.
+
                final EntityResult result = new EntityResult(column.getType());
 
                // Collecting visitor for ObjAttributes and toOne relationships

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/util/ArrayUtil.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/util/ArrayUtil.java 
b/cayenne-server/src/main/java/org/apache/cayenne/util/ArrayUtil.java
new file mode 100644
index 0000000..80df070
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/util/ArrayUtil.java
@@ -0,0 +1,260 @@
+/*****************************************************************
+ *   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.util;
+
+/**
+ * @since 4.1
+ */
+public final class ArrayUtil {
+
+    public static int[][] sliceArray(int[] array, int batchSize) {
+        if(array == null) {
+            return null;
+        }
+        int length = array.length;
+
+        if(length <= batchSize) {
+            return new int[][]{array};
+        }
+
+        int batches = length / batchSize;
+        if(length % batchSize > 0) {
+            batches++;
+        }
+
+        int[][] result = new int[batches][];
+        int offset = 0;
+        for(int i=0; i<batches; i++) {
+            int nextSize = i < batches - 1 ? batchSize : length - offset;
+            result[i] = new int[nextSize];
+            System.arraycopy(array, offset, result[i], 0, nextSize);
+            offset += nextSize;
+        }
+        return result;
+    }
+
+    public static long[][] sliceArray(long[] array, int batchSize) {
+        if(array == null) {
+            return null;
+        }
+        int length = array.length;
+
+        if(length <= batchSize) {
+            return new long[][]{array};
+        }
+
+        int batches = length / batchSize;
+        if(length % batchSize > 0) {
+            batches++;
+        }
+
+        long[][] result = new long[batches][];
+        int offset = 0;
+        for(int i=0; i<batches; i++) {
+            int nextSize = i < batches - 1 ? batchSize : length - offset;
+            result[i] = new long[nextSize];
+            System.arraycopy(array, offset, result[i], 0, nextSize);
+            offset += nextSize;
+        }
+        return result;
+    }
+
+    public static float[][] sliceArray(float[] array, int batchSize) {
+        if(array == null) {
+            return null;
+        }
+        int length = array.length;
+
+        if(length <= batchSize) {
+            return new float[][]{array};
+        }
+
+        int batches = length / batchSize;
+        if(length % batchSize > 0) {
+            batches++;
+        }
+
+        float[][] result = new float[batches][];
+        int offset = 0;
+        for(int i=0; i<batches; i++) {
+            int nextSize = i < batches - 1 ? batchSize : length - offset;
+            result[i] = new float[nextSize];
+            System.arraycopy(array, offset, result[i], 0, nextSize);
+            offset += nextSize;
+        }
+        return result;
+    }
+
+    public static double[][] sliceArray(double[] array, int batchSize) {
+        if(array == null) {
+            return null;
+        }
+        int length = array.length;
+
+        if(length <= batchSize) {
+            return new double[][]{array};
+        }
+
+        int batches = length / batchSize;
+        if(length % batchSize > 0) {
+            batches++;
+        }
+
+        double[][] result = new double[batches][];
+        int offset = 0;
+        for(int i=0; i<batches; i++) {
+            int nextSize = i < batches - 1 ? batchSize : length - offset;
+            result[i] = new double[nextSize];
+            System.arraycopy(array, offset, result[i], 0, nextSize);
+            offset += nextSize;
+        }
+        return result;
+    }
+
+    public static short[][] sliceArray(short[] array, int batchSize) {
+        if(array == null) {
+            return null;
+        }
+        int length = array.length;
+
+        if(length <= batchSize) {
+            return new short[][]{array};
+        }
+
+        int batches = length / batchSize;
+        if(length % batchSize > 0) {
+            batches++;
+        }
+
+        short[][] result = new short[batches][];
+        int offset = 0;
+        for(int i=0; i<batches; i++) {
+            int nextSize = i < batches - 1 ? batchSize : length - offset;
+            result[i] = new short[nextSize];
+            System.arraycopy(array, offset, result[i], 0, nextSize);
+            offset += nextSize;
+        }
+        return result;
+    }
+
+    public static char[][] sliceArray(char[] array, int batchSize) {
+        if(array == null) {
+            return null;
+        }
+        int length = array.length;
+
+        if(length <= batchSize) {
+            return new char[][]{array};
+        }
+
+        int batches = length / batchSize;
+        if(length % batchSize > 0) {
+            batches++;
+        }
+
+        char[][] result = new char[batches][];
+        int offset = 0;
+        for(int i=0; i<batches; i++) {
+            int nextSize = i < batches - 1 ? batchSize : length - offset;
+            result[i] = new char[nextSize];
+            System.arraycopy(array, offset, result[i], 0, nextSize);
+            offset += nextSize;
+        }
+        return result;
+    }
+
+    public static boolean[][] sliceArray(boolean[] array, int batchSize) {
+        if(array == null) {
+            return null;
+        }
+        int length = array.length;
+
+        if(length <= batchSize) {
+            return new boolean[][]{array};
+        }
+
+        int batches = length / batchSize;
+        if(length % batchSize > 0) {
+            batches++;
+        }
+
+        boolean[][] result = new boolean[batches][];
+        int offset = 0;
+        for(int i=0; i<batches; i++) {
+            int nextSize = i < batches - 1 ? batchSize : length - offset;
+            result[i] = new boolean[nextSize];
+            System.arraycopy(array, offset, result[i], 0, nextSize);
+            offset += nextSize;
+        }
+        return result;
+    }
+
+    public static byte[][] sliceArray(byte[] array, int batchSize) {
+        if(array == null) {
+            return null;
+        }
+        int length = array.length;
+
+        if(length <= batchSize) {
+            return new byte[][]{array};
+        }
+
+        int batches = length / batchSize;
+        if(length % batchSize > 0) {
+            batches++;
+        }
+
+        byte[][] result = new byte[batches][];
+        int offset = 0;
+        for(int i=0; i<batches; i++) {
+            int nextSize = i < batches - 1 ? batchSize : length - offset;
+            result[i] = new byte[nextSize];
+            System.arraycopy(array, offset, result[i], 0, nextSize);
+            offset += nextSize;
+        }
+        return result;
+    }
+
+    public static Object[][] sliceArray(Object[] array, int batchSize) {
+        if(array == null) {
+            return null;
+        }
+        int length = array.length;
+
+        if(length <= batchSize) {
+            return new Object[][]{array};
+        }
+
+        int batches = length / batchSize;
+        if(length % batchSize > 0) {
+            batches++;
+        }
+
+        Object[][] result = new Object[batches][];
+        int offset = 0;
+        for(int i=0; i<batches; i++) {
+            int nextSize = i < batches - 1 ? batchSize : length - offset;
+            result[i] = new Object[nextSize];
+            System.arraycopy(array, offset, result[i], 0, nextSize);
+            offset += nextSize;
+        }
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/FlattenedRelationshipsIT.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/FlattenedRelationshipsIT.java 
b/cayenne-server/src/test/java/org/apache/cayenne/FlattenedRelationshipsIT.java
index 7a10d10..03d3774 100644
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/FlattenedRelationshipsIT.java
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/FlattenedRelationshipsIT.java
@@ -76,8 +76,7 @@ public class FlattenedRelationshipsIT extends ServerCase {
         tFlattenedTest2.setColumns("FT2_ID", "FT1_ID", "NAME");
 
         tFlattenedTest3 = new TableHelper(dbHelper, "FLATTENED_TEST_3");
-        tFlattenedTest3.setColumns("FT3_ID", "FT2_ID", "NAME").setColumnTypes(
-                Types.INTEGER, Types.INTEGER, Types.VARCHAR);
+        tFlattenedTest3.setColumns("FT3_ID", "FT2_ID", "NAME");
 
         tComplexJoin = new TableHelper(dbHelper, "COMPLEX_JOIN");
         tComplexJoin.setColumns("PK", "FT1_FK", "FT3_FK", "EXTRA_COLUMN");

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/BaseColumnExtractorTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/BaseColumnExtractorTest.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/BaseColumnExtractorTest.java
new file mode 100644
index 0000000..155c968
--- /dev/null
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/BaseColumnExtractorTest.java
@@ -0,0 +1,43 @@
+/*****************************************************************
+ *   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.sql.Types;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+
+/**
+ * @since 4.2
+ */
+public class BaseColumnExtractorTest {
+
+    DbEntity createMockDbEntity(String entityName) {
+        DbEntity entity = new DbEntity(entityName);
+        DbAttribute id = new DbAttribute("id");
+        id.setPrimaryKey(true);
+        id.setType(Types.BIGINT);
+        DbAttribute name = new DbAttribute("name");
+        name.setType(Types.VARBINARY);
+        entity.addAttribute(id);
+        entity.addAttribute(name);
+        return entity;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStageTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStageTest.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStageTest.java
new file mode 100644
index 0000000..dd1c7e7
--- /dev/null
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStageTest.java
@@ -0,0 +1,55 @@
+/*****************************************************************
+ *   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 org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.EmptyNode;
+import org.apache.cayenne.exp.Property;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class ColumnDescriptorStageTest {
+
+    @Test
+    public void perform() {
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withDistinct(true)
+                .withMetaData(new MockQueryMetadataBuilder()
+                        .withSuppressDistinct()
+                        .build())
+                .build();
+        TranslatorContext context = new MockTranslatorContext(wrapper);
+
+        context.addResultNode(new EmptyNode());
+        context.addResultNode(new EmptyNode(), "key");
+        context.addResultNode(new EmptyNode(), false, Property.COUNT, "key2");
+
+        ColumnDescriptorStage stage = new ColumnDescriptorStage();
+        stage.perform(context);
+
+        assertEquals(1, context.getColumnDescriptors().size());
+        ColumnDescriptor descriptor = 
context.getColumnDescriptors().iterator().next();
+        assertEquals("key", descriptor.getDataRowKey());
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractorTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractorTest.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractorTest.java
new file mode 100644
index 0000000..2836ab8
--- /dev/null
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractorTest.java
@@ -0,0 +1,94 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.select;
+
+import java.sql.Types;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.property.BaseProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class CustomColumnSetExtractorTest extends BaseColumnExtractorTest {
+
+    @Test
+    public void testExtractWithoutPrefix() {
+        DbEntity mockDbEntity = createMockDbEntity("mock");
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withMetaData(new MockQueryMetadataBuilder()
+                        .withDbEntity(mockDbEntity)
+                        .build())
+                .build();
+        TranslatorContext context = new MockTranslatorContext(wrapper);
+
+        DataMap dataMap = new DataMap();
+        dataMap.addDbEntity(mockDbEntity);
+
+        ObjEntity entity = new ObjEntity();
+        entity.setName("mock");
+        entity.setDataMap(dataMap);
+        entity.setDbEntity(mockDbEntity);
+
+        ObjAttribute attribute = new ObjAttribute();
+        attribute.setName("not_name");
+        attribute.setDbAttributePath("name");
+        attribute.setType("my.type");
+        entity.addAttribute(attribute);
+
+        dataMap.addObjEntity(entity);
+
+        EntityResolver resolver = new EntityResolver();
+        resolver.addDataMap(dataMap);
+
+        BaseProperty<?> property0 = 
PropertyFactory.createBase(ExpressionFactory.dbPathExp("name"), String.class);
+        Collection<BaseProperty<?>> properties = 
Collections.singleton(property0);
+
+        CustomColumnSetExtractor extractor = new 
CustomColumnSetExtractor(context, properties);
+        extractor.extract();
+
+        assertEquals(1, context.getResultNodeList().size());
+
+        ResultNodeDescriptor descriptor0 = context.getResultNodeList().get(0);
+
+        assertSame(property0, descriptor0.getProperty());
+        assertNotNull(descriptor0.getNode());
+        assertThat(descriptor0.getNode(), instanceOf(ColumnNode.class));
+        assertFalse(descriptor0.isAggregate());
+        assertTrue(descriptor0.isInDataRow());
+        assertNotNull(descriptor0.getDbAttribute());
+        assertNull(descriptor0.getDataRowKey());
+        assertEquals(Types.VARBINARY, descriptor0.getJdbcType());
+        assertEquals("java.lang.String", descriptor0.getJavaType());
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DbEntityColumnExtractorTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DbEntityColumnExtractorTest.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DbEntityColumnExtractorTest.java
new file mode 100644
index 0000000..4585347
--- /dev/null
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DbEntityColumnExtractorTest.java
@@ -0,0 +1,121 @@
+/*****************************************************************
+ *   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 org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.JoinType;
+import org.junit.Test;
+
+import java.sql.Types;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
+public class DbEntityColumnExtractorTest extends BaseColumnExtractorTest {
+
+    @Test
+    public void testExtractNoPrefix() {
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withMetaData(new MockQueryMetadataBuilder()
+                        .withDbEntity(createMockDbEntity("mock"))
+                        .build())
+                .build();
+        TranslatorContext context = new MockTranslatorContext(wrapper);
+
+        DbEntityColumnExtractor extractor = new 
DbEntityColumnExtractor(context);
+        extractor.extract(null);
+
+        assertEquals(2, context.getResultNodeList().size());
+
+        ResultNodeDescriptor descriptor0 = context.getResultNodeList().get(0);
+        ResultNodeDescriptor descriptor1 = context.getResultNodeList().get(1);
+
+        assertNull(descriptor0.getProperty());
+        assertNotNull(descriptor0.getNode());
+        assertThat(descriptor0.getNode(), instanceOf(ColumnNode.class));
+        assertFalse(descriptor0.isAggregate());
+        assertTrue(descriptor0.isInDataRow());
+        assertEquals("id", descriptor0.getDataRowKey());
+        assertNotNull(descriptor0.getDbAttribute());
+        assertEquals(Types.BIGINT, descriptor0.getJdbcType());
+
+        assertNull(descriptor1.getProperty());
+        assertNotNull(descriptor1.getNode());
+        assertThat(descriptor1.getNode(), instanceOf(ColumnNode.class));
+        assertFalse(descriptor1.isAggregate());
+        assertTrue(descriptor1.isInDataRow());
+        assertNotNull(descriptor1.getDbAttribute());
+        assertEquals("name", descriptor1.getDataRowKey());
+        assertEquals(Types.VARBINARY, descriptor1.getJdbcType());
+        assertEquals("byte[]", descriptor1.getJavaType());
+    }
+
+    @Test
+    public void testExtractWithPrefix() {
+        DbEntity mockDbEntity = createMockDbEntity("mock1");
+        DbEntity mock2DbEntity = createMockDbEntity("mock2");
+        DataMap dataMap = new DataMap();
+        dataMap.addDbEntity(mockDbEntity);
+        dataMap.addDbEntity(mock2DbEntity);
+        mockDbEntity.setDataMap(dataMap);
+
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withMetaData(new MockQueryMetadataBuilder()
+                        .withDbEntity(mockDbEntity)
+                        .build())
+                .build();
+        TranslatorContext context = new MockTranslatorContext(wrapper);
+
+        DbRelationship relationship = new DbRelationship();
+        relationship.setSourceEntity(mockDbEntity);
+        relationship.setTargetEntityName("mock1");
+        context.getTableTree().addJoinTable("prefix", relationship, 
JoinType.INNER);
+
+        DbEntityColumnExtractor extractor = new 
DbEntityColumnExtractor(context);
+        extractor.extract("prefix");
+
+        assertEquals(2, context.getResultNodeList().size());
+
+        ResultNodeDescriptor descriptor0 = context.getResultNodeList().get(0);
+        ResultNodeDescriptor descriptor1 = context.getResultNodeList().get(1);
+
+        assertNull(descriptor0.getProperty());
+        assertNotNull(descriptor0.getNode());
+        assertThat(descriptor0.getNode(), instanceOf(ColumnNode.class));
+        assertFalse(descriptor0.isAggregate());
+        assertTrue(descriptor0.isInDataRow());
+        assertEquals("prefix.id", descriptor0.getDataRowKey());
+        assertNotNull(descriptor0.getDbAttribute());
+        assertEquals(Types.BIGINT, descriptor0.getJdbcType());
+
+        assertNull(descriptor1.getProperty());
+        assertNotNull(descriptor1.getNode());
+        assertThat(descriptor1.getNode(), instanceOf(ColumnNode.class));
+        assertFalse(descriptor1.isAggregate());
+        assertTrue(descriptor1.isInDataRow());
+        assertNotNull(descriptor1.getDbAttribute());
+        assertEquals("prefix.name", descriptor1.getDataRowKey());
+        assertEquals(Types.VARBINARY, descriptor1.getJdbcType());
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultObjectSelectTranslatorIT.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultObjectSelectTranslatorIT.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultObjectSelectTranslatorIT.java
new file mode 100644
index 0000000..04b156c
--- /dev/null
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultObjectSelectTranslatorIT.java
@@ -0,0 +1,136 @@
+/*****************************************************************
+ *   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 org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.testdo.testmap.Painting;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class DefaultObjectSelectTranslatorIT extends ServerCase {
+
+    @Inject
+    DataContext context;
+
+    @Inject
+    DbAdapter adapter;
+
+    @Test
+    public void simpleSql() {
+        SelectQuery<Artist> select = SelectQuery.query(Artist.class);
+        DefaultSelectTranslator translator = new 
DefaultSelectTranslator(select, adapter, context.getEntityResolver());
+
+        String sql = translator.getSql();
+        assertTrue(sql.startsWith("SELECT "));
+        assertTrue(sql.contains("t0.ARTIST_NAME"));
+        assertTrue(sql.contains("t0.DATE_OF_BIRTH"));
+        assertTrue(sql.contains("t0.ARTIST_ID"));
+        assertTrue(sql.indexOf("FROM ARTIST t0") > 
sql.indexOf("t0.ARTIST_ID"));
+
+        assertEquals(0, translator.getBindings().length);
+        assertEquals(3, translator.getResultColumns().length);
+        assertEquals("ARTIST_NAME", 
translator.getResultColumns()[0].getDataRowKey());
+        assertEquals("DATE_OF_BIRTH", 
translator.getResultColumns()[1].getDataRowKey());
+        assertEquals("ARTIST_ID", 
translator.getResultColumns()[2].getDataRowKey());
+        assertFalse(translator.hasJoins());
+        assertFalse(translator.isSuppressingDistinct());
+    }
+
+    @Test
+    public void selectWithComplexWhere() {
+        SelectQuery<Artist> select = SelectQuery.query(Artist.class, 
Artist.ARTIST_NAME.eq("artist")
+                
.andExp(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).eq("painting")));
+
+        DefaultSelectTranslator translator = new 
DefaultSelectTranslator(select, adapter, context.getEntityResolver());
+
+        String sql = translator.getSql();
+        assertTrue(sql.startsWith("SELECT DISTINCT"));
+        assertTrue(sql.contains("t0.ARTIST_NAME"));
+        assertTrue(sql.contains("t0.DATE_OF_BIRTH"));
+        assertTrue(sql.contains("t0.ARTIST_ID"));
+        assertTrue(sql.indexOf("FROM ARTIST t0") > 
sql.indexOf("t0.ARTIST_ID"));
+        assertTrue(sql.indexOf("JOIN PAINTING t1") > sql.indexOf("FROM ARTIST 
t0"));
+        assertTrue(sql.indexOf("WHERE") > sql.indexOf("JOIN PAINTING t1"));
+
+        assertEquals(2, translator.getBindings().length);
+        assertEquals("ARTIST_NAME", 
translator.getBindings()[0].getAttribute().getName());
+        assertEquals("PAINTING_TITLE", 
translator.getBindings()[1].getAttribute().getName());
+
+        assertEquals(3, translator.getResultColumns().length);
+        assertEquals("ARTIST_NAME", 
translator.getResultColumns()[0].getDataRowKey());
+        assertEquals("DATE_OF_BIRTH", 
translator.getResultColumns()[1].getDataRowKey());
+        assertEquals("ARTIST_ID", 
translator.getResultColumns()[2].getDataRowKey());
+
+        assertTrue(translator.hasJoins());
+        assertFalse(translator.isSuppressingDistinct());
+    }
+
+    @Test
+    public void selectWithJointPrefetch() {
+        SelectQuery<Painting> select = SelectQuery.query(Painting.class);
+        select.addPrefetch(Painting.TO_ARTIST.joint());
+
+        DefaultSelectTranslator translator = new 
DefaultSelectTranslator(select, adapter, context.getEntityResolver());
+
+        String sql = translator.getSql();
+
+        assertTrue(sql.startsWith("SELECT DISTINCT"));
+        assertTrue(sql.contains("t0.ESTIMATED_PRICE"));
+        assertTrue(sql.contains("t0.PAINTING_DESCRIPTION"));
+        assertTrue(sql.contains("t0.PAINTING_TITLE"));
+        assertTrue(sql.contains("t0.ARTIST_ID"));
+        assertTrue(sql.contains("t0.GALLERY_ID"));
+        assertTrue(sql.contains("t0.PAINTING_ID"));
+        assertTrue(sql.contains("t1.ARTIST_ID"));
+        assertTrue(sql.contains("t1.DATE_OF_BIRTH"));
+        assertTrue(sql.contains("t1.ARTIST_NAME"));
+        assertTrue(sql.indexOf("FROM PAINTING t0") > 
sql.indexOf("t1.ARTIST_NAME"));
+        assertTrue(sql.indexOf("LEFT JOIN ARTIST t1") > sql.indexOf("FROM 
PAINTING t0"));
+
+        assertEquals(0, translator.getBindings().length);
+
+        assertEquals(9, translator.getResultColumns().length);
+        assertEquals("ESTIMATED_PRICE", 
translator.getResultColumns()[0].getDataRowKey());
+        assertEquals("PAINTING_DESCRIPTION", 
translator.getResultColumns()[1].getDataRowKey());
+        assertEquals("PAINTING_TITLE", 
translator.getResultColumns()[2].getDataRowKey());
+        assertEquals("ARTIST_ID", 
translator.getResultColumns()[3].getDataRowKey());
+        assertEquals("GALLERY_ID", 
translator.getResultColumns()[4].getDataRowKey());
+        assertEquals("PAINTING_ID", 
translator.getResultColumns()[5].getDataRowKey());
+
+        assertEquals("toArtist.ARTIST_ID", 
translator.getResultColumns()[6].getDataRowKey());
+        assertEquals("toArtist.DATE_OF_BIRTH", 
translator.getResultColumns()[7].getDataRowKey());
+        assertEquals("toArtist.ARTIST_NAME", 
translator.getResultColumns()[8].getDataRowKey());
+
+        assertTrue(translator.hasJoins());
+        assertFalse(translator.isSuppressingDistinct());
+    }
+}
\ No newline at end of file

Reply via email to