You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2019/01/09 10:46:52 UTC

[01/11] cayenne git commit: CAY-2465 New SelectTranslator implementation

Repository: cayenne
Updated Branches:
  refs/heads/master eb0373e81 -> 60c556675


http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingTranslatorIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingTranslatorIT.java
deleted file mode 100644
index 0aff229..0000000
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingTranslatorIT.java
+++ /dev/null
@@ -1,136 +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 static org.junit.Assert.assertEquals;
-
-import java.util.Arrays;
-
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.di.Inject;
-import org.apache.cayenne.exp.ExpressionFactory;
-import org.apache.cayenne.exp.FunctionExpressionFactory;
-import org.apache.cayenne.query.Ordering;
-import org.apache.cayenne.query.SelectQuery;
-import org.apache.cayenne.query.SortOrder;
-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.ServerCaseDataSourceFactory;
-import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.junit.Test;
-
-@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
-public class OrderingTranslatorIT extends ServerCase {
-
-	@Inject
-	private DataNode node;
-
-	@Inject
-	private ServerCaseDataSourceFactory dataSourceFactory;
-
-	/**
-	 * Tests ascending ordering on string attribute.
-	 */
-	@Test
-	public void testAppendPart1() throws Exception {
-		Ordering o1 = new Ordering("artistName", SortOrder.ASCENDING);
-		doTestAppendPart("ta.ARTIST_NAME", o1);
-	}
-
-	/**
-	 * Tests descending ordering on string attribute.
-	 */
-	@Test
-	public void testAppendPart2() throws Exception {
-		Ordering o1 = new Ordering("artistName", SortOrder.DESCENDING);
-		doTestAppendPart("ta.ARTIST_NAME DESC", o1);
-	}
-
-	@Test
-	public void testAppendPart3() throws Exception {
-
-		Ordering o1 = new Ordering("artistName", SortOrder.DESCENDING);
-		Ordering o2 = new Ordering("paintingArray.estimatedPrice", SortOrder.ASCENDING);
-
-		doTestAppendPart("ta.ARTIST_NAME DESC, ta.ESTIMATED_PRICE", o1, o2);
-	}
-
-	/**
-	 * Tests ascending case-insensitive ordering on string attribute.
-	 */
-	@Test
-	public void testAppendPart4() throws Exception {
-		Ordering o1 = new Ordering("artistName", SortOrder.ASCENDING_INSENSITIVE);
-		doTestAppendPart("UPPER(ta.ARTIST_NAME)", o1);
-	}
-
-	@Test
-	public void testAppendPart5() throws Exception {
-
-		Ordering o1 = new Ordering("artistName", SortOrder.DESCENDING_INSENSITIVE);
-		Ordering o2 = new Ordering("paintingArray.estimatedPrice", SortOrder.ASCENDING);
-
-		doTestAppendPart("UPPER(ta.ARTIST_NAME) DESC, ta.ESTIMATED_PRICE", o1, o2);
-	}
-
-	@Test
-	public void testAppendPart6() throws Exception {
-		Ordering o1 = new Ordering("artistName", SortOrder.ASCENDING_INSENSITIVE);
-		Ordering o2 = new Ordering("paintingArray.estimatedPrice", SortOrder.ASCENDING_INSENSITIVE);
-
-		doTestAppendPart("UPPER(ta.ARTIST_NAME), UPPER(ta.ESTIMATED_PRICE)", o1, o2);
-	}
-
-	@Test
-	public void testAppendFunctionExpression1() throws Exception {
-		Ordering o1 = new Ordering(FunctionExpressionFactory.absExp("paintingArray.estimatedPrice"));
-
-		doTestAppendPart("ABS(ta.ESTIMATED_PRICE)", o1);
-	}
-
-	@Test
-	public void testAppendFunctionExpression2() throws Exception {
-		Ordering o1 = new Ordering(FunctionExpressionFactory.countExp(ExpressionFactory.pathExp("dateOfBirth")), SortOrder.ASCENDING_INSENSITIVE);
-		Ordering o2 = new Ordering(FunctionExpressionFactory.sqrtExp("paintingArray.estimatedPrice"), SortOrder.DESCENDING);
-
-		doTestAppendPart("UPPER(COUNT(ta.DATE_OF_BIRTH)), SQRT(ta.ESTIMATED_PRICE) DESC", o1, o2);
-	}
-
-	@Test(expected = CayenneRuntimeException.class)
-	public void testAppendIllegalExpression() throws Exception {
-		Ordering o1 = new Ordering(ExpressionFactory.and(ExpressionFactory.expTrue(), ExpressionFactory.expFalse()));
-		// should throw exception
-		doTestAppendPart("TRUE AND FALSE", o1);
-	}
-
-	private void doTestAppendPart(String expectedSQL, Ordering... orderings) {
-
-		SelectQuery<Artist> q = SelectQuery.query(Artist.class);
-		q.addOrderings(Arrays.asList(orderings));
-
-		TstQueryAssembler assembler = new TstQueryAssembler(q, node.getAdapter(), node.getEntityResolver());
-		StringBuilder out = new StringBuilder();
-		String translated = new OrderingTranslator(assembler).appendPart(out).toString();
-
-		assertEquals(expectedSQL, translated);
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/PathComponentsTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/PathComponentsTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/PathComponentsTest.java
new file mode 100644
index 0000000..89e9a57
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/PathComponentsTest.java
@@ -0,0 +1,77 @@
+/*****************************************************************
+ *   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.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.1
+ */
+public class PathComponentsTest {
+
+    private PathComponents components1;
+    private PathComponents components2;
+    private PathComponents components3;
+
+    @Before
+    public void setUp() {
+        components1 = new PathComponents("a");
+        components2 = new PathComponents("a+.bcd");
+        components3 = new PathComponents("a.bc.defg");
+    }
+
+    @Test
+    public void size() {
+        assertEquals(1, components1.size());
+        assertEquals(2, components2.size());
+        assertEquals(3, components3.size());
+    }
+
+    @Test
+    public void getLast() {
+        assertEquals("a", components1.getLast());
+        assertEquals("bcd", components2.getLast());
+        assertEquals("defg", components3.getLast());
+    }
+
+    @Test
+    public void getParent() {
+        assertEquals("", components1.getParent());
+        assertEquals("a+", components2.getParent());
+        assertEquals("a.bc", components3.getParent());
+    }
+
+    @Test
+    public void getAll() {
+        assertArrayEquals(new String[]{"a"}, components1.getAll());
+        assertArrayEquals(new String[]{"a+", "bcd"}, components2.getAll());
+        assertArrayEquals(new String[]{"a", "bc", "defg"}, components3.getAll());
+    }
+
+    @Test
+    public void getPath() {
+        assertEquals("a", components1.getPath());
+        assertEquals("a+.bcd", components2.getPath());
+        assertEquals("a.bc.defg", components3.getPath());
+    }
+}
\ 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/QualifierTranslationStageTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslationStageTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslationStageTest.java
new file mode 100644
index 0000000..9b56c81
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslationStageTest.java
@@ -0,0 +1,89 @@
+/*****************************************************************
+ *   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.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ValueNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.WhereNode;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class QualifierTranslationStageTest {
+
+    private TranslatorContext context;
+
+    @Before
+    public void prepareContext() {
+        DbEntity dbEntity = new DbEntity();
+        dbEntity.setName("mock");
+        DbAttribute dbAttribute = new DbAttribute();
+        dbAttribute.setName("path");
+        dbEntity.addAttribute(dbAttribute);
+
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withQualifier(ExpressionFactory.greaterOrEqualDbExp("path", 10))
+                .withMetaData(new MockQueryMetadataBuilder()
+                        .withDbEntity(dbEntity)
+                        .build())
+                .build();
+        context = new MockTranslatorContext(wrapper);
+    }
+
+    @Test
+    public void perform() {
+        QualifierTranslationStage stage = new QualifierTranslationStage();
+        stage.perform(context);
+
+        Node select = context.getSelectBuilder().build();
+
+        // Content of "select" node:
+        //
+        //      Where
+        //        |
+        //   OpExpression
+        //    /        \
+        // Column     Value
+
+        assertEquals(1, select.getChildrenCount());
+        assertThat(select.getChild(0), instanceOf(WhereNode.class));
+        Node op = select.getChild(0).getChild(0);
+        assertThat(op, instanceOf(OpExpressionNode.class));
+        assertEquals(">=", ((OpExpressionNode)op).getOp());
+        assertEquals(2, op.getChildrenCount());
+        assertThat(op.getChild(0), instanceOf(ColumnNode.class));
+        assertThat(op.getChild(1), instanceOf(ValueNode.class));
+
+        ColumnNode columnNode = (ColumnNode)op.getChild(0);
+        ValueNode valueNode = (ValueNode)op.getChild(1);
+        assertEquals("path", columnNode.getColumn());
+        assertEquals(10, valueNode.getValue());
+    }
+}
\ 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/QualifierTranslatorIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
deleted file mode 100644
index 2f2c71d..0000000
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
+++ /dev/null
@@ -1,164 +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 static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import java.util.Arrays;
-
-import org.apache.cayenne.ObjectId;
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.di.Inject;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.ExpressionFactory;
-import org.apache.cayenne.query.MockQuery;
-import org.apache.cayenne.query.SelectQuery;
-import org.apache.cayenne.testdo.testmap.Artist;
-import org.apache.cayenne.testdo.testmap.Exhibit;
-import org.apache.cayenne.testdo.testmap.Gallery;
-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.ServerCaseDataSourceFactory;
-import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.junit.Test;
-
-@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
-public class QualifierTranslatorIT extends ServerCase {
-
-	@Inject
-	private DataNode node;
-
-	@Inject
-	private ServerCaseDataSourceFactory dataSourceFactory;
-
-	// TODO: not an integration test; extract into *Test
-	@Test
-	public void testNonQualifiedQuery() {
-		TstQueryAssembler qa = new TstQueryAssembler(new MockQuery(), node.getAdapter(), node.getEntityResolver());
-
-		try {
-			new QualifierTranslator(qa).appendPart(new StringBuilder());
-			fail();
-		} catch (ClassCastException ccex) {
-			// exception expected
-		}
-	}
-
-	// TODO: not an integration test; extract into *Test
-	@Test
-	public void testNullQualifier() {
-		TstQueryAssembler qa = new TstQueryAssembler(new SelectQuery<>(), node.getAdapter(),
-				node.getEntityResolver());
-
-		StringBuilder out = new StringBuilder();
-		new QualifierTranslator(qa).appendPart(out);
-		assertEquals(0, out.length());
-	}
-
-	@Test
-	public void testBinary_In1() {
-		doExpressionTest(Exhibit.class, "toGallery.galleryName in ('g1', 'g2', 'g3')", "ta.GALLERY_NAME IN (?, ?, ?)");
-	}
-
-	@Test
-	public void testBinary_In2() {
-		Expression exp = ExpressionFactory.inExp("toGallery.galleryName",
-				Arrays.asList("g1", "g2", "g3"));
-		doExpressionTest(Exhibit.class, exp, "ta.GALLERY_NAME IN (?, ?, ?)");
-	}
-
-	@Test
-	public void testBinary_In3() {
-		Expression exp = ExpressionFactory.inExp("toGallery.galleryName", "g1", "g2", "g3");
-		doExpressionTest(Exhibit.class, exp, "ta.GALLERY_NAME IN (?, ?, ?)");
-	}
-
-	@Test
-	public void testBinary_Like() {
-		doExpressionTest(Exhibit.class, "toGallery.galleryName like 'a%'", "ta.GALLERY_NAME LIKE ?");
-	}
-
-	@Test
-	public void testBinary_LikeIgnoreCase() {
-		doExpressionTest(Exhibit.class, "toGallery.galleryName likeIgnoreCase 'a%'",
-				"UPPER(ta.GALLERY_NAME) LIKE UPPER(?)");
-	}
-
-	@Test
-	public void testBinary_IsNull() {
-		doExpressionTest(Exhibit.class, "toGallery.galleryName = null", "ta.GALLERY_NAME IS NULL");
-	}
-
-	@Test
-	public void testBinary_IsNotNull() {
-		doExpressionTest(Exhibit.class, "toGallery.galleryName != null", "ta.GALLERY_NAME IS NOT NULL");
-	}
-
-	@Test
-	public void testTernary_Between() {
-		doExpressionTest(Painting.class, "estimatedPrice between 3000 and 15000", "ta.ESTIMATED_PRICE BETWEEN ? AND ?");
-	}
-
-	@Test
-	public void testExtras() {
-		ObjectId oid1 = new ObjectId("Gallery", "GALLERY_ID", 1);
-		ObjectId oid2 = new ObjectId("Gallery", "GALLERY_ID", 2);
-		Gallery g1 = new Gallery();
-		Gallery g2 = new Gallery();
-		g1.setObjectId(oid1);
-		g2.setObjectId(oid2);
-
-		Expression e1 = ExpressionFactory.matchExp("toGallery", g1);
-		Expression e2 = e1.orExp(ExpressionFactory.matchExp("toGallery", g2));
-
-		doExpressionTest(Exhibit.class, e2, "(ta.GALLERY_ID = ?) OR (ta.GALLERY_ID = ?)");
-	}
-
-	@Test
-	public void testTrim() {
-		doExpressionTest(Artist.class, Artist.ARTIST_NAME.trim().like("P%"),
-				"TRIM(ta.ARTIST_NAME) LIKE ?");
-	}
-
-	@Test
-	public void testConcat() {
-		doExpressionTest(Artist.class, Artist.ARTIST_NAME.concat(Artist.DATE_OF_BIRTH).like("P%"),
-				"CONCAT(ta.ARTIST_NAME, ta.DATE_OF_BIRTH) LIKE ?");
-	}
-
-	private void doExpressionTest(Class<?> queryType, String qualifier, String expectedSQL) {
-		doExpressionTest(queryType, ExpressionFactory.exp(qualifier), expectedSQL);
-	}
-
-	private void doExpressionTest(Class<?> queryType, Expression qualifier, String expectedSQL) {
-
-		SelectQuery<?> q = new SelectQuery<>(queryType);
-		q.setQualifier(qualifier);
-
-		TstQueryAssembler qa = new TstQueryAssembler(q, node.getAdapter(), node.getEntityResolver());
-
-		StringBuilder out = new StringBuilder();
-		String translated = new QualifierTranslator(qa).appendPart(out).toString();
-		assertEquals(expectedSQL, translated);
-
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorTest.java
new file mode 100644
index 0000000..1f78ae0
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorTest.java
@@ -0,0 +1,524 @@
+/*****************************************************************
+ *   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.Arrays;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.sqlbuilder.sqltree.BetweenNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.BitwiseNotNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.EqualNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.InNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NotEqualNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NotNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SelectNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TextNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ValueNode;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.exp.parser.ASTAsterisk;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+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.apache.cayenne.query.ObjectSelect;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class QualifierTranslatorTest {
+
+    private QualifierTranslator translator;
+
+    @Before
+    public void prepareTranslator() {
+        DbEntity dbEntity = new DbEntity();
+        dbEntity.setName("mock");
+
+        DbAttribute dbAttributeA = new DbAttribute();
+        dbAttributeA.setName("a");
+        dbEntity.addAttribute(dbAttributeA);
+
+        DbAttribute dbAttributeB = new DbAttribute();
+        dbAttributeB.setName("b");
+        dbAttributeB.setPrimaryKey(true);
+        dbEntity.addAttribute(dbAttributeB);
+
+        ObjEntity entity = new ObjEntity();
+        entity.setName("mock");
+        ObjAttribute attribute = new ObjAttribute();
+        attribute.setName("a");
+        attribute.setDbAttributePath("a");
+        entity.addAttribute(attribute);
+        entity.setDbEntity(dbEntity);
+
+        DataMap dataMap = new DataMap();
+        dataMap.addDbEntity(dbEntity);
+        dataMap.addObjEntity(entity);
+
+        EntityResolver resolver = new EntityResolver();
+        resolver.addDataMap(dataMap);
+
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withMetaData(new MockQueryMetadataBuilder()
+                        .withDbEntity(dbEntity)
+                        .withObjEntity(entity)
+                        .build())
+                .build();
+        TranslatorContext context = new MockTranslatorContext(wrapper, resolver);
+        translator = new QualifierTranslator(context);
+    }
+
+    @Test
+    public void translateNull() {
+        assertNull(translator.translate((Expression)null));
+        assertNull(translator.translate((Property<?>)null));
+    }
+
+    @Test
+    public void translateIn() {
+        {
+            Node in = translate("db:a in (1,2)");
+            assertThat(in, instanceOf(InNode.class));
+            assertEquals(2, in.getChildrenCount());
+            assertFalse(((InNode) in).isNot());
+            assertThat(in.getChild(0), instanceOf(ColumnNode.class));
+            assertThat(in.getChild(1), instanceOf(ValueNode.class));
+            assertEquals("a", ((ColumnNode) in.getChild(0)).getColumn());
+            assertArrayEquals(new Object[]{1, 2}, (Object[]) ((ValueNode) in.getChild(1)).getValue());
+        }
+
+        {
+            Node in = translator.translate(ExpressionFactory.inExp("a", Arrays.asList(1, 2, 3)));
+            assertThat(in, instanceOf(InNode.class));
+            assertEquals(2, in.getChildrenCount());
+            assertFalse(((InNode) in).isNot());
+            assertThat(in.getChild(0), instanceOf(ColumnNode.class));
+            assertThat(in.getChild(1), instanceOf(ValueNode.class));
+            assertEquals("a", ((ColumnNode) in.getChild(0)).getColumn());
+            assertArrayEquals(new Object[]{1, 2, 3}, (Object[]) ((ValueNode) in.getChild(1)).getValue());
+        }
+
+        {
+            Node notIn = translate("db:a not in (1,2)");
+            assertThat(notIn, instanceOf(InNode.class));
+            assertEquals(2, notIn.getChildrenCount());
+            assertTrue(((InNode) notIn).isNot());
+            assertThat(notIn.getChild(0), instanceOf(ColumnNode.class));
+            assertThat(notIn.getChild(1), instanceOf(ValueNode.class));
+            assertEquals("a", ((ColumnNode) notIn.getChild(0)).getColumn());
+            assertArrayEquals(new Object[]{1, 2}, (Object[]) ((ValueNode) notIn.getChild(1)).getValue());
+        }
+
+    }
+
+    @Test
+    public void translateBetween() {
+        {
+            Node between = translate("db:a between 1 and 5");
+            assertThat(between, instanceOf(BetweenNode.class));
+            assertEquals(3, between.getChildrenCount());
+            assertFalse(((BetweenNode) between).isNot());
+            assertThat(between.getChild(0), instanceOf(ColumnNode.class));
+            assertThat(between.getChild(1), instanceOf(ValueNode.class));
+            assertThat(between.getChild(2), instanceOf(ValueNode.class));
+        }
+
+        {
+            Node notBetween = translate("db:b not between 2 and 6");
+            assertThat(notBetween, instanceOf(BetweenNode.class));
+            assertEquals(3, notBetween.getChildrenCount());
+            assertTrue(((BetweenNode) notBetween).isNot());
+            assertThat(notBetween.getChild(0), instanceOf(ColumnNode.class));
+            assertThat(notBetween.getChild(1), instanceOf(ValueNode.class));
+            assertThat(notBetween.getChild(2), instanceOf(ValueNode.class));
+        }
+    }
+
+    @Test
+    public void translateNot() {
+        Node not = translate("not true");
+        assertThat(not, instanceOf(NotNode.class));
+        assertEquals(1, not.getChildrenCount());
+        assertThat(not.getChild(0), instanceOf(TextNode.class));
+        assertEquals(" 1=1", ((TextNode)not.getChild(0)).getText());
+    }
+
+    @Test
+    public void translateBitwiseNot() {
+        Node not = translate("~123");
+        assertThat(not, instanceOf(BitwiseNotNode.class));
+        assertEquals(1, not.getChildrenCount());
+        assertThat(not.getChild(0), instanceOf(ValueNode.class));
+        assertEquals(123, ((ValueNode)not.getChild(0)).getValue());
+    }
+
+    @Test
+    public void translateEqual() {
+        {
+            Node eq = translate("db:a = 123");
+            assertThat(eq, instanceOf(EqualNode.class));
+            assertEquals(2, eq.getChildrenCount());
+            assertThat(eq.getChild(0), instanceOf(ColumnNode.class));
+            assertThat(eq.getChild(1), instanceOf(ValueNode.class));
+            assertEquals("a", ((ColumnNode) eq.getChild(0)).getColumn());
+            assertEquals(123, ((ValueNode) eq.getChild(1)).getValue());
+        }
+
+        {
+            Node neq = translate("db:b != 321");
+            assertThat(neq, instanceOf(NotEqualNode.class));
+            assertEquals(2, neq.getChildrenCount());
+            assertThat(neq.getChild(0), instanceOf(ColumnNode.class));
+            assertThat(neq.getChild(1), instanceOf(ValueNode.class));
+            assertEquals("b", ((ColumnNode) neq.getChild(0)).getColumn());
+            assertEquals(321, ((ValueNode) neq.getChild(1)).getValue());
+        }
+    }
+
+    @Test
+    public void translateLike() {
+        {
+            // no support for escape char in exp parser
+            Node like = translator.translate(ExpressionFactory.likeExp("a", "abc", '~'));
+            assertThat(like, instanceOf(LikeNode.class));
+            assertEquals('~', ((LikeNode) like).getEscape());
+            assertFalse(((LikeNode) like).isIgnoreCase());
+            assertFalse(((LikeNode) like).isNot());
+            assertEquals(2, like.getChildrenCount());
+            assertThat(like.getChild(0), instanceOf(ColumnNode.class));
+            assertThat(like.getChild(1), instanceOf(ValueNode.class));
+            assertEquals("a", ((ColumnNode) like.getChild(0)).getColumn());
+            assertEquals("abc", ((ValueNode) like.getChild(1)).getValue());
+        }
+
+        {
+            Node notLike = translate("db:a not like 'abc'");
+            assertThat(notLike, instanceOf(LikeNode.class));
+            assertEquals(0, ((LikeNode) notLike).getEscape());
+            assertFalse(((LikeNode) notLike).isIgnoreCase());
+            assertTrue(((LikeNode) notLike).isNot());
+            assertEquals(2, notLike.getChildrenCount());
+            assertThat(notLike.getChild(0), instanceOf(ColumnNode.class));
+            assertThat(notLike.getChild(1), instanceOf(ValueNode.class));
+            assertEquals("a", ((ColumnNode) notLike.getChild(0)).getColumn());
+            assertEquals("abc", ((ValueNode) notLike.getChild(1)).getValue());
+        }
+
+        {
+            Node likeIgnoreCase = translate("db:a likeIgnoreCase 'abc'");
+            assertThat(likeIgnoreCase, instanceOf(LikeNode.class));
+            assertEquals(0, ((LikeNode) likeIgnoreCase).getEscape());
+            assertTrue(((LikeNode) likeIgnoreCase).isIgnoreCase());
+            assertFalse(((LikeNode) likeIgnoreCase).isNot());
+            assertEquals(2, likeIgnoreCase.getChildrenCount());
+            assertThat(likeIgnoreCase.getChild(0), instanceOf(ColumnNode.class));
+            assertThat(likeIgnoreCase.getChild(1), instanceOf(ValueNode.class));
+            assertEquals("a", ((ColumnNode) likeIgnoreCase.getChild(0)).getColumn());
+            assertEquals("abc", ((ValueNode) likeIgnoreCase.getChild(1)).getValue());
+        }
+
+        {
+            Node notLikeIgnoreCase = translate("db:a not likeIgnoreCase 'abc'");
+            assertThat(notLikeIgnoreCase, instanceOf(LikeNode.class));
+            assertEquals(0, ((LikeNode) notLikeIgnoreCase).getEscape());
+            assertTrue(((LikeNode) notLikeIgnoreCase).isIgnoreCase());
+            assertTrue(((LikeNode) notLikeIgnoreCase).isNot());
+            assertEquals(2, notLikeIgnoreCase.getChildrenCount());
+            assertThat(notLikeIgnoreCase.getChild(0), instanceOf(ColumnNode.class));
+            assertThat(notLikeIgnoreCase.getChild(1), instanceOf(ValueNode.class));
+            assertEquals("a", ((ColumnNode) notLikeIgnoreCase.getChild(0)).getColumn());
+            assertEquals("abc", ((ValueNode) notLikeIgnoreCase.getChild(1)).getValue());
+        }
+    }
+
+    @Test
+    public void translateFunctionCall() {
+        {
+            Node function = translate("trim(a)");
+            assertThat(function, instanceOf(FunctionNode.class));
+            assertEquals("TRIM", ((FunctionNode) function).getFunctionName());
+            assertNull(((FunctionNode) function).getAlias());
+            assertEquals(1, function.getChildrenCount());
+            assertThat(function.getChild(0), instanceOf(ColumnNode.class));
+            assertEquals("a", ((ColumnNode) function.getChild(0)).getColumn());
+        }
+
+        {
+            Node function = translate("year(a)");
+            assertThat(function, instanceOf(FunctionNode.class));
+            assertEquals("YEAR", ((FunctionNode) function).getFunctionName());
+            assertNull(((FunctionNode) function).getAlias());
+            assertEquals(1, function.getChildrenCount());
+            assertThat(function.getChild(0), instanceOf(ColumnNode.class));
+            assertEquals("a", ((ColumnNode) function.getChild(0)).getColumn());
+        }
+    }
+
+    @Test
+    public void translateMathExp() {
+        {
+            Node op = translate("1 + 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals("+", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("1 - 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals("-", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("1 / 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals("/", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("1 * 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals("*", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("-2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals("-", ((OpExpressionNode)op).getOp());
+            assertEquals(1, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("1 & 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals("&", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("1 | 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals("|", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("1 ^ 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals("^", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("1 << 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals("<<", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("1 >> 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals(">>", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+    }
+
+    @Test
+    public void translateComparision() {
+        {
+            Node op = translate("a < 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals("<", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("a > 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals(">", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("a <= 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals("<=", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("a >= 2");
+            assertThat(op, instanceOf(OpExpressionNode.class));
+            assertEquals(">=", ((OpExpressionNode)op).getOp());
+            assertEquals(2, op.getChildrenCount());
+        }
+    }
+
+    @Test
+    public void translateConst() {
+        {
+            Node op = translate("true");
+            assertThat(op, instanceOf(TextNode.class));
+            assertEquals(" 1=1", ((TextNode)op).getText());
+            assertEquals(0, op.getChildrenCount());
+        }
+
+        {
+            Node op = translate("false");
+            assertThat(op, instanceOf(TextNode.class));
+            assertEquals(" 1=0", ((TextNode)op).getText());
+            assertEquals(0, op.getChildrenCount());
+        }
+
+        {
+            Node op = translator.translate(new ASTAsterisk());
+            assertThat(op, instanceOf(TextNode.class));
+            assertEquals(" *", ((TextNode)op).getText());
+            assertEquals(0, op.getChildrenCount());
+        }
+    }
+
+    @Test
+    public void translateExists() {
+        Node exists = translator.translate(ExpressionFactory.exists(ObjectSelect.dbQuery("mock")));
+        assertThat(exists, instanceOf(FunctionNode.class));
+        assertEquals("EXISTS", ((FunctionNode) exists).getFunctionName());
+        assertEquals(1, exists.getChildrenCount());
+        assertThat(exists.getChild(0), instanceOf(SelectNode.class));
+    }
+
+    @Test
+    public void translateFullObject() {
+        Node fullObj = translator.translate(ExpressionFactory.fullObjectExp());
+        assertThat(fullObj, instanceOf(ColumnNode.class));
+        ColumnNode columnNode = (ColumnNode)fullObj;
+        assertEquals("b", columnNode.getColumn());
+    }
+
+    @Test(expected = CayenneRuntimeException.class)
+    public void translateEnclosingObject() {
+        // can't translate enclosing exp not in nested query
+        translator.translate(ExpressionFactory.enclosingObjectExp(ExpressionFactory.dbPathExp("a")));
+    }
+
+    @Test
+    public void translateAnd() {
+        Node and = translate("true and false");
+        assertNotNull(and);
+        assertThat(and, instanceOf(OpExpressionNode.class));
+        assertEquals("AND", ((OpExpressionNode)and).getOp());
+        assertEquals(2, and.getChildrenCount());
+        assertThat(and.getChild(0), instanceOf(TextNode.class));
+        assertEquals(" 1=1", ((TextNode)and.getChild(0)).getText());
+        assertThat(and.getChild(1), instanceOf(TextNode.class));
+        assertEquals(" 1=0", ((TextNode)and.getChild(1)).getText());
+    }
+
+    @Test
+    public void translateOr() {
+        Node or = translate("true or false");
+        assertNotNull(or);
+        assertThat(or, instanceOf(OpExpressionNode.class));
+        assertEquals("OR", ((OpExpressionNode)or).getOp());
+        assertEquals(2, or.getChildrenCount());
+        assertThat(or.getChild(0), instanceOf(TextNode.class));
+        assertThat(or.getChild(1), instanceOf(TextNode.class));
+    }
+
+    @Test
+    public void translateComplexExp() {
+        Node result = translate("(a >= 1 + 2 / 3 << 4) and (db:b != true)");
+
+        {
+            assertNotNull(result);
+            assertThat(result, instanceOf(OpExpressionNode.class));
+            assertEquals("AND", ((OpExpressionNode) result).getOp());
+            assertEquals(2, result.getChildrenCount());
+            assertThat(result.getChild(0), instanceOf(OpExpressionNode.class));
+            assertThat(result.getChild(1), instanceOf(NotEqualNode.class));
+        }
+
+        {
+            OpExpressionNode left = (OpExpressionNode) result.getChild(0);
+            assertEquals(">=", left.getOp());
+            assertEquals(2, left.getChildrenCount());
+            assertThat(left.getChild(0), instanceOf(ColumnNode.class));
+            assertEquals("a", ((ColumnNode)left.getChild(0)).getColumn());
+            assertThat(left.getChild(1), instanceOf(OpExpressionNode.class));
+            {
+                OpExpressionNode shift = (OpExpressionNode)left.getChild(1);
+                assertEquals("<<", shift.getOp());
+                assertThat(shift.getChild(0), instanceOf(OpExpressionNode.class));
+                {
+                    OpExpressionNode plus = (OpExpressionNode)shift.getChild(0);
+                    assertEquals("+", plus.getOp());
+                    assertEquals(2, plus.getChildrenCount());
+                    assertThat(plus.getChild(0), instanceOf(ValueNode.class));
+                    assertEquals(1, ((ValueNode)plus.getChild(0)).getValue());
+                    assertThat(plus.getChild(1), instanceOf(OpExpressionNode.class));
+                    {
+                        OpExpressionNode div = (OpExpressionNode)plus.getChild(1);
+                        assertEquals("/", div.getOp());
+                        assertEquals(2, div.getChildrenCount());
+                        assertThat(div.getChild(0), instanceOf(ValueNode.class));
+                        assertEquals(2, ((ValueNode)div.getChild(0)).getValue());
+                        assertThat(div.getChild(0), instanceOf(ValueNode.class));
+                        assertEquals(3, ((ValueNode)div.getChild(1)).getValue());
+                    }
+                }
+                assertThat(shift.getChild(1), instanceOf(ValueNode.class));
+                assertEquals(4, ((ValueNode)shift.getChild(1)).getValue());
+            }
+        }
+
+        {
+            NotEqualNode right = (NotEqualNode) result.getChild(1);
+            assertEquals(2, right.getChildrenCount());
+            assertThat(right.getChild(0), instanceOf(ColumnNode.class));
+            assertEquals("b", ((ColumnNode)right.getChild(0)).getColumn());
+            assertThat(right.getChild(1), instanceOf(ValueNode.class));
+            assertEquals(Boolean.TRUE, ((ValueNode)right.getChild(1)).getValue());
+        }
+    }
+
+    private Node translate(String s) {
+        return translator.translate(ExpressionFactory.exp(s));
+    }
+
+}
\ 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/QueryAssemblerIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QueryAssemblerIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QueryAssemblerIT.java
deleted file mode 100644
index 05c51e6..0000000
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QueryAssemblerIT.java
+++ /dev/null
@@ -1,61 +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 static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.di.Inject;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.query.SelectQuery;
-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.Before;
-import org.junit.Test;
-
-@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
-public class QueryAssemblerIT extends ServerCase {
-
-	@Inject
-	private DataNode node;
-
-	private TstQueryAssembler qa;
-
-	@Before
-	public void setUp() throws Exception {
-		this.qa = new TstQueryAssembler(new SelectQuery<Object>(), node.getAdapter(), node.getEntityResolver());
-	}
-
-	@Test
-	public void testGetQuery() throws Exception {
-		assertNotNull(qa.getQuery());
-	}
-
-	@Test
-	public void testAddToParamList() throws Exception {
-
-		assertEquals(0, qa.getBindings().length);
-
-		qa.addToParamList(new DbAttribute(), new Object());
-		assertEquals(1, qa.getBindings().length);
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java
deleted file mode 100644
index 72a3803..0000000
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/TstQueryAssembler.java
+++ /dev/null
@@ -1,70 +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.ArrayList;
-import java.util.List;
-
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.map.JoinType;
-import org.apache.cayenne.query.Query;
-
-public class TstQueryAssembler extends QueryAssembler {
-
-	protected List<DbRelationship> dbRels;
-
-	public TstQueryAssembler(Query q, DbAdapter adapter, EntityResolver resolver) {
-		super(q, adapter, resolver);
-		dbRels = new ArrayList<DbRelationship>();
-	}
-
-	@Override
-	public void dbRelationshipAdded(DbRelationship relationship, JoinType joinType, String joinSplitAlias) {
-		dbRels.add(relationship);
-	}
-
-	@Override
-	public String getCurrentAlias() {
-		return "ta";
-	}
-
-	@Override
-	public void resetJoinStack() {
-		// noop
-	}
-
-	@Override
-	public boolean supportsTableAliases() {
-		return true;
-	}
-
-	@Override
-	public String getAliasForExpression(Expression exp) {
-		return null;
-	}
-
-	@Override
-	protected void doTranslate() {
-		this.sql = "SELECT * FROM ARTIST";
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/exp/property/PathAliasesIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/property/PathAliasesIT.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/property/PathAliasesIT.java
index e73402d..8c2bdd5 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/property/PathAliasesIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/property/PathAliasesIT.java
@@ -167,7 +167,7 @@ public class PathAliasesIT extends ServerCase {
         List<Object[]> artistAndPaintingCount = ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME, Artist.PAINTING_ARRAY.count())
                 .having(Artist.PAINTING_ARRAY.alias("p1").count().lt(5L))
                 .select(context);
-        assertEquals(4, artistAndPaintingCount.size());
+        assertEquals(19, artistAndPaintingCount.size());
         assertTrue((Long)artistAndPaintingCount.get(0)[1] < 5);
     }
 
@@ -296,7 +296,7 @@ public class PathAliasesIT extends ServerCase {
         List<Object[]> artistAndPaintingCount = ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME, Artist.PAINTING_ARRAY.count())
                 .having(PropertyFactory.createBase(e1, Number.class).count().lt(5L))
                 .select(context);
-        assertEquals(4, artistAndPaintingCount.size());
+        assertEquals(19, artistAndPaintingCount.size());
         assertTrue((Long)artistAndPaintingCount.get(0)[1] < 5);
     }
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
index a0b2486..cbd2e03 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
@@ -56,6 +56,7 @@ import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
 
+import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.junit.Assert.*;
 
 /**
@@ -492,6 +493,7 @@ public class ColumnSelectIT extends ServerCase {
                 PropertyFactory.createSelf(Artist.class)
         ).select(context);
         assertEquals(21, result.size());
+
         for(Object[] next : result) {
             long count = (Long)next[0];
             Artist artist = (Artist)next[1];
@@ -502,12 +504,12 @@ public class ColumnSelectIT extends ServerCase {
             Artist artist3 = (Artist)next[6];
 
             assertTrue(paintingTitle.startsWith("painting"));
-            assertTrue(count == 4L || count == 5L);
             assertEquals("tate modern", galleryName);
             assertEquals(PersistenceState.COMMITTED, artist.getPersistenceState());
             assertEquals(PersistenceState.COMMITTED, artist2.getPersistenceState());
             assertEquals(PersistenceState.COMMITTED, artist3.getPersistenceState());
             assertEquals(artistName, artist.getArtistName());
+            assertTrue(count == 4L || count == 5L);
         }
     }
 
@@ -607,7 +609,7 @@ public class ColumnSelectIT extends ServerCase {
                 .pageSize(10)
                 .select(context);
         assertNotNull(a);
-        assertEquals(5, a.size());
+        assertEquals(20, a.size());
         int idx = 0;
         for(Object[] next : a) {
             assertNotNull(next);
@@ -655,7 +657,7 @@ public class ColumnSelectIT extends ServerCase {
                 .pageSize(10)
                 .select(context);
         assertNotNull(a);
-        assertEquals(5, a.size());
+        assertEquals(20, a.size());
         int idx = 0;
         for(Object[] next : a) {
             assertNotNull(next);
@@ -673,14 +675,17 @@ public class ColumnSelectIT extends ServerCase {
                 .columns(Artist.ARTIST_NAME, artistFull, Artist.PAINTING_ARRAY.flat())
                 .pageSize(10)
                 .select(context);
+
         assertNotNull(a);
-        assertEquals(21, a.size());
+        assertEquals(36, a.size());
         int idx = 0;
         for(Object[] next : a) {
             assertNotNull(next);
             assertEquals("" + idx, String.class, next[0].getClass());
             assertEquals("" + idx, Artist.class, next[1].getClass());
-            assertEquals("" + idx, Painting.class, next[2].getClass());
+            if(next[2] != null) {
+                assertEquals("" + idx, Painting.class, next[2].getClass());
+            }
             idx++;
         }
     }
@@ -728,9 +733,9 @@ public class ColumnSelectIT extends ServerCase {
     private void checkPrefetchResults(List<Object[]> result) {
         assertEquals(21, result.size());
         for(Object[] next : result) {
-            assertTrue(next[0] instanceof Artist);
-            assertTrue(next[1] instanceof java.util.Date);
-            assertTrue(next[2] instanceof String);
+            assertThat(next[0], instanceOf(Artist.class));
+            assertThat(next[1], instanceOf(java.util.Date.class));
+            assertThat(next[2], instanceOf(String.class));
             Artist artist = (Artist)next[0];
             assertEquals(PersistenceState.COMMITTED, artist.getPersistenceState());
 
@@ -777,7 +782,7 @@ public class ColumnSelectIT extends ServerCase {
     }
 
     private void checkAggregatePrefetchResults(List<Object[]> result) {
-        assertEquals(5, result.size());
+        assertEquals(20, result.size());
         for(Object[] next : result) {
             assertTrue(next[0] instanceof Artist);
             assertTrue(next[1] instanceof Long);
@@ -847,7 +852,7 @@ public class ColumnSelectIT extends ServerCase {
         List<Object[]> result = ObjectSelect.query(Artist.class)
                 .columns(artistProperty, Artist.ARTIST_NAME, Artist.PAINTING_ARRAY.count())
                 .select(context);
-        assertEquals(5, result.size());
+        assertEquals(20, result.size());
 
         for(Object[] next : result) {
             assertTrue(next[0] instanceof Artist);
@@ -898,15 +903,19 @@ public class ColumnSelectIT extends ServerCase {
         List<Object[]> result = ObjectSelect.query(Artist.class)
                 .columns(artistProperty, Artist.PAINTING_ARRAY.flat(), Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY))
                 .select(context);
-        assertEquals(21, result.size());
+        assertEquals(36, result.size());
 
         for(Object[] next : result) {
             assertTrue(next[0] instanceof Artist);
-            assertTrue(next[1] instanceof Painting);
-            assertTrue(next[2] instanceof Gallery);
             assertEquals(PersistenceState.COMMITTED, ((Artist)next[0]).getPersistenceState());
-            assertEquals(PersistenceState.COMMITTED, ((Painting)(next[1])).getPersistenceState());
-            assertEquals(PersistenceState.COMMITTED, ((Gallery)(next[2])).getPersistenceState());
+            if(next[1] != null) {
+                assertTrue(next[1] instanceof Painting);
+                assertEquals(PersistenceState.COMMITTED, ((Painting)(next[1])).getPersistenceState());
+            }
+            if(next[2] != null) {
+                assertTrue(next[2] instanceof Gallery);
+                assertEquals(PersistenceState.COMMITTED, ((Gallery) (next[2])).getPersistenceState());
+            }
         }
     }
 
@@ -918,24 +927,29 @@ public class ColumnSelectIT extends ServerCase {
         assertEquals(21, result.size());
     }
 
-    @Test(expected = CayenneRuntimeException.class)
+    @Test
     public void testSelfPropertyInOrderBy() {
         EntityProperty<Artist> artistProperty = PropertyFactory.createSelf(Artist.class);
-        ObjectSelect.query(Artist.class)
+        List<Artist> artists = ObjectSelect.query(Artist.class)
                 .column(artistProperty)
                 .orderBy(artistProperty.desc())
                 .select(context);
+        assertEquals(20, artists.size());
+        assertEquals("artist20", artists.get(0).getArtistName());
+        assertEquals("artist1", artists.get(19).getArtistName());
     }
 
-    @Test(expected = CayenneRuntimeException.class)
+    @Test
     public void testSelfPropertyInWhere() {
         Artist artist = ObjectSelect.query(Artist.class).selectFirst(context);
         EntityProperty<Artist> artistProperty = PropertyFactory.createSelf(Artist.class);
-        List<Artist> result = ObjectSelect.query(Artist.class)
+        Artist selectedArtist = ObjectSelect.query(Artist.class)
                 .column(artistProperty)
                 .where(artistProperty.eq(artist))
-                .select(context);
-        assertNotNull(result);
+                .orderBy(artistProperty.asc())
+                .selectOne(context);
+        assertNotNull(selectedArtist);
+        assertEquals(artist.getArtistName(), selectedArtist.getArtistName());
     }
 
     @Test

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/unit/IngresUnitDbAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/IngresUnitDbAdapter.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/IngresUnitDbAdapter.java
index aa043ec..279b096 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/IngresUnitDbAdapter.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/IngresUnitDbAdapter.java
@@ -21,6 +21,9 @@ package org.apache.cayenne.unit;
 
 import org.apache.cayenne.dba.DbAdapter;
 
+/**
+ * Ingress doesn't support sorting by an aggregate expression (ORDER BY COUNT(some_field))
+ */
 public class IngresUnitDbAdapter extends UnitDbAdapter {
 
     public IngresUnitDbAdapter(DbAdapter adapter) {
@@ -51,4 +54,9 @@ public class IngresUnitDbAdapter extends UnitDbAdapter {
     public boolean supportsSelectBooleanExpression() {
         return false;
     }
+
+    @Override
+    public boolean isLowerCaseNames() {
+        return true;
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/util/ArrayUtilTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/util/ArrayUtilTest.java b/cayenne-server/src/test/java/org/apache/cayenne/util/ArrayUtilTest.java
new file mode 100644
index 0000000..8b159b1
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/util/ArrayUtilTest.java
@@ -0,0 +1,354 @@
+/*****************************************************************
+ *   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;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.1
+ */
+public class ArrayUtilTest {
+
+    @Test
+    public void sliceIntArray() {
+        int[] array = {1, 2, 3, 4, 5, 6, 7};
+
+        int[][] result = ArrayUtil.sliceArray(array, 2);
+
+        assertEquals(4, result.length);
+        assertArrayEquals(new int[]{1, 2}, result[0]);
+        assertArrayEquals(new int[]{3, 4}, result[1]);
+        assertArrayEquals(new int[]{5, 6}, result[2]);
+        assertArrayEquals(new int[]{7},    result[3]);
+
+        int[][] result2 = ArrayUtil.sliceArray(array, 4);
+
+        assertEquals(2, result2.length);
+        assertArrayEquals(new int[]{1, 2, 3, 4}, result2[0]);
+        assertArrayEquals(new int[]{5, 6, 7}, result2[1]);
+
+        int[][] result3 = ArrayUtil.sliceArray(array, 7);
+
+        assertEquals(1, result3.length);
+        assertArrayEquals(array, result3[0]);
+
+        int[][] result4 = ArrayUtil.sliceArray(array, 10);
+
+        assertEquals(1, result4.length);
+        assertArrayEquals(array, result4[0]);
+
+        int[] array2 = {1, 2, 3, 4, 5, 6, 7, 8};
+        int[][] result5 = ArrayUtil.sliceArray(array2, 4);
+
+        assertEquals(2, result5.length);
+        assertArrayEquals(new int[]{1, 2, 3, 4}, result5[0]);
+        assertArrayEquals(new int[]{5, 6, 7, 8}, result5[1]);
+    }
+
+    @Test
+    public void sliceLongArray() {
+        long[] array = {1, 2, 3, 4, 5, 6, 7};
+
+        long[][] result = ArrayUtil.sliceArray(array, 2);
+
+        assertEquals(4, result.length);
+        assertArrayEquals(new long[]{1, 2}, result[0]);
+        assertArrayEquals(new long[]{3, 4}, result[1]);
+        assertArrayEquals(new long[]{5, 6}, result[2]);
+        assertArrayEquals(new long[]{7},    result[3]);
+
+        long[][] result2 = ArrayUtil.sliceArray(array, 4);
+
+        assertEquals(2, result2.length);
+        assertArrayEquals(new long[]{1, 2, 3, 4}, result2[0]);
+        assertArrayEquals(new long[]{5, 6, 7}, result2[1]);
+
+        long[][] result3 = ArrayUtil.sliceArray(array, 7);
+
+        assertEquals(1, result3.length);
+        assertArrayEquals(array, result3[0]);
+
+        long[][] result4 = ArrayUtil.sliceArray(array, 10);
+
+        assertEquals(1, result4.length);
+        assertArrayEquals(array, result4[0]);
+
+        long[] array2 = {1, 2, 3, 4, 5, 6, 7, 8};
+        long[][] result5 = ArrayUtil.sliceArray(array2, 4);
+
+        assertEquals(2, result5.length);
+        assertArrayEquals(new long[]{1, 2, 3, 4}, result5[0]);
+        assertArrayEquals(new long[]{5, 6, 7, 8}, result5[1]);
+    }
+
+    @Test
+    public void sliceFloatArray() {
+        float[] array = {1, 2, 3, 4, 5, 6, 7};
+
+        float[][] result = ArrayUtil.sliceArray(array, 2);
+
+        assertEquals(4, result.length);
+        assertArrayEquals(new float[]{1, 2}, result[0], 0.000001f);
+        assertArrayEquals(new float[]{3, 4}, result[1], 0.000001f);
+        assertArrayEquals(new float[]{5, 6}, result[2], 0.000001f);
+        assertArrayEquals(new float[]{7},    result[3], 0.000001f);
+
+        float[][] result2 = ArrayUtil.sliceArray(array, 4);
+
+        assertEquals(2, result2.length);
+        assertArrayEquals(new float[]{1, 2, 3, 4}, result2[0], 0.000001f);
+        assertArrayEquals(new float[]{5, 6, 7}, result2[1], 0.000001f);
+
+        float[][] result3 = ArrayUtil.sliceArray(array, 7);
+
+        assertEquals(1, result3.length);
+        assertArrayEquals(array, result3[0], 0.000001f);
+
+        float[][] result4 = ArrayUtil.sliceArray(array, 10);
+
+        assertEquals(1, result4.length);
+        assertArrayEquals(array, result4[0], 0.000001f);
+
+        float[] array2 = {1, 2, 3, 4, 5, 6, 7, 8};
+        float[][] result5 = ArrayUtil.sliceArray(array2, 4);
+
+        assertEquals(2, result5.length);
+        assertArrayEquals(new float[]{1, 2, 3, 4}, result5[0], 0.000001f);
+        assertArrayEquals(new float[]{5, 6, 7, 8}, result5[1], 0.000001f);
+    }
+
+    @Test
+    public void sliceDoubleArray() {
+        double[] array = {1, 2, 3, 4, 5, 6, 7};
+
+        double[][] result = ArrayUtil.sliceArray(array, 2);
+
+        assertEquals(4, result.length);
+        assertArrayEquals(new double[]{1, 2}, result[0], 0.000001);
+        assertArrayEquals(new double[]{3, 4}, result[1], 0.000001);
+        assertArrayEquals(new double[]{5, 6}, result[2], 0.000001);
+        assertArrayEquals(new double[]{7},    result[3], 0.000001);
+
+        double[][] result2 = ArrayUtil.sliceArray(array, 4);
+
+        assertEquals(2, result2.length);
+        assertArrayEquals(new double[]{1, 2, 3, 4}, result2[0], 0.000001);
+        assertArrayEquals(new double[]{5, 6, 7}, result2[1], 0.000001);
+
+        double[][] result3 = ArrayUtil.sliceArray(array, 7);
+
+        assertEquals(1, result3.length);
+        assertArrayEquals(array, result3[0], 0.000001);
+
+        double[][] result4 = ArrayUtil.sliceArray(array, 10);
+
+        assertEquals(1, result4.length);
+        assertArrayEquals(array, result4[0], 0.000001);
+
+        double[] array2 = {1, 2, 3, 4, 5, 6, 7, 8};
+        double[][] result5 = ArrayUtil.sliceArray(array2, 4);
+
+        assertEquals(2, result5.length);
+        assertArrayEquals(new double[]{1, 2, 3, 4}, result5[0], 0.000001);
+        assertArrayEquals(new double[]{5, 6, 7, 8}, result5[1], 0.000001);
+    }
+
+    @Test
+    public void sliceCharArray() {
+        char[] array = {1, 2, 3, 4, 5, 6, 7};
+
+        char[][] result = ArrayUtil.sliceArray(array, 2);
+
+        assertEquals(4, result.length);
+        assertArrayEquals(new char[]{1, 2}, result[0]);
+        assertArrayEquals(new char[]{3, 4}, result[1]);
+        assertArrayEquals(new char[]{5, 6}, result[2]);
+        assertArrayEquals(new char[]{7},    result[3]);
+
+        char[][] result2 = ArrayUtil.sliceArray(array, 4);
+
+        assertEquals(2, result2.length);
+        assertArrayEquals(new char[]{1, 2, 3, 4}, result2[0]);
+        assertArrayEquals(new char[]{5, 6, 7}, result2[1]);
+
+        char[][] result3 = ArrayUtil.sliceArray(array, 7);
+
+        assertEquals(1, result3.length);
+        assertArrayEquals(array, result3[0]);
+
+        char[][] result4 = ArrayUtil.sliceArray(array, 10);
+
+        assertEquals(1, result4.length);
+        assertArrayEquals(array, result4[0]);
+
+        char[] array2 = {1, 2, 3, 4, 5, 6, 7, 8};
+        char[][] result5 = ArrayUtil.sliceArray(array2, 4);
+
+        assertEquals(2, result5.length);
+        assertArrayEquals(new char[]{1, 2, 3, 4}, result5[0]);
+        assertArrayEquals(new char[]{5, 6, 7, 8}, result5[1]);
+    }
+
+    @Test
+    public void sliceShortArray() {
+        short[] array = {1, 2, 3, 4, 5, 6, 7};
+
+        short[][] result = ArrayUtil.sliceArray(array, 2);
+
+        assertEquals(4, result.length);
+        assertArrayEquals(new short[]{1, 2}, result[0]);
+        assertArrayEquals(new short[]{3, 4}, result[1]);
+        assertArrayEquals(new short[]{5, 6}, result[2]);
+        assertArrayEquals(new short[]{7},    result[3]);
+
+        short[][] result2 = ArrayUtil.sliceArray(array, 4);
+
+        assertEquals(2, result2.length);
+        assertArrayEquals(new short[]{1, 2, 3, 4}, result2[0]);
+        assertArrayEquals(new short[]{5, 6, 7}, result2[1]);
+
+        short[][] result3 = ArrayUtil.sliceArray(array, 7);
+
+        assertEquals(1, result3.length);
+        assertArrayEquals(array, result3[0]);
+
+        short[][] result4 = ArrayUtil.sliceArray(array, 10);
+
+        assertEquals(1, result4.length);
+        assertArrayEquals(array, result4[0]);
+
+        short[] array2 = {1, 2, 3, 4, 5, 6, 7, 8};
+        short[][] result5 = ArrayUtil.sliceArray(array2, 4);
+
+        assertEquals(2, result5.length);
+        assertArrayEquals(new short[]{1, 2, 3, 4}, result5[0]);
+        assertArrayEquals(new short[]{5, 6, 7, 8}, result5[1]);
+    }
+
+    @Test
+    public void sliceByteArray() {
+        byte[] array = {1, 2, 3, 4, 5, 6, 7};
+
+        byte[][] result = ArrayUtil.sliceArray(array, 2);
+
+        assertEquals(4, result.length);
+        assertArrayEquals(new byte[]{1, 2}, result[0]);
+        assertArrayEquals(new byte[]{3, 4}, result[1]);
+        assertArrayEquals(new byte[]{5, 6}, result[2]);
+        assertArrayEquals(new byte[]{7},    result[3]);
+
+        byte[][] result2 = ArrayUtil.sliceArray(array, 4);
+
+        assertEquals(2, result2.length);
+        assertArrayEquals(new byte[]{1, 2, 3, 4}, result2[0]);
+        assertArrayEquals(new byte[]{5, 6, 7}, result2[1]);
+
+        byte[][] result3 = ArrayUtil.sliceArray(array, 7);
+
+        assertEquals(1, result3.length);
+        assertArrayEquals(array, result3[0]);
+
+        byte[][] result4 = ArrayUtil.sliceArray(array, 10);
+
+        assertEquals(1, result4.length);
+        assertArrayEquals(array, result4[0]);
+
+        byte[] array2 = {1, 2, 3, 4, 5, 6, 7, 8};
+        byte[][] result5 = ArrayUtil.sliceArray(array2, 4);
+
+        assertEquals(2, result5.length);
+        assertArrayEquals(new byte[]{1, 2, 3, 4}, result5[0]);
+        assertArrayEquals(new byte[]{5, 6, 7, 8}, result5[1]);
+    }
+
+    @Test
+    public void sliceBooleanArray() {
+        boolean[] array = {true, true, false, true, false, false, true};
+
+        boolean[][] result = ArrayUtil.sliceArray(array, 2);
+
+        assertEquals(4, result.length);
+        assertArrayEquals(new boolean[]{true, true}, result[0]);
+        assertArrayEquals(new boolean[]{false, true}, result[1]);
+        assertArrayEquals(new boolean[]{false, false}, result[2]);
+        assertArrayEquals(new boolean[]{true},    result[3]);
+
+        boolean[][] result2 = ArrayUtil.sliceArray(array, 4);
+
+        assertEquals(2, result2.length);
+        assertArrayEquals(new boolean[]{true, true, false, true}, result2[0]);
+        assertArrayEquals(new boolean[]{false, false, true}, result2[1]);
+
+        boolean[][] result3 = ArrayUtil.sliceArray(array, 7);
+
+        assertEquals(1, result3.length);
+        assertArrayEquals(array, result3[0]);
+
+        boolean[][] result4 = ArrayUtil.sliceArray(array, 10);
+
+        assertEquals(1, result4.length);
+        assertArrayEquals(array, result4[0]);
+
+        boolean[] array2 = {true, true, false, true, false, false, true, false};
+        boolean[][] result5 = ArrayUtil.sliceArray(array2, 4);
+
+        assertEquals(2, result5.length);
+        assertArrayEquals(new boolean[]{true, true, false, true}, result5[0]);
+        assertArrayEquals(new boolean[]{false, false, true, false}, result5[1]);
+    }
+
+    @Test
+    public void sliceObjectArray() {
+        Object[] array = {1, 2, 3, 4, 5, 6, 7};
+
+        Object[][] result = ArrayUtil.sliceArray(array, 2);
+
+        assertEquals(4, result.length);
+        assertArrayEquals(new Object[]{1, 2}, result[0]);
+        assertArrayEquals(new Object[]{3, 4}, result[1]);
+        assertArrayEquals(new Object[]{5, 6}, result[2]);
+        assertArrayEquals(new Object[]{7},    result[3]);
+
+        Object[][] result2 = ArrayUtil.sliceArray(array, 4);
+
+        assertEquals(2, result2.length);
+        assertArrayEquals(new Object[]{1, 2, 3, 4}, result2[0]);
+        assertArrayEquals(new Object[]{5, 6, 7}, result2[1]);
+
+        Object[][] result3 = ArrayUtil.sliceArray(array, 7);
+
+        assertEquals(1, result3.length);
+        assertArrayEquals(array, result3[0]);
+
+        Object[][] result4 = ArrayUtil.sliceArray(array, 10);
+
+        assertEquals(1, result4.length);
+        assertArrayEquals(array, result4[0]);
+
+        Object[] array2 = {1, 2, 3, 4, 5, 6, 7, 8};
+        Object[][] result5 = ArrayUtil.sliceArray(array2, 4);
+
+        assertEquals(2, result5.length);
+        assertArrayEquals(new Object[]{1, 2, 3, 4}, result5[0]);
+        assertArrayEquals(new Object[]{5, 6, 7, 8}, result5[1]);
+    }
+}
\ No newline at end of file


[10/11] cayenne git commit: CAY-2466 New internal API to build SQL string - node internals optimization

Posted by nt...@apache.org.
CAY-2466 New internal API to build SQL string
  - node internals optimization


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

Branch: refs/heads/master
Commit: 1f10c920dccc7f0a1b6e6611dd27d612c6b435a4
Parents: f6b2dac
Author: Nikita Timofeev <st...@gmail.com>
Authored: Wed Jan 9 11:42:18 2019 +0300
Committer: Nikita Timofeev <st...@gmail.com>
Committed: Wed Jan 9 11:42:18 2019 +0300

----------------------------------------------------------------------
 .../cayenne/access/sqlbuilder/sqltree/Node.java |  57 ++++++----
 .../access/sqlbuilder/sqltree/NodeTest.java     | 110 +++++++++++++++++++
 2 files changed, 145 insertions(+), 22 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/1f10c920/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/Node.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/Node.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/Node.java
index d535028..550425f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/Node.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/Node.java
@@ -19,14 +19,10 @@
 
 package org.apache.cayenne.access.sqlbuilder.sqltree;
 
-import java.util.ArrayList;
-import java.util.List;
-
 import org.apache.cayenne.access.sqlbuilder.NodeTreeVisitor;
 import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
 import org.apache.cayenne.access.sqlbuilder.StringBuilderAppendable;
 
-
 /**
  * @since 4.2
  */
@@ -34,7 +30,9 @@ public abstract class Node {
 
     protected Node parent;
 
-    protected List<Node> children;
+    protected Node[] children;
+
+    protected int childrenCount;
 
     protected final NodeType type;
 
@@ -47,33 +45,46 @@ public abstract class Node {
     }
 
     public Node addChild(int index, Node node) {
-        children.add(index, node);
+        if(children.length <= childrenCount) {
+            // expand + copy with empty slot at index
+            Node[] newChildren = new Node[children.length + 4];
+            System.arraycopy(children, 0, newChildren, 0, index);
+            System.arraycopy(children, index, newChildren, index + 1, (childrenCount - index));
+            children = newChildren;
+        } else {
+            // move tail after index on one position
+            System.arraycopy(children, index, children, index + 1, (childrenCount - index));
+        }
+        children[index] = node;
+        childrenCount++;
         node.setParent(this);
         return this;
     }
 
     public Node addChild(Node node) {
         if(children == null) {
-            children = new ArrayList<>(4);
+            children = new Node[4];
+        } else if(children.length <= childrenCount) {
+            Node[] newChildren = new Node[children.length + 4];
+            System.arraycopy(children, 0, newChildren, 0, children.length);
+            children = newChildren;
         }
-        children.add(node);
+        children[childrenCount++] = node;
         node.setParent(this);
         return this;
     }
 
     public Node getChild(int idx) {
-        return children.get(idx);
+        return children[idx];
     }
 
     public int getChildrenCount() {
-        if(children == null) {
-            return 0;
-        }
-        return children.size();
+        return childrenCount;
     }
 
     public void replaceChild(int idx, Node node) {
-        children.set(idx, node).setParent(null);
+        children[idx].setParent(null);
+        children[idx] = node;
         node.setParent(this);
     }
 
@@ -89,13 +100,14 @@ public abstract class Node {
         if(!visitor.onNodeStart(this)) {
             return;
         }
-        int count = getChildrenCount();
-        for(int i=0; i<count; i++) {
-            if(!visitor.onChildNodeStart(this, getChild(i), i, i < (count - 1))) {
+
+        for(int i=0; i<childrenCount; i++) {
+            boolean hasMore = i < (childrenCount - 1);
+            if(!visitor.onChildNodeStart(this, children[i], i, hasMore)) {
                 return;
             }
-            getChild(i).visit(visitor);
-            visitor.onChildNodeEnd(this, getChild(i), i, i < (count - 1));
+            children[i].visit(visitor);
+            visitor.onChildNodeEnd(this, children[i], i, hasMore);
         }
         visitor.onNodeEnd(this);
     }
@@ -107,9 +119,10 @@ public abstract class Node {
     public <T extends Node> T deepCopy() {
         Node node = this.copy();
         if(children != null) {
-            node.children = new ArrayList<>(children.size());
-            for(Node child : children) {
-                node.children.add(child.deepCopy());
+            node.children = new Node[childrenCount];
+            node.childrenCount = childrenCount;
+            for(int i=0; i<childrenCount; i++) {
+                node.children[i] = children[i].deepCopy();
             }
         }
         return (T)node;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/1f10c920/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/sqltree/NodeTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/sqltree/NodeTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/sqltree/NodeTest.java
new file mode 100644
index 0000000..3a8e920
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/sqltree/NodeTest.java
@@ -0,0 +1,110 @@
+/*****************************************************************
+ *   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.sqlbuilder.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class NodeTest {
+
+    static private class TestNode extends Node {
+        @Override
+        public Node copy() {
+            return new TestNode();
+        }
+
+        @Override
+        public QuotingAppendable append(QuotingAppendable buffer) {
+            return buffer.append("test");
+        }
+    }
+
+    private Node node;
+
+    @Before
+    public void createNewNode() {
+        node = new TestNode();
+    }
+
+    @Test
+    public void testInsert() {
+        assertEquals(0, node.childrenCount);
+        assertNull(node.children);
+
+        node.addChild(new TestNode());
+
+        assertEquals(1, node.childrenCount);
+        assertNotNull(node.children);
+        assertEquals(4, node.children.length);
+
+        TestNode node1 = new TestNode();
+        this.node.addChild(0, node1);
+
+        assertSame(node1, node.children[0]);
+        assertEquals(2, this.node.childrenCount);
+        assertEquals(4, this.node.children.length);
+
+        TestNode node2 = new TestNode();
+        this.node.addChild(0, node2);
+        assertSame(node2, node.children[0]);
+
+        TestNode node3 = new TestNode();
+        this.node.addChild(0, node3);
+        assertSame(node3, node.children[0]);
+
+        TestNode node4 = new TestNode();
+        this.node.addChild(0, node4);
+        assertSame(node4, node.children[0]);
+
+        assertEquals(5, this.node.childrenCount);
+        assertEquals(8, this.node.children.length);
+    }
+
+    @Test
+    public void testAdd() {
+        assertEquals(0, node.childrenCount);
+        assertNull(node.children);
+
+        node.addChild(new TestNode());
+
+        assertEquals(1, node.childrenCount);
+        assertNotNull(node.children);
+        assertEquals(4, node.children.length);
+
+        node.addChild(new TestNode());
+        node.addChild(new TestNode());
+        node.addChild(new TestNode());
+
+        assertEquals(4, node.childrenCount);
+        assertEquals(4, node.children.length);
+
+        node.addChild(new TestNode());
+
+        assertEquals(5, node.childrenCount);
+        assertEquals(8, node.children.length);
+    }
+
+}
\ No newline at end of file


[03/11] cayenne git commit: CAY-2465 New SelectTranslator implementation

Posted by nt...@apache.org.
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


[07/11] cayenne git commit: CAY-2465 New SelectTranslator implementation

Posted by nt...@apache.org.
http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java
new file mode 100644
index 0000000..b330608
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java
@@ -0,0 +1,129 @@
+/*****************************************************************
+ *   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.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.Entity;
+
+/**
+ * @since 4.2
+ */
+abstract class PathProcessor<T extends Entity> implements PathTranslationResult {
+
+    public static final char OUTER_JOIN_INDICATOR = '+';
+    public static final char SPLIT_PATH_INDICATOR = '#';
+
+    protected final Map<String, String> pathSplitAliases;
+    protected final TranslatorContext context;
+    protected final List<String> attributePaths;
+    protected final List<DbAttribute> attributes;
+    protected final StringBuilder currentDbPath;
+
+    protected boolean lastComponent;
+    protected boolean isOuterJoin;
+    protected T entity;
+    protected DbRelationship relationship;
+    protected String currentAlias;
+
+    public PathProcessor(TranslatorContext context, T entity) {
+        this.context = Objects.requireNonNull(context);
+        this.entity = Objects.requireNonNull(entity);
+        this.pathSplitAliases = context.getMetadata().getPathSplitAliases();
+        this.currentDbPath = new StringBuilder();
+        this.attributes = new ArrayList<>(1);
+        this.attributePaths = new ArrayList<>(1);
+    }
+
+    public PathTranslationResult process(String path) {
+        PathComponents components = new PathComponents(path);
+        String[] rawComponents = components.getAll();
+        for(int i=0; i<rawComponents.length; i++) {
+            String next = rawComponents[i];
+            isOuterJoin = false;
+            lastComponent = i == rawComponents.length - 1;
+            String alias = pathSplitAliases.get(next);
+            if(alias != null) {
+                currentAlias = next;
+                processAliasedAttribute(next, alias);
+                currentAlias = null;
+            } else {
+                if(next.charAt(next.length() - 1) == OUTER_JOIN_INDICATOR) {
+                    isOuterJoin = true;
+                    next = next.substring(0, next.length() - 1);
+                }
+                processNormalAttribute(next);
+            }
+        }
+
+        return this;
+    }
+
+    protected void addAttribute(String path, DbAttribute attribute) {
+        attributePaths.add(path);
+        attributes.add(attribute);
+    }
+
+    abstract protected void processAliasedAttribute(String next, String alias);
+
+    abstract protected void processNormalAttribute(String next);
+
+    @Override
+    public List<DbAttribute> getDbAttributes() {
+        return attributes;
+    }
+
+    @Override
+    public List<String> getAttributePaths() {
+        return attributePaths;
+    }
+
+    @Override
+    public Optional<DbRelationship> getDbRelationship() {
+        if(relationship == null) {
+            return Optional.empty();
+        }
+        return Optional.of(relationship);
+    }
+
+    @Override
+    public String getFinalPath() {
+        return currentDbPath.toString();
+    }
+
+    protected void appendCurrentPath(String nextSegment) {
+        if(currentDbPath.length() > 0) {
+            currentDbPath.append('.');
+        }
+        currentDbPath.append(nextSegment);
+        if(currentAlias != null) {
+            currentDbPath.append(SPLIT_PATH_INDICATOR).append(currentAlias);
+        }
+        if(isOuterJoin) {
+            currentDbPath.append(OUTER_JOIN_INDICATOR);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
new file mode 100644
index 0000000..f4158de
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   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.List;
+import java.util.Optional;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbRelationship;
+
+/**
+ * Interface that describes result of path translation
+ *
+ * @since 4.2
+ */
+interface PathTranslationResult {
+
+    String getFinalPath();
+
+    Optional<DbRelationship> getDbRelationship();
+
+    List<DbAttribute> getDbAttributes();
+
+    List<String> getAttributePaths();
+
+    default DbAttribute getLastAttribute() {
+        return getDbAttributes().get(getDbAttributes().size() - 1);
+    }
+
+    default String getLastAttributePath() {
+        return getAttributePaths().get(getAttributePaths().size() - 1);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslator.java
new file mode 100644
index 0000000..c3dd57f
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslator.java
@@ -0,0 +1,60 @@
+/*****************************************************************
+ *   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.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * @since 4.2
+ */
+class PathTranslator {
+
+    private final Map<String, PathTranslationResult> objResultCache = new ConcurrentHashMap<>();
+    private final Map<String, PathTranslationResult> dbResultCache = new ConcurrentHashMap<>();
+
+    private final TranslatorContext context;
+
+    PathTranslator(TranslatorContext context) {
+        this.context = context;
+    }
+
+    PathTranslationResult translatePath(ObjEntity entity, String path, String parentPath) {
+        return objResultCache.computeIfAbsent(parentPath + '.' + entity.getName() + '.' + path,
+                (k) -> new ObjPathProcessor(context, entity, parentPath).process(path));
+    }
+
+    PathTranslationResult translatePath(ObjEntity entity, String path) {
+        return translatePath(entity, path, null);
+    }
+
+    PathTranslationResult translatePath(DbEntity entity, String path, String parentPath) {
+        return dbResultCache.computeIfAbsent(parentPath + '.' + entity.getName() + '.' + path,
+                (k) -> new DbPathProcessor(context, entity, parentPath).process(path));
+    }
+
+    PathTranslationResult translatePath(DbEntity entity, String path) {
+        return translatePath(entity, path, null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..90c0f3d
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.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.access.translator.select;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.JoinType;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.query.PrefetchSelectQuery;
+import org.apache.cayenne.query.PrefetchTreeNode;
+import org.apache.cayenne.query.Select;
+import org.apache.cayenne.reflect.ClassDescriptor;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.table;
+
+/**
+ * @since 4.2
+ */
+class PrefetchNodeStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        updatePrefetchNodes(context);
+        processJoint(context);
+        processPrefetchQuery(context);
+    }
+
+    private void updatePrefetchNodes(TranslatorContext context) {
+        if(context.getQuery().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()) {
+            prefetch.setEntityName(context.getMetadata().getObjEntity().getName());
+        }
+    }
+
+    private void processJoint(TranslatorContext context) {
+        PrefetchTreeNode prefetch = context.getQuery().getPrefetchTree();
+        if(prefetch == null) {
+            return;
+        }
+
+        ObjEntity objEntity = context.getMetadata().getObjEntity();
+
+        for(PrefetchTreeNode node : prefetch.adjacentJointNodes()) {
+            Expression prefetchExp = ExpressionFactory.exp(node.getPath());
+            ASTDbPath dbPrefetch = (ASTDbPath) objEntity.translateToDbPath(prefetchExp);
+            final String dbPath = dbPrefetch.getPath();
+            DbEntity dbEntity = objEntity.getDbEntity();
+
+            PathComponents components = new PathComponents(dbPath);
+            StringBuilder fullPath = new StringBuilder();
+            for(String c : components.getAll()) {
+                DbRelationship rel = dbEntity.getRelationship(c);
+                if(rel == null) {
+                    throw new CayenneRuntimeException("Unable to resolve path %s for entity %s", dbPath, objEntity.getName());
+                }
+                if(fullPath.length() > 0) {
+                    fullPath.append('.');
+                }
+                context.getTableTree().addJoinTable("p:" + fullPath.append(c).toString(), rel, JoinType.LEFT_OUTER);
+                dbEntity = rel.getTargetEntity();
+            }
+
+            ObjRelationship targetRel = (ObjRelationship) prefetchExp.evaluate(objEntity);
+            ClassDescriptor prefetchClassDescriptor = context.getResolver().getClassDescriptor(targetRel.getTargetEntityName());
+
+            DescriptorColumnExtractor columnExtractor = new DescriptorColumnExtractor(context, prefetchClassDescriptor);
+            columnExtractor.extract("p:" + dbPath);
+        }
+    }
+
+    private void processPrefetchQuery(TranslatorContext context) {
+        Select<?> select = context.getQuery().unwrap();
+        if(!(select instanceof PrefetchSelectQuery)) {
+            return;
+        }
+
+        PathTranslator pathTranslator = context.getPathTranslator();
+        PrefetchSelectQuery<?> prefetchSelectQuery = (PrefetchSelectQuery<?>) select;
+        for(String prefetchPath: prefetchSelectQuery.getResultPaths()) {
+            ASTDbPath pathExp = (ASTDbPath) context.getMetadata().getClassDescriptor().getEntity()
+                    .translateToDbPath(ExpressionFactory.exp(prefetchPath));
+
+            String path = pathExp.getPath();
+            PathTranslationResult result = pathTranslator
+                    .translatePath(context.getMetadata().getDbEntity(), path);
+            result.getDbRelationship().ifPresent(r -> {
+                DbEntity targetEntity = r.getTargetEntity();
+                context.getTableTree().addJoinTable(path, r, JoinType.INNER);
+                for (DbAttribute pk : targetEntity.getPrimaryKeys()) {
+                    // note that we may select a source attribute, but label it as target for simplified snapshot processing
+                    String finalPath = path + '.' + pk.getName();
+                    String alias = context.getTableTree().aliasForPath(path);
+                    Node columnNode = table(alias).column(pk).build();
+                    context.addResultNode(columnNode, finalPath).setDbAttribute(pk);
+                }
+            });
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java
new file mode 100644
index 0000000..c4d2ba1
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java
@@ -0,0 +1,74 @@
+/*****************************************************************
+ *   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.Node;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.exp.parser.ASTObjPath;
+import org.apache.cayenne.exp.parser.SimpleNode;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.reflect.ClassDescriptor;
+
+/**
+ * @since 4.2
+ */
+class QualifierTranslationStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        QualifierTranslator translator = context.getQualifierTranslator();
+
+        Expression expression = context.getQuery().getQualifier();
+
+        // Attaching Obj entity's qualifier
+        ObjEntity entity = context.getMetadata().getObjEntity();
+        if (entity != null) {
+            ClassDescriptor descriptor = context.getMetadata().getClassDescriptor();
+            Expression entityQualifier = descriptor.getEntityInheritanceTree().qualifierForEntityAndSubclasses();
+            if (entityQualifier != null) {
+                expression = expression == null ? entityQualifier : expression.andExp(entityQualifier);
+            }
+        }
+
+        // Attaching root Db entity's qualifier
+        DbEntity dbEntity = context.getMetadata().getDbEntity();
+        if (dbEntity != null) {
+            Expression dbQualifier = dbEntity.getQualifier();
+            if (dbQualifier != null) {
+                dbQualifier = dbQualifier.transform(node -> {
+                    if (node instanceof ASTObjPath) {
+                        return new ASTDbPath(((SimpleNode) node).getOperand(0));
+                    }
+                    return node;
+                });
+
+                expression = expression == null ? dbQualifier : expression.andExp(dbQualifier);
+            }
+        }
+
+        Node qualifierNode = translator.translate(expression);
+
+        if(qualifierNode != null) {
+            context.getSelectBuilder().where(qualifierNode);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
index 3795976..d48a53f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
@@ -19,672 +19,394 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import java.io.IOException;
-import java.util.Arrays;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
-import java.util.function.Function;
+import java.util.Set;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
-import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.access.sqlbuilder.ExpressionNodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.ValueNodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.sqltree.BetweenNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.BitwiseNotNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.EmptyNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.EqualNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.InNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NotEqualNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NotNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TextNode;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.TraversalHandler;
 import org.apache.cayenne.exp.parser.ASTDbPath;
-import org.apache.cayenne.exp.parser.ASTExtract;
+import org.apache.cayenne.exp.parser.ASTFullObject;
 import org.apache.cayenne.exp.parser.ASTFunctionCall;
 import org.apache.cayenne.exp.parser.ASTObjPath;
+import org.apache.cayenne.exp.parser.ASTSubquery;
 import org.apache.cayenne.exp.parser.PatternMatchNode;
 import org.apache.cayenne.exp.parser.SimpleNode;
+import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.JoinType;
-import org.apache.cayenne.map.ObjEntity;
-import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.SelectQuery;
-import org.apache.cayenne.reflect.ClassDescriptor;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
+import static org.apache.cayenne.exp.Expression.*;
 
 /**
- * Translates query qualifier to SQL. Used as a helper class by query
- * translators.
+ * @since 4.2
  */
-public class QualifierTranslator extends QueryAssemblerHelper implements TraversalHandler {
-
-	protected DataObjectMatchTranslator objectMatchTranslator;
-	protected boolean matchingObject;
-	protected boolean caseInsensitive;
-
-	/**
-	 * @since 4.0
-	 */
-	protected boolean useAliasForExpressions;
-
-	/**
-	 * @since 4.0
-	 */
-	protected Expression waitingForEndNode;
-
-	/**
-	 * @since 4.0
-	 */
-	protected Expression qualifier;
-
-	public QualifierTranslator(QueryAssembler queryAssembler) {
-		super(queryAssembler);
-
-		caseInsensitive = false;
-	}
-
-	/**
-	 * Translates query qualifier to SQL WHERE clause. Qualifier is obtained
-	 * from the parent queryAssembler.
-	 * 
-	 * @since 3.0
-	 */
-	@Override
-	protected void doAppendPart() {
-		doAppendPart(extractQualifier());
-	}
-
-	public void setCaseInsensitive(boolean caseInsensitive) {
-		this.caseInsensitive = caseInsensitive;
-	}
-
-	/**
-	 * Explicitly set qualifier.
-	 * It will be used instead of extracting qualifier from the query itself.
-	 * @since 4.0
-	 */
-	public void setQualifier(Expression qualifier) {
-		this.qualifier = qualifier;
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	public void setUseAliasForExpressions(boolean useAliasForExpressions) {
-		this.useAliasForExpressions = useAliasForExpressions;
-	}
-
-	/**
-	 * Translates query qualifier to SQL WHERE clause. Qualifier is a method
-	 * parameter.
-	 * 
-	 * @since 3.0
-	 */
-	protected void doAppendPart(Expression rootNode) {
-		if (rootNode == null) {
-			return;
-		}
-		rootNode.traverse(this);
-	}
-
-	protected Expression extractQualifier() {
-		// if additional qualifier is set, use it
-		if(this.qualifier != null) {
-			return this.qualifier;
-		}
-
-		Query q = queryAssembler.getQuery();
-
-		Expression qualifier = ((SelectQuery<?>) q).getQualifier();
-
-		// append Entity qualifiers, taking inheritance into account
-		ObjEntity entity = getObjEntity();
-
-		if (entity != null) {
-
-			ClassDescriptor descriptor = queryAssembler.getEntityResolver().getClassDescriptor(entity.getName());
-			Expression entityQualifier = descriptor.getEntityInheritanceTree().qualifierForEntityAndSubclasses();
-			if (entityQualifier != null) {
-				qualifier = (qualifier != null) ? qualifier.andExp(entityQualifier) : entityQualifier;
-			}
-		}
-
-		// Attaching root Db entity's qualifier
-		if (getDbEntity() != null) {
-			Expression dbQualifier = getDbEntity().getQualifier();
-			if (dbQualifier != null) {
-				dbQualifier = dbQualifier.transform(new DbEntityQualifierTransformer());
-
-				qualifier = qualifier == null ? dbQualifier : qualifier.andExp(dbQualifier);
-			}
-		}
-
-		return qualifier;
-	}
-
-	/**
-	 * Called before processing an expression to initialize
-	 * objectMatchTranslator if needed.
-	 */
-	protected void detectObjectMatch(Expression exp) {
-		// On demand initialization of
-		// objectMatchTranslator is not possible since there may be null
-		// object values that would not allow to detect the need for
-		// such translator in the right time (e.g.: null = dbpath)
-
-		matchingObject = false;
-
-		if (exp.getOperandCount() != 2) {
-			// only binary expressions are supported
-			return;
-		}
-
-		// check if there are DataObjects among direct children of the
-		// Expression
-		for (int i = 0; i < 2; i++) {
-			Object op = exp.getOperand(i);
-			if (op instanceof Persistent || op instanceof ObjectId) {
-				matchingObject = true;
-
-				if (objectMatchTranslator == null) {
-					objectMatchTranslator = new DataObjectMatchTranslator();
-				} else {
-					objectMatchTranslator.reset();
-				}
-				break;
-			}
-		}
-	}
-
-	protected void appendObjectMatch() throws IOException {
-		if (!matchingObject || objectMatchTranslator == null) {
-			throw new IllegalStateException("An invalid attempt to append object match.");
-		}
-
-		// turn off special handling, so that all the methods behave as a
-		// superclass's
-		// impl.
-		matchingObject = false;
-
-		boolean first = true;
-
-		DbRelationship relationship = objectMatchTranslator.getRelationship();
-		if (!relationship.isToMany() && !relationship.isToPK()) {
-			queryAssembler.dbRelationshipAdded(relationship, JoinType.INNER, objectMatchTranslator.getJoinSplitAlias());
-		}
-
-		Iterator<String> it = objectMatchTranslator.keys();
-		while (it.hasNext()) {
-			if (first) {
-				first = false;
-			} else {
-				out.append(" AND ");
-			}
-
-			String key = it.next();
-			DbAttribute attr = objectMatchTranslator.getAttribute(key);
-			Object val = objectMatchTranslator.getValue(key);
-
-			processColumn(attr);
-			out.append(objectMatchTranslator.getOperation());
-			appendLiteral(val, attr, objectMatchTranslator.getExpression());
-		}
-
-		objectMatchTranslator.reset();
-	}
-
-	@Override
-	public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
-
-		if(waitingForEndNode != null) {
-			return;
-		}
-
-		if (!hasMoreChildren) {
-			return;
-		}
-
-		Appendable out = (matchingObject) ? new StringBuilder() : this.out;
-
-		try {
-			switch (node.getType()) {
-			case Expression.AND:
-				out.append(" AND ");
-				break;
-			case Expression.OR:
-				out.append(" OR ");
-				break;
-			case Expression.EQUAL_TO:
-				// translate NULL as IS NULL
-				if (childIndex == 0 && node.getOperandCount() == 2 && node.getOperand(1) == null) {
-					out.append(" IS ");
-				} else {
-					out.append(" = ");
-				}
-				break;
-			case Expression.NOT_EQUAL_TO:
-				// translate NULL as IS NOT NULL
-				if (childIndex == 0 && node.getOperandCount() == 2 && node.getOperand(1) == null) {
-					out.append(" IS NOT ");
-				} else {
-					out.append(" <> ");
-				}
-				break;
-			case Expression.LESS_THAN:
-				out.append(" < ");
-				break;
-			case Expression.GREATER_THAN:
-				out.append(" > ");
-				break;
-			case Expression.LESS_THAN_EQUAL_TO:
-				out.append(" <= ");
-				break;
-			case Expression.GREATER_THAN_EQUAL_TO:
-				out.append(" >= ");
-				break;
-			case Expression.IN:
-				out.append(" IN ");
-				break;
-			case Expression.NOT_IN:
-				out.append(" NOT IN ");
-				break;
-			case Expression.LIKE:
-				out.append(" LIKE ");
-				break;
-			case Expression.NOT_LIKE:
-				out.append(" NOT LIKE ");
-				break;
-			case Expression.LIKE_IGNORE_CASE:
-				if (caseInsensitive) {
-					out.append(" LIKE ");
-				} else {
-					out.append(") LIKE UPPER(");
-				}
-				break;
-			case Expression.NOT_LIKE_IGNORE_CASE:
-				if (caseInsensitive) {
-					out.append(" NOT LIKE ");
-				} else {
-					out.append(") NOT LIKE UPPER(");
-				}
-				break;
-			case Expression.ADD:
-				out.append(" + ");
-				break;
-			case Expression.SUBTRACT:
-				out.append(" - ");
-				break;
-			case Expression.MULTIPLY:
-				out.append(" * ");
-				break;
-			case Expression.DIVIDE:
-				out.append(" / ");
-				break;
-			case Expression.BETWEEN:
-				if (childIndex == 0) {
-					out.append(" BETWEEN ");
-				} else if (childIndex == 1) {
-					out.append(" AND ");
-				}
-				break;
-			case Expression.NOT_BETWEEN:
-				if (childIndex == 0) {
-					out.append(" NOT BETWEEN ");
-				} else if (childIndex == 1) {
-					out.append(" AND ");
-				}
-				break;
-			case Expression.BITWISE_OR:
-				out.append(" ").append(operandForBitwiseOr()).append(" ");
-				break;
-			case Expression.BITWISE_AND:
-				out.append(" ").append(operandForBitwiseAnd()).append(" ");
-				break;
-			case Expression.BITWISE_XOR:
-				out.append(" ").append(operandForBitwiseXor()).append(" ");
-				break;
-			case Expression.BITWISE_LEFT_SHIFT:
-				out.append(" ").append(operandForBitwiseLeftShift()).append(" ");
-				break;
-			case Expression.BITWISE_RIGHT_SHIFT:
-				out.append(" ").append(operandForBitwiseRightShift()).append("");
-				break;
-			}
-		} catch (IOException ioex) {
-			throw new CayenneRuntimeException("Error appending content", ioex);
-		}
-
-		if (matchingObject) {
-			objectMatchTranslator.setOperation(out.toString());
-			objectMatchTranslator.setExpression(node);
-		}
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	protected String operandForBitwiseNot() {
-		return "~";
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	protected String operandForBitwiseOr() {
-		return "|";
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	protected String operandForBitwiseAnd() {
-		return "&";
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	protected String operandForBitwiseXor() {
-		return "^";
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	protected String operandForBitwiseLeftShift() {
-		return "<<";
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	protected String operandForBitwiseRightShift() {
-		return ">>";
-	}
-
-	@Override
-	public void startNode(Expression node, Expression parentNode) {
-
-		if(waitingForEndNode != null) {
-			return;
-		}
-
-		if(useAliasForExpressions) {
-			String alias = queryAssembler.getAliasForExpression(node);
-			if(alias != null) {
-				out.append(alias);
-				waitingForEndNode = node;
-				return;
-			}
-		}
-
-		boolean parenthesisNeeded = parenthesisNeeded(node, parentNode);
-
-		if(node.getType() == Expression.FUNCTION_CALL) {
-			if(node instanceof ASTExtract) {
-				appendExtractFunction((ASTExtract) node);
-			} else {
-				appendFunction((ASTFunctionCall) node);
-			}
-			if(parenthesisNeeded) {
-				out.append("(");
-			}
-			return;
-		}
-
-		if(node.getType() == Expression.FULL_OBJECT && parentNode != null) {
-			throw new CayenneRuntimeException("Expression is not supported in where clause.");
-		}
-
-		int count = node.getOperandCount();
-
-		if (count == 2) {
-			// binary nodes are the only ones that currently require this
-			detectObjectMatch(node);
-		}
-
-		if (parenthesisNeeded) {
-			out.append('(');
-		}
-
-		if (count == 0) {
-			// not all databases handle true/false
-			if (node.getType() == Expression.TRUE) {
-				out.append("1 = 1");
-			} else if (node.getType() == Expression.FALSE) {
-				out.append("1 = 0");
-			} else if (node.getType() == Expression.ASTERISK) {
-				out.append("*");
-			}
-		}
-
-		if (count == 1) {
-			if (node.getType() == Expression.NEGATIVE) {
-				out.append('-');
-			} else if (node.getType() == Expression.NOT) {
-				out.append("NOT ");
-			} else if (node.getType() == Expression.BITWISE_NOT) {
-				out.append(operandForBitwiseNot());
-			}
-		} else if ((node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE)
-				&& !caseInsensitive) {
-			out.append("UPPER(");
-		}
-
-	}
-
-	/**
-	 * @since 1.1
-	 */
-	@Override
-	public void endNode(Expression node, Expression parentNode) {
-
-		if(waitingForEndNode != null) {
-			if(node == waitingForEndNode) {
-				waitingForEndNode = null;
-			}
-			return;
-		}
-
-		try {
-			// check if we need to use objectMatchTranslator to finish building the expression
-			if (node.getOperandCount() == 2 && matchingObject) {
-				appendObjectMatch();
-			}
-
-			boolean parenthesisNeeded = parenthesisNeeded(node, parentNode);
-			boolean likeIgnoreCase = (node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE);
-			boolean isPatternMatchNode = PatternMatchNode.class.isAssignableFrom(node.getClass());
-
-			// closing UPPER parenthesis
-			if (likeIgnoreCase && !caseInsensitive) {
-				out.append(')');
-			}
-
-			if (isPatternMatchNode) {
-				appendLikeEscapeCharacter((PatternMatchNode) node);
-			}
-
-			// clean up trailing comma in function argument list
-			if(node.getType() == Expression.FUNCTION_CALL) {
-				clearLastFunctionArgDivider((ASTFunctionCall)node);
-			}
-
-			// closing LIKE parenthesis
-			if (parenthesisNeeded) {
-				out.append(')');
-			}
-
-			// if inside function call, put comma between arguments
-			if(parentNode != null && parentNode.getType() == Expression.FUNCTION_CALL) {
-				appendFunctionArgDivider((ASTFunctionCall) parentNode);
-			}
-		} catch (IOException ioex) {
-			throw new CayenneRuntimeException("Error appending content", ioex);
-		}
-	}
-
-	@Override
-	public void objectNode(Object leaf, Expression parentNode) {
-		if(waitingForEndNode != null) {
-			return;
-		}
-
-		try {
-			switch (parentNode.getType()) {
-				case Expression.OBJ_PATH:
-					appendObjPath(parentNode);
-					break;
-				case Expression.DB_PATH:
-					appendDbPath(parentNode);
-					break;
-				case Expression.LIST:
-					appendList(parentNode, paramsDbType(parentNode));
-					break;
-				case Expression.FUNCTION_CALL:
-					appendFunctionArg(leaf, (ASTFunctionCall)parentNode);
-					break;
-				default:
-					appendLiteral(leaf, paramsDbType(parentNode), parentNode);
-			}
-		} catch (IOException ioex) {
-			throw new CayenneRuntimeException("Error appending content", ioex);
-		}
-	}
-
-	protected boolean parenthesisNeeded(Expression node, Expression parentNode) {
-		if (node.getType() == Expression.FUNCTION_CALL) {
-			return ((ASTFunctionCall)node).needParenthesis();
-		}
-
-		if (parentNode == null) {
-			return false;
-		}
-
-		// only unary expressions can go w/o parenthesis
-		if (node.getOperandCount() > 1) {
-			return true;
-		}
-
-		if (node.getType() == Expression.OBJ_PATH
-				|| node.getType() == Expression.DB_PATH
-				|| node.getType() == Expression.ASTERISK) {
-			return false;
-		}
-
-		return true;
-	}
-
-	private final void appendList(Expression listExpr, DbAttribute paramDesc) throws IOException {
-		Iterator<?> it;
-		Object list = listExpr.getOperand(0);
-		if (list instanceof List) {
-			it = ((List<?>) list).iterator();
-		} else if (list instanceof Object[]) {
-			it = Arrays.asList((Object[]) list).iterator();
-		} else {
-			String className = (list != null) ? list.getClass().getName() : "<null>";
-			throw new IllegalArgumentException("Unsupported type for the list expressions: " + className);
-		}
-
-		// process first element outside the loop
-		// (unroll loop to avoid condition checking
-		if (it.hasNext()) {
-			appendLiteral(it.next(), paramDesc, listExpr);
-		} else {
-			return;
-		}
-
-		while (it.hasNext()) {
-			out.append(", ");
-			appendLiteral(it.next(), paramDesc, listExpr);
-		}
-	}
-
-	@Override
-	protected void appendLiteral(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
-
-		if (!matchingObject) {
-			super.appendLiteral(val, attr, parentExpression);
-		} else if (val == null || (val instanceof Persistent)) {
-			objectMatchTranslator.setDataObject((Persistent) val);
-		} else if (val instanceof ObjectId) {
-			objectMatchTranslator.setObjectId((ObjectId) val);
-		} else {
-			throw new IllegalArgumentException("Attempt to use literal other than DataObject during object match.");
-		}
-	}
-
-	@Override
-	protected void processRelTermination(DbRelationship rel, JoinType joinType, String joinSplitAlias) {
-
-		if (!matchingObject) {
-			super.processRelTermination(rel, joinType, joinSplitAlias);
-		} else {
-			if (rel.isToMany()) {
-				// append joins
-				queryAssembler.dbRelationshipAdded(rel, joinType, joinSplitAlias);
-			}
-			objectMatchTranslator.setRelationship(rel, joinSplitAlias);
-		}
-	}
-
-	/**
-	 * Append function name to result SQL
-	 * Override this method to rename or skip function if generic name isn't supported on target DB.
-	 * @since 4.0
-	 */
-	protected void appendFunction(ASTFunctionCall functionExpression) {
-		out.append(functionExpression.getFunctionName());
-	}
-
-	/**
-	 * Special case for extract date/time parts functions as they have many variants
-	 * @since 4.0
-	 */
-	protected void appendExtractFunction(ASTExtract functionExpression) {
-		appendFunction(functionExpression);
-	}
-
-	/**
-	 * Append scalar argument of a function call
-	 * Used only for values stored in ASTScalar other
-	 * expressions appended in objectNode() method
-	 *
-	 * @since 4.0
-	 */
-	protected void appendFunctionArg(Object value, ASTFunctionCall functionExpression) throws IOException {
-		// Create fake DbAttribute to pass argument info down to bind it to SQL prepared statement
-		DbAttribute dbAttrForArg = new DbAttribute();
-		dbAttrForArg.setType(TypesMapping.getSqlTypeByJava(value.getClass()));
-		super.appendLiteral(value, dbAttrForArg, functionExpression);
-		appendFunctionArgDivider(functionExpression);
-	}
-
-	/**
-	 * Append divider between function arguments.
-	 * In overriding methods can be replaced e.g. for " || " for CONCAT operation
-	 * @since 4.0
-	 */
-	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
-		out.append(", ");
-	}
-
-	/**
-	 * Clear last divider as we currently don't now position of argument until parent element is ended.
-	 * @since 4.0
-	 */
-	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
-		if(functionExpression.getOperandCount() > 0) {
-			out.delete(out.length() - 2, out.length());
-		}
-	}
-
-	/**
-	 * Class to translate DB Entity qualifiers annotation to Obj-entity
-	 * qualifiers annotation This is done by changing all Obj-paths to Db-paths
-	 * and rejecting all original Db-paths
-	 */
-	class DbEntityQualifierTransformer implements Function<Object, Object> {
-
-		public Object apply(Object input) {
-			if (input instanceof ASTObjPath) {
-				return new ASTDbPath(((SimpleNode) input).getOperand(0));
-			}
-			return input;
-		}
-	}
+class QualifierTranslator implements TraversalHandler {
+
+    private final TranslatorContext context;
+    private final PathTranslator pathTranslator;
+    private final Set<Object> expressionsToSkip;
+    private final Deque<Node> nodeStack;
+
+    private Node currentNode;
+
+    QualifierTranslator(TranslatorContext context) {
+        this.context = context;
+        this.pathTranslator = context.getPathTranslator();
+        this.expressionsToSkip = new HashSet<>();
+        this.nodeStack = new ArrayDeque<>();
+    }
+
+    Node translate(BaseProperty<?> property) {
+        if(property == null) {
+            return null;
+        }
+
+        Node result = translate(property.getExpression());
+        if(property.getAlias() != null) {
+            return aliased(result, property.getAlias()).build();
+        }
+        return result;
+    }
+
+    Node translate(Expression qualifier) {
+        if(qualifier == null) {
+            return null;
+        }
+
+        Node rootNode = new EmptyNode();
+        expressionsToSkip.clear();
+        boolean hasCurrentNode = currentNode != null;
+        if(hasCurrentNode) {
+            nodeStack.push(currentNode);
+        }
+
+        currentNode = rootNode;
+        qualifier.traverse(this);
+
+        if(hasCurrentNode) {
+            currentNode = nodeStack.pop();
+        } else {
+            currentNode = null;
+        }
+
+        if(rootNode.getChildrenCount() == 1) {
+            // trim empty node
+            Node child = rootNode.getChild(0);
+            child.setParent(null);
+            return child;
+        }
+        return rootNode;
+    }
+
+    @Override
+    public void startNode(Expression node, Expression parentNode) {
+        if(expressionsToSkip.contains(node) || expressionsToSkip.contains(parentNode)) {
+            return;
+        }
+        Node nextNode = expressionNodeToSqlNode(node, parentNode);
+        if(nextNode == null) {
+            return;
+        }
+        currentNode.addChild(nextNode);
+        nextNode.setParent(currentNode);
+        currentNode = nextNode;
+    }
+
+    private Node expressionNodeToSqlNode(Expression node, Expression parentNode) {
+        switch (node.getType()) {
+            case NOT_IN:
+                return new InNode(true);
+            case IN:
+                return new InNode(false);
+            case NOT_BETWEEN:
+            case BETWEEN:
+                return new BetweenNode(node.getType() == NOT_BETWEEN);
+            case NOT:
+                return new NotNode();
+            case BITWISE_NOT:
+                return new BitwiseNotNode();
+            case EQUAL_TO:
+                return new EqualNode();
+            case NOT_EQUAL_TO:
+                return new NotEqualNode();
+
+            case LIKE:
+            case NOT_LIKE:
+            case LIKE_IGNORE_CASE:
+            case NOT_LIKE_IGNORE_CASE:
+                PatternMatchNode patternMatchNode = (PatternMatchNode)node;
+                boolean not = node.getType() == NOT_LIKE || node.getType() == NOT_LIKE_IGNORE_CASE;
+                return new LikeNode(patternMatchNode.isIgnoringCase(), not, patternMatchNode.getEscapeChar());
+
+            case OBJ_PATH:
+                String path = (String)node.getOperand(0);
+                PathTranslationResult result = pathTranslator.translatePath(context.getMetadata().getObjEntity(), path);
+                return processPathTranslationResult(node, parentNode, result);
+
+            case DB_PATH:
+                String dbPath = (String)node.getOperand(0);
+                PathTranslationResult dbResult = pathTranslator.translatePath(context.getMetadata().getDbEntity(), dbPath);
+                return processPathTranslationResult(node, parentNode, dbResult);
+
+            case FUNCTION_CALL:
+                ASTFunctionCall functionCall = (ASTFunctionCall)node;
+                return function(functionCall.getFunctionName()).build();
+
+            case ADD:
+            case SUBTRACT:
+            case MULTIPLY:
+            case DIVIDE:
+            case NEGATIVE:
+            case BITWISE_AND:
+            case BITWISE_LEFT_SHIFT:
+            case BITWISE_OR:
+            case BITWISE_RIGHT_SHIFT:
+            case BITWISE_XOR:
+            case OR:
+            case AND:
+            case LESS_THAN:
+            case LESS_THAN_EQUAL_TO:
+            case GREATER_THAN:
+            case GREATER_THAN_EQUAL_TO:
+                return new OpExpressionNode(expToStr(node.getType()));
+
+            case TRUE:
+            case FALSE:
+            case ASTERISK:
+                return new TextNode(' ' + expToStr(node.getType()));
+
+            case EXISTS:
+                return new FunctionNode("EXISTS", null, false);
+
+            case SUBQUERY:
+                ASTSubquery subquery = (ASTSubquery)node;
+                DefaultSelectTranslator translator = new DefaultSelectTranslator(subquery.getQuery(), context);
+                translator.translate();
+                return translator.getContext().getSelectBuilder().build();
+
+            case ENCLOSING_OBJECT:
+                // Translate via parent context's translator
+                Expression expression = (Expression) node.getOperand(0);
+                if(context.getParentContext() == null) {
+                    throw new CayenneRuntimeException("Unable to translate qualifier, no parent context to use for expression " + node);
+                }
+                expressionsToSkip.add(expression);
+                return context.getParentContext().getQualifierTranslator().translate(expression);
+
+            case FULL_OBJECT:
+                ASTFullObject fullObject = (ASTFullObject)node;
+                if(fullObject.getOperandCount() == 0) {
+                    Collection<DbAttribute> dbAttributes = context.getMetadata().getDbEntity().getPrimaryKeys();
+                    if(dbAttributes.size() > 1) {
+                        throw new CayenneRuntimeException("Unable to translate reference on entity with more than one PK.");
+                    }
+                    DbAttribute attribute = dbAttributes.iterator().next();
+                    String alias = context.getTableTree().aliasForAttributePath(attribute.getName());
+                    return table(alias).column(attribute).build();
+                } else {
+                    return null;
+                }
+        }
+
+        return null;
+    }
+
+    private Node processPathTranslationResult(Expression node, Expression parentNode, PathTranslationResult result) {
+        if(result.getDbRelationship().isPresent()
+                && result.getDbAttributes().size() > 1
+                && result.getDbRelationship().get().getTargetEntity().getPrimaryKeys().size() > 1) {
+            return createMultiAttributeMatch(node, parentNode, result);
+        } else if(result.getDbAttributes().isEmpty()) {
+            return new EmptyNode();
+        } else {
+            String alias = context.getTableTree().aliasForPath(result.getLastAttributePath());
+            return table(alias).column(result.getLastAttribute()).build();
+        }
+    }
+
+    private Node createMultiAttributeMatch(Expression node, Expression parentNode, PathTranslationResult result) {
+        ObjectId objectId = null;
+        expressionsToSkip.add(node);
+        expressionsToSkip.add(parentNode);
+
+        int siblings = parentNode.getOperandCount();
+        for(int i=0; i<siblings; i++) {
+            Object operand = parentNode.getOperand(i);
+            if(node == operand) {
+                continue;
+            }
+
+            if(operand instanceof Persistent) {
+                objectId = ((Persistent) operand).getObjectId();
+                break;
+            } else if(operand instanceof ObjectId) {
+                objectId = (ObjectId)operand;
+                break;
+            } else if(operand instanceof ASTObjPath) {
+                // TODO: support comparision of multi attribute ObjPath with other multi attribute ObjPath
+                throw new UnsupportedOperationException("Comparision of multiple attributes not supported for ObjPath");
+            }
+        }
+
+        if(objectId == null) {
+            throw new CayenneRuntimeException("Multi attribute ObjPath isn't matched with valid value. " +
+                    "List or Persistent object required.");
+        }
+
+        ExpressionNodeBuilder expressionNodeBuilder = null;
+        ExpressionNodeBuilder eq;
+
+        Iterator<DbAttribute> pkIt = result.getDbRelationship().get().getTargetEntity().getPrimaryKeys().iterator();
+        int count = result.getDbAttributes().size();
+
+        for(int i=0; i<count; i++) {
+            String path = result.getAttributePaths().get(i);
+            DbAttribute attribute = result.getDbAttributes().get(i);
+            DbAttribute pk = pkIt.next();
+            String alias = context.getTableTree().aliasForPath(path);
+            Object nextValue = objectId.getIdSnapshot().get(pk.getName());
+            eq = table(alias).column(attribute).eq(value(nextValue));
+            if (expressionNodeBuilder == null) {
+                expressionNodeBuilder = eq;
+            } else {
+                expressionNodeBuilder = expressionNodeBuilder.and(eq);
+            }
+        }
+
+        return expressionNodeBuilder.build();
+    }
+
+    @Override
+    public void endNode(Expression node, Expression parentNode) {
+        if(expressionsToSkip.contains(node) || expressionsToSkip.contains(parentNode)) {
+            return;
+        }
+        if(currentNode.getParent() != null) {
+            currentNode = currentNode.getParent();
+        }
+    }
+
+    @Override
+    public void objectNode(Object leaf, Expression parentNode) {
+        if(expressionsToSkip.contains(parentNode)) {
+            return;
+        }
+        if(parentNode.getType() == Expression.OBJ_PATH || parentNode.getType() == Expression.DB_PATH) {
+            return;
+        }
+
+        ValueNodeBuilder valueNodeBuilder = value(leaf).attribute(findDbAttribute(parentNode));
+        if(parentNode.getType() == Expression.LIST) {
+            valueNodeBuilder.array(true);
+        }
+        Node nextNode = valueNodeBuilder.build();
+
+        currentNode.addChild(nextNode);
+        nextNode.setParent(currentNode);
+    }
+
+    protected DbAttribute findDbAttribute(Expression node) {
+        int len = node.getOperandCount();
+        if (len != 2) {
+            if (node instanceof SimpleNode) {
+                Expression parent = (Expression) ((SimpleNode) node).jjtGetParent();
+                if (parent != null) {
+                    node = parent;
+                } else {
+                    return null;
+                }
+            }
+        }
+
+        PathTranslationResult result = null;
+        for(int i=0; i<node.getOperandCount(); i++) {
+            Object op = node.getOperand(i);
+            if(op instanceof ASTObjPath) {
+                result = pathTranslator.translatePath(context.getMetadata().getObjEntity(), ((ASTObjPath) op).getPath());
+                break;
+            } else if(op instanceof ASTDbPath) {
+                result = pathTranslator.translatePath(context.getMetadata().getDbEntity(), ((ASTDbPath) op).getPath());
+                break;
+            }
+        }
+
+        if(result == null) {
+            return null;
+        }
+
+        return result.getLastAttribute();
+    }
+
+    @Override
+    public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
+    }
+
+    private String expToStr(int type) {
+        switch (type) {
+            case AND:
+                return "AND";
+            case OR:
+                return "OR";
+            case LESS_THAN:
+                return "<";
+            case LESS_THAN_EQUAL_TO:
+                return "<=";
+            case GREATER_THAN:
+                return ">";
+            case GREATER_THAN_EQUAL_TO:
+                return ">=";
+            case ADD:
+                return "+";
+            case SUBTRACT:
+                return "-";
+            case MULTIPLY:
+                return "*";
+            case DIVIDE:
+                return "/";
+            case NEGATIVE:
+                return "-";
+            case BITWISE_AND:
+                return "&";
+            case BITWISE_OR:
+                return "|";
+            case BITWISE_XOR:
+                return "^";
+            case BITWISE_NOT:
+                return "!";
+            case BITWISE_LEFT_SHIFT:
+                return "<<";
+            case BITWISE_RIGHT_SHIFT:
+                return ">>";
+            case TRUE:
+                return "1=1";
+            case FALSE:
+                return "1=0";
+            case ASTERISK:
+                return "*";
+            default:
+                return "{other}";
+        }
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
deleted file mode 100644
index 3293168..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssembler.java
+++ /dev/null
@@ -1,197 +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
- * <p/>
- * http://www.apache.org/licenses/LICENSE-2.0
- * <p/>
- * 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.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.cayenne.access.translator.DbAttributeBinding;
-import org.apache.cayenne.access.types.ExtendedType;
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.map.JoinType;
-import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.QueryMetadata;
-
-/**
- * Abstract superclass of Query translators.
- */
-public abstract class QueryAssembler {
-
-	protected Query query;
-	protected QueryMetadata queryMetadata;
-	protected boolean translated;
-	protected String sql;
-	protected DbAdapter adapter;
-	protected EntityResolver entityResolver;
-	protected List<DbAttributeBinding> bindings;
-	/**
-	 * @since 4.0
-	 */
-	protected AddBindingListener addBindingListener;
-
-	/**
-	 * @since 4.0
-	 */
-	public QueryAssembler(Query query, DbAdapter adapter, EntityResolver entityResolver) {
-		this.entityResolver = entityResolver;
-		this.adapter = adapter;
-		this.query = query;
-		this.queryMetadata = query.getMetaData(entityResolver);
-		this.bindings = new ArrayList<>();
-	}
-
-	/**
-	 * Returns aliases for the path splits defined in the query.
-	 *
-	 * @since 3.0
-	 */
-	protected Map<String, String> getPathAliases() {
-		return queryMetadata.getPathSplitAliases();
-	}
-
-	public EntityResolver getEntityResolver() {
-		return entityResolver;
-	}
-
-	public DbAdapter getAdapter() {
-		return adapter;
-	}
-
-	/**
-	 * Returns query object being processed.
-	 */
-	public Query getQuery() {
-		return query;
-	}
-
-	public QueryMetadata getQueryMetadata() {
-		return queryMetadata;
-	}
-
-	/**
-	 * A callback invoked by a child qualifier or ordering processor allowing
-	 * query assembler to reset its join stack.
-	 *
-	 * @since 3.0
-	 */
-	public abstract void resetJoinStack();
-
-	/**
-	 * Returns an alias of the table which is currently at the top of the join
-	 * stack.
-	 *
-	 * @since 3.0
-	 */
-	public abstract String getCurrentAlias();
-
-	/**
-	 * Appends a join with given semantics to the query.
-	 *
-	 * @since 3.0
-	 */
-	public abstract void dbRelationshipAdded(DbRelationship relationship, JoinType joinType, String joinSplitAlias);
-
-	/**
-	 * Translates query into an SQL string formatted to use in a
-	 * PreparedStatement.
-	 */
-	public String getSql() {
-		ensureTranslated();
-		return sql;
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	protected void ensureTranslated() {
-		if (!translated) {
-			doTranslate();
-			translated = true;
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	protected abstract void doTranslate();
-
-	/**
-	 * Returns <code>true</code> if table aliases are supported. Default
-	 * implementation returns false.
-	 */
-	public boolean supportsTableAliases() {
-		return false;
-	}
-
-	/**
-	 * Registers <code>anObject</code> as a PreparedStatement parameter.
-	 *
-	 * @param anObject
-	 *            object that represents a value of DbAttribute
-	 * @param dbAttr
-	 *            DbAttribute being processed.
-	 */
-	public void addToParamList(DbAttribute dbAttr, Object anObject) {
-		ExtendedType extendedType = anObject != null
-				? adapter.getExtendedTypes().getRegisteredType(anObject.getClass())
-				: adapter.getExtendedTypes().getDefaultType();
-
-		DbAttributeBinding binding = new DbAttributeBinding(dbAttr);
-		binding.setStatementPosition(bindings.size() + 1);
-		binding.setValue(anObject);
-		binding.setExtendedType(extendedType);
-
-		bindings.add(binding);
-		if(addBindingListener != null) {
-			addBindingListener.onAdd(binding);
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	public DbAttributeBinding[] getBindings() {
-		return bindings.toArray(new DbAttributeBinding[bindings.size()]);
-	}
-
-    /**
-     * @since 4.0
-     */
-	public abstract String getAliasForExpression(Expression exp);
-
-	/**
-	 * @since 4.0
-	 */
-	public void setAddBindingListener(AddBindingListener addBindingListener) {
-		this.addBindingListener = addBindingListener;
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	protected interface AddBindingListener {
-		void onAdd(DbAttributeBinding binding);
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
deleted file mode 100644
index fcdad65..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
+++ /dev/null
@@ -1,488 +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 org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.ObjectId;
-import org.apache.cayenne.Persistent;
-import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.parser.PatternMatchNode;
-import org.apache.cayenne.exp.parser.SimpleNode;
-import org.apache.cayenne.map.*;
-import org.apache.cayenne.util.CayenneMapEntry;
-
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Translates parts of the query to SQL. Always works in the context of parent
- * Translator.
- */
-public abstract class QueryAssemblerHelper {
-
-	protected QueryAssembler queryAssembler;
-	protected StringBuilder out;
-	protected QuotingStrategy strategy;
-
-	/**
-	 * Force joining tables for all relations, not only for toMany
-	 * @since 4.0
-	 */
-	private boolean forceJoinForRelations;
-
-	/**
-	 * Creates QueryAssemblerHelper initializing with parent
-	 * {@link QueryAssembler} and output buffer object.
-	 */
-	public QueryAssemblerHelper(QueryAssembler queryAssembler) {
-		this.queryAssembler = queryAssembler;
-		strategy = queryAssembler.getAdapter().getQuotingStrategy();
-	}
-
-	public ObjEntity getObjEntity() {
-		return queryAssembler.getQueryMetadata().getObjEntity();
-	}
-
-	public DbEntity getDbEntity() {
-		return queryAssembler.getQueryMetadata().getDbEntity();
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	public StringBuilder appendPart(StringBuilder out) {
-		this.out = out;
-		doAppendPart();
-		return out;
-	}
-
-	/**
-	 * Sets ouput buffer
-	 */
-	void setOut(StringBuilder out) {
-		this.out = out;
-	}
-
-	/**
-	 * @return output buffer
-	 */
-	StringBuilder getOut() {
-		return out;
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	protected abstract void doAppendPart();
-
-	/**
-	 * <p>
-	 * Outputs the standard JDBC (database agnostic) expression for supplying
-	 * the escape character to the database server when supplying a LIKE clause.
-	 * This has been factored-out because some database adaptors handle LIKE
-	 * differently and they need access to this common method in order not to
-	 * repeat this code.
-	 * <p>
-	 * If there is no escape character defined then this method will not output
-	 * anything. An escape character of 0 will mean no escape character.
-	 * 
-	 * @since 3.1
-	 */
-	protected void appendLikeEscapeCharacter(PatternMatchNode patternMatchNode) throws IOException {
-		char escapeChar = patternMatchNode.getEscapeChar();
-
-		if ('?' == escapeChar) {
-			throw new CayenneRuntimeException("the escape character of '?' is illegal for LIKE clauses.");
-		}
-
-		if (0 != escapeChar) {
-			out.append(" {escape '");
-			out.append(escapeChar);
-			out.append("'}");
-		}
-	}
-
-	/**
-	 * Processes parts of the OBJ_PATH expression.
-	 */
-	protected void appendObjPath(Expression pathExp) {
-
-		queryAssembler.resetJoinStack();
-		String joinSplitAlias = null;
-
-		for (PathComponent<ObjAttribute, ObjRelationship> component : getObjEntity().resolvePath(pathExp,
-				queryAssembler.getPathAliases())) {
-
-			if (component.isAlias()) {
-				joinSplitAlias = component.getName();
-				for (PathComponent<ObjAttribute, ObjRelationship> aliasPart : component.getAliasedPath()) {
-
-					ObjRelationship relationship = aliasPart.getRelationship();
-
-					if (relationship == null) {
-						throw new IllegalStateException("Non-relationship aliased path part: " + aliasPart.getName());
-					}
-
-					if (aliasPart.isLast() && component.isLast()) {
-						processRelTermination(relationship, aliasPart.getJoinType(), joinSplitAlias);
-					} else {
-						// find and add joins ....
-						for (DbRelationship dbRel : relationship.getDbRelationships()) {
-							queryAssembler.dbRelationshipAdded(dbRel, aliasPart.getJoinType(), joinSplitAlias);
-						}
-					}
-				}
-
-				continue;
-			}
-
-			ObjRelationship relationship = component.getRelationship();
-			ObjAttribute attribute = component.getAttribute();
-
-			if (relationship != null) {
-
-				// if this is a last relationship in the path,
-				// it needs special handling
-				if (component.isLast()) {
-					processRelTermination(relationship, component.getJoinType(), joinSplitAlias);
-				} else {
-					// find and add joins ....
-					for (DbRelationship dbRel : relationship.getDbRelationships()) {
-						queryAssembler.dbRelationshipAdded(dbRel, component.getJoinType(), joinSplitAlias);
-					}
-				}
-			} else {
-				Iterator<CayenneMapEntry> dbPathIterator = attribute.getDbPathIterator();
-				while (dbPathIterator.hasNext()) {
-					Object pathPart = dbPathIterator.next();
-
-					if (pathPart == null) {
-						throw new CayenneRuntimeException("ObjAttribute has no component: %s", attribute.getName());
-					} else if (pathPart instanceof DbRelationship) {
-						queryAssembler.dbRelationshipAdded((DbRelationship) pathPart, JoinType.INNER, joinSplitAlias);
-					} else if (pathPart instanceof DbAttribute) {
-						processColumnWithQuoteSqlIdentifiers((DbAttribute) pathPart, pathExp);
-					}
-				}
-
-			}
-		}
-	}
-
-	protected void appendDbPath(Expression pathExp) {
-
-		queryAssembler.resetJoinStack();
-		String joinSplitAlias = null;
-
-		for (PathComponent<DbAttribute, DbRelationship> component : getDbEntity().resolvePath(pathExp,
-				queryAssembler.getPathAliases())) {
-
-			if (component.isAlias()) {
-				joinSplitAlias = component.getName();
-				for (PathComponent<DbAttribute, DbRelationship> aliasPart : component.getAliasedPath()) {
-
-					DbRelationship relationship = aliasPart.getRelationship();
-
-					if (relationship == null) {
-						throw new IllegalStateException("Non-relationship aliased path part: " + aliasPart.getName());
-					}
-
-					if (aliasPart.isLast() && component.isLast()) {
-						processRelTermination(relationship, aliasPart.getJoinType(), joinSplitAlias);
-					} else {
-						queryAssembler.dbRelationshipAdded(relationship, component.getJoinType(), joinSplitAlias);
-					}
-				}
-
-				continue;
-			}
-
-			DbRelationship relationship = component.getRelationship();
-
-			if (relationship != null) {
-
-				// if this is a last relationship in the path,
-				// it needs special handling
-				if (component.isLast()) {
-					processRelTermination(relationship, component.getJoinType(), joinSplitAlias);
-				} else {
-					// find and add joins ....
-					queryAssembler.dbRelationshipAdded(relationship, component.getJoinType(), joinSplitAlias);
-				}
-			} else {
-				processColumnWithQuoteSqlIdentifiers(component.getAttribute(), pathExp);
-			}
-		}
-	}
-
-	protected void processColumn(DbAttribute dbAttr) {
-		processColumnWithQuoteSqlIdentifiers(dbAttr, null);
-	}
-
-	protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) {
-
-		String alias = (queryAssembler.supportsTableAliases()) ? queryAssembler.getCurrentAlias() : null;
-		out.append(strategy.quotedIdentifier(dbAttr.getEntity(), alias, dbAttr.getName()));
-	}
-
-	/**
-	 * Appends SQL code to the query buffer to handle <code>val</code> as a
-	 * parameter to the PreparedStatement being built. Adds <code>val</code>
-	 * into QueryAssembler parameter list.
-	 * <p>
-	 * If <code>val</code> is null, "NULL" is appended to the query.
-	 * </p>
-	 * <p>
-	 * If <code>val</code> is a DataObject, its primary key value is used as a
-	 * parameter. <i>Only objects with a single column primary key can be
-	 * used.</i>
-	 * 
-	 * @param val
-	 *            object that should be appended as a literal to the query. Must
-	 *            be of one of "standard JDBC" types, null or a DataObject.
-	 * @param attr
-	 *            DbAttribute that has information on what type of parameter is
-	 *            being appended.
-	 */
-	protected void appendLiteral(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
-
-		if (val == null) {
-			out.append("NULL");
-		} else if (val instanceof Persistent) {
-			// TODO: see cay1796
-			// This check is unlikely to happen,
-			// since Expression got ObjectId from Persistent object.
-			// Left for future research.
-			ObjectId id = ((Persistent) val).getObjectId();
-
-			// check if this id is acceptable to be a parameter
-			if (id == null) {
-				throw new CayenneRuntimeException("Can't use TRANSIENT object as a query parameter.");
-			}
-
-			if (id.isTemporary()) {
-				throw new CayenneRuntimeException("Can't use NEW object as a query parameter.");
-			}
-
-			Map<String, Object> snap = id.getIdSnapshot();
-			if (snap.size() != 1) {
-				throw new CayenneRuntimeException("Object must have a single primary key column to serve " +
-						"as a query parameter. This object has %s: %s", snap.size(), snap);
-			}
-
-			// checks have been passed, use id value
-			appendLiteralDirect(snap.get(snap.keySet().iterator().next()), attr, parentExpression);
-		} else if (val instanceof ObjectId) {
-
-			ObjectId id = (ObjectId) val;
-
-			if (id.isTemporary()) {
-				throw new CayenneRuntimeException("Can't use NEW object as a query parameter.");
-			}
-
-			Map<String, Object> snap = id.getIdSnapshot();
-			if (snap.size() != 1) {
-				throw new CayenneRuntimeException("Object must have a single primary key column to serve " +
-						"as a query parameter. This object has %s: %s", snap.size(), snap);
-			}
-
-			// checks have been passed, use id value
-			appendLiteralDirect(snap.get(snap.keySet().iterator().next()), attr, parentExpression);
-		} else {
-			appendLiteralDirect(val, attr, parentExpression);
-		}
-	}
-
-	/**
-	 * Appends SQL code to the query buffer to handle <code>val</code> as a
-	 * parameter to the PreparedStatement being built. Adds <code>val</code>
-	 * into QueryAssembler parameter list.
-	 */
-	protected void appendLiteralDirect(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
-		out.append('?');
-		queryAssembler.addToParamList(attr, val);
-	}
-
-	/**
-	 * Returns database type of expression parameters or null if it can not be
-	 * determined.
-	 */
-	protected DbAttribute paramsDbType(Expression e) {
-		int len = e.getOperandCount();
-
-		// for unary expressions, find parent binary - this is a hack mainly to
-		// support
-		// ASTList
-		if (len < 2) {
-
-			if (e instanceof SimpleNode) {
-				Expression parent = (Expression) ((SimpleNode) e).jjtGetParent();
-				if (parent != null) {
-					return paramsDbType(parent);
-				}
-			}
-
-			return null;
-		}
-
-		// naive algorithm:
-
-		// if at least one of the sibling operands is a
-		// OBJ_PATH or DB_PATH expression, use its attribute type as
-		// a final answer.
-
-		// find attribute or relationship matching the value
-		DbAttribute attribute = null;
-		DbRelationship relationship = null;
-		for (int i = 0; i < len; i++) {
-			Object op = e.getOperand(i);
-
-			if (op instanceof Expression) {
-				Expression expression = (Expression) op;
-				if (expression.getType() == Expression.OBJ_PATH) {
-					PathComponent<ObjAttribute, ObjRelationship> last = getObjEntity().lastPathComponent(expression,
-							queryAssembler.getPathAliases());
-
-					// TODO: handle EmbeddableAttribute
-					// if (last instanceof EmbeddableAttribute)
-					// break;
-
-					if (last.getAttribute() != null) {
-						attribute = last.getAttribute().getDbAttribute();
-						break;
-					} else if (last.getRelationship() != null) {
-						List<DbRelationship> dbPath = last.getRelationship().getDbRelationships();
-						if (dbPath.size() > 0) {
-							relationship = dbPath.get(dbPath.size() - 1);
-							break;
-						}
-					}
-				} else if (expression.getType() == Expression.DB_PATH) {
-					PathComponent<DbAttribute, DbRelationship> last = getDbEntity().lastPathComponent(expression,
-							queryAssembler.getPathAliases());
-					if (last.getAttribute() != null) {
-						attribute = last.getAttribute();
-						break;
-					} else if (last.getRelationship() != null) {
-						relationship = last.getRelationship();
-						break;
-					}
-				}
-			}
-		}
-
-		if (attribute != null) {
-			return attribute;
-		}
-
-		if (relationship != null) {
-			// Can't properly handle multiple joins....
-			if (relationship.getJoins().size() == 1) {
-				DbJoin join = relationship.getJoins().get(0);
-				return join.getSource();
-			}
-		}
-
-		return null;
-	}
-
-	/**
-	 * Processes case when an OBJ_PATH expression ends with relationship. If
-	 * this is a "to many" relationship, a join is added and a column expression
-	 * for the target entity primary key. If this is a "to one" relationship,
-	 * column expression for the source foreign key is added.
-	 * 
-	 * @since 3.0
-	 */
-	protected void processRelTermination(ObjRelationship rel, JoinType joinType, String joinSplitAlias) {
-
-		Iterator<DbRelationship> dbRels = rel.getDbRelationships().iterator();
-
-		// scan DbRelationships
-		while (dbRels.hasNext()) {
-			DbRelationship dbRel = dbRels.next();
-
-			// if this is a last relationship in the path,
-			// it needs special handling
-			if (!dbRels.hasNext()) {
-				processRelTermination(dbRel, joinType, joinSplitAlias);
-			} else {
-				// find and add joins ....
-				queryAssembler.dbRelationshipAdded(dbRel, joinType, joinSplitAlias);
-			}
-		}
-	}
-
-	/**
-	 * Handles case when a DB_NAME expression ends with relationship. If this is
-	 * a "to many" relationship, a join is added and a column expression for the
-	 * target entity primary key. If this is a "to one" relationship, column
-	 * expression for the source foreign key is added.
-	 * 
-	 * @since 3.0
-	 */
-	protected void processRelTermination(DbRelationship rel, JoinType joinType, String joinSplitAlias) {
-
-		if (forceJoinForRelations || rel.isToMany()) {
-			// append joins
-			queryAssembler.dbRelationshipAdded(rel, joinType, joinSplitAlias);
-		}
-
-		// get last DbRelationship on the list
-		List<DbJoin> joins = rel.getJoins();
-		if (joins.size() != 1) {
-			String msg = "OBJ_PATH expressions are only supported for a single-join relationships. " +
-					"This relationship has %s joins.";
-			throw new CayenneRuntimeException(msg, joins.size());
-		}
-
-		DbJoin join = joins.get(0);
-
-		DbAttribute attribute;
-
-		if (rel.isToMany()) {
-			DbEntity ent = join.getRelationship().getTargetEntity();
-			Collection<DbAttribute> pk = ent.getPrimaryKeys();
-			if (pk.size() != 1) {
-				String msg = "DB_NAME expressions can only support targets with a single column PK. " +
-						"This entity has %d columns in primary key.";
-				throw new CayenneRuntimeException(msg, pk.size());
-			}
-
-			attribute = pk.iterator().next();
-		} else {
-			attribute = forceJoinForRelations ? join.getTarget() : join.getSource();
-		}
-
-		processColumn(attribute);
-	}
-
-	/**
-	 * Force joining tables for all relations, not only for toMany
-	 * @since 4.0
-	 */
-	protected void setForceJoinForRelations(boolean forceJoinForRelations) {
-		this.forceJoinForRelations = forceJoinForRelations;
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java
new file mode 100644
index 0000000..1488c6a
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java
@@ -0,0 +1,138 @@
+/*****************************************************************
+ *   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.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NodeType;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SimpleNodeTreeVisitor;
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.exp.parser.ASTAggregateFunctionCall;
+import org.apache.cayenne.exp.property.BaseProperty;
+import org.apache.cayenne.map.DbAttribute;
+
+/**
+ * @since 4.2
+ */
+class ResultNodeDescriptor {
+    private final Node node;
+    private final boolean inDataRow;
+    private final boolean isAggregate;
+    private final BaseProperty<?> property;
+
+    private String dataRowKey;
+    private DbAttribute dbAttribute;
+    private String javaType;
+
+    ResultNodeDescriptor(Node node, boolean inDataRow, BaseProperty<?> property, String dataRowKey) {
+        this.node = node;
+        this.inDataRow = inDataRow;
+        this.property = property;
+        this.dataRowKey = dataRowKey;
+        this.isAggregate = property != null
+                && property.getExpression() instanceof ASTAggregateFunctionCall;
+    }
+
+    public boolean isAggregate() {
+        return isAggregate;
+    }
+
+    public boolean isInDataRow() {
+        return inDataRow;
+    }
+
+    public BaseProperty<?> getProperty() {
+        return property;
+    }
+
+    public Node getNode() {
+        return node;
+    }
+
+    public String getDataRowKey() {
+        if (dataRowKey != null) {
+            return dataRowKey;
+        }
+        if (property != null) {
+            return property.getAlias();
+        }
+        if (getDbAttribute() != null) {
+            return getDbAttribute().getName();
+        }
+        return null;
+    }
+
+    public void setDataRowKey(String dataRowKey) {
+        this.dataRowKey = dataRowKey;
+    }
+
+    public ResultNodeDescriptor setJavaType(String javaType) {
+        this.javaType = javaType;
+        return this;
+    }
+
+    public ResultNodeDescriptor setDbAttribute(DbAttribute dbAttribute) {
+        this.dbAttribute = dbAttribute;
+        return this;
+    }
+
+    public String getJavaType() {
+        if (javaType != null) {
+            return javaType;
+        }
+        if (property != null) {
+            return property.getType().getCanonicalName();
+        }
+        if (getDbAttribute() != null) {
+            return TypesMapping.getJavaBySqlType(getDbAttribute().getType());
+        }
+        return null;
+    }
+
+    public int getJdbcType() {
+        if (getDbAttribute() != null) {
+            return getDbAttribute().getType();
+        }
+
+        if (getProperty() != null) {
+            return TypesMapping.getSqlTypeByJava(getProperty().getType());
+        }
+
+        return TypesMapping.NOT_DEFINED;
+    }
+
+    public DbAttribute getDbAttribute() {
+        if (this.dbAttribute != null) {
+            return this.dbAttribute;
+        }
+        DbAttribute[] dbAttribute = {null};
+        node.visit(new SimpleNodeTreeVisitor() {
+            @Override
+            public boolean onNodeStart(Node node) {
+                if (node.getType() == NodeType.COLUMN) {
+                    dbAttribute[0] = ((ColumnNode) node).getAttribute();
+                    return false;
+                }
+                return true;
+            }
+        });
+        return this.dbAttribute = dbAttribute[0];
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java
new file mode 100644
index 0000000..9551035
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java
@@ -0,0 +1,44 @@
+/*****************************************************************
+ *   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.SQLGenerationVisitor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+
+/**
+ * @since 4.2
+ */
+class SQLGenerationStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        if(context.isSkipSQLGeneration()) {
+            return;
+        }
+        // Build final SQL tree
+        Node node = context.getSelectBuilder().build();
+        // convert to database flavour
+        node = context.getAdapter().getSqlTreeProcessor().apply(node);
+        // generate SQL
+        SQLGenerationVisitor visitor = new SQLGenerationVisitor(new DefaultQuotingAppendable(context));
+        node.visit(visitor);
+        context.setFinalSQL(visitor.getSQLString());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectQueryWrapper.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..576bb8d
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectQueryWrapper.java
@@ -0,0 +1,84 @@
+/*****************************************************************
+ *   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.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;
+
+/**
+ * @since 4.2
+ */
+public class SelectQueryWrapper implements TranslatableQueryWrapper {
+
+    private final SelectQuery<?> selectQuery;
+
+    public SelectQueryWrapper(SelectQuery<?> selectQuery) {
+        this.selectQuery = Objects.requireNonNull(selectQuery);
+    }
+
+    @Override
+    public boolean isDistinct() {
+        return selectQuery.isDistinct();
+    }
+
+    @Override
+    public QueryMetadata getMetaData(EntityResolver resolver) {
+        return selectQuery.getMetaData(resolver);
+    }
+
+    @Override
+    public PrefetchTreeNode getPrefetchTree() {
+        return selectQuery.getPrefetchTree();
+    }
+
+    @Override
+    public Expression getQualifier() {
+        return selectQuery.getQualifier();
+    }
+
+    @Override
+    public Collection<Ordering> getOrderings() {
+        return selectQuery.getOrderings();
+    }
+
+    @Override
+    public Collection<BaseProperty<?>> getColumns() {
+        return selectQuery.getColumns();
+    }
+
+    @Override
+    public Expression getHavingQualifier() {
+        return selectQuery.getHavingQualifier();
+    }
+
+    @Override
+    public Select<?> unwrap() {
+        return selectQuery;
+    }
+}


[04/11] cayenne git commit: CAY-2465 New SelectTranslator implementation

Posted by nt...@apache.org.
http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseJoinStack.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseJoinStack.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseJoinStack.java
deleted file mode 100644
index 9add9df..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseJoinStack.java
+++ /dev/null
@@ -1,115 +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.openbase;
-
-import java.util.List;
-
-import org.apache.cayenne.access.translator.select.JoinStack;
-import org.apache.cayenne.access.translator.select.JoinTreeNode;
-import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.map.DataMap;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbJoin;
-import org.apache.cayenne.map.DbRelationship;
-
-/**
- * OpenBase does not support standard JOIN keyword and have strange syntax for
- * defining inner/outer joins
- * 
- * @see http ://www.openbase.com/help/KnowledgeBase/
- *      400_OpenBaseSQL/401_SelectStatements.html
- * @since 3.0
- */
-class OpenBaseJoinStack extends JoinStack {
-
-	protected OpenBaseJoinStack(DbAdapter dbAdapter, QueryAssembler assembler) {
-		super(dbAdapter, assembler);
-	}
-
-	@Override
-	protected void appendJoinSubtree(StringBuilder out, JoinTreeNode node) {
-		DbRelationship relationship = node.getRelationship();
-
-		if (relationship == null) {
-			return;
-		}
-
-		DbEntity targetEntity = relationship.getTargetEntity();
-		String targetAlias = node.getTargetTableAlias();
-
-		out.append(", ").append(targetEntity.getFullyQualifiedName()).append(' ').append(targetAlias);
-
-		for (JoinTreeNode child : node.getChildren()) {
-			appendJoinSubtree(out, child);
-		}
-	}
-
-	@Override
-	protected void appendQualifier(StringBuilder out, boolean firstQualifierElement) {
-		boolean first = firstQualifierElement;
-		for (JoinTreeNode node : rootNode.getChildren()) {
-			if (!first) {
-				out.append(" AND ");
-			}
-			appendQualifierSubtree(out, node);
-			first = false;
-		}
-	}
-
-	protected void appendQualifierSubtree(StringBuilder out, JoinTreeNode node) {
-		DbRelationship relationship = node.getRelationship();
-
-		String srcAlias = node.getSourceTableAlias();
-		String targetAlias = node.getTargetTableAlias();
-
-		List<DbJoin> joins = relationship.getJoins();
-		int len = joins.size();
-		for (int i = 0; i < len; i++) {
-			DbJoin join = joins.get(i);
-
-			if (i > 0) {
-				out.append(" AND ");
-			}
-
-			out.append(srcAlias).append('.').append(join.getSourceName());
-
-			switch (node.getJoinType()) {
-			case INNER:
-				out.append(" = ");
-				break;
-			case LEFT_OUTER:
-				out.append(" * ");
-				break;
-			default:
-				throw new IllegalArgumentException("Unsupported join type: " + node.getJoinType());
-			}
-
-			out.append(targetAlias).append('.').append(join.getTargetName());
-
-		}
-
-		for (JoinTreeNode child : node.getChildren()) {
-			out.append(" AND ");
-			appendQualifierSubtree(out, child);
-		}
-
-	}
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseQualifierTranslator.java
deleted file mode 100644
index dbedc60..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseQualifierTranslator.java
+++ /dev/null
@@ -1,177 +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.openbase;
-
-import java.io.IOException;
-
-import org.apache.cayenne.CayenneRuntimeException;
-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.PatternMatchNode;
-import org.apache.cayenne.map.DbAttribute;
-
-/**
- * Translates query qualifier to SQL. Used as a helper class by query
- * translators.
- * 
- * @since 1.1
- */
-public class OpenBaseQualifierTranslator extends QualifierTranslator {
-
-	public OpenBaseQualifierTranslator(QueryAssembler queryAssembler) {
-		super(queryAssembler);
-	}
-
-	@Override
-	public void startNode(Expression node, Expression parentNode) {
-
-		if (node.getOperandCount() == 2) {
-			// binary nodes are the only ones that currently require this
-			detectObjectMatch(node);
-
-			if (parenthesisNeeded(node, parentNode)) {
-				out.append('(');
-			}
-
-			// super implementation has special handling
-			// of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-			// OpenBase is case-insensitive by default
-			// ...
-
-		} else {
-			super.startNode(node, parentNode);
-		}
-	}
-
-	@Override
-	public void endNode(Expression node, Expression parentNode) {
-		if (node.getOperandCount() == 2) {
-
-			try {
-				// check if we need to use objectMatchTranslator to finish
-				// building the
-				// expression
-				if (matchingObject) {
-					appendObjectMatch();
-				}
-
-				if (PatternMatchNode.class.isAssignableFrom(node.getClass())) {
-					appendLikeEscapeCharacter((PatternMatchNode) node);
-				}
-
-				if (parenthesisNeeded(node, parentNode)) {
-					out.append(')');
-				}
-
-				// super implementation has special handling
-				// of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-				// OpenBase is case-insensitive by default
-				// ...
-			} catch (IOException ioex) {
-				throw new CayenneRuntimeException("Error appending content", ioex);
-			}
-		} else {
-			super.endNode(node, parentNode);
-		}
-	}
-
-	@Override
-	protected void appendLiteralDirect(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
-
-		// Special handling of string matching is needed:
-		// Case-sensitive LIKE must be converted to [x][Y][z] format
-		if (val instanceof String
-				&& (parentExpression.getType() == Expression.LIKE || parentExpression.getType() == Expression.NOT_LIKE)) {
-
-			val = caseSensitiveLikePattern((String) val);
-		}
-
-		super.appendLiteralDirect(val, attr, parentExpression);
-	}
-
-	private String caseSensitiveLikePattern(String pattern) {
-		int len = pattern.length();
-		StringBuilder buffer = new StringBuilder(len * 3);
-
-		for (int i = 0; i < len; i++) {
-			char c = pattern.charAt(i);
-			if (c == '%' || c == '?') {
-				buffer.append(c);
-			} else {
-				buffer.append("[").append(c).append("]");
-			}
-		}
-
-		return buffer.toString();
-	}
-
-	@Override
-	public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
-		if (!hasMoreChildren) {
-			return;
-		}
-
-		// super implementation has special handling
-		// of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-		// OpenBase is case-insensitive by default
-		// ...
-
-		try {
-			switch (node.getType()) {
-
-			case Expression.LIKE_IGNORE_CASE:
-				finishedChildNodeAppendExpression(node, " LIKE ");
-				break;
-			case Expression.NOT_LIKE_IGNORE_CASE:
-				finishedChildNodeAppendExpression(node, " NOT LIKE ");
-				break;
-			default:
-				super.finishedChild(node, childIndex, hasMoreChildren);
-			}
-		} catch (IOException ioex) {
-			throw new CayenneRuntimeException("Error appending content", ioex);
-		}
-	}
-
-	private void finishedChildNodeAppendExpression(Expression node, String operation) throws IOException {
-		Appendable out = matchingObject ? new StringBuilder() : this.out;
-		out.append(operation);
-		if (matchingObject) {
-			objectMatchTranslator.setOperation(out.toString());
-			objectMatchTranslator.setExpression(node);
-		}
-	}
-
-	@Override
-	protected void appendExtractFunction(ASTExtract functionExpression) {
-		switch (functionExpression.getPart()) {
-			case DAY_OF_WEEK:
-			case DAY_OF_MONTH:
-			case DAY_OF_YEAR:
-				// openbase variants are without '_'
-				out.append(functionExpression.getPart().name().replace("_", ""));
-				break;
-			default:
-				appendFunction(functionExpression);
-		}
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSQLTreeProcessor.java
new file mode 100644
index 0000000..29eaaa1
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSQLTreeProcessor.java
@@ -0,0 +1,109 @@
+/*****************************************************************
+ *   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.openbase;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
+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.ValueNode;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+
+/**
+ * @since 4.2
+ */
+public class OpenBaseSQLTreeProcessor extends BaseSQLTreeProcessor {
+
+    @Override
+    protected void onLikeNode(Node parent, LikeNode child, int index) {
+        // OpenBase is case-insensitive by default
+        if(child.isIgnoreCase()) {
+            replaceChild(parent, index, new LikeNode(false, child.isNot(), child.getEscape()));
+        }
+    }
+
+    @Override
+    protected void onValueNode(Node parent, ValueNode child, int index) {
+        // Special handling of string matching is needed:
+        // Case-sensitive LIKE must be converted to [x][Y][z] format
+        if(parent.getType() == NodeType.LIKE) {
+            if(!((LikeNode)parent).isIgnoreCase() && child.getValue() instanceof CharSequence) {
+                replaceChild(parent, index,
+                        new ValueNode(caseSensitiveLikePattern((CharSequence)child.getValue()), child.isArray(), child.getAttribute()));
+            }
+        }
+    }
+
+    @Override
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+        replaceChild(parent, index, new OpenBaseLimitNode(child));
+    }
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        switch (child.getFunctionName()) {
+            case "DAY_OF_WEEK":
+            case "DAY_OF_MONTH":
+            case "DAY_OF_YEAR":
+                replaceChild(parent, index, new FunctionNode(child.getFunctionName().replace("_", ""), child.getAlias()));
+                break;
+        }
+    }
+
+    private String caseSensitiveLikePattern(CharSequence pattern) {
+        int len = pattern.length();
+        StringBuilder buffer = new StringBuilder(len * 3);
+
+        for (int i = 0; i < len; i++) {
+            char c = pattern.charAt(i);
+            if (c == '%' || c == '?') {
+                buffer.append(c);
+            } else {
+                buffer.append("[").append(c).append("]");
+            }
+        }
+
+        return buffer.toString();
+    }
+
+    private static class OpenBaseLimitNode extends Node {
+
+        private final LimitOffsetNode child;
+
+        public OpenBaseLimitNode(LimitOffsetNode child) {
+            this.child = child;
+        }
+
+        @Override
+        public QuotingAppendable append(QuotingAppendable buffer) {
+            if(child.getLimit() > 0) {
+                buffer.append(" RETURN RESULTS ").append(child.getLimit());
+            }
+            return buffer;
+        }
+
+        @Override
+        public Node copy() {
+            return new OpenBaseLimitNode(child);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSelectTranslator.java
deleted file mode 100644
index e4ff7cc..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseSelectTranslator.java
+++ /dev/null
@@ -1,53 +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.openbase;
-
-import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
-import org.apache.cayenne.access.translator.select.JoinStack;
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.query.Query;
-
-/**
- * @since 1.2
- */
-class OpenBaseSelectTranslator extends DefaultSelectTranslator {
-
-	/**
-	 * @since 4.0
-	 */
-	public OpenBaseSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
-		super(query, adapter, entityResolver);
-	}
-
-	@Override
-	protected JoinStack createJoinStack() {
-		return new OpenBaseJoinStack(getAdapter(), this);
-	}
-
-	@Override
-	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-		int limit = queryMetadata.getFetchLimit();
-		if (limit > 0) {
-			buffer.append(" RETURN RESULTS ").append(limit);
-		}
-	}
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8Adapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8Adapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8Adapter.java
index 60f820e..a51f4d8 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8Adapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8Adapter.java
@@ -20,19 +20,14 @@
 package org.apache.cayenne.dba.oracle;
 
 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.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
 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;
 
 import java.lang.reflect.Method;
@@ -84,14 +79,6 @@ public class Oracle8Adapter extends OracleAdapter {
 	}
 
 	/**
-	 * @since 4.0
-	 */
-	@Override
-	public SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver) {
-		return new Oracle8SelectTranslator(query, this, entityResolver);
-	}
-
-	/**
 	 * Uses OracleActionBuilder to create the right action.
 	 */
 	@Override
@@ -109,10 +96,4 @@ public class Oracle8Adapter extends OracleAdapter {
 		return super.findResource(name);
 	}
 
-	@Override
-	public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-		QualifierTranslator translator = new Oracle8QualifierTranslator(queryAssembler);
-		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/oracle/Oracle8JoinStack.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8JoinStack.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8JoinStack.java
deleted file mode 100644
index 183775a..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8JoinStack.java
+++ /dev/null
@@ -1,108 +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.oracle;
-
-import java.util.List;
-
-import org.apache.cayenne.access.translator.select.JoinStack;
-import org.apache.cayenne.access.translator.select.JoinTreeNode;
-import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.map.DataMap;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbJoin;
-import org.apache.cayenne.map.DbRelationship;
-
-/**
- * @since 3.0
- */
-// cloned from OpenBaseJoin stack... need better strategies of reuse...
-class Oracle8JoinStack extends JoinStack {
-
-	Oracle8JoinStack(DbAdapter dbAdapter, QueryAssembler assembler) {
-		super(dbAdapter, assembler);
-	}
-
-	@Override
-	protected void appendJoinSubtree(StringBuilder out, JoinTreeNode node) {
-		DbRelationship relationship = node.getRelationship();
-
-		if (relationship == null) {
-			return;
-		}
-
-		DbEntity targetEntity = relationship.getTargetEntity();
-		String targetAlias = node.getTargetTableAlias();
-
-		out.append(", ").append(targetEntity.getFullyQualifiedName()).append(' ').append(targetAlias);
-
-		for (JoinTreeNode child : node.getChildren()) {
-			appendJoinSubtree(out, child);
-		}
-	}
-
-	@Override
-	protected void appendQualifier(StringBuilder out, boolean firstQualifierElement) {
-		boolean first = firstQualifierElement;
-		for (JoinTreeNode node : rootNode.getChildren()) {
-			if (!first) {
-				out.append(" AND ");
-			}
-			appendQualifierSubtree(out, node);
-			first = false;
-		}
-	}
-
-	protected void appendQualifierSubtree(StringBuilder out, JoinTreeNode node) {
-		DbRelationship relationship = node.getRelationship();
-
-		String srcAlias = node.getSourceTableAlias();
-		String targetAlias = node.getTargetTableAlias();
-
-		List<DbJoin> joins = relationship.getJoins();
-		int len = joins.size();
-		for (int i = 0; i < len; i++) {
-			DbJoin join = joins.get(i);
-
-			if (i > 0) {
-				out.append(" AND ");
-			}
-
-			out.append(srcAlias).append('.').append(join.getSourceName());
-
-			switch (node.getJoinType()) {
-			case INNER:
-				out.append(" = ");
-				break;
-			case LEFT_OUTER:
-				out.append(" * ");
-				break;
-			default:
-				throw new IllegalArgumentException("Unsupported join type: " + node.getJoinType());
-			}
-
-			out.append(targetAlias).append('.').append(join.getTargetName());
-		}
-
-		for (JoinTreeNode child : node.getChildren()) {
-			out.append(" AND ");
-			appendQualifierSubtree(out, child);
-		}
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8QualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8QualifierTranslator.java
deleted file mode 100644
index d756392..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8QualifierTranslator.java
+++ /dev/null
@@ -1,49 +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.oracle;
-
-import org.apache.cayenne.access.translator.select.QueryAssembler;
-
-/**
- * Extends the TrimmingQualifierTranslator that Cayenne normally uses for
- * Oracle. Overrides doAppendPart() to wrap the qualifierBuffer in parentheses
- * if it contains an "OR" expression. This avoids a bug that can happen on
- * Oracle8 if the query also contains a join.
- * 
- * @since 3.0
- */
-class Oracle8QualifierTranslator extends OracleQualifierTranslator {
-
-	public Oracle8QualifierTranslator(QueryAssembler queryAssembler) {
-		super(queryAssembler);
-	}
-
-	@Override
-	protected void doAppendPart() {
-		super.doAppendPart();
-
-		if (out instanceof StringBuilder) {
-			StringBuilder buffer = (StringBuilder) out;
-			if (buffer.indexOf(" OR ") != -1) {
-				buffer.insert(0, '(');
-				buffer.append(')');
-			}
-		}
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectTranslator.java
deleted file mode 100644
index 6df771c..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8SelectTranslator.java
+++ /dev/null
@@ -1,47 +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.oracle;
-
-import org.apache.cayenne.access.translator.select.JoinStack;
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.query.Query;
-
-/**
- * @since 3.0
- */
-class Oracle8SelectTranslator extends OracleSelectTranslator {
-
-	/**
-	 * @since 4.0
-	 */
-	public Oracle8SelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
-		super(query, adapter, entityResolver);
-	}
-
-	/**
-	 * Returns an old style joint stack for Oracle8 that does not support
-	 * explicit join syntax.
-	 */
-	@Override
-	protected JoinStack createJoinStack() {
-		return new Oracle8JoinStack(getAdapter(), this);
-	}
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
index 77f2be1..cc58f0d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
@@ -21,11 +21,9 @@ package org.apache.cayenne.dba.oracle;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+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.ByteType;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
@@ -39,12 +37,10 @@ import org.apache.cayenne.dba.PkGenerator;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.BatchQuery;
 import org.apache.cayenne.query.InsertBatchQuery;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
-import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.query.UpdateBatchQuery;
 import org.apache.cayenne.resource.ResourceLocator;
 
@@ -57,6 +53,7 @@ import java.sql.Types;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * DbAdapter implementation for <a href="http://www.oracle.com">Oracle RDBMS
@@ -171,11 +168,11 @@ public class OracleAdapter extends JdbcAdapter {
 	}
 
 	/**
-	 * @since 4.0
+	 * @since 4.2
 	 */
 	@Override
-	public SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver) {
-		return new OracleSelectTranslator(query, this, entityResolver);
+	public Function<Node, Node> getSqlTreeProcessor() {
+		return new OracleSQLTreeProcessor();
 	}
 
 	/**
@@ -281,16 +278,6 @@ public class OracleAdapter extends JdbcAdapter {
 	}
 
 	/**
-	 * Returns a trimming translator.
-	 */
-	@Override
-	public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-		QualifierTranslator translator = new Oracle8QualifierTranslator(queryAssembler);
-		translator.setCaseInsensitive(caseInsensitiveCollations);
-		return translator;
-	}
-
-	/**
 	 * Uses OracleActionBuilder to create the right action.
 	 *
 	 * @since 1.2

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java
deleted file mode 100644
index fcc76a4..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java
+++ /dev/null
@@ -1,212 +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.oracle;
-
-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.exp.parser.ASTIn;
-import org.apache.cayenne.exp.parser.ASTList;
-import org.apache.cayenne.exp.parser.ASTNotIn;
-import org.apache.cayenne.exp.parser.ASTPath;
-import org.apache.cayenne.exp.parser.Node;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Function;
-
-/**
- * Oracle qualifier translator. In particular, trims INs with more than 1000
- * elements to an OR-set of INs with &lt;= 1000 elements
- */
-public class OracleQualifierTranslator extends TrimmingQualifierTranslator {
-
-	public OracleQualifierTranslator(QueryAssembler queryAssembler) {
-		super(queryAssembler, OracleAdapter.TRIM_FUNCTION);
-	}
-
-	@Override
-	protected void doAppendPart(Expression rootNode) {
-		if (rootNode == null) {
-			return;
-		}
-		rootNode = rootNode.transform(new INTrimmer());
-		rootNode.traverse(this);
-	}
-
-	public static class INTrimmer implements Function<Object, Object> {
-
-		public Expression trimmedInExpression(Expression exp, int maxInSize) {
-			Expression list = (Expression) exp.getOperand(1);
-			Object[] objects = (Object[]) list.evaluate(null);
-
-			if (objects.length <= maxInSize) {
-				return exp;
-			}
-
-			Expression trimmed = trimmedInExpression((ASTPath) exp.getOperand(0), objects, maxInSize);
-			if (exp instanceof ASTNotIn) {
-				return trimmed.notExp();
-			}
-			return trimmed;
-		}
-
-		Expression trimmedInExpression(ASTPath path, Object[] values, int maxInSize) {
-			Expression res = null;
-
-			List<Object> in = new ArrayList<>(maxInSize);
-			for (Object v : values) {
-				in.add(v);
-				if (in.size() == maxInSize) {
-					Expression inExp = new ASTIn(path, new ASTList(in));
-					res = res != null ? res.orExp(inExp) : inExp;
-					in = new ArrayList<>(maxInSize);
-				}
-			}
-			if (in.size() > 0) {
-				Expression inExp = new ASTIn(path, new ASTList(in));
-				res = res != null ? res.orExp(inExp) : inExp;
-			}
-			return res;
-		}
-
-		public Object apply(Object input) {
-			if (input instanceof ASTIn || input instanceof ASTNotIn) {
-				return trimmedInExpression((Expression) input, 1000);
-			}
-			return input;
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	public void endNode(Expression node, Expression parentNode) {
-		super.endNode(node, parentNode);
-		if(node.getType() == Expression.FUNCTION_CALL) {
-			if("LOCATE".equals(((ASTFunctionCall)node).getFunctionName())) {
-				// order of args in INSTR is different, so swap them back
-				swapNodeChildren((ASTFunctionCall)node, 0, 1);
-			}
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	protected void appendFunction(ASTFunctionCall functionExpression) {
-		if("CONCAT".equals(functionExpression.getFunctionName())) {
-			// CONCAT(x, y, z) -> (x || y || z)
-		} else if("SUBSTRING".equals(functionExpression.getFunctionName())) {
-			out.append("SUBSTR");
-		} else if("LOCATE".equals(functionExpression.getFunctionName())) {
-			// LOCATE(substr, str) -> INSTR(str, substr)
-			out.append("INSTR");
-			swapNodeChildren(functionExpression, 0, 1);
-		} else if("CURRENT_TIME".equals(functionExpression.getFunctionName())) {
-			out.append("{fn CURTIME()}");
-		} else {
-			super.appendFunction(functionExpression);
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
-		if("CONCAT".equals(functionExpression.getFunctionName())) {
-			out.append(" || ");
-		} else {
-			super.appendFunctionArgDivider(functionExpression);
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
-		if("CONCAT".equals(functionExpression.getFunctionName())) {
-			out.delete(out.length() - " || ".length(), out.length());
-		} else {
-			super.clearLastFunctionArgDivider(functionExpression);
-		}
-
-		if(functionExpression instanceof ASTExtract) {
-			switch (((ASTExtract)functionExpression).getPart()) {
-				case DAY_OF_YEAR:
-					out.append(", 'DDD'");
-					break;
-				case DAY_OF_WEEK:
-					out.append(", 'D'");
-					break;
-				case WEEK:
-					out.append(", 'IW'");
-					break;
-			}
-			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) {
-		switch (functionExpression.getPart()) {
-			// use TO_CHAR(date, format) function for parts that is unsupported by EXTRACT()
-			case DAY_OF_YEAR:
-			case DAY_OF_WEEK:
-			case WEEK:
-				out.append("TO_CHAR(");
-				break;
-			case DAY_OF_MONTH:
-				out.append("EXTRACT(DAY FROM ");
-				break;
-			default:
-				out.append("EXTRACT(");
-				out.append(functionExpression.getPart().name());
-				out.append(" FROM ");
-		}
-
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	private void swapNodeChildren(Node node, int i, int j) {
-		Node ni = node.jjtGetChild(i);
-		Node nj = node.jjtGetChild(j);
-		node.jjtAddChild(ni, j);
-		node.jjtAddChild(nj, i);
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTreeProcessor.java
new file mode 100644
index 0000000..ba84579
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTreeProcessor.java
@@ -0,0 +1,241 @@
+/*****************************************************************
+ *   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.oracle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.cayenne.access.sqlbuilder.ExpressionNodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.SelectBuilder;
+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.InNode;
+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.ValueNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TrimmingColumnNode;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+import org.apache.cayenne.util.ArrayUtil;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
+
+/**
+ * @since 4.2
+ */
+public class OracleSQLTreeProcessor extends BaseSQLTreeProcessor {
+
+    private static final int ORACLE_IN_BATCH_SIZE = 1000;
+
+    private SelectBuilder selectBuilder;
+
+    private Node root;
+
+    @Override
+    protected void onResultNode(Node parent, Node child, int index) {
+        for(int i=0; i<child.getChildrenCount(); i++) {
+            child.replaceChild(i, aliased(child.getChild(i), "c" + i).build());
+        }
+    }
+
+    @Override
+    protected void onColumnNode(Node parent, ColumnNode child, int index) {
+        replaceChild(parent, index, new TrimmingColumnNode(child));
+    }
+
+    @Override
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+        if(child.getLimit() > 0 || child.getOffset() > 0) {
+            int limit = child.getLimit();
+            int offset = child.getOffset();
+            int max = (limit <= 0) ? Integer.MAX_VALUE : limit + offset;
+
+            /*
+             Transform query with limit/offset into following form:
+             SELECT * FROM (
+                SELECT tid.*, ROWNUM rnum
+                FROM ( MAIN_QUERY ) tid
+                WHERE ROWNUM <= OFFSET + LIMIT
+             ) WHERE rnum > OFFSET
+             */
+            selectBuilder = select(all())
+                    .from(select(text(" tid.*"), text(" ROWNUM rnum")) // using text not column to avoid quoting
+                            .from(aliased(() -> root, "tid"))
+                            .where(exp(text(" ROWNUM")).lte(value(max))))
+                    .where(exp(text(" rnum")).gt(value(offset)));
+        }
+        parent.replaceChild(index, new EmptyNode());
+    }
+
+    @Override
+    protected void onInNode(Node parent, InNode child, int index) {
+        boolean not = child.isNot();
+        Node arg = child.getChild(0);
+        Node childNode = child.getChild(1);
+        if(childNode.getType() != NodeType.VALUE) {
+            return;
+        }
+
+        ValueNode valueNode = (ValueNode)childNode;
+        Object value = valueNode.getValue();
+        if(!value.getClass().isArray()) {
+            return;
+        }
+
+        List<Node> newChildren = new ArrayList<>();
+
+        // need to slice for batches of 1000 values
+        if(value instanceof Object[]) {
+            for(Object[] slice : ArrayUtil.sliceArray((Object[])value, ORACLE_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof int[]) {
+            for(int[] slice : ArrayUtil.sliceArray((int[])value, ORACLE_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof long[]) {
+            for(long[] slice : ArrayUtil.sliceArray((long[])value, ORACLE_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof float[]) {
+            for(float[] slice : ArrayUtil.sliceArray((float[])value, ORACLE_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof double[]) {
+            for(double[] slice : ArrayUtil.sliceArray((double[])value, ORACLE_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof short[]) {
+            for(short[] slice : ArrayUtil.sliceArray((short[])value, ORACLE_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof char[]) {
+            for(char[] slice : ArrayUtil.sliceArray((char[])value, ORACLE_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof boolean[]) {
+            for(boolean[] slice : ArrayUtil.sliceArray((boolean[])value, ORACLE_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof byte[]) {
+            for(byte[] slice : ArrayUtil.sliceArray((byte[])value, ORACLE_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        }
+
+        ExpressionNodeBuilder exp = exp(node(newChildren.get(0)));
+        for(int i=1; i<newChildren.size(); i++) {
+            if(not) {
+                exp = exp.and(node(newChildren.get(i)));
+            } else {
+                exp = exp.or(node(newChildren.get(i)));
+            }
+        }
+        parent.replaceChild(index, exp.build());
+    }
+
+    private InNode newSliceNode(InNode child, Node arg, ValueNode valueNode, Object slice) {
+        InNode nextNode = new InNode(child.isNot());
+        nextNode.addChild(arg.deepCopy());
+        nextNode.addChild(new ValueNode(slice, valueNode.isArray(), valueNode.getAttribute()));
+        return nextNode;
+    }
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        String functionName = child.getFunctionName();
+        Node functionReplacement = null;
+        switch (functionName) {
+            case "LOCATE":
+                functionReplacement = new FunctionNode("INSTR", child.getAlias(), true);
+                for(int i=0; i<=1; i++) {
+                    functionReplacement.addChild(child.getChild(1-i));
+                }
+                parent.replaceChild(index, functionReplacement);
+                return;
+
+            case "DAY_OF_YEAR":
+            case "DAY_OF_WEEK":
+            case "WEEK":
+                functionReplacement = new FunctionNode("TO_CHAR", child.getAlias(), true);
+                functionReplacement.addChild(child.getChild(0));
+                if("DAY_OF_YEAR".equals(functionName)) {
+                    functionName = "'DDD'";
+                } else if("DAY_OF_WEEK".equals(functionName)) {
+                    functionName = "'D'";
+                } else {
+                    functionName = "'IW'";
+                }
+                functionReplacement.addChild(new TextNode(functionName));
+                parent.replaceChild(index, functionReplacement);
+                return;
+
+            case "SUBSTRING":
+                functionReplacement = new FunctionNode("SUBSTR", child.getAlias(), true);
+                break;
+            case "CONCAT":
+                functionReplacement = new OpExpressionNode("||");
+                break;
+            case "CURRENT_TIMESTAMP":
+            case "CURRENT_DATE":
+                functionReplacement = new FunctionNode(functionName, child.getAlias(), false);
+                break;
+            case "CURRENT_TIME":
+                functionReplacement = new FunctionNode("{fn CURTIME()}", child.getAlias(), false);
+                break;
+            case "YEAR":
+            case "MONTH":
+            case "DAY":
+            case "DAY_OF_MONTH":
+            case "HOUR":
+            case "MINUTE":
+            case "SECOND":
+                functionReplacement = new FunctionNode("EXTRACT", child.getAlias(), true) {
+                    @Override
+                    public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
+                        buffer.append(' ');
+                    }
+                };
+                if("DAY_OF_MONTH".equals(functionName)) {
+                    functionName = "DAY";
+                }
+                functionReplacement.addChild(new TextNode(functionName + " FROM "));
+                break;
+        }
+
+        if(functionReplacement != null) {
+            replaceChild(parent, index, functionReplacement);
+        }
+    }
+
+    @Override
+    public Node apply(Node node) {
+        root = node;
+        super.apply(node);
+        if(selectBuilder != null) {
+            return selectBuilder.build();
+        }
+        return node;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java
deleted file mode 100644
index c98aad9..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSelectTranslator.java
+++ /dev/null
@@ -1,78 +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.oracle;
-
-import java.util.List;
-
-import org.apache.cayenne.access.DataNode;
-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;
-
-/**
- * Select translator that implements Oracle-specific optimizations.
- * 
- */
-class OracleSelectTranslator extends DefaultSelectTranslator {
-
-	/**
-	 * @since 4.0
-	 */
-	public OracleSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
-		super(query, adapter, entityResolver);
-	}
-
-	@Override
-	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-		int offset = queryMetadata.getFetchOffset();
-		int limit = queryMetadata.getFetchLimit();
-
-		if (limit > 0 || offset > 0) {
-			int max = (limit <= 0) ? Integer.MAX_VALUE : limit + offset;
-
-			buffer.insert(0, "select * from ( select tid.*, ROWNUM rnum from (");
-			buffer.append(") tid where ROWNUM <=").append(max).append(") where rnum  > ").append(offset);
-		}
-	}
-
-	@Override
-	protected void appendSelectColumns(StringBuilder buffer, List<String> selectColumnExpList) {
-
-		// we need to add aliases to all columns to make fetch
-		// limit and offset work properly on Oracle (see CAY-1266)
-
-		// append columns (unroll the loop's first element)
-		int columnCount = selectColumnExpList.size();
-		buffer.append(selectColumnExpList.get(0));
-		if(!selectColumnExpList.get(0).contains(" AS ")) {
-			buffer.append(" AS c0");
-		}
-
-		// assume there is at least 1 element
-		for (int i = 1; i < columnCount; i++) {
-			buffer.append(", ");
-			buffer.append(selectColumnExpList.get(i));
-			if(!selectColumnExpList.get(i).contains(" AS ")) {
-				buffer.append(" AS c").append(i);
-			}
-		}
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgreSQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgreSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgreSQLTreeProcessor.java
new file mode 100644
index 0000000..9bbc34e
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgreSQLTreeProcessor.java
@@ -0,0 +1,83 @@
+/*****************************************************************
+ *   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.postgres;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TrimmingColumnNode;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+import org.apache.cayenne.dba.postgres.sqltree.PositionFunctionNode;
+import org.apache.cayenne.dba.postgres.sqltree.PostgresExtractFunctionNode;
+import org.apache.cayenne.dba.postgres.sqltree.PostgresLikeNode;
+import org.apache.cayenne.dba.postgres.sqltree.PostgresLimitOffsetNode;
+
+/**
+ * @since 4.2
+ */
+public class PostgreSQLTreeProcessor extends BaseSQLTreeProcessor {
+
+    private static final Set<String> EXTRACT_FUNCTION_NAMES = new HashSet<>(Arrays.asList(
+        "DAY_OF_MONTH", "DAY", "MONTH", "HOUR", "WEEK", "YEAR", "DAY_OF_WEEK", "DAY_OF_YEAR", "MINUTE", "SECOND"
+    ));
+
+    @Override
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+        replaceChild(parent, index, new PostgresLimitOffsetNode(child), false);
+    }
+
+    @Override
+    protected void onColumnNode(Node parent, ColumnNode child, int index) {
+        replaceChild(parent, index, new TrimmingColumnNode(child));
+    }
+
+    @Override
+    protected void onLikeNode(Node parent, LikeNode child, int index) {
+        if(child.isIgnoreCase()) {
+            replaceChild(parent, index, new PostgresLikeNode(child.isNot(), child.getEscape()));
+        }
+    }
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        Node replacement = null;
+        String functionName = child.getFunctionName();
+        if(EXTRACT_FUNCTION_NAMES.contains(functionName)) {
+            replacement = new PostgresExtractFunctionNode(functionName);
+        } else if("CURRENT_DATE".equals(functionName)
+                || "CURRENT_TIME".equals(functionName)
+                || "CURRENT_TIMESTAMP".equals(functionName)) {
+            replacement = new FunctionNode(functionName, child.getAlias(), false);
+        } else if("LOCATE".equals(functionName)) {
+            replacement = new PositionFunctionNode(child.getAlias());
+        }
+
+        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/postgres/PostgresAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
index dbee328..24033e1 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
@@ -21,10 +21,8 @@ package org.apache.cayenne.dba.postgres;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
 import org.apache.cayenne.access.translator.ParameterBinding;
-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.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
@@ -39,10 +37,8 @@ import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
-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;
 
 import java.sql.PreparedStatement;
@@ -52,6 +48,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * DbAdapter implementation for <a href="http://www.postgresql.org">PostgreSQL
@@ -80,12 +77,12 @@ public class PostgresAdapter extends JdbcAdapter {
 		setSupportsGeneratedKeys(true);
 	}
 
-	/**
-	 * @since 4.0
-	 */
+    /**
+     * @since 4.2
+     */
 	@Override
-	public SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver) {
-		return new PostgresSelectTranslator(query, this, entityResolver);
+	public Function<Node, Node> getSqlTreeProcessor() {
+		return new PostgreSQLTreeProcessor();
 	}
 
 	/**
@@ -258,16 +255,6 @@ public class PostgresAdapter extends JdbcAdapter {
 	}
 
 	/**
-	 * Returns a trimming translator.
-	 */
-	@Override
-	public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-		QualifierTranslator translator = new PostgresQualifierTranslator(queryAssembler);
-		translator.setCaseInsensitive(caseInsensitiveCollations);
-		return translator;
-	}
-
-	/**
 	 * @see JdbcAdapter#createPkGenerator()
 	 */
 	@Override

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java
deleted file mode 100644
index 3fdc22c..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java
+++ /dev/null
@@ -1,189 +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.postgres;
-
-import java.io.IOException;
-
-import org.apache.cayenne.CayenneRuntimeException;
-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.exp.parser.PatternMatchNode;
-
-/**
- * Uses Postgres extensions to optimize various translations.
- * 
- * @since 1.1
- */
-public class PostgresQualifierTranslator extends TrimmingQualifierTranslator {
-
-	public PostgresQualifierTranslator(QueryAssembler queryAssembler) {
-		super(queryAssembler, "RTRIM");
-	}
-
-	@Override
-	public void startNode(Expression node, Expression parentNode) {
-		// super implementation has special handling of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-		// Postgres uses ILIKE
-		boolean likeIgnoreCase = (node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE);
-		if (likeIgnoreCase) {
-			// binary nodes are the only ones that currently require this
-			detectObjectMatch(node);
-			if (parenthesisNeeded(node, parentNode)) {
-				out.append('(');
-			}
-		} else {
-			super.startNode(node, parentNode);
-		}
-	}
-
-	@Override
-	public void endNode(Expression node, Expression parentNode) {
-		// super implementation has special handling of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-		// Postgres uses ILIKE
-		boolean likeIgnoreCase = (node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE);
-
-		if (likeIgnoreCase) {
-			try {
-				if (matchingObject) {
-					appendObjectMatch();
-				}
-
-				if (PatternMatchNode.class.isAssignableFrom(node.getClass())) {
-					appendLikeEscapeCharacter((PatternMatchNode) node);
-				}
-
-				if (parenthesisNeeded(node, parentNode)) {
-					out.append(')');
-				}
-			} catch (IOException ioex) {
-				throw new CayenneRuntimeException("Error appending content", ioex);
-			}
-		} else {
-			super.endNode(node, parentNode);
-		}
-	}
-
-	@Override
-	public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
-		if (!hasMoreChildren) {
-			return;
-		}
-
-		try {
-			// use ILIKE
-			switch (node.getType()) {
-				case Expression.LIKE_IGNORE_CASE:
-					finishedChildNodeAppendExpression(node, " ILIKE ");
-					break;
-				case Expression.NOT_LIKE_IGNORE_CASE:
-					finishedChildNodeAppendExpression(node, " NOT ILIKE ");
-					break;
-				default:
-					super.finishedChild(node, childIndex, hasMoreChildren);
-			}
-		} catch (IOException ioex) {
-			throw new CayenneRuntimeException("Error appending content", ioex);
-		}
-	}
-
-	private void finishedChildNodeAppendExpression(Expression node, String operation) throws IOException {
-		Appendable buf = matchingObject ? new StringBuilder() : this.out;
-		buf.append(operation);
-		if (matchingObject) {
-			objectMatchTranslator.setOperation(buf.toString());
-			objectMatchTranslator.setExpression(node);
-		}
-	}
-
-    /**
-     * @since 4.0
-     */
-	@Override
-	protected void appendFunction(ASTFunctionCall functionExpression) {
-		if("LOCATE".equals(functionExpression.getFunctionName())) {
-			out.append("POSITION");
-		} else {
-			super.appendFunction(functionExpression);
-		}
-	}
-
-    /**
-     * @since 4.0
-     */
-	@Override
-	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
-		if("LOCATE".equals(functionExpression.getFunctionName())) {
-			out.append(" in ");
-		} else {
-			super.appendFunctionArgDivider(functionExpression);
-		}
-	}
-
-    /**
-     * @since 4.0
-     */
-	@Override
-	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
-		if("LOCATE".equals(functionExpression.getFunctionName())) {
-			out.delete(out.length() - " in ".length(), out.length());
-		} else {
-			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("EXTRACT(");
-		switch (functionExpression.getPart()) {
-			case DAY_OF_MONTH:
-				out.append("day");
-				break;
-			case DAY_OF_WEEK:
-				out.append("dow");
-				break;
-			case DAY_OF_YEAR:
-				out.append("doy");
-				break;
-			default:
-				out.append(functionExpression.getPartCamelCaseName());
-		}
-
-		out.append(" FROM ");
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectTranslator.java
deleted file mode 100644
index 8d1eded..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectTranslator.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.postgres;
-
-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;
-
-/**
- * @since 1.2
- */
-class PostgresSelectTranslator extends DefaultSelectTranslator {
-
-	/**
-	 * @since 4.0
-	 */
-	public PostgresSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
-		super(query, adapter, entityResolver);
-	}
-
-	@Override
-	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-
-		// limit results
-		int offset = queryMetadata.getFetchOffset();
-		int limit = queryMetadata.getFetchLimit();
-
-		if (limit > 0) {
-			buffer.append(" LIMIT ").append(limit);
-		}
-
-		if (offset > 0) {
-			buffer.append(" OFFSET ").append(offset);
-		}
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PositionFunctionNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PositionFunctionNode.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PositionFunctionNode.java
new file mode 100644
index 0000000..39c1df7
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PositionFunctionNode.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.postgres.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+
+/**
+ * @since 4.2
+ */
+public class PositionFunctionNode extends FunctionNode {
+    public PositionFunctionNode(String alias) {
+        super("POSITION", alias, true);
+    }
+
+    @Override
+    public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
+        buffer.append(" IN ");
+    }
+
+    @Override
+    public Node copy() {
+        return new PositionFunctionNode(getAlias());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresExtractFunctionNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresExtractFunctionNode.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresExtractFunctionNode.java
new file mode 100644
index 0000000..ff4224b
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresExtractFunctionNode.java
@@ -0,0 +1,59 @@
+/*****************************************************************
+ *   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.postgres.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+
+/**
+ * @since 4.2
+ */
+public class PostgresExtractFunctionNode extends Node {
+    private final String functionName;
+
+    public PostgresExtractFunctionNode(String functionName) {
+        this.functionName = functionName;
+    }
+
+    @Override
+    public QuotingAppendable append(QuotingAppendable buffer) {
+        buffer.append(" EXTRACT(");
+        if ("DAY_OF_MONTH".equals(functionName)) {
+            buffer.append("day");
+        } else if ("DAY_OF_WEEK".equals(functionName)) {
+            buffer.append("dow");
+        } else if ("DAY_OF_YEAR".equals(functionName)) {
+            buffer.append("doy");
+        } else {
+            buffer.append(functionName);
+        }
+        return buffer.append(" FROM ");
+    }
+
+    @Override
+    public void appendChildrenEnd(QuotingAppendable buffer) {
+        buffer.append(")");
+    }
+
+    @Override
+    public Node copy() {
+        return new PostgresExtractFunctionNode(functionName);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresLikeNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresLikeNode.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresLikeNode.java
new file mode 100644
index 0000000..4f0e236
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresLikeNode.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.postgres.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+
+/**
+ * @since 4.2
+ */
+public class PostgresLikeNode extends LikeNode {
+    public PostgresLikeNode(boolean isNot, char escape) {
+        super(false, isNot, escape);
+    }
+
+    @Override
+    public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
+        if (not) {
+            buffer.append(" NOT");
+        }
+        buffer.append(" ILIKE ");
+    }
+
+    @Override
+    public Node copy() {
+        return new PostgresLikeNode(isNot(), getEscape());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresLimitOffsetNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresLimitOffsetNode.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresLimitOffsetNode.java
new file mode 100644
index 0000000..045091a
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/sqltree/PostgresLimitOffsetNode.java
@@ -0,0 +1,60 @@
+/*****************************************************************
+ *   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.postgres.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+
+/**
+ * @since 4.2
+ */
+public class PostgresLimitOffsetNode extends LimitOffsetNode {
+
+    public PostgresLimitOffsetNode(LimitOffsetNode node) {
+        super(node.getLimit(), node.getOffset());
+    }
+
+    private PostgresLimitOffsetNode(int limit, int offset) {
+        super(limit, offset);
+    }
+
+    @Override
+    public QuotingAppendable append(QuotingAppendable buffer) {
+        if(limit == 0 && offset == 0) {
+            return buffer;
+        }
+
+        if(limit > 0) {
+            buffer.append(" LIMIT ").append(limit);
+        }
+
+        if(offset > 0) {
+            buffer.append(" OFFSET ").append(offset);
+        }
+
+        return buffer;
+    }
+
+    @Override
+    public Node copy() {
+        return new PostgresLimitOffsetNode(limit, offset);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteActionBuilder.java
----------------------------------------------------------------------
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 5f5cdeb..c4cc53c 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
@@ -22,6 +22,7 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.dba.JdbcActionBuilder;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.query.SQLTemplate;
+import org.apache.cayenne.query.SelectQuery;
 
 /**
  * @since 3.0
@@ -36,4 +37,12 @@ class SQLiteActionBuilder extends JdbcActionBuilder {
     public SQLAction sqlAction(SQLTemplate query) {
         return new SQLiteSQLTemplateAction(query, dataNode);
     }
+
+    /**
+     * @since 4.1
+     */
+    @Override
+    public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
+        return new SQLiteSelectAction(query, dataNode);
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
index 2542282..20943ee 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
@@ -19,8 +19,7 @@
 package org.apache.cayenne.dba.sqlite;
 
 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.sqlbuilder.sqltree.Node;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
@@ -36,11 +35,11 @@ import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.resource.ResourceLocator;
 
-import java.sql.Types;
 import java.util.Calendar;
 import java.util.Collection;
 import java.util.GregorianCalendar;
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * A SQLite database adapter that works with Zentus JDBC driver. See
@@ -87,11 +86,11 @@ public class SQLiteAdapter extends JdbcAdapter {
     }
 
     /**
-     * @since 4.0
+     * @since 4.2
      */
     @Override
-    public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-        return new SQLiteQualifierTranslator(queryAssembler);
+    public Function<Node, Node> getSqlTreeProcessor() {
+        return new SQLiteTreeProcessor();
     }
 
     @Override
@@ -112,18 +111,6 @@ public class SQLiteAdapter extends JdbcAdapter {
         return query.createSQLAction(new SQLiteActionBuilder(node));
     }
 
-    private int mapNTypes(int sqlType) {
-        switch (sqlType) {
-            case Types.NCHAR : return Types.CHAR;
-            case Types.NCLOB : return Types.CLOB;
-            case Types.NVARCHAR : return Types.VARCHAR;
-            case Types.LONGNVARCHAR : return Types.LONGVARCHAR;
-
-            default:
-                return sqlType;
-        }
-    }
-
     /**
      * Appends AUTOINCREMENT clause to the column definition for generated columns.
      */

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteQualifierTranslator.java
deleted file mode 100644
index d31019b..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteQualifierTranslator.java
+++ /dev/null
@@ -1,162 +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.sqlite;
-
-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;
-import org.apache.cayenne.exp.parser.Node;
-
-/**
- * @since 4.0
- */
-public class SQLiteQualifierTranslator extends QualifierTranslator {
-
-    public SQLiteQualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler);
-    }
-
-    @Override
-    public void endNode(Expression node, Expression parentNode) {
-        super.endNode(node, parentNode);
-        if(node.getType() == Expression.FUNCTION_CALL) {
-            if("LOCATE".equals(((ASTFunctionCall)node).getFunctionName())) {
-                // order of args in INSTR is different, so swap them back
-                swapNodeChildren((ASTFunctionCall)node, 0, 1);
-            }
-        }
-    }
-
-    @Override
-    protected void appendFunction(ASTFunctionCall functionExpression) {
-        switch (functionExpression.getFunctionName()) {
-            case "MOD":
-            case "CONCAT":
-                // noop
-                break;
-            case "SUBSTRING":
-                out.append("SUBSTR");
-                break;
-            case "LOCATE":
-                // LOCATE(substr, str) -> INSTR(str, substr)
-                out.append("INSTR");
-                swapNodeChildren(functionExpression, 0, 1);
-                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":
-                out.delete(out.length() - 3, out.length());
-                break;
-            case "CONCAT":
-                out.delete(out.length() - 4, out.length());
-                break;
-            default:
-                super.clearLastFunctionArgDivider(functionExpression);
-        }
-        if(functionExpression instanceof ASTExtract) {
-            out.append(") as integer)");
-        }
-    }
-
-    @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);
-    }
-
-
-    /**
-     * Translates to cast(strftime('format', column) as integer).
-     * Depends on connection property "date_class", can be set in connection URL (date_class=text).
-     *
-     * https://www.sqlite.org/lang_datefunc.html
-     */
-    @Override
-    protected void appendExtractFunction(ASTExtract functionExpression) {
-        out.append("cast(strftime(");
-
-        switch (functionExpression.getPart()) {
-            case YEAR:
-                out.append("'%Y'");
-                break;
-            case MONTH:
-                out.append("'%m'");
-                break;
-            case WEEK:
-                out.append("'%W'");
-                break;
-            case DAY:
-            case DAY_OF_MONTH:
-                out.append("'%d'");
-                break;
-            case DAY_OF_WEEK:
-                out.append("'%w'");
-                break;
-            case DAY_OF_YEAR:
-                out.append("'%j'");
-                break;
-            case HOUR:
-                out.append("'%H'");
-                break;
-            case MINUTE:
-                out.append("'%M'");
-                break;
-            case SECOND:
-                out.append("'%S'");
-                break;
-        }
-
-        out.append(", ");
-    }
-
-    private void swapNodeChildren(Node node, int i, int j) {
-        Node ni = node.jjtGetChild(i);
-        Node nj = node.jjtGetChild(j);
-        node.jjtAddChild(ni, j);
-        node.jjtAddChild(nj, i);
-    }
-}


[05/11] cayenne git commit: CAY-2465 New SelectTranslator implementation

Posted by nt...@apache.org.
http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdSQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdSQLTreeProcessor.java
new file mode 100644
index 0000000..e97fd2e
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdSQLTreeProcessor.java
@@ -0,0 +1,189 @@
+/*****************************************************************
+ *   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.firebird;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.cayenne.access.sqlbuilder.ExpressionNodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.InNode;
+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.ValueNode;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+import org.apache.cayenne.dba.firebird.sqltree.FirebirdLimitNode;
+import org.apache.cayenne.dba.firebird.sqltree.FirebirdSubstringFunctionNode;
+import org.apache.cayenne.util.ArrayUtil;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.exp;
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.node;
+
+/**
+ * @since 4.2
+ */
+public class FirebirdSQLTreeProcessor extends BaseSQLTreeProcessor {
+
+    private static final int FIREBIRD_IN_BATCH_SIZE = 1500;
+
+    @Override
+    protected void onValueNode(Node parent, ValueNode child, int index) {
+        replaceChild(parent, index, new ValueNode(child.getValue(), child.isArray(), child.getAttribute()) {
+            @Override
+            protected void appendStringValue(QuotingAppendable buffer, CharSequence value) {
+                buffer.append("CAST(");
+                super.appendStringValue(buffer, value);
+                buffer.append(" AS VARCHAR(").append(value.length()).append("))");
+            }
+        });
+    }
+
+    @Override
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+        if(child.getLimit() == 0 && child.getOffset() == 0) {
+            return;
+        }
+        int from = child.getOffset() + 1;
+        int to = child.getLimit() == 0 ? Integer.MAX_VALUE : from + child.getLimit();
+        replaceChild(parent, index, new FirebirdLimitNode(from, to));
+    }
+
+    @Override
+    protected void onInNode(Node parent, InNode child, int index) {
+        Node arg = child.getChild(0);
+        Node childNode = child.getChild(1);
+        if(childNode.getType() != NodeType.VALUE) {
+            return;
+        }
+
+        ValueNode valueNode = (ValueNode)childNode;
+        Object value = valueNode.getValue();
+        if(!value.getClass().isArray()) {
+            return;
+        }
+
+        List<Node> newChildren = new ArrayList<>();
+
+        // need to slice for batches of 1500 values
+        if(value instanceof Object[]) {
+            for(Object[] slice : ArrayUtil.sliceArray((Object[])value, FIREBIRD_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof int[]) {
+            for(int[] slice : ArrayUtil.sliceArray((int[])value, FIREBIRD_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof long[]) {
+            for(long[] slice : ArrayUtil.sliceArray((long[])value, FIREBIRD_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof float[]) {
+            for(float[] slice : ArrayUtil.sliceArray((float[])value, FIREBIRD_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof double[]) {
+            for(double[] slice : ArrayUtil.sliceArray((double[])value, FIREBIRD_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof short[]) {
+            for(short[] slice : ArrayUtil.sliceArray((short[])value, FIREBIRD_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof char[]) {
+            for(char[] slice : ArrayUtil.sliceArray((char[])value, FIREBIRD_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof boolean[]) {
+            for(boolean[] slice : ArrayUtil.sliceArray((boolean[])value, FIREBIRD_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        } else if(value instanceof byte[]) {
+            for(byte[] slice : ArrayUtil.sliceArray((byte[])value, FIREBIRD_IN_BATCH_SIZE)) {
+                newChildren.add(newSliceNode(child, arg, valueNode, slice));
+            }
+        }
+
+        ExpressionNodeBuilder exp = exp(node(newChildren.get(0)));
+        for(int i=1; i<newChildren.size(); i++) {
+            exp = exp.or(node(newChildren.get(i)));
+        }
+        parent.replaceChild(index, exp.build());
+    }
+
+    private InNode newSliceNode(InNode child, Node arg, ValueNode valueNode, Object slice) {
+        InNode nextNode = new InNode(child.isNot());
+        nextNode.addChild(arg.deepCopy());
+        nextNode.addChild(new ValueNode(slice, valueNode.isArray(), valueNode.getAttribute()));
+        return nextNode;
+    }
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        switch (child.getFunctionName()) {
+            case "LENGTH":
+                replaceChild(parent, index, new FunctionNode("CHAR_LENGTH", child.getAlias()));
+                break;
+            case "LOCATE":
+                replaceChild(parent, index, new FunctionNode("POSITION", child.getAlias()));
+                break;
+            case "CONCAT":
+                replaceChild(parent, index, new OpExpressionNode("||"));
+                break;
+
+            case "SUBSTRING":
+                replaceChild(parent, index, new FirebirdSubstringFunctionNode(child.getAlias()));
+                break;
+
+            case "YEAR":
+            case "MONTH":
+            case "DAY":
+            case "DAY_OF_MONTH":
+            case "DAY_OF_WEEK":
+            case "DAY_OF_YEAR":
+            case "WEEK":
+            case "HOUR":
+            case "MINUTE":
+            case "SECOND":
+                Node functionReplacement = new FunctionNode("EXTRACT", child.getAlias(), true) {
+                    @Override
+                    public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
+                        buffer.append(' ');
+                    }
+                };
+
+                String partName = child.getFunctionName();
+                if("DAY_OF_MONTH".equals(partName)) {
+                    partName = "DAY";
+                } else if("DAY_OF_WEEK".equals(partName)) {
+                    partName = "WEEKDAY";
+                } else if("DAY_OF_YEAR".equals(partName)) {
+                    partName = "YEARDAY";
+                }
+                functionReplacement.addChild(new TextNode(partName + " FROM "));
+                replaceChild(parent, index, functionReplacement);
+                break;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdSelectAction.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..184a909
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdSelectAction.java
@@ -0,0 +1,40 @@
+/*****************************************************************
+ *   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.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;
+
+/**
+ * @since 4.1
+ */
+public class FirebirdSelectAction extends SelectAction {
+
+    public FirebirdSelectAction(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/firebird/sqltree/FirebirdLimitNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/sqltree/FirebirdLimitNode.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/sqltree/FirebirdLimitNode.java
new file mode 100644
index 0000000..9572e79
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/sqltree/FirebirdLimitNode.java
@@ -0,0 +1,50 @@
+/*****************************************************************
+ *   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.firebird.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+
+/**
+ * @since 4.2
+ */
+public class FirebirdLimitNode extends Node {
+    private final int from;
+    private final int to;
+
+    public FirebirdLimitNode(int from, int to) {
+        this.from = from;
+        this.to = to;
+    }
+
+    @Override
+    public QuotingAppendable append(QuotingAppendable buffer) {
+        buffer.append(" ROWS ");
+        if(from > 0) {
+            buffer.append(from).append(" TO ");
+        }
+        return buffer.append(to);
+    }
+
+    @Override
+    public Node copy() {
+        return new FirebirdLimitNode(from, to);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/sqltree/FirebirdSubstringFunctionNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/sqltree/FirebirdSubstringFunctionNode.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/sqltree/FirebirdSubstringFunctionNode.java
new file mode 100644
index 0000000..f7fe3e1
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/sqltree/FirebirdSubstringFunctionNode.java
@@ -0,0 +1,65 @@
+/*****************************************************************
+ *   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.firebird.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+
+/**
+ * SUBSTRING function for Firebird
+ *
+ * It has following format:
+ *
+ * SUBSTRING (string FROM CAST(? AS INTEGER) FOR CAST(? AS INTEGER))
+ *
+ * @since 4.1
+ */
+public class FirebirdSubstringFunctionNode extends FunctionNode {
+    public FirebirdSubstringFunctionNode(String alias) {
+        super("SUBSTRING", alias);
+    }
+
+    @Override
+    public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
+        if(skipContent()) {
+            return;
+        }
+        if(childIdx == 0) {
+            buffer.append(" FROM CAST(");
+        } else if(childIdx == 1) {
+            buffer.append(" AS INTEGER) FOR CAST(");
+        }
+    }
+
+    @Override
+    public void appendChildrenEnd(QuotingAppendable buffer) {
+        if(skipContent()) {
+            return;
+        }
+        buffer.append(" AS INTEGER)");
+        super.appendChildrenEnd(buffer);
+    }
+
+    @Override
+    public Node copy() {
+        return new FirebirdSubstringFunctionNode(getAlias());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java
index 69b9876..76d1db6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java
@@ -20,9 +20,7 @@
 package org.apache.cayenne.dba.frontbase;
 
 import org.apache.cayenne.CayenneRuntimeException;
-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.ExtendedTypeMap;
@@ -36,8 +34,6 @@ import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.resource.ResourceLocator;
 
 import java.sql.Types;
@@ -45,6 +41,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * DbAdapter implementation for <a href="http://www.frontbase.com/">FrontBase
@@ -79,20 +76,12 @@ public class FrontBaseAdapter extends JdbcAdapter {
 		setSupportsBatchUpdates(true);
 	}
 
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	public SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver) {
-		return new FrontBaseSelectTranslator(query, this, entityResolver);
-	}
-
-	/**
-	 * @since 4.0
-	 */
+    /**
+     * @since 4.2
+     */
 	@Override
-	public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-		return new FrontBaseQualifierTranslator(queryAssembler);
+	public Function<Node, Node> getSqlTreeProcessor() {
+		return new FrontBaseSQLTreeProcessor();
 	}
 
 	@Override

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseQualifierTranslator.java
deleted file mode 100644
index fa97c6d..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseQualifierTranslator.java
+++ /dev/null
@@ -1,134 +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.frontbase;
-
-import org.apache.cayenne.CayenneRuntimeException;
-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 FrontBaseQualifierTranslator extends QualifierTranslator {
-
-    private int substringArg = 0;
-
-    public FrontBaseQualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler);
-    }
-
-    @Override
-    protected void appendFunction(ASTFunctionCall functionExpression) {
-        switch (functionExpression.getFunctionName()) {
-            case "CONCAT":
-                // noop
-                break;
-            case "LOCATE":
-                out.append("POSITION");
-                break;
-            case "LENGTH":
-                out.append("CHAR_LENGTH");
-                break;
-            case "SUBSTRING":
-                substringArg = 0;
-            default:
-                super.appendFunction(functionExpression);
-        }
-    }
-
-    @Override
-    protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
-        switch (functionExpression.getFunctionName()) {
-            case "CONCAT":
-                out.append(" || ");
-                break;
-            case "LOCATE":
-                out.append(" IN ");
-                break;
-            case "SUBSTRING":
-                // SUBSTRING (str FROM offset FOR length)
-                switch (substringArg++) {
-                    case 0:
-                        out.append(" FROM ");
-                        break;
-                    case 1:
-                        out.append(" FOR ");
-                        break;
-                }
-                break;
-            default:
-                super.appendFunctionArgDivider(functionExpression);
-        }
-    }
-
-    @Override
-    protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
-        switch (functionExpression.getFunctionName()) {
-            case "CONCAT":
-                out.delete(out.length() - " || ".length(), out.length());
-                break;
-            case "LOCATE":
-                out.delete(out.length() - " IN ".length(), out.length());
-                break;
-            case "SUBSTRING":
-                // no offset arg
-                if(substringArg == 2) {
-                    out.delete(out.length() - " FOR ".length(), 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("EXTRACT(");
-        switch (functionExpression.getPart()) {
-            case DAY_OF_WEEK:
-            case DAY_OF_YEAR:
-            case WEEK:
-                throw new CayenneRuntimeException("Function %s() is unsupported in FrontBase.", functionExpression.getPartCamelCaseName());
-            case DAY_OF_MONTH:
-                out.append("DAY");
-                break;
-            default:
-                out.append(functionExpression.getPart().name());
-        }
-        out.append(" FROM ");
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSQLTreeProcessor.java
new file mode 100644
index 0000000..4fce3d7
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSQLTreeProcessor.java
@@ -0,0 +1,104 @@
+/*****************************************************************
+ *   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.frontbase;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+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;
+
+/**
+ * @since 4.2
+ */
+public class FrontBaseSQLTreeProcessor extends BaseSQLTreeProcessor {
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        switch (child.getFunctionName()) {
+            case "CONCAT":
+                replaceChild(parent, index, new OpExpressionNode("||"));
+                break;
+            case "LOCATE":
+                // POSITION (substr IN str)
+                replaceChild(parent, index, new FunctionNode("POSITION", child.getAlias()) {
+                    @Override
+                    public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
+                        buffer.append(" IN ");
+                    }
+                });
+                break;
+            case "LENGTH":
+                replaceChild(parent, index, new FunctionNode("CHAR_LENGTH", child.getAlias()));
+                break;
+            case "SUBSTRING":
+                // SUBSTRING (str FROM offset FOR length)
+                replaceChild(parent, index, new FunctionNode("SUBSTRING", child.getAlias()){
+                    @Override
+                    public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
+                        if(childIdx == 0) {
+                            buffer.append(" FROM ");
+                        } else if(childIdx == 1) {
+                            buffer.append(" FOR ");
+                        }
+                    }
+                });
+                break;
+            case "YEAR":
+            case "MONTH":
+            case "DAY":
+            case "DAY_OF_MONTH":
+            case "HOUR":
+            case "MINUTE":
+            case "SECOND":
+                Node functionReplacement = new ExtractFunctionNode(child.getAlias());
+                String functionName = child.getFunctionName();
+                if("DAY_OF_MONTH".equals(functionName)) {
+                    functionName = "DAY";
+                }
+                functionReplacement.addChild(new TextNode(functionName));
+                replaceChild(parent, index, functionReplacement);
+                break;
+
+            case "DAY_OF_WEEK":
+            case "DAY_OF_YEAR":
+            case "WEEK":
+                throw new CayenneRuntimeException("Function %s() is unsupported in FrontBase.", child.getFunctionName());
+        }
+    }
+
+    private static class ExtractFunctionNode extends FunctionNode {
+        public ExtractFunctionNode(String alias) {
+            super("EXTRACT", alias, true);
+        }
+
+        @Override
+        public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
+            buffer.append(" FROM ");
+        }
+
+        @Override
+        public Node copy() {
+            return new ExtractFunctionNode(getAlias());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSelectTranslator.java
deleted file mode 100644
index 1e71f5f..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseSelectTranslator.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.frontbase;
-
-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;
-
-/**
- * @since 1.2
- */
-class FrontBaseSelectTranslator extends DefaultSelectTranslator {
-
-	static final String SELECT_PREFIX = "SELECT";
-
-	/**
-	 * @since 4.0
-	 */
-	public FrontBaseSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
-		super(query, adapter, entityResolver);
-	}
-
-	@Override
-	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-
-		int limit = queryMetadata.getFetchLimit();
-		if (limit > 0 && buffer.length() > SELECT_PREFIX.length()) {
-
-			if (SELECT_PREFIX.equals(buffer.substring(0, SELECT_PREFIX.length()))) {
-				buffer.insert(SELECT_PREFIX.length(), " TOP " + limit);
-			}
-		}
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2ActionBuilder.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..2cf2a10
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2ActionBuilder.java
@@ -0,0 +1,39 @@
+/*****************************************************************
+ *   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.h2;
+
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.dba.JdbcActionBuilder;
+import org.apache.cayenne.query.SQLAction;
+import org.apache.cayenne.query.SelectQuery;
+
+/**
+ * @since 4.1
+ */
+public class H2ActionBuilder extends JdbcActionBuilder {
+    public H2ActionBuilder(DataNode node) {
+        super(node);
+    }
+
+    @Override
+    public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
+        return new H2SelectAction(query, dataNode);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java
index 6b47636..39cc43c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.dba.h2;
 
+import org.apache.cayenne.access.DataNode;
+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;
@@ -28,9 +30,12 @@ import org.apache.cayenne.dba.JdbcAdapter;
 import org.apache.cayenne.dba.PkGenerator;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.resource.ResourceLocator;
 
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * DbAdapter implementation for <a href="http://www.h2database.com/">H2
@@ -66,6 +71,19 @@ public class H2Adapter extends JdbcAdapter {
         }
     }
 
+    /**
+     * @since 4.2
+     */
+    @Override
+    public Function<Node, Node> getSqlTreeProcessor() {
+        return new H2SQLTreeProcessor();
+    }
+
+    @Override
+    public SQLAction getAction(Query query, DataNode node) {
+        return query.createSQLAction(new H2ActionBuilder(node));
+    }
+
     @Override
     protected PkGenerator createPkGenerator() {
         return new H2PkGenerator(this);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2SQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2SQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2SQLTreeProcessor.java
new file mode 100644
index 0000000..cd797f5
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2SQLTreeProcessor.java
@@ -0,0 +1,37 @@
+/*****************************************************************
+ *   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.h2;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OffsetFetchNextNode;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+
+/**
+ * @since 4.2
+ */
+public class H2SQLTreeProcessor extends BaseSQLTreeProcessor {
+
+    @Override
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+        replaceChild(parent, index, new OffsetFetchNextNode(child), false);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2SelectAction.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..df7380d
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2SelectAction.java
@@ -0,0 +1,39 @@
+/*****************************************************************
+ *   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.h2;
+
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.jdbc.SelectAction;
+import org.apache.cayenne.query.SelectQuery;
+
+/**
+ * @since 4.1
+ */
+public class H2SelectAction extends SelectAction {
+
+    public H2SelectAction(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/hsqldb/HSQLDBAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
index 64c59b3..99f4ae0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
@@ -21,12 +21,9 @@ package org.apache.cayenne.dba.hsqldb;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
-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.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
@@ -40,18 +37,15 @@ import org.apache.cayenne.map.DbAttribute;
 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.Query;
 import org.apache.cayenne.query.SQLAction;
-import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.resource.ResourceLocator;
 
-import java.sql.PreparedStatement;
-import java.sql.SQLException;
 import java.sql.Types;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * DbAdapter implementation for the <a href="http://hsqldb.sourceforge.net/">
@@ -91,22 +85,11 @@ public class HSQLDBAdapter extends JdbcAdapter {
 	}
 
 	/**
-	 * @since 4.0
-	 */
-	@Override
-	public SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver) {
-		return new HSQLSelectTranslator(query, this, entityResolver);
-	}
-
-	/**
-	 * Returns a trimming translator.
-	 * @since 4.0
+	 * @since 4.2
 	 */
 	@Override
-	public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-		QualifierTranslator translator = new HSQLQualifierTranslator(queryAssembler);
-		translator.setCaseInsensitive(caseInsensitiveCollations);
-		return translator;
+	public Function<Node, Node> getSqlTreeProcessor() {
+		return new HSQLTreeProcessor();
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLQualifierTranslator.java
deleted file mode 100644
index fce980c..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLQualifierTranslator.java
+++ /dev/null
@@ -1,86 +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.hsqldb;
-
-import java.io.IOException;
-
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.access.translator.select.TrimmingQualifierTranslator;
-import org.apache.cayenne.exp.parser.ASTExtract;
-import org.apache.cayenne.exp.parser.ASTFunctionCall;
-import org.apache.cayenne.exp.parser.PatternMatchNode;
-
-/**
- * @since 4.0
- */
-public class HSQLQualifierTranslator extends TrimmingQualifierTranslator {
-
-    public HSQLQualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler, HSQLDBAdapter.TRIM_FUNCTION);
-    }
-
-    @Override
-    protected void appendLikeEscapeCharacter(PatternMatchNode patternMatchNode)
-            throws IOException {
-
-        char escapeChar = patternMatchNode.getEscapeChar();
-
-        if ('?' == escapeChar) {
-            throw new CayenneRuntimeException("the escape character of '?' is illegal for LIKE clauses.");
-        }
-
-        if (0 != escapeChar) {
-            // this is a difference with super implementation - HSQL driver seems does not
-            // support JDBC escape syntax, so creating an explicit SQL escape:
-            out.append(" ESCAPE '");
-            out.append(escapeChar);
-            out.append("'");
-        }
-    }
-
-    @Override
-    protected void appendFunction(ASTFunctionCall functionExpression) {
-        // from documentation:
-        // CURRENT_TIME returns a value of TIME WITH TIME ZONE type.
-        // LOCALTIME returns a value of TIME type.
-        // CURTIME() is a synonym for LOCALTIME.
-        // use LOCALTIME to better align with other DBs
-        if("CURRENT_TIME".equals(functionExpression.getFunctionName())) {
-            out.append("LOCALTIME");
-        } else {
-            super.appendFunction(functionExpression);
-        }
-    }
-
-    @Override
-    protected void appendExtractFunction(ASTExtract functionExpression) {
-        switch (functionExpression.getPart()) {
-            case DAY_OF_WEEK:
-            case DAY_OF_MONTH:
-            case DAY_OF_YEAR:
-                // hsqldb variants are without '_'
-                out.append(functionExpression.getPart().name().replace("_", ""));
-                break;
-            default:
-                appendFunction(functionExpression);
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectTranslator.java
deleted file mode 100644
index ce3bb58..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLSelectTranslator.java
+++ /dev/null
@@ -1,57 +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.hsqldb;
-
-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;
-
-/**
- * @since 1.2
- */
-class HSQLSelectTranslator extends DefaultSelectTranslator {
-
-	/**
-	 * @since 4.0
-	 */
-	public HSQLSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
-		super(query, adapter, entityResolver);
-	}
-
-	@Override
-	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-		int offset = queryMetadata.getFetchOffset();
-		int limit = queryMetadata.getFetchLimit();
-
-		if (offset > 0 || limit > 0) {
-			buffer.append(" LIMIT ");
-
-			// both OFFSET and LIMIT must be present, so come up with defaults
-			// if one of
-			// them is not set by the user
-			if (limit == 0) {
-				limit = Integer.MAX_VALUE;
-			}
-
-			buffer.append(limit).append(" OFFSET ").append(offset);
-		}
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLTreeProcessor.java
new file mode 100644
index 0000000..152f3ca
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLTreeProcessor.java
@@ -0,0 +1,70 @@
+/*****************************************************************
+ *   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.hsqldb;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TrimmingColumnNode;
+
+/**
+ * @since 4.2
+ */
+public class HSQLTreeProcessor extends BaseSQLTreeProcessor {
+
+    @Override
+    protected void onColumnNode(Node parent, ColumnNode child, int index) {
+        replaceChild(parent, index, new TrimmingColumnNode(child));
+    }
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        Node replacement = getReplacementForFunction(child);
+        if(replacement != null) {
+            replaceChild(parent, index, replacement);
+        }
+    }
+
+    private Node getReplacementForFunction(FunctionNode child) {
+        switch (child.getFunctionName()) {
+            case "DAY_OF_MONTH":
+            case "DAY_OF_WEEK":
+            case "DAY_OF_YEAR":
+                // hsqldb variants are without '_'
+                return new FunctionNode(child.getFunctionName().replace("_", ""), child.getAlias(), true);
+
+            case "CURRENT_DATE":
+            case "CURRENT_TIMESTAMP":
+                return new FunctionNode(child.getFunctionName(), child.getAlias(), false);
+            case "CURRENT_TIME":
+                // from documentation:
+                // CURRENT_TIME returns a value of TIME WITH TIME ZONE type.
+                // LOCALTIME returns a value of TIME type.
+                // CURTIME() is a synonym for LOCALTIME.
+                // use LOCALTIME to better align with other DBs
+                return new FunctionNode("LOCALTIME", child.getAlias(), false);
+            case "CONCAT":
+                return new OpExpressionNode("||");
+        }
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
index 60a39b6..08c156d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
@@ -21,11 +21,8 @@ package org.apache.cayenne.dba.ingres;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
 import org.apache.cayenne.access.translator.ParameterBinding;
-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.translator.select.TrimmingQualifierTranslator;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
@@ -37,16 +34,15 @@ import org.apache.cayenne.dba.PkGenerator;
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
-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;
 
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.sql.Types;
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * DbAdapter implementation for <a
@@ -75,17 +71,12 @@ public class IngresAdapter extends JdbcAdapter {
 		setSupportsGeneratedKeys(true);
 	}
 
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	public SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver) {
-		return new IngresSelectTranslator(query, this, entityResolver);
-	}
-
+    /**
+     * @since 4.2
+     */
 	@Override
-	public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-		return new IngresQualifierTranslator(queryAssembler);
+	public Function<Node, Node> getSqlTreeProcessor() {
+		return new IngressSQLTreeProcessor();
 	}
 
 	@Override

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresQualifierTranslator.java
deleted file mode 100644
index 8334ff4..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresQualifierTranslator.java
+++ /dev/null
@@ -1,122 +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.ingres;
-
-import java.io.IOException;
-
-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.exp.parser.Node;
-
-/**
- * @since 4.0
- */
-class IngresQualifierTranslator extends TrimmingQualifierTranslator {
-
-    IngresQualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler, IngresAdapter.TRIM_FUNCTION);
-    }
-
-    @Override
-    public void endNode(Expression node, Expression parentNode) {
-        super.endNode(node, parentNode);
-        if(node.getType() == Expression.FUNCTION_CALL) {
-            if("LOCATE".equals(((ASTFunctionCall)node).getFunctionName())) {
-                // order of args in ingres version of LOCATE is different, so swap them back
-                swapNodeChildren((ASTFunctionCall)node, 0, 1);
-            }
-        }
-    }
-
-    @Override
-    protected void appendFunction(ASTFunctionCall functionExpression) {
-        if("CONCAT".equals(functionExpression.getFunctionName())) {
-            // noop
-        } else if("LOCATE".equals(functionExpression.getFunctionName())) {
-            // order of args in ingres version of LOCATE is different
-            // LOCATE(substr, str) -> LOCATE(str, substr)
-            out.append("LOCATE");
-            swapNodeChildren(functionExpression, 0, 1);
-        } else if("TRIM".equals(functionExpression.getFunctionName())) {
-            // simple TRIM removes only trailing spaces
-            out.append("LTRIM(RTRIM");
-        } else {
-            super.appendFunction(functionExpression);
-        }
-    }
-
-    @Override
-    protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
-        if("CONCAT".equals(functionExpression.getFunctionName())) {
-            out.append(" + ");
-        } else {
-            super.appendFunctionArgDivider(functionExpression);
-        }
-    }
-
-    @Override
-    protected void appendFunctionArg(Object value, ASTFunctionCall functionExpression) throws IOException {
-        if("SUBSTRING".equals(functionExpression.getFunctionName())) {
-            out.append("CAST(");
-            super.appendFunctionArg(value, functionExpression);
-            clearLastFunctionArgDivider(functionExpression);
-            out.append(" AS INTEGER)");
-            appendFunctionArgDivider(functionExpression);
-        } else {
-            super.appendFunctionArg(value, functionExpression);
-        }
-    }
-
-    @Override
-    protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
-        if("CONCAT".equals(functionExpression.getFunctionName())) {
-            out.delete(out.length() - " + ".length(), out.length());
-        } else {
-            super.clearLastFunctionArgDivider(functionExpression);
-            if("TRIM".equals(functionExpression.getFunctionName())) {
-                out.append(")");
-            }
-        }
-    }
-
-    @Override
-    protected void appendExtractFunction(ASTExtract functionExpression) {
-        switch (functionExpression.getPart()) {
-            case DAY_OF_WEEK:
-            case DAY_OF_MONTH:
-            case DAY_OF_YEAR:
-                // ingres variants are without '_'
-                out.append(functionExpression.getPart().name().replace("_", ""));
-                break;
-            default:
-                appendFunction(functionExpression);
-        }
-    }
-
-    private void swapNodeChildren(Node node, int i, int j) {
-        Node ni = node.jjtGetChild(i);
-        Node nj = node.jjtGetChild(j);
-        node.jjtAddChild(ni, j);
-        node.jjtAddChild(nj, i);
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectTranslator.java
deleted file mode 100644
index f466d5f..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresSelectTranslator.java
+++ /dev/null
@@ -1,49 +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.ingres;
-
-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 IngresSelectTranslator extends DefaultSelectTranslator {
-    
-    /**
-     * @since 4.0
-     */
-    public IngresSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
-        super(query, adapter, entityResolver);
-    }
-
-    @Override
-    protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-        // limit results
-        int offset = queryMetadata.getFetchOffset();
-        int limit = queryMetadata.getFetchLimit();
-
-        if (offset > 0) {
-            buffer.append(" OFFSET ").append(offset);
-        }
-        
-        if (limit > 0) {
-            buffer.append(" FETCH NEXT ").append(limit).append(" ROWS ONLY ");
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngressSQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngressSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngressSQLTreeProcessor.java
new file mode 100644
index 0000000..02a21fd
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngressSQLTreeProcessor.java
@@ -0,0 +1,108 @@
+/*****************************************************************
+ *   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.ingres;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+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.OffsetFetchNextNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TrimmingColumnNode;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+
+/**
+ * @since 4.2
+ */
+public class IngressSQLTreeProcessor extends BaseSQLTreeProcessor {
+
+    @Override
+    protected void onColumnNode(Node parent, ColumnNode child, int index) {
+        replaceChild(parent, index, new TrimmingColumnNode(child));
+    }
+
+    @Override
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+        replaceChild(parent, index, new OffsetFetchNextNode(child) {
+            @Override
+            public QuotingAppendable append(QuotingAppendable buffer) {
+                // OFFSET X FETCH NEXT Y ROWS ONLY
+                if(offset > 0) {
+                    buffer.append("OFFSET ").append(offset);
+                }
+                if(limit > 0) {
+                    buffer.append("FETCH NEXT ").append(limit).append(" ROWS ONLY");
+                }
+                return buffer;
+            }
+        });
+    }
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        switch (child.getFunctionName()) {
+            case "CONCAT":
+                replaceChild(parent, index, new OpExpressionNode("+"));
+                return;
+            case "LOCATE":
+                Node child0 = child.getChild(0);
+                child.replaceChild(0, child.getChild(1));
+                child.replaceChild(1, child0);
+                return;
+            case "SUBSTRING":
+                Node replacement = new FunctionNode("SUBSTRING", child.getAlias(), true) {
+                    @Override
+                    public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
+                        // 0, CAST(1 AS INTEGER), CAST(2 AS INTEGER)
+                        if(childIdx == 1 || childIdx == 2) {
+                            buffer.append(" AS INTEGER)");
+                        }
+                        if(childIdx == 0 || childIdx == 1) {
+                            buffer.append(", CAST(");
+                        }
+                    }
+
+                    @Override
+                    public void appendChildrenEnd(QuotingAppendable buffer) {
+                        buffer.append(" AS INTEGER)");
+                        super.appendChildrenEnd(buffer);
+                    }
+                };
+                replaceChild(parent, index, replacement);
+                return;
+            case "TRIM":
+                replaceChild(parent, index, new FunctionNode("RTRIM(LTRIM", child.getAlias(), true) {
+                    @Override
+                    public void appendChildrenEnd(QuotingAppendable buffer) {
+                        buffer.append(')');
+                        super.appendChildrenEnd(buffer);
+                    }
+                });
+                return;
+            case "DAY_OF_WEEK":
+            case "DAY_OF_MONTH":
+            case "DAY_OF_YEAR":
+                // ingres variants are without '_'
+                replaceChild(parent, index, new FunctionNode(child.getFunctionName().replace("_", ""), child.getAlias(), true));
+                return;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
index b50fbc0..5230990 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
@@ -21,12 +21,10 @@ package org.apache.cayenne.dba.mysql;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+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.ejbql.JdbcEJBQLTranslatorFactory;
-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.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
@@ -43,10 +41,8 @@ import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
-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;
 
 import java.sql.PreparedStatement;
@@ -59,6 +55,7 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * DbAdapter implementation for <a href="http://www.mysql.com">MySQL RDBMS</a>.
@@ -109,16 +106,12 @@ public class MySQLAdapter extends JdbcAdapter {
 		return new DefaultQuotingStrategy("`", "`");
 	}
 
+    /**
+     * @since 4.2
+     */
 	@Override
-	public SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver) {
-		return new MySQLSelectTranslator(query, this, entityResolver);
-	}
-
-	@Override
-	public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-		QualifierTranslator translator = new MySQLQualifierTranslator(queryAssembler);
-		translator.setCaseInsensitive(caseInsensitiveCollations);
-		return translator;
+	public Function<Node, Node> getSqlTreeProcessor() {
+		return MySQLTreeProcessor.getInstance();
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLQualifierTranslator.java
deleted file mode 100644
index 2b93c22..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLQualifierTranslator.java
+++ /dev/null
@@ -1,105 +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.mysql;
-
-import java.io.IOException;
-
-import org.apache.cayenne.CayenneRuntimeException;
-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.PatternMatchNode;
-
-class MySQLQualifierTranslator extends QualifierTranslator {
-
-    public MySQLQualifierTranslator(QueryAssembler queryAssembler) {
-        super(queryAssembler);
-    }
-
-    @Override
-    protected void appendLikeEscapeCharacter(PatternMatchNode patternMatchNode)
-            throws IOException {
-
-        char escapeChar = patternMatchNode.getEscapeChar();
-
-        if ('?' == escapeChar) {
-            throw new CayenneRuntimeException("the escape character of '?' is illegal for LIKE clauses.");
-        }
-
-        if (0 != escapeChar) {
-            // this is a difference with super implementation - MySQL driver does not
-            // support JDBC escape syntax, so creating an explicit SQL escape:
-            out.append(" ESCAPE '");
-            out.append(escapeChar);
-            out.append("'");
-        }
-    }
-
-    @Override
-    public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
-        
-        if (!caseInsensitive) {
-            super.finishedChild(node, childIndex, hasMoreChildren);
-        }
-        else {
-            
-            if (!hasMoreChildren) {
-                return;
-            }
-            
-            // if we have something except LIKE or NOT LIKE then no need in specific handling
-            if (node.getType() != Expression.LIKE
-                    && node.getType() != Expression.NOT_LIKE) {
-                super.finishedChild(node, childIndex, hasMoreChildren);
-                return;
-            }
-            
-            try {
-                Appendable out = (matchingObject) ? new StringBuilder() : this.out;
-                
-                switch (node.getType()) {
-                    case Expression.LIKE:
-                        out.append(" LIKE BINARY ");
-                        break;
-                    case Expression.NOT_LIKE:
-                        out.append(" NOT LIKE BINARY ");
-                        break;
-                }
-            }
-            catch (IOException ioex) {
-                throw new CayenneRuntimeException("Error appending content", ioex);
-            }
-        }
-    }
-
-    @Override
-    protected void appendExtractFunction(ASTExtract functionExpression) {
-        switch (functionExpression.getPart()) {
-            case DAY_OF_WEEK:
-            case DAY_OF_MONTH:
-            case DAY_OF_YEAR:
-                // mysql variants are without '_'
-                out.append(functionExpression.getPart().name().replace("_", ""));
-                break;
-            default:
-                appendFunction(functionExpression);
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectTranslator.java
deleted file mode 100644
index c9f0809..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLSelectTranslator.java
+++ /dev/null
@@ -1,57 +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.mysql;
-
-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;
-
-/**
- * @since 1.2
- */
-class MySQLSelectTranslator extends DefaultSelectTranslator {
-
-	/**
-	 * @since 4.0
-	 */
-	public MySQLSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
-		super(query, adapter, entityResolver);
-	}
-
-	@Override
-	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-		int offset = queryMetadata.getFetchOffset();
-		int limit = queryMetadata.getFetchLimit();
-
-		if (offset > 0 || limit > 0) {
-			buffer.append(" LIMIT ");
-
-			// both OFFSET and LIMIT must be present, so come up with defaults
-			// if one of
-			// them is not set by the user
-			if (limit == 0) {
-				limit = Integer.MAX_VALUE;
-			}
-
-			buffer.append(limit).append(" OFFSET ").append(offset);
-		}
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLTreeProcessor.java
new file mode 100644
index 0000000..d816112
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLTreeProcessor.java
@@ -0,0 +1,68 @@
+/*****************************************************************
+ *   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.mysql;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+import org.apache.cayenne.dba.mysql.sqltree.MysqlLikeNode;
+import org.apache.cayenne.dba.mysql.sqltree.MysqlLimitOffsetNode;
+
+/**
+ * @since 4.2
+ */
+public class MySQLTreeProcessor extends BaseSQLTreeProcessor {
+
+    private static final MySQLTreeProcessor INSTANCE = new MySQLTreeProcessor();
+
+    public static MySQLTreeProcessor getInstance() {
+        return INSTANCE;
+    }
+
+    private MySQLTreeProcessor() {
+    }
+
+    @Override
+    protected void onLikeNode(Node parent, LikeNode child, int index) {
+        if(!child.isIgnoreCase()) {
+            replaceChild(parent, index, new MysqlLikeNode(child.isNot(), child.getEscape()));
+        }
+    }
+
+    @Override
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+        Node replacement = new MysqlLimitOffsetNode(child.getLimit(), child.getOffset());
+        replaceChild(parent, index, replacement, false);
+    }
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        String functionName = child.getFunctionName();
+        if("DAY_OF_MONTH".equals(functionName)
+                || "DAY_OF_WEEK".equals(functionName)
+                || "DAY_OF_YEAR".equals(functionName)) {
+            FunctionNode replacement = new FunctionNode(functionName.replace("_", ""), child.getAlias(), true);
+            replaceChild(parent, index, replacement);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/sqltree/MysqlLikeNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/sqltree/MysqlLikeNode.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/sqltree/MysqlLikeNode.java
new file mode 100644
index 0000000..5f25f14
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/sqltree/MysqlLikeNode.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.mysql.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+
+/**
+ * @since 4.2
+ */
+public class MysqlLikeNode extends LikeNode {
+    public MysqlLikeNode(boolean isNot, char escape) {
+        super(false, isNot, escape);
+    }
+
+    @Override
+    public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
+        if (not) {
+            buffer.append(" NOT");
+        }
+        buffer.append(" LIKE BINARY ");
+    }
+
+    @Override
+    public Node copy() {
+        return new MysqlLikeNode(isNot(), getEscape());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/sqltree/MysqlLimitOffsetNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/sqltree/MysqlLimitOffsetNode.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/sqltree/MysqlLimitOffsetNode.java
new file mode 100644
index 0000000..9bc0105
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/sqltree/MysqlLimitOffsetNode.java
@@ -0,0 +1,40 @@
+/*****************************************************************
+ *   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.mysql.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+
+/**
+ * @since 4.2
+ */
+public class MysqlLimitOffsetNode extends LimitOffsetNode {
+
+    public MysqlLimitOffsetNode(int limit, int offset) {
+        // Per MySQL documentation: "To retrieve all rows from a certain offset up to the end of the result set,
+        // you can use some large number for the second parameter."
+        super(limit == 0 && offset > 0 ? Integer.MAX_VALUE : limit, offset);
+    }
+
+    @Override
+    public Node copy() {
+        return new MysqlLimitOffsetNode(limit, offset);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseAdapter.java
index 1a30779..c245942 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseAdapter.java
@@ -25,11 +25,10 @@ import java.sql.ResultSet;
 import java.sql.Types;
 import java.util.Iterator;
 import java.util.List;
+import java.util.function.Function;
 
 import org.apache.cayenne.CayenneRuntimeException;
-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.ByteType;
 import org.apache.cayenne.access.types.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
@@ -46,8 +45,6 @@ import org.apache.cayenne.map.DbAttribute;
 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.SelectQuery;
 import org.apache.cayenne.resource.ResourceLocator;
 
 /**
@@ -78,11 +75,11 @@ public class OpenBaseAdapter extends JdbcAdapter {
     }
 
     /**
-     * @since 4.0
+     * @since 4.2
      */
     @Override
-    public SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver) {
-        return new OpenBaseSelectTranslator(query, this, entityResolver);
+    public Function<Node, Node> getSqlTreeProcessor() {
+        return new OpenBaseSQLTreeProcessor();
     }
 
     @Override
@@ -126,14 +123,6 @@ public class OpenBaseAdapter extends JdbcAdapter {
     }
 
     /**
-     * Returns OpenBase-specific translator for queries.
-     */
-    @Override
-    public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-        return new OpenBaseQualifierTranslator(queryAssembler);
-    }
-
-    /**
      * Creates and returns a primary key generator. Overrides superclass
      * implementation to return an instance of OpenBasePkGenerator that uses
      * built-in multi-server primary key generation.


[11/11] cayenne git commit: CAY-2468 Support subqueries in Expression API

Posted by nt...@apache.org.
CAY-2468 Support subqueries in Expression API


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

Branch: refs/heads/master
Commit: 60c55667568bffaf92f738289a6ca7b2f2b2bc68
Parents: 1f10c92
Author: Nikita Timofeev <st...@gmail.com>
Authored: Wed Jan 9 11:43:12 2019 +0300
Committer: Nikita Timofeev <st...@gmail.com>
Committed: Wed Jan 9 11:43:12 2019 +0300

----------------------------------------------------------------------
 .../java/org/apache/cayenne/exp/Expression.java |  18 +-
 .../apache/cayenne/exp/ExpressionFactory.java   |  54 ++++++
 .../cayenne/exp/parser/ASTEnclosingObject.java  |  71 ++++++++
 .../apache/cayenne/exp/parser/ASTExists.java    |  62 +++++++
 .../cayenne/exp/parser/ASTFullObject.java       |   8 +
 .../org/apache/cayenne/exp/parser/ASTIn.java    |   4 +-
 .../org/apache/cayenne/exp/parser/ASTNotIn.java |   4 +-
 .../apache/cayenne/exp/parser/ASTSubquery.java  |  95 +++++++++++
 .../cayenne/exp/parser/PatternMatchNode.java    |   7 +
 .../cayenne/exp/property/BaseProperty.java      |  22 +++
 .../cayenne/exp/property/DateProperty.java      |   8 +
 .../cayenne/exp/property/EntityProperty.java    |   9 +
 .../cayenne/exp/property/ListProperty.java      |   8 +
 .../cayenne/exp/property/MapProperty.java       |   7 +
 .../cayenne/exp/property/NumericProperty.java   |   8 +
 .../cayenne/exp/property/SetProperty.java       |   8 +
 .../cayenne/exp/property/StringProperty.java    |   8 +
 .../cayenne/query/ObjectSelect_SubqueryIT.java  | 167 +++++++++++++++++++
 18 files changed, 561 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
index 569416c..0e099d7 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
@@ -19,10 +19,7 @@
 
 package org.apache.cayenne.exp;
 
-import static org.apache.cayenne.exp.ExpressionFactory.exp;
-
 import java.io.IOException;
-import java.io.PrintWriter;
 import java.io.Serializable;
 import java.util.Collection;
 import java.util.HashMap;
@@ -167,6 +164,21 @@ public abstract class Expression implements Serializable, XMLSerializable {
 	 */
 	public static final int FULL_OBJECT = 47;
 
+	/**
+	 * @since 4.2
+	 */
+	public static final int ENCLOSING_OBJECT = 48;
+
+	/**
+	 * @since 4.2
+	 */
+	public static final int EXISTS = 49;
+
+	/**
+	 * @since 4.2
+	 */
+	public static final int SUBQUERY = 50;
+
 	protected int type;
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
----------------------------------------------------------------------
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 3ad5e77..ad9775e 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
@@ -31,7 +31,9 @@ import org.apache.cayenne.exp.parser.ASTBitwiseRightShift;
 import org.apache.cayenne.exp.parser.ASTBitwiseXor;
 import org.apache.cayenne.exp.parser.ASTDbPath;
 import org.apache.cayenne.exp.parser.ASTDivide;
+import org.apache.cayenne.exp.parser.ASTEnclosingObject;
 import org.apache.cayenne.exp.parser.ASTEqual;
+import org.apache.cayenne.exp.parser.ASTExists;
 import org.apache.cayenne.exp.parser.ASTFalse;
 import org.apache.cayenne.exp.parser.ASTFullObject;
 import org.apache.cayenne.exp.parser.ASTGreater;
@@ -54,6 +56,7 @@ import org.apache.cayenne.exp.parser.ASTObjPath;
 import org.apache.cayenne.exp.parser.ASTOr;
 import org.apache.cayenne.exp.parser.ASTPath;
 import org.apache.cayenne.exp.parser.ASTScalar;
+import org.apache.cayenne.exp.parser.ASTSubquery;
 import org.apache.cayenne.exp.parser.ASTSubtract;
 import org.apache.cayenne.exp.parser.ASTTrue;
 import org.apache.cayenne.exp.parser.ExpressionParser;
@@ -61,6 +64,9 @@ import org.apache.cayenne.exp.parser.ExpressionParserTokenManager;
 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.SelectQuery;
 
 import java.io.Reader;
 import java.io.StringReader;
@@ -1257,6 +1263,13 @@ public class ExpressionFactory {
 	}
 
 	/**
+	 * @since 4.2
+	 */
+	public static Expression enclosingObjectExp(Expression exp) {
+		return new ASTEnclosingObject(exp);
+	}
+
+	/**
 	 * @since 4.0
 	 */
 	public static Expression and(Collection<Expression> expressions) {
@@ -1343,4 +1356,45 @@ public class ExpressionFactory {
 			throw new ExpressionException("%s", th, message != null ? message : "");
 		}
 	}
+
+	/**
+	 * @since 4.2
+	 */
+	public static Expression exists(SelectQuery<?> subQuery) {
+		return new ASTExists(new ASTSubquery(subQuery));
+	}
+
+	/**
+	 * @since 4.2
+	 */
+	public static Expression exists(ObjectSelect<?> subQuery) {
+		return new ASTExists(new ASTSubquery(subQuery));
+	}
+
+	/**
+	 * @since 4.2
+	 */
+	public static Expression exists(ColumnSelect<?> subQuery) {
+		return new ASTExists(new ASTSubquery(subQuery));
+	}
+
+	/**
+	 * @since 4.2
+	 */
+	public static Expression inExp(Expression exp, ColumnSelect<?> subQuery) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTIn((SimpleNode)exp, new ASTSubquery(subQuery));
+	}
+
+	/**
+	 * @since 4.2
+	 */
+	public static Expression notInExp(Expression exp, ColumnSelect<?> subQuery) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTNotIn((SimpleNode)exp, new ASTSubquery(subQuery));
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEnclosingObject.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEnclosingObject.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEnclosingObject.java
new file mode 100644
index 0000000..2789781
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEnclosingObject.java
@@ -0,0 +1,71 @@
+/*****************************************************************
+ *   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.exp.parser;
+
+import java.io.IOException;
+
+import org.apache.cayenne.exp.Expression;
+
+/**
+ * @since 4.2
+ */
+public class ASTEnclosingObject extends SimpleNode {
+
+    public ASTEnclosingObject(Expression expression) {
+        this();
+        Node node = wrapChild(expression);
+        jjtAddChild(node, 0);
+        node.jjtSetParent(this);
+    }
+
+    public ASTEnclosingObject() {
+        this(0);
+    }
+
+    protected ASTEnclosingObject(int i) {
+        super(i);
+    }
+
+    @Override
+    protected String getExpressionOperator(int index) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        return o;
+    }
+
+    @Override
+    public void appendAsString(Appendable out) throws IOException {
+        out.append("SUPER:");
+        super.appendAsString(out);
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTEnclosingObject(id);
+    }
+
+    @Override
+    public int getType() {
+        return Expression.ENCLOSING_OBJECT;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTExists.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTExists.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTExists.java
new file mode 100644
index 0000000..9f06dc2
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTExists.java
@@ -0,0 +1,62 @@
+/*****************************************************************
+ *   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.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+
+/**
+ * @since 4.2
+ */
+public class ASTExists extends ConditionNode {
+
+    public ASTExists(ASTSubquery subquery) {
+        super(0);
+        jjtAddChild(subquery, 0);
+    }
+
+    ASTExists(int id) {
+        super(id);
+    }
+
+    @Override
+    protected int getRequiredChildrenCount() {
+        return 1;
+    }
+
+    @Override
+    protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+        return null;
+    }
+
+    @Override
+    protected String getExpressionOperator(int index) {
+        return null;
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTExists(id);
+    }
+
+    @Override
+    public int getType() {
+        return Expression.EXISTS;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java
index 25f6d97..da15668 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.exp.parser;
 
+import java.io.IOException;
+
 import org.apache.cayenne.exp.Expression;
 
 /**
@@ -52,6 +54,12 @@ public class ASTFullObject extends SimpleNode {
     }
 
     @Override
+    public void appendAsString(Appendable out) throws IOException {
+        out.append(":FULL_OBJECT:");
+        super.appendAsString(out);
+    }
+
+    @Override
     public Expression shallowCopy() {
         return new ASTFullObject(id);
     }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
index d1801ce..000a4c6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
@@ -42,10 +42,10 @@ public class ASTIn extends ConditionNode {
 		super(ExpressionParserTreeConstants.JJTIN);
 	}
 
-	public ASTIn(SimpleNode path, ASTList list) {
+	public ASTIn(SimpleNode path, SimpleNode node) {
 		super(ExpressionParserTreeConstants.JJTIN);
 		jjtAddChild(path, 0);
-		jjtAddChild(list, 1);
+		jjtAddChild(node, 1);
 		connectChildren();
 	}
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
index 4ce7d46..74761e2 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
@@ -37,10 +37,10 @@ public class ASTNotIn extends ConditionNode {
         super(ExpressionParserTreeConstants.JJTNOTIN);
     }
 
-    public ASTNotIn(SimpleNode path, ASTList list) {
+    public ASTNotIn(SimpleNode path, SimpleNode node) {
         super(ExpressionParserTreeConstants.JJTNOTIN);
         jjtAddChild(path, 0);
-        jjtAddChild(list, 1);
+        jjtAddChild(node, 1);
         connectChildren();
     }
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSubquery.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..f157501
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSubquery.java
@@ -0,0 +1,95 @@
+/*****************************************************************
+ *   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.exp.parser;
+
+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.SelectQueryWrapper;
+import org.apache.cayenne.access.translator.select.TranslatableQueryWrapper;
+
+/**
+ * @since 4.2
+ */
+public class ASTSubquery extends SimpleNode {
+
+    private final TranslatableQueryWrapper query;
+
+    public ASTSubquery(SelectQuery<?> query) {
+        this(new SelectQueryWrapper(query));
+    }
+
+    public ASTSubquery(ObjectSelect<?> query) {
+        this(new ObjectSelectWrapper(query));
+    }
+
+    public ASTSubquery(ColumnSelect<?> query) {
+        this(new ColumnSelectWrapper(query));
+    }
+
+    public ASTSubquery(TranslatableQueryWrapper query) {
+        super(0);
+        this.query = query;
+    }
+
+    @Override
+    protected String getExpressionOperator(int index) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) {
+        ObjectContext context;
+        if(o instanceof Persistent) {
+            context = ((Persistent) o).getObjectContext();
+        } else {
+            throw new UnsupportedOperationException("Can't evaluate subquery expression against non-persistent object");
+        }
+
+        return context.select(query.unwrap());
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTSubquery(query);
+    }
+
+    @Override
+    public int getType() {
+        return Expression.SUBQUERY;
+    }
+
+    @Override
+    public void appendAsString(Appendable out) throws IOException {
+        out.append("EXISTS");
+    }
+
+    public TranslatableQueryWrapper getQuery() {
+        return query;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/PatternMatchNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/PatternMatchNode.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/PatternMatchNode.java
index 6fb7bff..f850443 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/PatternMatchNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/PatternMatchNode.java
@@ -85,6 +85,13 @@ public abstract class PatternMatchNode extends ConditionNode {
         escapeChar = value;
     }
 
+    /**
+     * @since 4.2
+     */
+    public boolean isIgnoringCase() {
+        return ignoringCase;
+    }
+
     protected boolean matchPattern(String string) {
         return (string != null) && getPattern().matcher(string).find();
     }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/property/BaseProperty.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/BaseProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/BaseProperty.java
index 1ce3836..fa16417 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/BaseProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/BaseProperty.java
@@ -28,6 +28,7 @@ import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.FunctionExpressionFactory;
 import org.apache.cayenne.exp.parser.ASTPath;
+import org.apache.cayenne.query.ColumnSelect;
 import org.apache.cayenne.query.Ordering;
 import org.apache.cayenne.query.Orderings;
 import org.apache.cayenne.query.SortOrder;
@@ -386,4 +387,25 @@ public class BaseProperty<E> implements Property<E> {
     public Expression nin(Collection<E> values) {
         return ExpressionFactory.notInExp(getExpression(), values);
     }
+
+    /**
+     * @return An expression for finding objects with values in the given subquery
+     */
+    public Expression in(ColumnSelect<? extends E> subquery) {
+        return ExpressionFactory.inExp(getExpression(), subquery);
+    }
+
+    /**
+     * @return An expression for finding objects with values not in the given subquery
+     */
+    public Expression nin(ColumnSelect<? extends E> subquery) {
+        return ExpressionFactory.notInExp(getExpression(), subquery);
+    }
+
+    /**
+     * @return property that will be translated relative to parent query
+     */
+    public BaseProperty<E> enclosing() {
+        return PropertyFactory.createBase(ExpressionFactory.enclosingObjectExp(getExpression()), getType());
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/property/DateProperty.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/DateProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/DateProperty.java
index 8c32512..ab67541 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/DateProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/DateProperty.java
@@ -20,6 +20,7 @@
 package org.apache.cayenne.exp.property;
 
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.FunctionExpressionFactory;
 
 /**
@@ -137,4 +138,11 @@ public class DateProperty<E> extends BaseProperty<E> implements ComparableProper
     public DateProperty<E> min() {
         return PropertyFactory.createDate(FunctionExpressionFactory.minExp(getExpression()), getType());
     }
+
+    /**
+     * @return property that will be translated relative to parent query
+     */
+    public DateProperty<E> enclosing() {
+        return PropertyFactory.createDate(ExpressionFactory.enclosingObjectExp(getExpression()), getType());
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/property/EntityProperty.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/EntityProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/EntityProperty.java
index 41c225f..502e40b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/EntityProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/EntityProperty.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.exp.property;
 
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.parser.ASTPath;
 
 /**
@@ -70,4 +71,12 @@ public class EntityProperty<E extends Persistent> extends BaseProperty<E> implem
                 ? this
                 : PropertyFactory.createEntity(getName() + "+", getType());
     }
+
+    /**
+     * @return property that will be translated relative to parent query
+     */
+    public EntityProperty<E> enclosing() {
+        return PropertyFactory.createEntity(ExpressionFactory.enclosingObjectExp(getExpression()), getType());
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/property/ListProperty.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/ListProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/ListProperty.java
index ded0084..861fa69 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/ListProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/ListProperty.java
@@ -23,6 +23,7 @@ import java.util.List;
 
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.parser.ASTPath;
 
 /**
@@ -66,4 +67,11 @@ public class ListProperty<V extends Persistent> extends CollectionProperty<V, Li
                 ? this
                 : PropertyFactory.createList(getName() + "+", getEntityType());
     }
+
+    /**
+     * @return property that will be translated relative to parent query
+     */
+    public ListProperty<V> enclosing() {
+        return PropertyFactory.createList(null, ExpressionFactory.enclosingObjectExp(getExpression()), getEntityType());
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/property/MapProperty.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/MapProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/MapProperty.java
index b9b24b3..bd03eff 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/MapProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/MapProperty.java
@@ -232,4 +232,11 @@ public class MapProperty<K, V extends Persistent> extends BaseProperty<Map<K, V>
     protected Class<V> getEntityType() {
         return entityType;
     }
+
+    /**
+     * @return property that will be translated relative to parent query
+     */
+    public MapProperty<K, V> enclosing() {
+        return PropertyFactory.createMap(null, ExpressionFactory.enclosingObjectExp(getExpression()),  getKeyType(), getEntityType());
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/property/NumericProperty.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/NumericProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/NumericProperty.java
index 14e07d1..a1f67d9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/NumericProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/NumericProperty.java
@@ -20,6 +20,7 @@
 package org.apache.cayenne.exp.property;
 
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.FunctionExpressionFactory;
 import org.apache.cayenne.exp.parser.ASTAdd;
 import org.apache.cayenne.exp.parser.ASTDivide;
@@ -183,4 +184,11 @@ public class NumericProperty<E extends Number> extends BaseProperty<E> implement
         return PropertyFactory.createNumeric(alias, this.getExpression(), this.getType());
     }
 
+    /**
+     * @return property that will be translated relative to parent query
+     */
+    public NumericProperty<E> enclosing() {
+        return PropertyFactory.createNumeric(ExpressionFactory.enclosingObjectExp(getExpression()), getType());
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/property/SetProperty.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/SetProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/SetProperty.java
index 12b1fd2..b6c5d47 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/SetProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/SetProperty.java
@@ -23,6 +23,7 @@ import java.util.Set;
 
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.parser.ASTPath;
 
 /**
@@ -62,4 +63,11 @@ public class SetProperty<V extends Persistent> extends CollectionProperty<V, Set
                 ? this
                 : PropertyFactory.createSet(getName() + "+", getEntityType());
     }
+
+    /**
+     * @return property that will be translated relative to parent query
+     */
+    public SetProperty<V> enclosing() {
+        return PropertyFactory.createSet(null, ExpressionFactory.enclosingObjectExp(getExpression()), getEntityType());
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/main/java/org/apache/cayenne/exp/property/StringProperty.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/StringProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/StringProperty.java
index 87a6e0f..1c09822 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/StringProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/StringProperty.java
@@ -301,4 +301,12 @@ public class StringProperty<E extends CharSequence> extends BaseProperty<E> {
     public StringProperty<E> alias(String alias) {
         return PropertyFactory.createString(alias, this.getExpression(), this.getType());
     }
+
+    /**
+     * @return property that will be translated relative to parent query
+     */
+    public StringProperty<E> enclosing() {
+        return PropertyFactory.createString(ExpressionFactory.enclosingObjectExp(getExpression()), getType());
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/60c55667/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_SubqueryIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_SubqueryIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_SubqueryIT.java
new file mode 100644
index 0000000..8a91131
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_SubqueryIT.java
@@ -0,0 +1,167 @@
+/*****************************************************************
+ *   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.Date;
+import java.util.List;
+
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.testdo.testmap.Gallery;
+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.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class ObjectSelect_SubqueryIT extends ServerCase {
+
+    @Inject
+    DataContext context;
+
+    @Inject
+    private DBHelper dbHelper;
+
+    @Before
+    public void createArtistsDataSet() throws Exception {
+        TableHelper tArtist = new TableHelper(dbHelper, "ARTIST");
+        tArtist.setColumns("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH");
+
+        long dateBase = System.currentTimeMillis() - 2 * 24 * 3600 * 1000;
+        for (int i = 1; i <= 20; i++) {
+            tArtist.insert(i, "artist" + i, new java.sql.Date(dateBase + 24 * 3600 * 1000 * i));
+        }
+
+        TableHelper tGallery = new TableHelper(dbHelper, "GALLERY");
+        tGallery.setColumns("GALLERY_ID", "GALLERY_NAME");
+        tGallery.insert(1, "tate modern");
+
+        TableHelper tPaintings = new TableHelper(dbHelper, "PAINTING");
+        tPaintings.setColumns("PAINTING_ID", "PAINTING_TITLE", "ARTIST_ID", "GALLERY_ID");
+        for (int i = 1; i <= 20; i++) {
+            tPaintings.insert(i, "painting" + i, i % 5 + 1, 1);
+        }
+    }
+
+    @Test
+    public void selectQuery_simpleExists() {
+        long count = ObjectSelect.query(Artist.class)
+                .where(ExpressionFactory.exists(SelectQuery.query(Painting.class, Painting.PAINTING_TITLE.like("painting%"))))
+                .selectCount(context);
+        assertEquals(20L, count);
+    }
+
+    @Test
+    public void selectQuery_existsWithExpressionFromParentQuery() {
+        Expression exp = Painting.TO_ARTIST.eq(PropertyFactory.createSelf(Artist.class).enclosing())
+                .andExp(Painting.PAINTING_TITLE.like("painting%"))
+                .andExp(Artist.ARTIST_NAME.enclosing().like("art%"));
+
+        SelectQuery<Painting> subQuery = SelectQuery.query(Painting.class, exp);
+        subQuery.setColumns(Painting.PAINTING_TITLE);
+
+        long count = ObjectSelect.query(Artist.class)
+                .where(ExpressionFactory.exists(subQuery))
+                .selectCount(context);
+        assertEquals(5L, count);
+    }
+
+    @Test
+    public void selectQuery_twoLevelExists() {
+        Expression exp = Painting.PAINTING_TITLE.like("painting%")
+                .andExp(ExpressionFactory.exists(SelectQuery.query(Gallery.class)));
+        long count = ObjectSelect.query(Artist.class)
+                .where(ExpressionFactory.exists(SelectQuery.query(Painting.class, exp)))
+                .selectCount(context);
+        assertEquals(20L, count);
+    }
+
+    @Test
+    public void selectQuery_twoLevelExistsWithExpressionFromParentQuery() {
+        Expression deepNestedExp = Artist.ARTIST_NAME.enclosing().enclosing().like("art%")
+                .andExp(Painting.TO_GALLERY.enclosing().eq(PropertyFactory.createSelf(Gallery.class)));
+
+        Expression exp = Painting.PAINTING_TITLE.like("painting%")
+                .andExp(ExpressionFactory.exists(SelectQuery.query(Gallery.class, deepNestedExp)))
+                .andExp(Painting.TO_ARTIST.eq(PropertyFactory.createSelf(Artist.class).enclosing()));
+
+        long count = ObjectSelect.query(Artist.class)
+                .where(ExpressionFactory.exists(SelectQuery.query(Painting.class, exp)))
+                .selectCount(context);
+        assertEquals(5L, count);
+    }
+
+    @Test
+    public void objectSelect_twoLevelExistsWithExpressionFromParentQuery() {
+        ObjectSelect<Gallery> deepSubquery = ObjectSelect.query(Gallery.class)
+                .where(Artist.ARTIST_NAME.enclosing().enclosing().like("art%"))
+                .and(Painting.TO_GALLERY.enclosing().eq(PropertyFactory.createSelf(Gallery.class)));
+
+        ObjectSelect<Painting> subquery = ObjectSelect.query(Painting.class)
+                .where(Painting.PAINTING_TITLE.like("painting%"))
+                .and(Painting.TO_ARTIST.eq(PropertyFactory.createSelf(Artist.class).enclosing()))
+                .and(ExpressionFactory.exists(deepSubquery));
+
+        long count = ObjectSelect.query(Artist.class)
+                .where(ExpressionFactory.exists(subquery))
+                .selectCount(context);
+
+        assertEquals(5L, count);
+    }
+
+    @Test
+    public void columnSelect_simpleInSubquery() {
+
+        ColumnSelect<String> subquery = ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME)
+                .where(Artist.DATE_OF_BIRTH.lt(new Date()));
+
+        long count = ObjectSelect.query(Painting.class)
+                .where(Painting.TO_ARTIST.dot(Artist.ARTIST_NAME).in(subquery))
+                .selectCount(context);
+
+        assertEquals(4L, count);
+    }
+
+    @Test
+    public void columnSelect_simpleNotInSubquery() {
+
+        ColumnSelect<String> subquery = ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME)
+                .where(Artist.DATE_OF_BIRTH.lt(new Date()));
+
+        long count = ObjectSelect.query(Painting.class)
+                .where(Painting.TO_ARTIST.dot(Artist.ARTIST_NAME).nin(subquery))
+                .selectCount(context);
+
+        assertEquals(16L, count);
+    }
+}


[09/11] cayenne git commit: CAY-2465 New SelectTranslator implementation

Posted by nt...@apache.org.
CAY-2465 New SelectTranslator implementation


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

Branch: refs/heads/master
Commit: f6b2dac9667343928324cd7ce364ee6e5df17275
Parents: eb0373e
Author: Nikita Timofeev <st...@gmail.com>
Authored: Wed Jan 9 11:41:46 2019 +0300
Committer: Nikita Timofeev <st...@gmail.com>
Committed: Wed Jan 9 11:41:46 2019 +0300

----------------------------------------------------------------------
 .../access/MixedResultIncrementalFaultList.java |   12 +-
 .../cayenne/access/jdbc/ColumnDescriptor.java   |    7 +
 .../access/sqlbuilder/AliasedNodeBuilder.java   |    6 +-
 .../access/sqlbuilder/ColumnNodeBuilder.java    |    7 +-
 .../cayenne/access/sqlbuilder/SQLBuilder.java   |   15 +
 .../access/sqlbuilder/ValueNodeBuilder.java     |    9 +-
 .../access/sqlbuilder/sqltree/AliasedNode.java  |   55 +
 .../access/sqlbuilder/sqltree/BetweenNode.java  |    4 +
 .../access/sqlbuilder/sqltree/ColumnNode.java   |    6 +-
 .../sqlbuilder/sqltree/ExpressionNode.java      |    4 +-
 .../access/sqlbuilder/sqltree/FunctionNode.java |    6 +-
 .../sqlbuilder/sqltree/OpExpressionNode.java    |    4 +
 .../access/sqlbuilder/sqltree/TextNode.java     |    4 +
 .../access/sqlbuilder/sqltree/ValueNode.java    |   27 +-
 .../translator/select/BaseColumnExtractor.java  |   44 +
 .../translator/select/BaseSQLTreeProcessor.java |  128 +++
 .../select/ColumnDescriptorStage.java           |   56 +
 .../translator/select/ColumnExtractor.java      |   33 +
 .../translator/select/ColumnExtractorStage.java |   54 +
 .../translator/select/ColumnSelectWrapper.java  |   84 ++
 .../select/CustomColumnSetExtractor.java        |  133 +++
 .../select/DataObjectMatchTranslator.java       |  152 ---
 .../select/DbEntityColumnExtractor.java         |   45 +
 .../translator/select/DbPathProcessor.java      |  108 ++
 .../select/DefaultQuotingAppendable.java        |   47 +
 .../select/DefaultSelectTranslator.java         | 1076 ++----------------
 .../select/DescriptorColumnExtractor.java       |  140 +++
 .../access/translator/select/DistinctStage.java |   71 ++
 .../access/translator/select/GroupByStage.java  |   54 +
 .../select/HavingTranslationStage.java          |   40 +
 .../translator/select/IdColumnExtractor.java    |   49 +
 .../access/translator/select/JoinStack.java     |  219 ----
 .../access/translator/select/JoinTreeNode.java  |  145 ---
 .../translator/select/LimitOffsetStage.java     |   37 +
 .../translator/select/ObjPathProcessor.java     |  132 +++
 .../translator/select/ObjectSelectWrapper.java  |   84 ++
 .../access/translator/select/OrderingStage.java |   81 ++
 .../translator/select/OrderingTranslator.java   |  127 ---
 .../translator/select/PathComponents.java       |   95 ++
 .../access/translator/select/PathProcessor.java |  129 +++
 .../select/PathTranslationResult.java           |   51 +
 .../translator/select/PathTranslator.java       |   60 +
 .../translator/select/PrefetchNodeStage.java    |  128 +++
 .../select/QualifierTranslationStage.java       |   74 ++
 .../translator/select/QualifierTranslator.java  | 1026 ++++++-----------
 .../translator/select/QueryAssembler.java       |  197 ----
 .../translator/select/QueryAssemblerHelper.java |  488 --------
 .../translator/select/ResultNodeDescriptor.java |  138 +++
 .../translator/select/SQLGenerationStage.java   |   44 +
 .../translator/select/SelectQueryWrapper.java   |   84 ++
 .../access/translator/select/TableTree.java     |  109 ++
 .../access/translator/select/TableTreeNode.java |   75 ++
 .../translator/select/TableTreeStage.java       |  104 ++
 .../select/TranslatableQueryWrapper.java        |   57 +
 .../translator/select/TranslationStage.java     |   27 +
 .../translator/select/TranslatorContext.java    |  245 ++++
 .../select/TrimmingQualifierTranslator.java     |   92 --
 .../org/apache/cayenne/dba/AutoAdapter.java     |    8 +-
 .../java/org/apache/cayenne/dba/DbAdapter.java  |   10 +-
 .../cayenne/dba/DefaultQuotingStrategy.java     |   48 +-
 .../org/apache/cayenne/dba/JdbcAdapter.java     |   25 +-
 .../org/apache/cayenne/dba/QuotingStrategy.java |   20 +-
 .../cayenne/dba/db2/DB2ActionBuilder.java       |    6 +
 .../org/apache/cayenne/dba/db2/DB2Adapter.java  |   12 +-
 .../cayenne/dba/db2/DB2QualifierTranslator.java |  174 ---
 .../cayenne/dba/db2/DB2SQLTreeProcessor.java    |   85 ++
 .../apache/cayenne/dba/db2/DB2SelectAction.java |   39 +
 .../cayenne/dba/derby/DerbyActionBuilder.java   |   40 +
 .../apache/cayenne/dba/derby/DerbyAdapter.java  |   23 +-
 .../dba/derby/DerbyQualifierTranslator.java     |  141 ---
 .../dba/derby/DerbySQLTreeProcessor.java        |   81 ++
 .../cayenne/dba/derby/DerbySelectAction.java    |   39 +
 .../dba/derby/sqltree/DerbyValueNode.java       |   51 +
 .../dba/firebird/FirebirdActionBuilder.java     |   41 +
 .../cayenne/dba/firebird/FirebirdAdapter.java   |   21 +-
 .../firebird/FirebirdQualifierTranslator.java   |  181 ---
 .../dba/firebird/FirebirdSQLTreeProcessor.java  |  189 +++
 .../dba/firebird/FirebirdSelectAction.java      |   40 +
 .../dba/firebird/sqltree/FirebirdLimitNode.java |   50 +
 .../sqltree/FirebirdSubstringFunctionNode.java  |   65 ++
 .../cayenne/dba/frontbase/FrontBaseAdapter.java |   25 +-
 .../frontbase/FrontBaseQualifierTranslator.java |  134 ---
 .../frontbase/FrontBaseSQLTreeProcessor.java    |  104 ++
 .../frontbase/FrontBaseSelectTranslator.java    |   52 -
 .../apache/cayenne/dba/h2/H2ActionBuilder.java  |   39 +
 .../org/apache/cayenne/dba/h2/H2Adapter.java    |   18 +
 .../cayenne/dba/h2/H2SQLTreeProcessor.java      |   37 +
 .../apache/cayenne/dba/h2/H2SelectAction.java   |   39 +
 .../cayenne/dba/hsqldb/HSQLDBAdapter.java       |   27 +-
 .../dba/hsqldb/HSQLQualifierTranslator.java     |   86 --
 .../dba/hsqldb/HSQLSelectTranslator.java        |   57 -
 .../cayenne/dba/hsqldb/HSQLTreeProcessor.java   |   70 ++
 .../cayenne/dba/ingres/IngresAdapter.java       |   23 +-
 .../dba/ingres/IngresQualifierTranslator.java   |  122 --
 .../dba/ingres/IngresSelectTranslator.java      |   49 -
 .../dba/ingres/IngressSQLTreeProcessor.java     |  108 ++
 .../apache/cayenne/dba/mysql/MySQLAdapter.java  |   21 +-
 .../dba/mysql/MySQLQualifierTranslator.java     |  105 --
 .../dba/mysql/MySQLSelectTranslator.java        |   57 -
 .../cayenne/dba/mysql/MySQLTreeProcessor.java   |   68 ++
 .../dba/mysql/sqltree/MysqlLikeNode.java        |   46 +
 .../dba/mysql/sqltree/MysqlLimitOffsetNode.java |   40 +
 .../cayenne/dba/openbase/OpenBaseAdapter.java   |   21 +-
 .../cayenne/dba/openbase/OpenBaseJoinStack.java |  115 --
 .../openbase/OpenBaseQualifierTranslator.java   |  177 ---
 .../dba/openbase/OpenBaseSQLTreeProcessor.java  |  109 ++
 .../dba/openbase/OpenBaseSelectTranslator.java  |   53 -
 .../cayenne/dba/oracle/Oracle8Adapter.java      |   19 -
 .../cayenne/dba/oracle/Oracle8JoinStack.java    |  108 --
 .../dba/oracle/Oracle8QualifierTranslator.java  |   49 -
 .../dba/oracle/Oracle8SelectTranslator.java     |   47 -
 .../cayenne/dba/oracle/OracleAdapter.java       |   23 +-
 .../dba/oracle/OracleQualifierTranslator.java   |  212 ----
 .../dba/oracle/OracleSQLTreeProcessor.java      |  241 ++++
 .../dba/oracle/OracleSelectTranslator.java      |   78 --
 .../dba/postgres/PostgreSQLTreeProcessor.java   |   83 ++
 .../cayenne/dba/postgres/PostgresAdapter.java   |   27 +-
 .../postgres/PostgresQualifierTranslator.java   |  189 ---
 .../dba/postgres/PostgresSelectTranslator.java  |   54 -
 .../postgres/sqltree/PositionFunctionNode.java  |   43 +
 .../sqltree/PostgresExtractFunctionNode.java    |   59 +
 .../dba/postgres/sqltree/PostgresLikeNode.java  |   46 +
 .../sqltree/PostgresLimitOffsetNode.java        |   60 +
 .../cayenne/dba/sqlite/SQLiteActionBuilder.java |    9 +
 .../cayenne/dba/sqlite/SQLiteAdapter.java       |   23 +-
 .../dba/sqlite/SQLiteQualifierTranslator.java   |  162 ---
 .../cayenne/dba/sqlite/SQLiteSelectAction.java  |   43 +
 .../cayenne/dba/sqlite/SQLiteTreeProcessor.java |  100 ++
 .../cayenne/dba/sqlserver/SQLServerAdapter.java |   23 +-
 .../sqlserver/SQLServerSelectTranslator.java    |   54 -
 .../dba/sqlserver/SQLServerTreeProcessor.java   |  128 +++
 .../SQLServerTrimmingQualifierTranslator.java   |  207 ----
 .../sqlserver/sqltree/SQLServerColumnNode.java  |   46 +
 .../cayenne/dba/sybase/SybaseAdapter.java       |   25 +-
 .../dba/sybase/SybaseQualifierTranslator.java   |  114 --
 .../dba/sybase/SybaseSelectTranslator.java      |   52 -
 .../org/apache/cayenne/query/ColumnSelect.java  |    7 +
 .../cayenne/query/SelectQueryMetadata.java      |   13 +-
 .../java/org/apache/cayenne/util/ArrayUtil.java |  260 +++++
 .../cayenne/FlattenedRelationshipsIT.java       |    3 +-
 .../select/BaseColumnExtractorTest.java         |   43 +
 .../select/ColumnDescriptorStageTest.java       |   55 +
 .../select/CustomColumnSetExtractorTest.java    |   94 ++
 .../select/DbEntityColumnExtractorTest.java     |  121 ++
 .../select/DefaultObjectSelectTranslatorIT.java |  136 +++
 .../select/DefaultSelectTranslatorIT.java       |  249 ++--
 .../select/DescriptorColumnExtractorTest.java   |   97 ++
 .../translator/select/DistinctStageTest.java    |  103 ++
 .../translator/select/GroupByStageTest.java     |   86 ++
 .../select/HavingTranslationStageTest.java      |   90 ++
 .../select/IdColumnExtractorTest.java           |  113 ++
 .../translator/select/LimitOffsetStageTest.java |   65 ++
 .../select/MockQueryMetadataBuilder.java        |  100 ++
 .../select/MockQueryWrapperBuilder.java         |  137 +++
 .../select/MockTranslatorContext.java           |   35 +
 .../translator/select/ObjPathProcessorIT.java   |   92 ++
 .../translator/select/ObjPathProcessorIT2.java  |   91 ++
 .../translator/select/ObjPathProcessorIT3.java  |   82 ++
 .../translator/select/ObjPathProcessorIT4.java  |   72 ++
 .../translator/select/OrderingStageTest.java    |  110 ++
 .../translator/select/OrderingTranslatorIT.java |  136 ---
 .../translator/select/PathComponentsTest.java   |   77 ++
 .../select/QualifierTranslationStageTest.java   |   89 ++
 .../select/QualifierTranslatorIT.java           |  164 ---
 .../select/QualifierTranslatorTest.java         |  524 +++++++++
 .../translator/select/QueryAssemblerIT.java     |   61 -
 .../translator/select/TstQueryAssembler.java    |   70 --
 .../cayenne/exp/property/PathAliasesIT.java     |    4 +-
 .../apache/cayenne/query/ColumnSelectIT.java    |   56 +-
 .../cayenne/unit/IngresUnitDbAdapter.java       |    8 +
 .../org/apache/cayenne/util/ArrayUtilTest.java  |  354 ++++++
 171 files changed, 9096 insertions(+), 6744 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java b/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java
index 9ca2136..f8c02b8 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java
@@ -46,7 +46,6 @@ import org.apache.cayenne.util.Util;
  * if there is no Persistent objects in the result Collection it will be iterated as is, without faulting anything.
  *
  * @see QueryMetadata#getPageSize()
- * @see org.apache.cayenne.access.translator.select.DefaultSelectTranslator
  * @see org.apache.cayenne.query.SelectQueryMetadata
  *
  * @since 4.0
@@ -142,7 +141,10 @@ class MixedResultIncrementalFaultList<E> extends IncrementalFaultList<E> {
                 for (int i = fromIndex; i < toIndex; i++) {
                     Object[] object = (Object[])elements.get(i);
                     if (helper.unresolvedSuspect(object[dataIdx])) {
-                        quals.add(buildIdQualifier(dataIdx, object));
+                        Expression nextQualifier = buildIdQualifier(dataIdx, object);
+                        if(nextQualifier != null) {
+                            quals.add(nextQualifier);
+                        }
                     }
                 }
 
@@ -188,7 +190,9 @@ class MixedResultIncrementalFaultList<E> extends IncrementalFaultList<E> {
     Expression buildIdQualifier(int index, Object[] data) {
         Map<String, Object> map;
         if(data[index] instanceof Map) {
-            map = (Map<String, Object>)data[index];
+            map = (Map<String, Object>) data[index];
+        } else if(data[index] == null) {
+            return null;
         } else {
             map = new HashMap<>();
             int i = 0;
@@ -235,6 +239,8 @@ class MixedResultIncrementalFaultList<E> extends IncrementalFaultList<E> {
                         return false;
                     }
                 }
+            } else if(dataInTheList[dataIdx] == null) {
+                return false;
             } else {
                 for(Object id : map.values()) {
                     if (!dataInTheList[dataIdx++].equals(id)) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java
index 632f5e3..90cd61b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java
@@ -161,6 +161,13 @@ public class ColumnDescriptor {
     }
 
     /**
+     * @since 4.2
+     */
+    public void setAttribute(DbAttribute attribute) {
+        this.attribute = attribute;
+    }
+
+    /**
      * Returns true if another object is a ColumnDescriptor with the same name,
      * name prefix, table and procedure names. Other fields are ignored in the
      * equality test.

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/AliasedNodeBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/AliasedNodeBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/AliasedNodeBuilder.java
index 50f07bd..aae7835 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/AliasedNodeBuilder.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/AliasedNodeBuilder.java
@@ -19,9 +19,8 @@
 
 package org.apache.cayenne.access.sqlbuilder;
 
-import org.apache.cayenne.access.sqlbuilder.sqltree.EmptyNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.AliasedNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
-import org.apache.cayenne.access.sqlbuilder.sqltree.TextNode;
 
 /**
  * @since 4.2
@@ -38,9 +37,8 @@ class AliasedNodeBuilder implements NodeBuilder {
 
     @Override
     public Node build() {
-        Node root = new EmptyNode();
+        Node root = new AliasedNode(alias);
         root.addChild(nodeBuilder.build());
-        root.addChild(new TextNode(" " + alias));
         return root;
     }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/ColumnNodeBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/ColumnNodeBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/ColumnNodeBuilder.java
index 8236744..55c1032 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/ColumnNodeBuilder.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/ColumnNodeBuilder.java
@@ -74,13 +74,10 @@ public class ColumnNodeBuilder implements ExpressionTrait {
 
     @Override
     public Node build() {
-        ColumnNode columnNode;
         if(unescaped) {
-            columnNode = new UnescapedColumnNode(table, column, alias, attribute);
-        } else {
-            columnNode = new ColumnNode(table, column, alias, attribute);
+            return new UnescapedColumnNode(table, column, alias, attribute);
         }
-        return columnNode;
+        return new ColumnNode(table, column, alias, attribute);
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLBuilder.java
index 0dcdfc1..dfa3d2e 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLBuilder.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLBuilder.java
@@ -19,6 +19,7 @@
 
 package org.apache.cayenne.access.sqlbuilder;
 
+import org.apache.cayenne.access.sqlbuilder.sqltree.AliasedNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
@@ -87,6 +88,17 @@ public final class SQLBuilder {
         if(suppressAlias(node)) {
             return node(node);
         }
+
+        if(node instanceof FunctionNode) {
+            ((FunctionNode) node).setAlias(alias);
+            return node(node);
+        }
+
+        if(node instanceof ColumnNode) {
+            ((ColumnNode) node).setAlias(alias);
+            return node(node);
+        }
+
         return new AliasedNodeBuilder(node(node), alias);
     }
 
@@ -154,6 +166,9 @@ public final class SQLBuilder {
             } else if(node.getType() == NodeType.FUNCTION && ((FunctionNode) node).getAlias() != null) {
                 suppressAlias = true;
                 return false;
+            } else if(node instanceof AliasedNode) {
+                suppressAlias = true;
+                return false;
             }
             return true;
         }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/ValueNodeBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/ValueNodeBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/ValueNodeBuilder.java
index 5e027c6..4e3e659 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/ValueNodeBuilder.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/ValueNodeBuilder.java
@@ -32,6 +32,8 @@ public class ValueNodeBuilder implements NodeBuilder, ExpressionTrait {
 
     private DbAttribute attribute;
 
+    private boolean isArray;
+
     ValueNodeBuilder(Object value) {
         this.value = value;
     }
@@ -41,8 +43,13 @@ public class ValueNodeBuilder implements NodeBuilder, ExpressionTrait {
         return this;
     }
 
+    public ValueNodeBuilder array(boolean isArray) {
+        this.isArray = isArray;
+        return this;
+    }
+
     @Override
     public Node build() {
-        return new ValueNode(value, attribute);
+        return new ValueNode(value, isArray, attribute);
     }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/AliasedNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/AliasedNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/AliasedNode.java
new file mode 100644
index 0000000..524fc14
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/AliasedNode.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.sqlbuilder.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+
+/**
+ * @since 4.2
+ */
+public class AliasedNode extends Node {
+
+    protected final String alias;
+
+    public AliasedNode(String alias) {
+        this.alias = alias;
+    }
+
+    @Override
+    public Node copy() {
+        return new AliasedNode(alias);
+    }
+
+    @Override
+    public QuotingAppendable append(QuotingAppendable buffer) {
+        return buffer;
+    }
+
+    @Override
+    public void appendChildrenEnd(QuotingAppendable buffer) {
+        super.appendChildrenEnd(buffer);
+        buffer.append(' ').append(alias);
+    }
+
+    public String getAlias() {
+        return alias;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/BetweenNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/BetweenNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/BetweenNode.java
index 1102a8a..6826931 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/BetweenNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/BetweenNode.java
@@ -49,4 +49,8 @@ public class BetweenNode extends ExpressionNode {
     public Node copy() {
         return new BetweenNode(not);
     }
+
+    public boolean isNot() {
+        return not;
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ColumnNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ColumnNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ColumnNode.java
index b610104..9c49a4b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ColumnNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ColumnNode.java
@@ -29,8 +29,8 @@ public class ColumnNode extends Node {
 
     protected final String table;
     protected final String column;
-    protected final String alias;
     protected final DbAttribute attribute;
+    protected String alias;
 
     public ColumnNode(String table, String column, String alias, DbAttribute attribute) {
         super(NodeType.COLUMN);
@@ -65,6 +65,10 @@ public class ColumnNode extends Node {
         return alias;
     }
 
+    public void setAlias(String alias) {
+        this.alias = alias;
+    }
+
     public DbAttribute getAttribute() {
         return attribute;
     }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ExpressionNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ExpressionNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ExpressionNode.java
index 8344380..3e9795e 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ExpressionNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ExpressionNode.java
@@ -41,14 +41,14 @@ public class ExpressionNode extends Node {
 
     @Override
     public void appendChildrenStart(QuotingAppendable buffer) {
-        if(parent.type != NodeType.WHERE && parent.type != NodeType.JOIN) {
+        if(parent != null && parent.type != NodeType.WHERE && parent.type != NodeType.JOIN) {
             buffer.append(" (");
         }
     }
 
     @Override
     public void appendChildrenEnd(QuotingAppendable buffer) {
-        if(parent.type != NodeType.WHERE && parent.type != NodeType.JOIN) {
+        if(parent != null && parent.type != NodeType.WHERE && parent.type != NodeType.JOIN) {
             buffer.append(" )");
         }
     }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/FunctionNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/FunctionNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/FunctionNode.java
index 23b0fa7..3673ed0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/FunctionNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/FunctionNode.java
@@ -28,8 +28,8 @@ import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
 public class FunctionNode extends Node {
 
     private final String functionName;
-    private final String alias;
     private final boolean needParentheses;
+    private String alias;
 
     public FunctionNode(String functionName, String alias) {
         this(functionName, alias, true);
@@ -103,6 +103,10 @@ public class FunctionNode extends Node {
         return alias;
     }
 
+    public void setAlias(String alias) {
+        this.alias = alias;
+    }
+
     @Override
     public Node copy() {
         return new FunctionNode(functionName, alias, needParentheses);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/OpExpressionNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/OpExpressionNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/OpExpressionNode.java
index 7099000..6b1b55d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/OpExpressionNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/OpExpressionNode.java
@@ -41,4 +41,8 @@ public class OpExpressionNode extends ExpressionNode {
     public Node copy() {
         return new OpExpressionNode(op);
     }
+
+    public String getOp() {
+        return op;
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TextNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TextNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TextNode.java
index 45ee5cf..afd0f47 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TextNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TextNode.java
@@ -41,4 +41,8 @@ public class TextNode extends Node {
     public Node copy() {
         return new TextNode(text);
     }
+
+    public CharSequence getText() {
+        return text;
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ValueNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ValueNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ValueNode.java
index 2ba5fbc..2911397 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ValueNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ValueNode.java
@@ -34,12 +34,14 @@ import org.apache.cayenne.map.DbAttribute;
 public class ValueNode extends Node {
 
     private final Object value;
+    private final boolean isArray;
     // Used as hint for type of this value
     private final DbAttribute attribute;
 
-    public ValueNode(Object value, DbAttribute attribute) {
+    public ValueNode(Object value, boolean isArray, DbAttribute attribute) {
         super(NodeType.VALUE);
         this.value = value;
+        this.isArray = isArray;
         this.attribute = attribute;
     }
 
@@ -51,6 +53,10 @@ public class ValueNode extends Node {
         return attribute;
     }
 
+    public boolean isArray() {
+        return isArray;
+    }
+
     @Override
     public QuotingAppendable append(QuotingAppendable buffer) {
         appendValue(value, buffer);
@@ -62,8 +68,7 @@ public class ValueNode extends Node {
             return;
         }
 
-        boolean isArray = val.getClass().isArray();
-        if(isArray) {
+        if(isArray && val.getClass().isArray()) {
             if(val instanceof short[]) {
                 appendValue((short[])val, buffer);
             } else if(val instanceof char[]) {
@@ -82,7 +87,7 @@ public class ValueNode extends Node {
                 appendValue((Object[]) val, buffer);
             } else if(val instanceof byte[]) {
                 // append byte[] array as single object
-                appendObjectValue(buffer, val);
+                appendValue((byte[])val, buffer);
             } else {
                 throw new CayenneRuntimeException("Unsupported array type %s", val.getClass().getName());
             }
@@ -227,6 +232,18 @@ public class ValueNode extends Node {
         }
     }
 
+    private void appendValue(byte[] val, QuotingAppendable buffer) {
+        boolean first = true;
+        for(byte i : val) {
+            if(first) {
+                first = false;
+            } else {
+                buffer.append(',');
+            }
+            appendValue(i, buffer);
+        }
+    }
+
     private void appendValue(Object[] val, QuotingAppendable buffer) {
         boolean first = true;
         for(Object i : val) {
@@ -241,6 +258,6 @@ public class ValueNode extends Node {
 
     @Override
     public Node copy() {
-        return new ValueNode(value, attribute);
+        return new ValueNode(value, isArray, attribute);
     }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseColumnExtractor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseColumnExtractor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseColumnExtractor.java
new file mode 100644
index 0000000..20cc5f7
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseColumnExtractor.java
@@ -0,0 +1,44 @@
+/*****************************************************************
+ *   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.Node;
+import org.apache.cayenne.map.DbAttribute;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.table;
+
+/**
+ * @since 4.2
+ */
+abstract class BaseColumnExtractor implements ColumnExtractor {
+
+    protected final TranslatorContext context;
+
+    BaseColumnExtractor(TranslatorContext context) {
+        this.context = context;
+    }
+
+    protected void addDbAttribute(String prefix, String labelPrefix, DbAttribute dba) {
+        String alias = context.getTableTree().aliasForPath(prefix);
+        String dataRowKey = labelPrefix != null ? labelPrefix + '.' + dba.getName() : dba.getName();
+        Node columnNode = table(alias).column(dba).build();
+        context.addResultNode(columnNode, dataRowKey).setDbAttribute(dba);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseSQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseSQLTreeProcessor.java
new file mode 100644
index 0000000..2119e26
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseSQLTreeProcessor.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.access.translator.select;
+
+import java.util.function.Function;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.DistinctNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.InNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SimpleNodeTreeVisitor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ValueNode;
+
+
+/**
+ * @since 4.2
+ */
+public class BaseSQLTreeProcessor extends SimpleNodeTreeVisitor implements Function<Node, Node> {
+
+    @Override
+    public Node apply(Node node) {
+        node.visit(this);
+        return node;
+    }
+
+    protected void onValueNode(Node parent, ValueNode child, int index) {
+    }
+
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+    }
+
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+    }
+
+    protected void onColumnNode(Node parent, ColumnNode child, int index) {
+    }
+
+    protected void onInNode(Node parent, InNode child, int index) {
+    }
+
+    protected void onLikeNode(Node parent, LikeNode child, int index) {
+    }
+
+    protected void onResultNode(Node parent, Node child, int index) {
+    }
+
+    protected void onDistinctNode(Node parent, DistinctNode child, int index) {
+    }
+
+    protected void onUndefinedNode(Node parent, Node child, int index) {
+    }
+
+    protected void replaceChild(Node parent, int index, Node newChild) {
+        replaceChild(parent, index, newChild, true);
+    }
+
+    protected void replaceChild(Node parent, int index, Node newChild, boolean transferChildren) {
+        if (transferChildren) {
+            Node oldChild = parent.getChild(index);
+            for (int i = 0; i < oldChild.getChildrenCount(); i++) {
+                newChild.addChild(oldChild.getChild(i));
+            }
+        }
+        parent.replaceChild(index, newChild);
+    }
+
+    @Override
+    public boolean onChildNodeStart(Node parent, Node child, int index, boolean hasMore) {
+        switch (child.getType()) {
+            case VALUE:
+                onValueNode(parent, (ValueNode) child, index);
+                break;
+
+            case FUNCTION:
+                onFunctionNode(parent, (FunctionNode) child, index);
+                break;
+
+            case LIMIT_OFFSET:
+                onLimitOffsetNode(parent, (LimitOffsetNode) child, index);
+                break;
+
+            case COLUMN:
+                onColumnNode(parent, (ColumnNode) child, index);
+                break;
+
+            case IN:
+                onInNode(parent, (InNode) child, index);
+                break;
+
+            case LIKE:
+                onLikeNode(parent, (LikeNode) child, index);
+                break;
+
+            case RESULT:
+                onResultNode(parent, child, index);
+                break;
+
+            case DISTINCT:
+                onDistinctNode(parent, (DistinctNode) child, index);
+                break;
+
+            default:
+                onUndefinedNode(parent, child, index);
+                break;
+        }
+        return true;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStage.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStage.java
new file mode 100644
index 0000000..d15803a
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStage.java
@@ -0,0 +1,56 @@
+/*****************************************************************
+ *   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.map.DbAttribute;
+
+/**
+ * @since 4.2
+ */
+class ColumnDescriptorStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        int i = 0;
+        for(ResultNodeDescriptor resultNode : context.getResultNodeList()) {
+            context.getSelectBuilder().result(resultNode::getNode);
+
+            if(!resultNode.isInDataRow()) {
+                continue;
+            }
+
+            String name;
+            DbAttribute attribute = resultNode.getDbAttribute();
+            if(attribute != null) {
+                name = attribute.getName();
+            } else {
+                // generated name
+                name = "c" + i++;
+            }
+
+            ColumnDescriptor descriptor = new ColumnDescriptor(name, resultNode.getJdbcType(), resultNode.getJavaType());
+            descriptor.setAttribute(attribute);
+            descriptor.setDataRowKey(resultNode.getDataRowKey());
+
+            context.getColumnDescriptors().add(descriptor);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnExtractor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnExtractor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnExtractor.java
new file mode 100644
index 0000000..e1551a7
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnExtractor.java
@@ -0,0 +1,33 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * @since 4.2
+ */
+interface ColumnExtractor {
+
+    void extract(String prefix);
+
+    default void extract() {
+        extract(null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnExtractorStage.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnExtractorStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnExtractorStage.java
new file mode 100644
index 0000000..cd51d77
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnExtractorStage.java
@@ -0,0 +1,54 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * Get result columns based on query, options are: <ol>
+ *    <li> for column queries - defined set of columns
+ *    <li> for paginated or nested queries - root pk columns
+ *    <li> for queries with defined class descriptor - full column set including flattened
+ *    <li> for everything else - all columns of the root db entity
+ * </ol>
+ *
+ * @since 4.2
+ */
+class ColumnExtractorStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        ColumnExtractor extractor;
+
+        if(context.getQuery().getColumns() != null && !context.getQuery().getColumns().isEmpty()) {
+            extractor = new CustomColumnSetExtractor(context, context.getQuery().getColumns());
+        } else if (context.getParentContext() != null || context.getMetadata().getPageSize() > 0) {
+            if(context.getMetadata().getObjEntity() != null) {
+                extractor = new IdColumnExtractor(context, context.getMetadata().getObjEntity());
+            } else {
+                extractor = new IdColumnExtractor(context, context.getMetadata().getDbEntity());
+            }
+        } else if (context.getMetadata().getClassDescriptor() != null) {
+            extractor = new DescriptorColumnExtractor(context, context.getMetadata().getClassDescriptor());
+        } else {
+            extractor = new DbEntityColumnExtractor(context);
+        }
+
+        extractor.extract();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnSelectWrapper.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..21a0069
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ColumnSelectWrapper.java
@@ -0,0 +1,84 @@
+/*****************************************************************
+ *   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;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java
new file mode 100644
index 0000000..534fd3a
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java
@@ -0,0 +1,133 @@
+/*****************************************************************
+ *   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.Map;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTPath;
+import org.apache.cayenne.exp.property.BaseProperty;
+import org.apache.cayenne.map.JoinType;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.util.Util;
+
+/**
+ * @since 4.2
+ */
+class CustomColumnSetExtractor implements ColumnExtractor {
+
+    private final TranslatorContext context;
+    private final Collection<BaseProperty<?>> columns;
+
+    CustomColumnSetExtractor(TranslatorContext context, Collection<BaseProperty<?>> columns) {
+        this.context = context;
+        this.columns = columns;
+    }
+
+    @Override
+    public void extract(String prefix) {
+        for (BaseProperty<?> property : columns) {
+            if (isFullObjectProp(property)) {
+                extractFullObject(prefix, property);
+            } else {
+                extractSimpleProperty(property);
+            }
+        }
+    }
+
+    private void extractSimpleProperty(BaseProperty<?> property) {
+        Node sqlNode = context.getQualifierTranslator().translate(property);
+        context.addResultNode(sqlNode, true, property, property.getAlias());
+    }
+
+    private boolean isFullObjectProp(BaseProperty<?> property) {
+        int expressionType = property.getExpression().getType();
+
+        // forbid direct selection of toMany relationships columns
+        if(property.getType() != null && (expressionType == Expression.OBJ_PATH || expressionType == Expression.DB_PATH)
+                && (Collection.class.isAssignableFrom(property.getType())
+                || Map.class.isAssignableFrom(property.getType()))) {
+            throw new CayenneRuntimeException("Can't directly select toMany relationship columns. " +
+                    "Either select it with aggregate functions like count() or with flat() function to select full related objects.");
+        }
+
+        // evaluate ObjPath with Persistent type as toOne relations and use it as full object
+        return expressionType == Expression.FULL_OBJECT
+                || (property.getType() != null
+                    && expressionType == Expression.OBJ_PATH
+                    && Persistent.class.isAssignableFrom(property.getType()));
+    }
+
+    private void extractFullObject(String prefix, BaseProperty<?> property) {
+        prefix = calculatePrefix(prefix, property);
+        ensureJoin(prefix);
+
+        ObjEntity entity = context.getResolver().getObjEntity(property.getType());
+
+        ColumnExtractor extractor;
+        if(context.getMetadata().getPageSize() > 0) {
+            extractor = new IdColumnExtractor(context, entity);
+        } else {
+            ClassDescriptor descriptor = context.getResolver().getClassDescriptor(entity.getName());
+            extractor = new DescriptorColumnExtractor(context, descriptor);
+        }
+
+        int index = context.getResultNodeList().size();
+
+        // extract required columns of entity
+        extractor.extract(prefix);
+
+        // Reset data row key as ObjectResolver expects it to match attribute name.
+        // Maybe we should change resolver, as it seems cleaner to have path from root as prefix in data row key.
+        for(int i=index; i<context.getResultNodeList().size(); i++) {
+            context.getResultNodeList().get(i).setDataRowKey(null);
+        }
+    }
+
+    private String calculatePrefix(String prefix, BaseProperty<?> property) {
+        Expression propertyExpression = property.getExpression();
+        int expressionType = property.getExpression().getType();
+
+        if(expressionType == Expression.FULL_OBJECT && propertyExpression.getOperandCount() > 0) {
+            Object op = propertyExpression.getOperand(0);
+            if(op instanceof ASTPath) {
+                prefix = ((ASTPath) op).getPath();
+            }
+        } else if(propertyExpression instanceof ASTPath) {
+            prefix = ((ASTPath) propertyExpression).getPath();
+        }
+
+        return prefix;
+    }
+
+    private void ensureJoin(String prefix) {
+        // ensure all joins for given property
+        if(!Util.isEmptyString(prefix)) {
+            PathTranslationResult result = context.getPathTranslator().translatePath(context.getMetadata().getObjEntity(), prefix);
+            result.getDbRelationship().ifPresent(relationship
+                    -> context.getTableTree().addJoinTable(result.getFinalPath(), relationship, JoinType.LEFT_OUTER));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DataObjectMatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DataObjectMatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DataObjectMatchTranslator.java
deleted file mode 100644
index 07f53f1..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DataObjectMatchTranslator.java
+++ /dev/null
@@ -1,152 +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.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.ObjectId;
-import org.apache.cayenne.Persistent;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbJoin;
-import org.apache.cayenne.map.DbRelationship;
-
-/**
- */
-public class DataObjectMatchTranslator {
-
-	protected Map<String, DbAttribute> attributes;
-	protected Map<String, Object> values;
-	protected String operation;
-	protected Expression expression;
-	protected DbRelationship relationship;
-	protected String joinSplitAlias;
-
-	public Expression getExpression() {
-		return expression;
-	}
-
-	public void setExpression(Expression expression) {
-		this.expression = expression;
-	}
-
-	public void reset() {
-		attributes = null;
-		values = null;
-		operation = null;
-		expression = null;
-		relationship = null;
-	}
-
-	/**
-	 * Initializes itself to do translation of the match ending with a
-	 * DbRelationship.
-	 * 
-	 * @since 3.0
-	 */
-	public void setRelationship(DbRelationship rel, String joinSplitAlias) {
-		this.relationship = rel;
-		this.joinSplitAlias = joinSplitAlias;
-		attributes = new HashMap<>(rel.getJoins().size() * 2);
-
-		if (rel.isToMany() || !rel.isToPK()) {
-
-			// match on target PK
-			DbEntity ent = rel.getTargetEntity();
-
-			// index by name
-			for (DbAttribute pkAttr : ent.getPrimaryKeys()) {
-				attributes.put(pkAttr.getName(), pkAttr);
-			}
-		} else {
-
-			// match on this FK
-			for (DbJoin join : rel.getJoins()) {
-				// index by target name
-				attributes.put(join.getTargetName(), join.getSource());
-			}
-		}
-	}
-
-	public void setDataObject(Persistent obj) {
-		if (obj == null) {
-			values = Collections.emptyMap();
-			return;
-		}
-
-		setObjectId(obj.getObjectId());
-	}
-
-	/**
-	 * @since 1.2
-	 */
-	public void setObjectId(ObjectId id) {
-		if (id == null) {
-			throw new CayenneRuntimeException(
-					"Null ObjectId, probably an attempt to use TRANSIENT object as a query parameter.");
-		} else if (id.isTemporary()) {
-			throw new CayenneRuntimeException(
-					"Temporary id, probably an attempt to use NEW object as a query parameter.");
-		} else {
-			values = id.getIdSnapshot();
-		}
-	}
-
-	public Iterator<String> keys() {
-		if (attributes == null) {
-			throw new IllegalStateException("An attempt to use uninitialized DataObjectMatchTranslator: "
-					+ "[attributes: null, values: " + values + "]");
-		}
-
-		return attributes.keySet().iterator();
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	public String getJoinSplitAlias() {
-		return joinSplitAlias;
-	}
-
-	public DbRelationship getRelationship() {
-		return relationship;
-	}
-
-	public DbAttribute getAttribute(String key) {
-		return attributes.get(key);
-	}
-
-	public Object getValue(String key) {
-		return values.get(key);
-	}
-
-	public void setOperation(String operation) {
-		this.operation = operation;
-	}
-
-	public String getOperation() {
-		return operation;
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DbEntityColumnExtractor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DbEntityColumnExtractor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DbEntityColumnExtractor.java
new file mode 100644
index 0000000..3354fac
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DbEntityColumnExtractor.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ *   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.Objects;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+
+/**
+ * @since 4.2
+ */
+class DbEntityColumnExtractor extends BaseColumnExtractor {
+
+    private final DbEntity dbEntity;
+
+    DbEntityColumnExtractor(TranslatorContext context) {
+        super(context);
+        this.dbEntity = Objects.requireNonNull(context.getMetadata().getDbEntity(), "No root entity");
+    }
+
+    @Override
+    public void extract(String prefix) {
+        for(DbAttribute attribute : dbEntity.getAttributes()) {
+            addDbAttribute(prefix, prefix, attribute);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DbPathProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DbPathProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DbPathProcessor.java
new file mode 100644
index 0000000..cccea12
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DbPathProcessor.java
@@ -0,0 +1,108 @@
+/*****************************************************************
+ *   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.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.JoinType;
+
+/**
+ * @since 4.2
+ */
+class DbPathProcessor extends PathProcessor<DbEntity> {
+
+    DbPathProcessor(TranslatorContext context, DbEntity entity, String parentPath) {
+        super(context, entity);
+        if(parentPath != null) {
+            currentDbPath.append(parentPath);
+        }
+    }
+
+    @Override
+    protected void processNormalAttribute(String next) {
+        DbAttribute dbAttribute = entity.getAttribute(next);
+        if(dbAttribute != null) {
+            processAttribute(dbAttribute);
+            return;
+        }
+
+        DbRelationship relationship = entity.getRelationship(next);
+        if(relationship != null) {
+            entity = relationship.getTargetEntity();
+            processRelationship(relationship);
+            return;
+        }
+
+        throw new IllegalStateException("Unable to resolve path: " + currentDbPath.toString() + "." + next);
+    }
+
+    @Override
+    protected void processAliasedAttribute(String next, String alias) {
+        DbRelationship relationship = entity.getRelationship(alias);
+        if(relationship == null) {
+            throw new IllegalStateException("Non-relationship aliased path part: " + alias);
+        }
+
+        processRelationship(relationship);
+    }
+
+    private void processAttribute(DbAttribute attribute) {
+        addAttribute(currentDbPath.toString(), attribute);
+        appendCurrentPath(attribute.getName());
+    }
+
+    private void processRelationship(DbRelationship relationship) {
+        if (lastComponent) {
+            // if this is a last relationship in the path, it needs special handling
+            processRelTermination(relationship);
+        } else {
+            appendCurrentPath(relationship.getName());
+            context.getTableTree().addJoinTable(currentDbPath.toString(), relationship, JoinType.LEFT_OUTER);
+            if(!relationship.isToMany()) {
+                String path = currentDbPath.toString();
+                for (DbAttribute attribute : relationship.getTargetEntity().getPrimaryKeys()) {
+                    addAttribute(path, attribute);
+                }
+            }
+        }
+    }
+
+    protected void processRelTermination(DbRelationship rel) {
+        this.relationship = rel;
+        String path = currentDbPath.toString();
+        appendCurrentPath(rel.getName());
+
+        if (rel.isToMany() || !rel.isToPK()) {
+            // match on target PK
+            context.getTableTree().addJoinTable(currentDbPath.toString(), rel, JoinType.LEFT_OUTER);
+            path = currentDbPath.toString();
+            for(DbAttribute attribute : rel.getTargetEntity().getPrimaryKeys()) {
+                addAttribute(path, attribute);
+            }
+        } else {
+            for(DbJoin join : rel.getJoins()) {
+                addAttribute(path, join.getSource());
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultQuotingAppendable.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultQuotingAppendable.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultQuotingAppendable.java
new file mode 100644
index 0000000..712cc5f
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultQuotingAppendable.java
@@ -0,0 +1,47 @@
+/*****************************************************************
+ *   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.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.StringBuilderAppendable;
+
+/**
+ * @since 4.2
+ */
+public class DefaultQuotingAppendable extends StringBuilderAppendable {
+
+    private final TranslatorContext context;
+
+    public DefaultQuotingAppendable(TranslatorContext context) {
+        super();
+        this.context = context;
+    }
+
+    @Override
+    public QuotingAppendable appendQuoted(CharSequence content) {
+        context.getQuotingStrategy().quotedIdentifier(context.getRootDbEntity(), content, builder);
+        return this;
+    }
+
+    @Override
+    public TranslatorContext getContext() {
+        return context;
+    }
+}


[02/11] cayenne git commit: CAY-2465 New SelectTranslator implementation

Posted by nt...@apache.org.
http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java
index bb2248f..5e0db67 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java
@@ -42,6 +42,9 @@ import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Test;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 
@@ -71,10 +74,10 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	@Test
 	public void testCreateSqlString1() throws Exception {
 		// query with qualifier and ordering
-		SelectQuery<Artist> q = new SelectQuery<Artist>(Artist.class, ExpressionFactory.likeExp("artistName", "a%"));
+		SelectQuery<Artist> q = new SelectQuery<>(Artist.class, ExpressionFactory.likeExp("artistName", "a%"));
 		q.addOrdering("dateOfBirth", SortOrder.ASCENDING);
 
-		DefaultSelectTranslator defaultSelectTranslator = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver());
+		SelectTranslator defaultSelectTranslator = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver());
 		String generatedSql = defaultSelectTranslator.getSql();
 
 		// do some simple assertions to make sure all parts are in
@@ -96,7 +99,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 		final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
 		final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
 
-		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+		SelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
 				dataNode.getEntityResolver());
 
 		entity.setQualifier(ExpressionFactory.exp("ARTIST_NAME = \"123\""));
@@ -111,11 +114,11 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 			assertTrue(generatedSql.startsWith("SELECT "));
 			assertTrue(generatedSql.indexOf(" FROM ") > 0);
 			if (generatedSql.contains("RTRIM")) {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) =") > generatedSql.indexOf("RTRIM("));
 			} else if (generatedSql.contains("TRIM")) {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) =") > generatedSql.indexOf("TRIM("));
 			} else {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
+				assertTrue(generatedSql.indexOf("ARTIST_NAME =") > 0);
 			}
 		} finally {
 			entity.setQualifier(null);
@@ -132,7 +135,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 		final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
 		final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
 
-		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+		SelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
 				dataNode.getEntityResolver());
 
 		entity.setQualifier(ExpressionFactory.exp("ARTIST_NAME = \"123\""));
@@ -147,11 +150,11 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 			assertTrue(generatedSql.startsWith("SELECT "));
 			assertTrue(generatedSql.indexOf(" FROM ") > 0);
 			if (generatedSql.contains("RTRIM")) {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) =") > generatedSql.indexOf("RTRIM("));
 			} else if (generatedSql.contains("TRIM")) {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) =") > generatedSql.indexOf("TRIM("));
 			} else {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
+				assertTrue(generatedSql.indexOf("ARTIST_NAME =") > 0);
 			}
 
 		} finally {
@@ -169,7 +172,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 		final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
 		final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
 
-		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+		SelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
 				dataNode.getEntityResolver());
 
 		entity.setQualifier(ExpressionFactory.exp("ARTIST_NAME = \"123\""));
@@ -184,11 +187,11 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 			assertTrue(generatedSql.startsWith("SELECT "));
 			assertTrue(generatedSql.indexOf(" FROM ") > 0);
 			if (generatedSql.contains("RTRIM")) {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) =") > generatedSql.indexOf("RTRIM("));
 			} else if (generatedSql.contains("TRIM")) {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) =") > generatedSql.indexOf("TRIM("));
 			} else {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
+				assertTrue(generatedSql.indexOf("ARTIST_NAME =") > 0);
 			}
 
 		} finally {
@@ -200,13 +203,13 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	@Test
 	public void testDbEntityQualifier_RelatedMatch() throws Exception {
 
-		SelectQuery<Artist> q = new SelectQuery(Painting.class,
+		SelectQuery<Painting> q = new SelectQuery<>(Painting.class,
 				ExpressionFactory.matchExp("toArtist.artistName", "foo"));
 
 		final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
 		final DbEntity middleEntity = context.getEntityResolver().getDbEntity("ARTIST_GROUP");
 
-		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+		SelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
 				dataNode.getEntityResolver());
 
 		entity.setQualifier(ExpressionFactory.exp("ARTIST_NAME = \"123\""));
@@ -221,11 +224,11 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 			assertTrue(generatedSql.startsWith("SELECT "));
 			assertTrue(generatedSql.indexOf(" FROM ") > 0);
 			if (generatedSql.contains("RTRIM")) {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("RTRIM("));
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) =") > generatedSql.indexOf("RTRIM("));
 			} else if (generatedSql.contains("TRIM")) {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME) = ") > generatedSql.indexOf("TRIM("));
+				assertTrue(generatedSql.indexOf("ARTIST_NAME) =") > generatedSql.indexOf("TRIM("));
 			} else {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
+				assertTrue(generatedSql.indexOf("ARTIST_NAME =") > 0);
 			}
 		} finally {
 			entity.setQualifier(null);
@@ -239,7 +242,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	@Test
 	public void testCreateSqlString2() throws Exception {
 		// query with "distinct" set
-		SelectQuery q = new SelectQuery(Artist.class);
+		SelectQuery<Artist> q = new SelectQuery<>(Artist.class);
 		q.setDistinct(true);
 
 		String generatedSql = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver())
@@ -258,7 +261,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	@Test
 	public void testCreateSqlString5() throws Exception {
 		// query with qualifier and ordering
-		SelectQuery q = new SelectQuery(ArtistExhibit.class);
+		SelectQuery<ArtistExhibit> q = new SelectQuery<>(ArtistExhibit.class);
 		q.setQualifier(ExpressionFactory.likeExp("toArtist.artistName", "a%"));
 		q.andQualifier(ExpressionFactory.likeExp("toExhibit.toGallery.paintingArray.toArtist.artistName", "a%"));
 
@@ -290,7 +293,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	@Test
 	public void testCreateSqlString6() throws Exception {
 		// query with qualifier and ordering
-		SelectQuery q = new SelectQuery(ArtistExhibit.class);
+		SelectQuery<ArtistExhibit> q = new SelectQuery<>(ArtistExhibit.class);
 		q.setQualifier(ExpressionFactory.likeExp("toArtist.artistName", "a%"));
 		q.andQualifier(ExpressionFactory.likeExp("toArtist.paintingArray.paintingTitle", "p%"));
 
@@ -318,7 +321,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	 */
 	@Test
 	public void testCreateSqlString7() throws Exception {
-		SelectQuery q = new SelectQuery(Artist.class);
+		SelectQuery<Artist> q = new SelectQuery<>(Artist.class);
 		q.setQualifier(ExpressionFactory.greaterExp("dateOfBirth", new Date()));
 		q.andQualifier(ExpressionFactory.lessExp("dateOfBirth", new Date()));
 
@@ -351,7 +354,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	 */
 	@Test
 	public void testCreateSqlString8() throws Exception {
-		SelectQuery q = new SelectQuery();
+		SelectQuery<?> q = new SelectQuery<>();
 		q.setRoot(Painting.class);
 		q.setQualifier(ExpressionFactory.greaterExp("toArtist.dateOfBirth", new Date()));
 		q.andQualifier(ExpressionFactory.lessExp("toArtist.dateOfBirth", new Date()));
@@ -379,7 +382,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	@Test
 	public void testCreateSqlString9() throws Exception {
 		// query for a compound ObjEntity with qualifier
-		SelectQuery q = new SelectQuery(CompoundPainting.class, ExpressionFactory.likeExp("artistName", "a%"));
+		SelectQuery<CompoundPainting> q = new SelectQuery<>(CompoundPainting.class, ExpressionFactory.likeExp("artistName", "a%"));
 
 		String sql = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver()).getSql();
 
@@ -431,10 +434,10 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	@Test
 	public void testCreateSqlString10() throws Exception {
 		// query with to-many joint prefetches
-		SelectQuery q = new SelectQuery(Artist.class);
+		SelectQuery<Artist> q = new SelectQuery<>(Artist.class);
 		q.addPrefetch(Artist.PAINTING_ARRAY.joint());
 
-		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+		SelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
 				dataNode.getEntityResolver());
 		String sql = transl.getSql();
 		assertNotNull(sql);
@@ -449,32 +452,31 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 		assertTrue(sql, sql.indexOf("PAINTING_ID") > 0);
 
 		// assert we have one join
-		assertEquals(1, transl.joinStack.size());
+		assertTrue(transl.hasJoins());
 	}
 
 	@Test
 	public void testCreateSqlString11() throws Exception {
 		// query with joint prefetches and other joins
-		SelectQuery q = new SelectQuery(Artist.class, ExpressionFactory.exp("paintingArray.paintingTitle = 'a'"));
+		SelectQuery<Artist> q = new SelectQuery<>(Artist.class, ExpressionFactory.exp("paintingArray.paintingTitle = 'a'"));
 		q.addPrefetch(Artist.PAINTING_ARRAY.joint());
 
-		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+		SelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
 				dataNode.getEntityResolver());
 
 		transl.getSql();
 
 		// assert we only have one join
-		assertEquals(2, transl.joinStack.size());
 		assertTrue(transl.hasJoins());
 	}
 
 	@Test
 	public void testCreateSqlString12() throws Exception {
 		// query with to-one joint prefetches
-		SelectQuery q = new SelectQuery(Painting.class);
+		SelectQuery<Painting> q = new SelectQuery<>(Painting.class);
 		q.addPrefetch(Painting.TO_ARTIST.joint());
 
-		DefaultSelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
+		SelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(),
 				dataNode.getEntityResolver());
 
 		String sql = transl.getSql();
@@ -490,14 +492,13 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 		assertTrue(sql, sql.indexOf("PAINTING_ID") > 0);
 
 		// assert we have one join
-		assertEquals(1, transl.joinStack.size());
 		assertTrue(transl.hasJoins());
 	}
 
 	@Test
 	public void testCreateSqlString13() throws Exception {
 		// query with invalid joint prefetches
-		SelectQuery q = new SelectQuery(Painting.class);
+		SelectQuery<Painting> q = new SelectQuery<>(Painting.class);
 		q.addPrefetch("invalid.invalid").setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
 
 		try {
@@ -512,7 +513,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	public void testCreateSqlStringWithQuoteSqlIdentifiers() throws Exception {
 
 		try {
-			SelectQuery q = new SelectQuery(Artist.class);
+			SelectQuery<Artist> q = new SelectQuery<>(Artist.class);
 			DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
 			entity.getDataMap().setQuotingSQLIdentifiers(true);
 			q.addOrdering("dateOfBirth", SortOrder.ASCENDING);
@@ -549,7 +550,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	public void testCreateSqlStringWithQuoteSqlIdentifiers2() throws Exception {
 
 		try {
-			SelectQuery q = new SelectQuery(Artist.class);
+			SelectQuery<Artist> q = new SelectQuery<>(Artist.class);
 			DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
 			entity.getDataMap().setQuotingSQLIdentifiers(true);
 			q.setQualifier(ExpressionFactory.greaterExp("dateOfBirth", new Date()));
@@ -574,14 +575,12 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 			int iWhere = s.indexOf(" WHERE ");
 			assertTrue(iWhere > iArtist);
 
-			int dateOfBirth2 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd
-					+ " > ?");
+			int dateOfBirth2 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd + " > ?");
 			assertTrue(dateOfBirth2 > iWhere);
 
 			int iAnd = s.indexOf(" AND ");
 			assertTrue(iAnd > iWhere);
-			int dateOfBirth3 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd
-					+ " < ?");
+			int dateOfBirth3 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd + " < ?");
 			assertTrue(dateOfBirth3 > iAnd);
 
 		} finally {
@@ -596,7 +595,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 		// query with joint prefetches and other joins
 		// and with QuoteSqlIdentifiers = true
 		try {
-			SelectQuery q = new SelectQuery(Artist.class, ExpressionFactory.exp("paintingArray.paintingTitle = 'a'"));
+			SelectQuery<Artist> q = new SelectQuery<>(Artist.class, ExpressionFactory.exp("paintingArray.paintingTitle = 'a'"));
 			q.addPrefetch(Artist.PAINTING_ARRAY.joint());
 
 			DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
@@ -607,61 +606,60 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 
 			String s = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver()).getSql();
 
-			assertTrue(s.startsWith("SELECT DISTINCT "));
+			assertTrue(s, s.startsWith("SELECT DISTINCT "));
 			int iFrom = s.indexOf(" FROM ");
-			assertTrue(iFrom > 0);
+			assertTrue(s, iFrom > 0);
 			int artistName = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_NAME" + charEnd);
-			assertTrue(artistName > 0 && artistName < iFrom);
+			assertTrue(s, artistName > 0 && artistName < iFrom);
 			int artistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
-			assertTrue(artistId > 0 && artistId < iFrom);
+			assertTrue(s, artistId > 0 && artistId < iFrom);
 			int dateOfBirth = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd);
-			assertTrue(dateOfBirth > 0 && dateOfBirth < iFrom);
+			assertTrue(s, dateOfBirth > 0 && dateOfBirth < iFrom);
 			int estimatedPrice = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ESTIMATED_PRICE" + charEnd);
-			assertTrue(estimatedPrice > 0 && estimatedPrice < iFrom);
+			assertTrue(s, estimatedPrice > 0 && estimatedPrice < iFrom);
 			int paintingDescription = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "PAINTING_DESCRIPTION"
 					+ charEnd);
-			assertTrue(paintingDescription > 0 && paintingDescription < iFrom);
+			assertTrue(s, paintingDescription > 0 && paintingDescription < iFrom);
 			int paintingTitle = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "PAINTING_TITLE" + charEnd);
-			assertTrue(paintingTitle > 0 && paintingTitle < iFrom);
+			assertTrue(s, paintingTitle > 0 && paintingTitle < iFrom);
 			int artistIdT1 = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
-			assertTrue(artistIdT1 > 0 && artistIdT1 < iFrom);
+			assertTrue(s, artistIdT1 > 0 && artistIdT1 < iFrom);
 			int galleryId = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "GALLERY_ID" + charEnd);
-			assertTrue(galleryId > 0 && galleryId < iFrom);
+			assertTrue(s, galleryId > 0 && galleryId < iFrom);
 			int paintingId = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "PAINTING_ID" + charEnd);
-			assertTrue(paintingId > 0 && paintingId < iFrom);
+			assertTrue(s, paintingId > 0 && paintingId < iFrom);
 			int iArtist = s.indexOf(charStart + "ARTIST" + charEnd + " " + charStart + "t0" + charEnd);
-			assertTrue(iArtist > iFrom);
+			assertTrue(s, iArtist > iFrom);
 			int iLeftJoin = s.indexOf("LEFT JOIN");
-			assertTrue(iLeftJoin > iFrom);
+			assertTrue(s, iLeftJoin > iFrom);
 			int iPainting = s.indexOf(charStart + "PAINTING" + charEnd + " " + charStart + "t1" + charEnd);
-			assertTrue(iPainting > iLeftJoin);
+			assertTrue(s, iPainting > iLeftJoin);
 			int iOn = s.indexOf(" ON ");
-			assertTrue(iOn > iLeftJoin);
+			assertTrue(s, iOn > iLeftJoin);
 			int iArtistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
-			assertTrue(iArtistId > iOn);
+			assertTrue(s, iArtistId > iOn);
 			int iArtistIdT1 = s
 					.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
-			assertTrue(iArtistIdT1 > iOn);
+			assertTrue(s, iArtistIdT1 > iOn);
 			int i = s.indexOf("=", iLeftJoin);
-			assertTrue(iArtistIdT1 > i || iArtistId > i);
+			assertTrue(s, iArtistIdT1 > i || iArtistId > i);
 			int iJoin = s.indexOf("JOIN");
-			assertTrue(iJoin > iLeftJoin);
+			assertTrue(s, iJoin > iLeftJoin);
 			int iPainting2 = s.indexOf(charStart + "PAINTING" + charEnd + " " + charStart + "t2" + charEnd);
-			assertTrue(iPainting2 > iJoin);
+			assertTrue(s, iPainting2 > iJoin);
 			int iOn2 = s.indexOf(" ON ");
-			assertTrue(iOn2 > iJoin);
+			assertTrue(s, iOn2 > iJoin);
 			int iArtistId2 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iJoin);
-			assertTrue(iArtistId2 > iOn2);
+			assertTrue(s, iArtistId2 > iOn2);
 			int iArtistId2T2 = s.indexOf(charStart + "t2" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iJoin);
-			assertTrue(iArtistId2T2 > iOn2);
+			assertTrue(s, iArtistId2T2 > iOn2);
 			int i2 = s.indexOf("=", iJoin);
-			assertTrue(iArtistId2T2 > i2 || iArtistId2 > i2);
+			assertTrue(s, iArtistId2T2 > i2 || iArtistId2 > i2);
 			int iWhere = s.indexOf(" WHERE ");
-			assertTrue(iWhere > iJoin);
+			assertTrue(s, iWhere > iJoin);
 
-			int paintingTitle2 = s.indexOf(charStart + "t2" + charEnd + "." + charStart + "PAINTING_TITLE" + charEnd
-					+ " = ?");
-			assertTrue(paintingTitle2 > iWhere);
+			int paintingTitle2 = s.indexOf(charStart + "t2" + charEnd + "." + charStart + "PAINTING_TITLE" + charEnd + " = ?");
+			assertTrue(s, paintingTitle2 > iWhere);
 
 		} finally {
 			DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
@@ -675,7 +673,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 		// query with to-one joint prefetches
 		// and with QuoteSqlIdentifiers = true
 		try {
-			SelectQuery q = new SelectQuery(Painting.class);
+			SelectQuery<Painting> q = new SelectQuery<>(Painting.class);
 			q.addPrefetch(Painting.TO_ARTIST.joint());
 
 			DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
@@ -686,45 +684,45 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 
 			String s = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver()).getSql();
 
-			assertTrue(s.startsWith("SELECT "));
+			assertTrue(s, s.startsWith("SELECT "));
 			int iFrom = s.indexOf(" FROM ");
-			assertTrue(iFrom > 0);
+			assertTrue(s, iFrom > 0);
 
 			int paintingDescription = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "PAINTING_DESCRIPTION"
 					+ charEnd);
-			assertTrue(paintingDescription > 0 && paintingDescription < iFrom);
+			assertTrue(s, paintingDescription > 0 && paintingDescription < iFrom);
 			int paintingTitle = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "PAINTING_TITLE" + charEnd);
-			assertTrue(paintingTitle > 0 && paintingTitle < iFrom);
+			assertTrue(s, paintingTitle > 0 && paintingTitle < iFrom);
 			int artistIdT1 = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
-			assertTrue(artistIdT1 > 0 && artistIdT1 < iFrom);
+			assertTrue(s, artistIdT1 > 0 && artistIdT1 < iFrom);
 			int estimatedPrice = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ESTIMATED_PRICE" + charEnd);
-			assertTrue(estimatedPrice > 0 && estimatedPrice < iFrom);
+			assertTrue(s, estimatedPrice > 0 && estimatedPrice < iFrom);
 			int galleryId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "GALLERY_ID" + charEnd);
-			assertTrue(galleryId > 0 && galleryId < iFrom);
+			assertTrue(s, galleryId > 0 && galleryId < iFrom);
 			int paintingId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "PAINTING_ID" + charEnd);
-			assertTrue(paintingId > 0 && paintingId < iFrom);
+			assertTrue(s, paintingId > 0 && paintingId < iFrom);
 			int artistName = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_NAME" + charEnd);
-			assertTrue(artistName > 0 && artistName < iFrom);
+			assertTrue(s, artistName > 0 && artistName < iFrom);
 			int artistId = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd);
-			assertTrue(artistId > 0 && artistId < iFrom);
+			assertTrue(s, artistId > 0 && artistId < iFrom);
 			int dateOfBirth = s.indexOf(charStart + "t1" + charEnd + "." + charStart + "DATE_OF_BIRTH" + charEnd);
-			assertTrue(dateOfBirth > 0 && dateOfBirth < iFrom);
+			assertTrue(s, dateOfBirth > 0 && dateOfBirth < iFrom);
 			int iPainting = s.indexOf(charStart + "PAINTING" + charEnd + " " + charStart + "t0" + charEnd);
-			assertTrue(iPainting > iFrom);
+			assertTrue(s, iPainting > iFrom);
 
 			int iLeftJoin = s.indexOf("LEFT JOIN");
-			assertTrue(iLeftJoin > iFrom);
+			assertTrue(s, iLeftJoin > iFrom);
 			int iArtist = s.indexOf(charStart + "ARTIST" + charEnd + " " + charStart + "t1" + charEnd);
-			assertTrue(iArtist > iLeftJoin);
+			assertTrue(s, iArtist > iLeftJoin);
 			int iOn = s.indexOf(" ON ");
-			assertTrue(iOn > iLeftJoin);
+			assertTrue(s, iOn > iLeftJoin);
 			int iArtistId = s.indexOf(charStart + "t0" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
-			assertTrue(iArtistId > iOn);
+			assertTrue(s, iArtistId > iOn);
 			int iArtistIdT1 = s
 					.indexOf(charStart + "t1" + charEnd + "." + charStart + "ARTIST_ID" + charEnd, iLeftJoin);
-			assertTrue(iArtistIdT1 > iOn);
+			assertTrue(s, iArtistIdT1 > iOn);
 			int i = s.indexOf("=", iLeftJoin);
-			assertTrue(iArtistIdT1 > i || iArtistId > i);
+			assertTrue(s, iArtistIdT1 > i || iArtistId > i);
 
 		} finally {
 			DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
@@ -737,16 +735,28 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	 */
 	@Test
 	public void testBuildResultColumns1() throws Exception {
-		SelectQuery q = new SelectQuery(Painting.class);
-		DefaultSelectTranslator tr = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver());
+		SelectQuery<Painting> q = new SelectQuery<>(Painting.class);
+		SelectTranslator tr = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver());
 
-		List<?> columns = tr.buildResultColumns();
+		tr.getSql();
+
+		List<ColumnDescriptor> columns = Arrays.asList(tr.getResultColumns());
+		columns.sort(Comparator.comparing(ColumnDescriptor::getName));
 
-		// all DbAttributes must be included
 		DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
-		for (final DbAttribute a : entity.getAttributes()) {
-			ColumnDescriptor c = new ColumnDescriptor(a, "t0");
-			assertTrue("No descriptor for " + a + ", columns: " + columns, columns.contains(c));
+		List<DbAttribute> attributes = new ArrayList<>(entity.getAttributes());
+		attributes.sort(Comparator.comparing(DbAttribute::getName));
+
+		// all DbAttributes must be included
+		assertEquals(attributes.size(), columns.size());
+
+		for(int i=0; i<attributes.size(); i++) {
+			DbAttribute attribute = attributes.get(i);
+			ColumnDescriptor descriptor = columns.get(i);
+			assertEquals(attribute, descriptor.getAttribute());
+			assertEquals(attribute.getName(), descriptor.getName());
+			assertEquals(attribute.getName(), descriptor.getDataRowKey());
+			assertEquals(attribute.getType(), descriptor.getJdbcType());
 		}
 	}
 
@@ -755,31 +765,32 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 	 */
 	@Test
 	public void testBuildResultColumns2() throws Exception {
-		SelectQuery q = new SelectQuery(Painting.class);
+		SelectQuery<Painting> q = new SelectQuery<>(Painting.class);
 		q.addPrefetch(Painting.TO_ARTIST.joint());
-		DefaultSelectTranslator tr = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver());
-
-		List<?> columns = tr.buildResultColumns();
-
-		// assert root entity columns
-		DbEntity entity = context.getEntityResolver().getDbEntity("PAINTING");
-		for (final DbAttribute a : entity.getAttributes()) {
-			ColumnDescriptor c = new ColumnDescriptor(a, "t0");
-			assertTrue("No descriptor for " + a + ", columns: " + columns, columns.contains(c));
-		}
-
-		// assert joined columns
-		DbEntity joined = context.getEntityResolver().getDbEntity("ARTIST");
-		for (final DbAttribute a : joined.getAttributes()) {
-
-			// skip ARTIST PK, it is joined from painting
-			if (Artist.ARTIST_ID_PK_COLUMN.equals(a.getName())) {
-				continue;
+		SelectTranslator tr = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver());
+
+		tr.getSql();
+
+		List<ColumnDescriptor> columns = Arrays.asList(tr.getResultColumns());
+		columns.sort(Comparator.comparing(ColumnDescriptor::getName));
+
+		DbEntity rootEntity = context.getEntityResolver().getDbEntity("PAINTING");
+		List<DbAttribute> attributes = new ArrayList<>(rootEntity.getAttributes());
+		DbEntity joinedEntity = context.getEntityResolver().getDbEntity("ARTIST");
+		attributes.addAll(joinedEntity.getAttributes());
+		attributes.sort(Comparator.comparing(DbAttribute::getName));
+
+		for(int i=0; i<attributes.size(); i++) {
+			DbAttribute attribute = attributes.get(i);
+			ColumnDescriptor descriptor = columns.get(i);
+			assertEquals(attribute, descriptor.getAttribute());
+			assertEquals(attribute.getName(), descriptor.getName());
+			if("ARTIST".equals(attribute.getEntity().getName())) {
+				assertEquals("toArtist." + attribute.getName(), descriptor.getDataRowKey());
+			} else {
+				assertEquals(attribute.getName(), descriptor.getDataRowKey());
 			}
-
-			ColumnDescriptor c = new ColumnDescriptor(a, "t1");
-			c.setDataRowKey("toArtist." + a.getName());
-			assertTrue("No descriptor for " + a + ", columns: " + columns, columns.contains(c));
+			assertEquals(attribute.getType(), descriptor.getJdbcType());
 		}
 	}
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractorTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractorTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractorTest.java
new file mode 100644
index 0000000..b0de2f29
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractorTest.java
@@ -0,0 +1,97 @@
+/*****************************************************************
+ *   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.access.sqlbuilder.sqltree.ColumnNode;
+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 DescriptorColumnExtractorTest extends BaseColumnExtractorTest {
+
+    @Test
+    public void testExtractNoPrefix() {
+        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);
+
+        DescriptorColumnExtractor extractor = new DescriptorColumnExtractor(context, resolver.getClassDescriptor("mock"));
+        extractor.extract();
+
+        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());
+        assertNotNull(descriptor0.getDbAttribute());
+        assertEquals("name", descriptor0.getDataRowKey());
+        assertEquals(Types.VARBINARY, descriptor0.getJdbcType());
+        assertEquals("my.type", descriptor0.getJavaType());
+
+        assertNull(descriptor1.getProperty());
+        assertNotNull(descriptor1.getNode());
+        assertThat(descriptor1.getNode(), instanceOf(ColumnNode.class));
+        assertFalse(descriptor1.isAggregate());
+        assertTrue(descriptor1.isInDataRow());
+        assertEquals("id", descriptor1.getDataRowKey());
+        assertNotNull(descriptor1.getDbAttribute());
+        assertEquals(Types.BIGINT, 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/DistinctStageTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DistinctStageTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DistinctStageTest.java
new file mode 100644
index 0000000..26ccfe4
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DistinctStageTest.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.access.translator.select;
+
+import java.sql.Types;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.map.DbAttribute;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class DistinctStageTest {
+
+    @Test
+    public void isUnsupportedForDistinct() {
+        assertTrue(DistinctStage.isUnsupportedForDistinct(Types.BLOB));
+        assertTrue(DistinctStage.isUnsupportedForDistinct(Types.CLOB));
+        assertTrue(DistinctStage.isUnsupportedForDistinct(Types.NCLOB));
+        assertTrue(DistinctStage.isUnsupportedForDistinct(Types.LONGVARCHAR));
+        assertTrue(DistinctStage.isUnsupportedForDistinct(Types.LONGNVARCHAR));
+        assertTrue(DistinctStage.isUnsupportedForDistinct(Types.LONGVARBINARY));
+        assertFalse(DistinctStage.isUnsupportedForDistinct(Types.INTEGER));
+        assertFalse(DistinctStage.isUnsupportedForDistinct(Types.DATE));
+        assertFalse(DistinctStage.isUnsupportedForDistinct(Types.CHAR));
+        assertFalse(DistinctStage.isUnsupportedForDistinct(Types.DECIMAL));
+        assertFalse(DistinctStage.isUnsupportedForDistinct(Types.FLOAT));
+        assertFalse(DistinctStage.isUnsupportedForDistinct(Types.VARCHAR));
+    }
+
+    @Test
+    public void noSuppression() {
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder().withDistinct(true).build();
+        TranslatorContext context = new MockTranslatorContext(wrapper);
+
+        assertFalse(context.isDistinctSuppression());
+
+        DistinctStage stage = new DistinctStage();
+        stage.perform(context);
+
+        assertFalse(context.isDistinctSuppression());
+    }
+
+    @Test
+    public void explicitSuppression() {
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withDistinct(true)
+                .withMetaData(new MockQueryMetadataBuilder()
+                        .withSuppressDistinct()
+                        .build())
+                .build();
+        TranslatorContext context = new MockTranslatorContext(wrapper);
+
+        assertFalse(context.isDistinctSuppression());
+
+        DistinctStage stage = new DistinctStage();
+        stage.perform(context);
+
+        assertTrue(context.isDistinctSuppression());
+    }
+
+    @Test
+    public void suppressionByType() {
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withDistinct(true)
+                .withMetaData(new MockQueryMetadataBuilder().build())
+                .build();
+        TranslatorContext context = new MockTranslatorContext(wrapper);
+
+        DbAttribute attribute = new DbAttribute();
+        attribute.setType(Types.LONGVARBINARY);
+        Node node = new ColumnNode("t0", "attr", null, attribute);
+        context.addResultNode(node);
+
+        assertFalse(context.isDistinctSuppression());
+
+        DistinctStage stage = new DistinctStage();
+        stage.perform(context);
+
+        assertTrue(context.isDistinctSuppression());
+    }
+}
\ 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/GroupByStageTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/GroupByStageTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/GroupByStageTest.java
new file mode 100644
index 0000000..d739714
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/GroupByStageTest.java
@@ -0,0 +1,86 @@
+/*****************************************************************
+ *   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.access.sqlbuilder.sqltree.EmptyNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.GroupByNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.exp.Property;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class GroupByStageTest {
+
+    private TranslatorContext context;
+
+    @Before
+    public void prepareContext() {
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder().build();
+        context = new MockTranslatorContext(wrapper);
+    }
+
+    // no result columns
+    @Test
+    public void emptyContext() {
+        GroupByStage stage = new GroupByStage();
+        stage.perform(context);
+
+        Node node = context.getSelectBuilder().build();
+        assertEquals(0, node.getChildrenCount());
+    }
+
+    // result column but no aggregates
+    @Test
+    public void noAggregates() {
+        context.addResultNode(new ColumnNode("t0", "column", null, null));
+
+        GroupByStage stage = new GroupByStage();
+        stage.perform(context);
+
+        Node node = context.getSelectBuilder().build();
+        assertEquals(0, node.getChildrenCount());
+    }
+
+    // result column + aggregate
+    @Test
+    public void groupByWithAggregates() {
+        context.addResultNode(new ColumnNode("t0", "column", null, null));
+        context.addResultNode(new EmptyNode(), true, Property.COUNT, "count");
+
+        GroupByStage stage = new GroupByStage();
+        stage.perform(context);
+
+        Node node = context.getSelectBuilder().build();
+        assertEquals(1, node.getChildrenCount());
+        assertThat(node.getChild(0), instanceOf(GroupByNode.class));
+        assertEquals(1, node.getChild(0).getChildrenCount());
+        assertThat(node.getChild(0).getChild(0), instanceOf(ColumnNode.class));
+        ColumnNode columnNode = (ColumnNode)node.getChild(0).getChild(0);
+        assertEquals("t0", columnNode.getTable());
+        assertEquals("column", columnNode.getColumn());
+    }
+}
\ 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/HavingTranslationStageTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/HavingTranslationStageTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/HavingTranslationStageTest.java
new file mode 100644
index 0000000..8d6160e
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/HavingTranslationStageTest.java
@@ -0,0 +1,90 @@
+/*****************************************************************
+ *   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.access.sqlbuilder.sqltree.HavingNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ValueNode;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @since 4.2
+ */
+public class HavingTranslationStageTest {
+
+    private TranslatorContext context;
+
+    @Before
+    public void prepareContext() {
+        DbEntity dbEntity = new DbEntity();
+        dbEntity.setName("mock");
+        DbAttribute dbAttribute = new DbAttribute();
+        dbAttribute.setName("path");
+        dbEntity.addAttribute(dbAttribute);
+
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withHavingQualifier(ExpressionFactory.greaterOrEqualDbExp("path", 10))
+                .withMetaData(new MockQueryMetadataBuilder()
+                        .withDbEntity(dbEntity)
+                        .build())
+                .build();
+        context = new MockTranslatorContext(wrapper);
+    }
+
+    @Test
+    public void perform() {
+        HavingTranslationStage stage = new HavingTranslationStage();
+        stage.perform(context);
+
+        Node select = context.getSelectBuilder().build();
+
+        // Content of "select" node:
+        //
+        //      Having
+        //        |
+        //   OpExpression
+        //    /        \
+        // Column     Value
+
+        assertEquals(1, select.getChildrenCount());
+        assertThat(select.getChild(0), instanceOf(HavingNode.class));
+        Node op = select.getChild(0).getChild(0);
+        assertThat(op, instanceOf(OpExpressionNode.class));
+        assertEquals(">=", ((OpExpressionNode)op).getOp());
+        assertEquals(2, op.getChildrenCount());
+        assertThat(op.getChild(0), instanceOf(ColumnNode.class));
+        assertThat(op.getChild(1), instanceOf(ValueNode.class));
+
+        ColumnNode columnNode = (ColumnNode)op.getChild(0);
+        ValueNode valueNode = (ValueNode)op.getChild(1);
+        assertEquals("path", columnNode.getColumn());
+        assertEquals(10, valueNode.getValue());
+    }
+}
\ 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/IdColumnExtractorTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/IdColumnExtractorTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/IdColumnExtractorTest.java
new file mode 100644
index 0000000..28df71b
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/IdColumnExtractorTest.java
@@ -0,0 +1,113 @@
+/*****************************************************************
+ *   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.apache.cayenne.map.ObjEntity;
+import org.junit.Test;
+
+import java.sql.Types;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
+public class IdColumnExtractorTest extends BaseColumnExtractorTest {
+
+    @Test
+    public void testExtractNoPrefix() {
+        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.setDataMap(dataMap);
+        entity.setDbEntity(mockDbEntity);
+
+        IdColumnExtractor extractor = new IdColumnExtractor(context, entity);
+        extractor.extract();
+
+        assertEquals(1, context.getResultNodeList().size());
+
+        ResultNodeDescriptor descriptor0 = context.getResultNodeList().get(0);
+
+        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());
+    }
+
+    @Test
+    public void testExtractWithPrefix() {
+        DbEntity mockDbEntity = createMockDbEntity("mock1");
+        DbEntity mock2DbEntity = createMockDbEntity("mock2");
+
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withMetaData(new MockQueryMetadataBuilder()
+                        .withDbEntity(mockDbEntity)
+                        .build())
+                .build();
+        TranslatorContext context = new MockTranslatorContext(wrapper);
+
+        ObjEntity entity = new ObjEntity();
+        entity.setDbEntity(mockDbEntity);
+
+        DataMap dataMap = new DataMap();
+        dataMap.addDbEntity(mockDbEntity);
+        dataMap.addDbEntity(mock2DbEntity);
+        mockDbEntity.setDataMap(dataMap);
+        entity.setDataMap(dataMap);
+
+        DbRelationship relationship = new DbRelationship();
+        relationship.setSourceEntity(mockDbEntity);
+        relationship.setTargetEntityName("mock1");
+        context.getTableTree().addJoinTable("prefix", relationship, JoinType.INNER);
+
+        IdColumnExtractor extractor = new IdColumnExtractor(context, entity);
+        extractor.extract("prefix");
+
+        assertEquals(1, context.getResultNodeList().size());
+
+        ResultNodeDescriptor descriptor0 = context.getResultNodeList().get(0);
+
+        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());
+    }
+}
\ 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/LimitOffsetStageTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/LimitOffsetStageTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/LimitOffsetStageTest.java
new file mode 100644
index 0000000..3cd86d1
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/LimitOffsetStageTest.java
@@ -0,0 +1,65 @@
+/*****************************************************************
+ *   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.LimitOffsetNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class LimitOffsetStageTest {
+
+    private TranslatorContext context;
+
+    @Before
+    public void prepareContext() {
+        DbEntity entity = new DbEntity();
+        entity.setName("mock");
+
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withMetaData(new MockQueryMetadataBuilder()
+                        .withDbEntity(entity)
+                        .withLimitOffset(123, 321)
+                        .build())
+                .build();
+        context = new MockTranslatorContext(wrapper);
+    }
+
+    @Test
+    public void perform() {
+        LimitOffsetStage stage = new LimitOffsetStage();
+        stage.perform(context);
+
+        Node select = context.getSelectBuilder().build();
+        Node child = select.getChild(0);
+        assertThat(child, instanceOf(LimitOffsetNode.class));
+
+        LimitOffsetNode limitOffsetNode = (LimitOffsetNode) child;
+        assertEquals(123, limitOffsetNode.getLimit());
+        assertEquals(321, limitOffsetNode.getOffset());
+    }
+}
\ 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/MockQueryMetadataBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryMetadataBuilder.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryMetadataBuilder.java
new file mode 100644
index 0000000..e665381
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryMetadataBuilder.java
@@ -0,0 +1,100 @@
+/*****************************************************************
+ *   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.Collections;
+import java.util.Map;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.query.MockQueryMetadata;
+import org.apache.cayenne.query.QueryMetadata;
+
+/**
+ * @since 4.2
+ */
+class MockQueryMetadataBuilder {
+
+    private ObjEntity objEntity;
+
+    private DbEntity dbEntity;
+
+    private int limit;
+
+    private int offset;
+
+    private boolean suppressDistinct;
+
+    MockQueryMetadataBuilder withDbEntity(DbEntity entity) {
+        this.dbEntity = entity;
+        return this;
+    }
+
+    MockQueryMetadataBuilder withObjEntity(ObjEntity entity) {
+        this.objEntity = entity;
+        return this;
+    }
+
+    MockQueryMetadataBuilder withLimitOffset(int limit, int offset) {
+        this.limit = limit;
+        this.offset = offset;
+        return this;
+    }
+
+    MockQueryMetadataBuilder withSuppressDistinct() {
+        this.suppressDistinct = true;
+        return this;
+    }
+
+    QueryMetadata build() {
+        return new MockQueryMetadata() {
+
+            @Override
+            public ObjEntity getObjEntity() {
+                return objEntity;
+            }
+
+            @Override
+            public DbEntity getDbEntity() {
+                return dbEntity;
+            }
+
+            @Override
+            public int getFetchOffset() {
+                return offset;
+            }
+
+            @Override
+            public int getFetchLimit() {
+                return limit;
+            }
+
+            @Override
+            public Map<String, String> getPathSplitAliases() {
+                return Collections.emptyMap();
+            }
+
+            @Override
+            public boolean isSuppressingDistinct() {
+                return suppressDistinct;
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryWrapperBuilder.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..3c43204
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockQueryWrapperBuilder.java
@@ -0,0 +1,137 @@
+/*****************************************************************
+ *   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 org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.property.BaseProperty;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.MockQueryMetadata;
+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
+ */
+class MockQueryWrapperBuilder {
+
+    private boolean distinct;
+
+    private QueryMetadata metaData;
+
+    private PrefetchTreeNode prefetchTreeNode;
+
+    private Expression qualifier;
+
+    private Collection<Ordering> orderings;
+
+    private Collection<BaseProperty<?>> columns;
+
+    private Expression havingQualifier;
+
+    private Select<?> mockSelect;
+
+    MockQueryWrapperBuilder withDistinct(boolean distinct) {
+        this.distinct = distinct;
+        return this;
+    }
+
+    MockQueryWrapperBuilder withMetaData(QueryMetadata metaData) {
+        this.metaData = metaData;
+        return this;
+    }
+
+    MockQueryWrapperBuilder withPrefetchTreeNode(PrefetchTreeNode prefetchTreeNode) {
+        this.prefetchTreeNode = prefetchTreeNode;
+        return this;
+    }
+
+    MockQueryWrapperBuilder withQualifier(Expression qualifier) {
+        this.qualifier = qualifier;
+        return this;
+    }
+
+    MockQueryWrapperBuilder withOrderings(Collection<Ordering> orderings) {
+        this.orderings = orderings;
+        return this;
+    }
+
+    MockQueryWrapperBuilder withColumns(Collection<BaseProperty<?>> columns) {
+        this.columns = columns;
+        return this;
+    }
+
+    MockQueryWrapperBuilder withHavingQualifier(Expression havingQualifier) {
+        this.havingQualifier = havingQualifier;
+        return this;
+    }
+
+    MockQueryWrapperBuilder withSelect(Select<?> select) {
+        this.mockSelect = select;
+        return this;
+    }
+
+    TranslatableQueryWrapper build() {
+        return new TranslatableQueryWrapper() {
+            @Override
+            public boolean isDistinct() {
+                return distinct;
+            }
+
+            @Override
+            public QueryMetadata getMetaData(EntityResolver resolver) {
+                return metaData != null ? metaData : new MockQueryMetadata();
+            }
+
+            @Override
+            public PrefetchTreeNode getPrefetchTree() {
+                return prefetchTreeNode;
+            }
+
+            @Override
+            public Expression getQualifier() {
+                return qualifier;
+            }
+
+            @Override
+            public Collection<Ordering> getOrderings() {
+                return orderings;
+            }
+
+            @Override
+            public Collection<BaseProperty<?>> getColumns() {
+                return columns;
+            }
+
+            @Override
+            public Expression getHavingQualifier() {
+                return havingQualifier;
+            }
+
+            @Override
+            public Select<?> unwrap() {
+                return mockSelect;
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockTranslatorContext.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockTranslatorContext.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockTranslatorContext.java
new file mode 100644
index 0000000..b18f5cf
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/MockTranslatorContext.java
@@ -0,0 +1,35 @@
+/*****************************************************************
+ *   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.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
+
+import static org.mockito.Mockito.mock;
+
+public class MockTranslatorContext extends TranslatorContext {
+    MockTranslatorContext(TranslatableQueryWrapper query) {
+        super(query, mock(DbAdapter.class), null, null);
+    }
+
+    MockTranslatorContext(TranslatableQueryWrapper query, EntityResolver resolver) {
+        super(query, mock(DbAdapter.class), resolver, null);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT.java
new file mode 100644
index 0000000..93f0141
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT.java
@@ -0,0 +1,92 @@
+/*****************************************************************
+ *   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.ObjectContext;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.query.SelectQuery;
+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.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.INHERITANCE_VERTICAL_PROJECT)
+public class ObjPathProcessorIT extends ServerCase {
+
+    @Inject
+    protected ObjectContext context;
+
+    private ObjPathProcessor pathProcessor;
+
+    @Before
+    public void prepareTranslationContext() {
+        TranslatorContext translatorContext = new TranslatorContext(
+                new SelectQueryWrapper(new SelectQuery<>()),
+                Mockito.mock(DbAdapter.class),
+                context.getEntityResolver(),
+                null
+        );
+        ObjEntity entity = context.getEntityResolver().getObjEntity("IvSub3");
+        pathProcessor = new ObjPathProcessor(translatorContext, entity, null);
+    }
+
+    @Test
+    public void testSimpleAttributePathTranslation() {
+        PathTranslationResult result = pathProcessor.process("name");
+        assertEquals(1, result.getDbAttributes().size());
+        assertEquals(1, result.getAttributePaths().size());
+
+        assertEquals("", result.getLastAttributePath());
+        assertEquals("NAME", result.getLastAttribute().getName());
+    }
+
+    @Test
+    public void testInheritedRelationshipPathTranslation() {
+        PathTranslationResult result = pathProcessor.process("ivRoot");
+        assertEquals(2, result.getDbAttributes().size());
+        assertEquals(2, result.getAttributePaths().size());
+
+        assertEquals("sub3", result.getAttributePaths().get(0));
+        assertEquals("ID", result.getDbAttributes().get(0).getName());
+
+        assertEquals("sub3", result.getAttributePaths().get(1));
+        assertEquals("IV_ROOT_ID", result.getDbAttributes().get(1).getName());
+    }
+
+    @Test
+    public void testFlattenedAttributePathTranslation() {
+        PathTranslationResult result = pathProcessor.process("ivRoot.discriminator");
+        assertEquals(1, result.getDbAttributes().size());
+        assertEquals(1, result.getAttributePaths().size());
+
+        assertEquals("sub3.ivRoot1", result.getAttributePaths().get(0));
+        assertEquals("DISCRIMINATOR", result.getDbAttributes().get(0).getName());
+    }
+
+}
\ 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/ObjPathProcessorIT2.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT2.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT2.java
new file mode 100644
index 0000000..484472d
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT2.java
@@ -0,0 +1,91 @@
+/*****************************************************************
+ *   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.ObjectContext;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.query.SelectQuery;
+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.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.COMPOUND_PROJECT)
+public class ObjPathProcessorIT2 extends ServerCase {
+
+    @Inject
+    protected ObjectContext context;
+
+    private ObjPathProcessor pathProcessor;
+
+    @Before
+    public void prepareTranslationContext() {
+        TranslatorContext translatorContext = new TranslatorContext(
+                new SelectQueryWrapper(new SelectQuery<>()),
+                Mockito.mock(DbAdapter.class),
+                context.getEntityResolver(),
+                null
+        );
+        ObjEntity entity = context.getEntityResolver().getObjEntity("CompoundFkTestEntity");
+        pathProcessor = new ObjPathProcessor(translatorContext, entity, null);
+    }
+
+    @Test
+    public void testSimpleAttributePathTranslation() {
+        PathTranslationResult result = pathProcessor.process("name");
+        assertEquals(1, result.getDbAttributes().size());
+        assertEquals(1, result.getAttributePaths().size());
+
+        assertEquals("", result.getLastAttributePath());
+        assertEquals("NAME", result.getLastAttribute().getName());
+    }
+
+    @Test
+    public void testCompoundRelationshipPathTranslation() {
+        PathTranslationResult result = pathProcessor.process("toCompoundPk");
+        assertEquals(2, result.getDbAttributes().size());
+        assertEquals(2, result.getAttributePaths().size());
+
+        assertEquals("", result.getAttributePaths().get(0));
+        assertEquals("F_KEY1", result.getDbAttributes().get(0).getName());
+
+        assertEquals("", result.getAttributePaths().get(1));
+        assertEquals("F_KEY2", result.getDbAttributes().get(1).getName());
+    }
+
+    @Test
+    public void testCompoundRelationshipFlattenedPathTranslation() {
+        PathTranslationResult result = pathProcessor.process("toCompoundPk.name");
+
+        assertEquals(1, result.getDbAttributes().size());
+
+        assertEquals("toCompoundPk", result.getLastAttributePath());
+        assertEquals("NAME", result.getLastAttribute().getName());
+    }
+}
\ 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/ObjPathProcessorIT3.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT3.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT3.java
new file mode 100644
index 0000000..8175777
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT3.java
@@ -0,0 +1,82 @@
+/*****************************************************************
+ *   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.ObjectContext;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.query.SelectQuery;
+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.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.RELATIONSHIPS_FLATTENED_PROJECT)
+public class ObjPathProcessorIT3 extends ServerCase {
+
+    @Inject
+    protected ObjectContext context;
+
+    private ObjPathProcessor pathProcessor;
+
+    @Before
+    public void prepareTranslationContext() {
+        TranslatorContext translatorContext = new TranslatorContext(
+                new SelectQueryWrapper(new SelectQuery<>()),
+                Mockito.mock(DbAdapter.class),
+                context.getEntityResolver(),
+                null
+        );
+        ObjEntity entity = context.getEntityResolver().getObjEntity("FlattenedTest5");
+        pathProcessor = new ObjPathProcessor(translatorContext, entity, null);
+    }
+
+    @Test
+    public void testSimpleAttributePathTranslation() {
+        PathTranslationResult result = pathProcessor.process("name");
+        assertEquals(1, result.getDbAttributes().size());
+        assertEquals(1, result.getAttributePaths().size());
+
+        assertEquals("", result.getLastAttributePath());
+        assertEquals("NAME", result.getLastAttribute().getName());
+    }
+
+    @Test
+    public void testFlattenedRelationshipPathTranslation() {
+        PathTranslationResult result = pathProcessor.process("toFT1");
+        assertEquals(2, result.getDbAttributes().size());
+        assertEquals(2, result.getAttributePaths().size());
+
+        assertEquals("complexJoin2", result.getAttributePaths().get(0));
+        assertEquals("PK", result.getDbAttributes().get(0).getName());
+
+        assertEquals("complexJoin2", result.getAttributePaths().get(1));
+        assertEquals("FT1_FK", result.getDbAttributes().get(1).getName());
+    }
+
+}
\ 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/ObjPathProcessorIT4.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT4.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT4.java
new file mode 100644
index 0000000..e84d06f
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/ObjPathProcessorIT4.java
@@ -0,0 +1,72 @@
+/*****************************************************************
+ *   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.ObjectContext;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.query.SelectQuery;
+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.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class ObjPathProcessorIT4 extends ServerCase {
+
+    @Inject
+    protected ObjectContext context;
+
+    private ObjPathProcessor pathProcessor;
+
+    @Before
+    public void prepareTranslationContext() {
+        TranslatorContext translatorContext = new TranslatorContext(
+                new SelectQueryWrapper(new SelectQuery<>()),
+                Mockito.mock(DbAdapter.class),
+                context.getEntityResolver(),
+                null
+        );
+        ObjEntity entity = context.getEntityResolver().getObjEntity("CompoundPainting");
+        pathProcessor = new ObjPathProcessor(translatorContext, entity, null);
+    }
+
+    @Test
+    public void testSimpleAttributePathTranslation() {
+        PathTranslationResult result = pathProcessor.process("textReview");
+        assertEquals(2, result.getDbAttributes().size());
+        assertEquals(2, result.getAttributePaths().size());
+
+        assertEquals("toPaintingInfo", result.getAttributePaths().get(0));
+        assertEquals("PAINTING_ID", result.getDbAttributes().get(0).getName());
+
+        assertEquals("toPaintingInfo", result.getAttributePaths().get(1));
+        assertEquals("TEXT_REVIEW", result.getDbAttributes().get(1).getName());
+    }
+
+}
\ 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/OrderingStageTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingStageTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingStageTest.java
new file mode 100644
index 0000000..6292052
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/OrderingStageTest.java
@@ -0,0 +1,110 @@
+/*****************************************************************
+ *   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.Collections;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.EmptyNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OrderByNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TextNode;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.query.Ordering;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class OrderingStageTest {
+
+    private TranslatorContext context;
+
+    @Before
+    public void prepareContext() {
+        DbEntity dbEntity = new DbEntity();
+        dbEntity.setName("mock");
+        DbAttribute dbAttribute = new DbAttribute();
+        dbAttribute.setName("path");
+        dbEntity.addAttribute(dbAttribute);
+
+        ObjEntity objEntity = new ObjEntity();
+        objEntity.setName("mock");
+        objEntity.setDbEntity(dbEntity);
+
+        ObjAttribute objAttribute = new ObjAttribute();
+        objAttribute.setName("path");
+        objAttribute.setDbAttributePath("path");
+        objEntity.addAttribute(objAttribute);
+
+        DataMap dataMap = new DataMap();
+        dataMap.addObjEntity(objEntity);
+        dataMap.addDbEntity(dbEntity);
+
+        Ordering ordering = new Ordering("path");
+        ordering.setDescending();
+
+        TranslatableQueryWrapper wrapper = new MockQueryWrapperBuilder()
+                .withOrderings(Collections.singleton(ordering))
+                .withMetaData(new MockQueryMetadataBuilder()
+                        .withDbEntity(dbEntity)
+                        .withObjEntity(objEntity)
+                        .build())
+                .build();
+        context = new MockTranslatorContext(wrapper);
+    }
+
+    @Test
+    public void perform() {
+        OrderingStage orderingStage = new OrderingStage();
+        orderingStage.perform(context);
+
+        Node select = context.getSelectBuilder().build();
+
+        // Content of "select" node:
+        //
+        //     OrderBy
+        //        |
+        //      Empty
+        //     /     \
+        // Column    "DESC"
+
+        Node child = select.getChild(0);
+        assertEquals(1, select.getChildrenCount());
+        assertThat(child, instanceOf(OrderByNode.class));
+        assertEquals(1, child.getChildrenCount());
+        assertThat(child.getChild(0), instanceOf(EmptyNode.class));
+        assertEquals(2, child.getChild(0).getChildrenCount());
+        assertThat(child.getChild(0).getChild(0), instanceOf(ColumnNode.class));
+        assertThat(child.getChild(0).getChild(1), instanceOf(TextNode.class));
+
+        ColumnNode columnNode = (ColumnNode)child.getChild(0).getChild(0);
+        assertEquals("path", columnNode.getColumn());
+        assertEquals("Node { DESC}", child.getChild(0).getChild(1).toString());
+    }
+}
\ No newline at end of file


[08/11] cayenne git commit: CAY-2465 New SelectTranslator implementation

Posted by nt...@apache.org.
http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
index 4f1a10c..4107dbe 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
@@ -16,993 +16,117 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
+
 package org.apache.cayenne.access.translator.select;
 
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.Persistent;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dba.TypesMapping;
-import org.apache.cayenne.exp.property.BaseProperty;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.ExpressionFactory;
-import org.apache.cayenne.exp.TraversalHelper;
-import org.apache.cayenne.exp.parser.ASTAggregateFunctionCall;
-import org.apache.cayenne.exp.parser.ASTDbPath;
-import org.apache.cayenne.map.DataMap;
-import org.apache.cayenne.map.DbAttribute;
-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.map.JoinType;
 import org.apache.cayenne.map.ObjAttribute;
-import org.apache.cayenne.map.ObjEntity;
-import org.apache.cayenne.map.ObjRelationship;
-import org.apache.cayenne.map.PathComponent;
-import org.apache.cayenne.query.PrefetchProcessor;
-import org.apache.cayenne.query.PrefetchSelectQuery;
-import org.apache.cayenne.query.PrefetchTreeNode;
-import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.ColumnSelect;
+import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.query.SelectQuery;
-import org.apache.cayenne.reflect.ArcProperty;
-import org.apache.cayenne.reflect.AttributeProperty;
-import org.apache.cayenne.reflect.ClassDescriptor;
-import org.apache.cayenne.reflect.PropertyVisitor;
-import org.apache.cayenne.reflect.ToManyProperty;
-import org.apache.cayenne.reflect.ToOneProperty;
-import org.apache.cayenne.util.CayenneMapEntry;
-import org.apache.cayenne.util.EqualsBuilder;
-import org.apache.cayenne.util.HashCodeBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.sql.Types;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 /**
- * @since 4.0
+ * Default translator of select queries ({@link SelectQuery}, {@link ObjectSelect} and {@link ColumnSelect}).
+ *
+ * @since 4.2
  */
-public class DefaultSelectTranslator extends QueryAssembler implements SelectTranslator {
-
-	private static final Logger logger = LoggerFactory.getLogger(SelectTranslator.class);
-
-	protected static final int[] UNSUPPORTED_DISTINCT_TYPES = { Types.BLOB, Types.CLOB, Types.NCLOB,
-			Types.LONGVARBINARY, Types.LONGVARCHAR, Types.LONGNVARCHAR };
-
-	protected static boolean isUnsupportedForDistinct(int type) {
-		for (int unsupportedDistinctType : UNSUPPORTED_DISTINCT_TYPES) {
-			if (unsupportedDistinctType == type) {
-				return true;
-			}
-		}
-
-		return false;
-	}
-
-	JoinStack joinStack;
-	List<ColumnDescriptor> resultColumns;
-	Map<ObjAttribute, ColumnDescriptor> attributeOverrides;
-	Map<ColumnDescriptor, ObjAttribute> defaultAttributesByColumn;
-
-	boolean suppressingDistinct;
-
-	/**
-	 * If set to <code>true</code>, indicates that distinct select query is
-	 * required no matter what the original query settings where. This flag can
-	 * be set when joins are created using "to-many" relationships.
-	 */
-	boolean forcingDistinct;
-
-	/**
-	 * Does this SQL have any aggregate function
-	 */
-	boolean haveAggregate;
-	Map<ColumnDescriptor, List<DbAttributeBinding>> groupByColumns;
-
-	/**
-	 * Callback for joins creation
-	 */
-	AddJoinListener joinListener;
-
-	JointPrefetchChecker jointPrefetchChecker = new JointPrefetchChecker();
-
-
-	public DefaultSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
-		super(query, adapter, entityResolver);
-	}
-
-	protected JoinStack getJoinStack() {
-		if (joinStack == null) {
-			joinStack = createJoinStack();
-		}
-		return joinStack;
-	}
-
-	protected JoinStack createJoinStack() {
-		return new JoinStack(getAdapter(), this);
-	}
-
-	@Override
-	protected void doTranslate() {
-
-		checkLimitAndJointPrefetch();
-
-		DataMap dataMap = queryMetadata.getDataMap();
-		JoinStack joins = getJoinStack();
-
-		QuotingStrategy strategy = getAdapter().getQuotingStrategy();
-		forcingDistinct = false;
-
-		// build column list
-		this.resultColumns = buildResultColumns();
-
-		// build qualifier
-		QualifierTranslator qualifierTranslator = adapter.getQualifierTranslator(this);
-		StringBuilder whereQualifierBuffer = qualifierTranslator.appendPart(new StringBuilder());
-
-		// build having qualifier
-		Expression havingQualifier = getSelectQuery().getHavingQualifier();
-		StringBuilder havingQualifierBuffer = null;
-		if(havingQualifier != null) {
-			haveAggregate = true;
-			QualifierTranslator havingQualifierTranslator = adapter.getQualifierTranslator(this);
-			havingQualifierTranslator.setQualifier(havingQualifier);
-			havingQualifierBuffer = havingQualifierTranslator.appendPart(new StringBuilder());
-		}
-
-		if(!haveAggregate && groupByColumns != null) {
-			// if no expression with aggregation function found
-			// in select columns and there is no having clause
-			groupByColumns.clear();
-		}
-
-		// build ORDER BY
-		OrderingTranslator orderingTranslator = new OrderingTranslator(this);
-		StringBuilder orderingBuffer = orderingTranslator.appendPart(new StringBuilder());
-
-		// assemble
-		StringBuilder queryBuf = new StringBuilder();
-		queryBuf.append("SELECT ");
-
-		// check if DISTINCT is appropriate
-		// side effect: "suppressingDistinct" flag may end up being flipped here
-		if (forcingDistinct || getSelectQuery().isDistinct()) {
-			suppressingDistinct = queryMetadata.isSuppressingDistinct();
-			if(!suppressingDistinct) {
-				for (ColumnDescriptor column : resultColumns) {
-					if (isUnsupportedForDistinct(column.getJdbcType())) {
-						suppressingDistinct = true;
-						break;
-					}
-				}
-			}
-
-			if (!suppressingDistinct) {
-				queryBuf.append(buildDistinctStatement()).append(" ");
-			}
-		}
-
-		// convert ColumnDescriptors to column names
-		List<String> selectColumnExpList = new ArrayList<>();
-		for (ColumnDescriptor column : resultColumns) {
-			String fullName;
-			if(column.isExpression()) {
-				fullName = column.getName();
-			} else {
-				fullName = strategy.quotedIdentifier(dataMap, column.getNamePrefix(), column.getName());
-			}
-			selectColumnExpList.add(fullName);
-		}
-
-		// append any column expressions used in the order by if this query
-		// uses the DISTINCT modifier
-		if (forcingDistinct || getSelectQuery().isDistinct()) {
-			List<String> orderByColumnList = orderingTranslator.getOrderByColumnList();
-			for (String orderByColumnExp : orderByColumnList) {
-				// Convert to ColumnDescriptors??
-				if (!selectColumnExpList.contains(orderByColumnExp)) {
-					selectColumnExpList.add(orderByColumnExp);
-				}
-			}
-		}
-
-		appendSelectColumns(queryBuf, selectColumnExpList);
-
-		// append from clause
-		queryBuf.append(" FROM ");
-
-		// append tables and joins
-		joins.appendRootWithQuoteSqlIdentifiers(queryBuf, getQueryMetadata().getDbEntity());
-
-		joins.appendJoins(queryBuf);
-		joins.appendQualifier(whereQualifierBuffer, whereQualifierBuffer.length() == 0);
-
-		// append qualifier
-		if (whereQualifierBuffer.length() > 0) {
-			queryBuf.append(" WHERE ");
-			queryBuf.append(whereQualifierBuffer);
-		}
-
-		if(groupByColumns != null && !groupByColumns.isEmpty()) {
-			queryBuf.append(" GROUP BY ");
-			appendGroupByColumns(queryBuf, groupByColumns);
-		}
-
-		// append HAVING qualifier
-		if(havingQualifierBuffer != null && havingQualifierBuffer.length() > 0) {
-			queryBuf.append(" HAVING ");
-			queryBuf.append(havingQualifierBuffer);
-		}
-
-		// append prebuilt ordering
-		if (orderingBuffer.length() > 0) {
-			queryBuf.append(" ORDER BY ").append(orderingBuffer);
-		}
-
-		if (!isSuppressingDistinct()) {
-			appendLimitAndOffsetClauses(queryBuf);
-		}
-
-		this.sql = queryBuf.toString();
-	}
-
-	/**
-	 * Warn user in case query uses both limit and joint prefetch, as we don't support this combination.
-	 */
-	private void checkLimitAndJointPrefetch() {
-		if(queryMetadata.getPrefetchTree() == null) {
-			return;
-		}
-
-		if(queryMetadata.getFetchLimit() == 0 && queryMetadata.getFetchOffset() == 0) {
-			return;
-		}
-
-		if(!jointPrefetchChecker.haveJointNode(queryMetadata.getPrefetchTree())) {
-			return;
-		}
-
-		logger.warn("Query uses both limit and joint prefetch, this most probably will lead to incorrect result. " +
-				"Either use disjointById prefetch or get full result set.");
-	}
-
-	/**
-	 * Allows subclasses to insert their own dialect of DISTINCT statement to
-	 * improve performance.
-	 *
-	 * @return string representing the DISTINCT statement
-	 * @since 4.0
-	 */
-	protected String buildDistinctStatement() {
-		return "DISTINCT";
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	protected void appendSelectColumns(StringBuilder buffer, List<String> selectColumnExpList) {
-
-		// append columns (unroll the loop's first element)
-		int columnCount = selectColumnExpList.size();
-		buffer.append(selectColumnExpList.get(0));
-
-		// assume there is at least 1 element
-		for (int i = 1; i < columnCount; i++) {
-			buffer.append(", ");
-			buffer.append(selectColumnExpList.get(i));
-		}
-	}
-
-	/**
-	 * Append columns to GROUP BY clause
-	 * @since 4.0
-	 */
-	protected void appendGroupByColumns(StringBuilder buffer, Map<ColumnDescriptor, List<DbAttributeBinding>>  groupByColumns) {
-		Iterator<Map.Entry<ColumnDescriptor, List<DbAttributeBinding>>> it = groupByColumns.entrySet().iterator();
-		Map.Entry<ColumnDescriptor, List<DbAttributeBinding>> entry = it.next();
-		appendGroupByColumn(buffer, entry);
-		while(it.hasNext()) {
-			entry = it.next();
-			buffer.append(", ");
-			appendGroupByColumn(buffer, entry);
-		}
-	}
-
-	/**
-	 * Append single column to GROUP BY clause
-	 * @since 4.0
-	 */
-	protected void appendGroupByColumn(StringBuilder buffer, Map.Entry<ColumnDescriptor, List<DbAttributeBinding>> entry) {
-		String fullName;
-		if(entry.getKey().isExpression()) {
-			fullName = entry.getKey().getDataRowKey();
-		} else {
-			QuotingStrategy strategy = getAdapter().getQuotingStrategy();
-			fullName = strategy.quotedIdentifier(queryMetadata.getDataMap(),
-					entry.getKey().getNamePrefix(), entry.getKey().getName());
-		}
-
-		buffer.append(fullName);
-
-		if(entry.getKey().getDataRowKey().equals(entry.getKey().getName())) {
-			for (DbAttributeBinding binding : entry.getValue()) {
-				addToParamList(binding.getAttribute(), binding.getValue());
-			}
-		}
-	}
-
-	/**
-	 * Handles appending optional limit and offset clauses. This implementation
-	 * does nothing, deferring to subclasses to define the LIMIT/OFFSET clause
-	 * syntax.
-	 * 
-	 * @since 3.0
-	 */
-	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
-
-	}
-
-	@Override
-	public String getCurrentAlias() {
-		return getJoinStack().getCurrentAlias();
-	}
-
-	/**
-	 * Returns a list of ColumnDescriptors for the query columns.
-	 * 
-	 * @since 1.2
-	 */
-	public ColumnDescriptor[] getResultColumns() {
-		if (resultColumns == null || resultColumns.isEmpty()) {
-			return new ColumnDescriptor[0];
-		}
-
-		return resultColumns.toArray(new ColumnDescriptor[resultColumns.size()]);
-	}
-
-	/**
-	 * Returns a map of ColumnDescriptors keyed by ObjAttribute for columns that
-	 * may need to be reprocessed manually due to incompatible mappings along
-	 * the inheritance hierarchy.
-	 * 
-	 * @since 1.2
-	 */
-	public Map<ObjAttribute, ColumnDescriptor> getAttributeOverrides() {
-		if (attributeOverrides != null) {
-			return attributeOverrides;
-		} else {
-			return Collections.emptyMap();
-		}
-	}
-
-	/**
-	 * Returns true if SelectTranslator determined that a query requiring
-	 * DISTINCT can't be run with DISTINCT keyword for internal reasons. If this
-	 * method returns true, DataNode may need to do in-memory distinct
-	 * filtering.
-	 * 
-	 * @since 1.1
-	 */
-	public boolean isSuppressingDistinct() {
-		return suppressingDistinct;
-	}
-
-	private SelectQuery<?> getSelectQuery() {
-		return (SelectQuery<?>) getQuery();
-	}
-
-	protected List<ColumnDescriptor> buildResultColumns() {
-
-		this.defaultAttributesByColumn = new HashMap<>();
-
-		List<ColumnDescriptor> columns = new ArrayList<>();
-		SelectQuery<?> query = getSelectQuery();
-
-		if(query.getColumns() != null && !query.getColumns().isEmpty()) {
-			appendOverriddenColumns(columns, query);
-		} else if (query.getRoot() instanceof DbEntity) {
-			appendDbEntityColumns(columns, query);
-		} else if (getQueryMetadata().getPageSize() > 0) {
-			appendIdColumns(columns, queryMetadata.getClassDescriptor().getEntity());
-		} else {
-			appendQueryColumns(columns, query, queryMetadata.getClassDescriptor(), null);
-		}
-
-		return columns;
-	}
-
-	/**
-	 * If query contains explicit column list, use only them
-	 */
-	<T> List<ColumnDescriptor> appendOverriddenColumns(List<ColumnDescriptor> columns, SelectQuery<T> query) {
-		groupByColumns = new HashMap<>();
-
-		QualifierTranslator qualifierTranslator = adapter.getQualifierTranslator(this);
-		AccumulatingBindingListener bindingListener = new AccumulatingBindingListener();
-		final String[] joinTableAliasForProperty = {null};
-		// capture last alias for joined table, will use it to resolve object columns
-		joinListener = () -> joinTableAliasForProperty[0] = getCurrentAlias();
-		setAddBindingListener(bindingListener);
-
-		for(BaseProperty<?> property : query.getColumns()) {
-			joinTableAliasForProperty[0] = null;
-			int expressionType = property.getExpression().getType();
-
-			// forbid direct selection of toMany relationships columns
-			if(property.getType() != null && (expressionType == Expression.OBJ_PATH || expressionType == Expression.DB_PATH)
-					&& (Collection.class.isAssignableFrom(property.getType())
-							|| Map.class.isAssignableFrom(property.getType()))) {
-				throw new CayenneRuntimeException("Can't directly select toMany relationship columns. " +
-						"Either select it with aggregate functions like count() or with flat() function to select full related objects.");
-			}
-
-			// evaluate ObjPath with Persistent type as toOne relations and use it as full object
-			boolean objectProperty = expressionType == Expression.FULL_OBJECT
-					|| (property.getType() != null
-						&& expressionType == Expression.OBJ_PATH
-						&& Persistent.class.isAssignableFrom(property.getType()));
-
-			// Qualifier Translator in case of Object Columns have side effect -
-			// it will create required joins, that we catch with listener above.
-			// And we force created join alias for all columns of Object we select.
-			qualifierTranslator.setQualifier(property.getExpression());
-			qualifierTranslator.setForceJoinForRelations(objectProperty);
-			StringBuilder builder = qualifierTranslator.appendPart(new StringBuilder());
-
-			if(objectProperty) {
-				// If we want full object, use appendQueryColumns method, to fully process class descriptor
-				List<ColumnDescriptor> classColumns = new ArrayList<>();
-				ObjEntity entity = entityResolver.getObjEntity(property.getType());
-				if(getQueryMetadata().getPageSize() > 0) {
-					appendIdColumns(classColumns, entity);
-				} else {
-					ClassDescriptor classDescriptor = entityResolver.getClassDescriptor(entity.getName());
-					appendQueryColumns(classColumns, query, classDescriptor, joinTableAliasForProperty[0]);
-				}
-				for(ColumnDescriptor descriptor : classColumns) {
-					columns.add(descriptor);
-					groupByColumns.put(descriptor, Collections.<DbAttributeBinding>emptyList());
-				}
-			} else {
-				// This property will go as scalar value
-				String alias = property.getAlias();
-				if (alias != null) {
-					builder.append(" AS ").append(alias);
-				}
-
-				int type = getJdbcTypeForProperty(property);
-				ColumnDescriptor descriptor;
-				if(property.getType() != null) {
-					descriptor = new ColumnDescriptor(builder.toString(), type, property.getType().getCanonicalName());
-				} else {
-					descriptor = new ColumnDescriptor(builder.toString(), type);
-				}
-				descriptor.setDataRowKey(alias);
-				descriptor.setIsExpression(true);
-				columns.add(descriptor);
-
-				if (isAggregate(property)) {
-					haveAggregate = true;
-				} else {
-					groupByColumns.put(descriptor, bindingListener.getBindings());
-				}
-				bindingListener.reset();
-			}
-		}
-
-		setAddBindingListener(null);
-		qualifierTranslator.setForceJoinForRelations(false);
-		joinListener = null;
-
-		return columns;
-	}
-
-	private int getJdbcTypeForProperty(BaseProperty<?> property) {
-		int expressionType = property.getExpression().getType();
-		if(expressionType == Expression.OBJ_PATH) {
-			// Scan obj path, stop as soon as DbAttribute found
-			for (PathComponent<ObjAttribute, ObjRelationship> component :
-					getQueryMetadata().getObjEntity().resolvePath(property.getExpression(), getPathAliases())) {
-				if(component.getAttribute() != null) {
-					Iterator<CayenneMapEntry> dbPathIterator = component.getAttribute().getDbPathIterator();
-					while (dbPathIterator.hasNext()) {
-						Object pathPart = dbPathIterator.next();
-						if (pathPart instanceof DbAttribute) {
-							return ((DbAttribute) pathPart).getType();
-						}
-					}
-				}
-			}
-		} else if(expressionType == Expression.DB_PATH) {
-			// Scan db path, stop as soon as DbAttribute found
-			for (PathComponent<DbAttribute, DbRelationship> component :
-					getQueryMetadata().getDbEntity().resolvePath(property.getExpression(), getPathAliases())) {
-				if(component.getAttribute() != null) {
-					return component.getAttribute().getType();
-				}
-			}
-		}
-		// NOTE: If no attribute found or expression have some other type
-	 	// return JDBC type based on Java type of the property.
-		// This can lead to incorrect behavior in case we deal with some custom type
-		// backed by ExtendedType with logic based on correct JDBC type provided (luckily not very common case).
-		// In general we can't map any meaningful type, as we don't know outcome of the expression in the property.
-		// If this ever become a problem correct type can be provided at the query call time, using meta data.
-		return TypesMapping.getSqlTypeByJava(property.getType());
-	}
-
-	private boolean isAggregate(BaseProperty<?> property) {
-		final boolean[] isAggregate = new boolean[1];
-		Expression exp = property.getExpression();
-		exp.traverse(new TraversalHelper() {
-			@Override
-			public void startNode(Expression node, Expression parentNode) {
-				if(node instanceof ASTAggregateFunctionCall) {
-					isAggregate[0] = true;
-				}
-			}
-		});
-
-		return isAggregate[0];
-	}
-
-	<T> List<ColumnDescriptor> appendDbEntityColumns(List<ColumnDescriptor> columns, SelectQuery<T> query) {
-
-		Set<ColumnTracker> attributes = new HashSet<>();
-
-		DbEntity table = getQueryMetadata().getDbEntity();
-		for (DbAttribute dba : table.getAttributes()) {
-			appendColumn(columns, null, dba, attributes, null);
-		}
-
-		return columns;
-	}
-
-	/**
-	 * Appends columns needed for object SelectQuery to the provided columns
-	 * list.
-	 */
-	<T> List<ColumnDescriptor> appendQueryColumns(final List<ColumnDescriptor> columns, SelectQuery<T> query, ClassDescriptor descriptor, final String tableAlias) {
-
-		final Set<ColumnTracker> attributes = new HashSet<>();
-
-		// fetched attributes include attributes that are either:
-		//
-		// * class properties
-		// * PK
-		// * FK used in relationship
-		// * joined prefetch PK
-		ObjEntity oe = descriptor.getEntity();
-
-		PropertyVisitor visitor = new PropertyVisitor() {
-
-			public boolean visitAttribute(AttributeProperty property) {
-				ObjAttribute oa = property.getAttribute();
-
-				resetJoinStack();
-				Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator();
-				while (dbPathIterator.hasNext()) {
-					Object pathPart = dbPathIterator.next();
-
-					if (pathPart == null) {
-						throw new CayenneRuntimeException("ObjAttribute has no component: %s", oa.getName());
-					} else if (pathPart instanceof DbRelationship) {
-						DbRelationship rel = (DbRelationship) pathPart;
-						dbRelationshipAdded(rel, JoinType.LEFT_OUTER, null);
-					} else if (pathPart instanceof DbAttribute) {
-						DbAttribute dbAttr = (DbAttribute) pathPart;
-
-						appendColumn(columns, oa, dbAttr, attributes, null, tableAlias);
-					}
-				}
-				return true;
-			}
-
-			public boolean visitToMany(ToManyProperty property) {
-				visitRelationship(property);
-				return true;
-			}
-
-			public boolean visitToOne(ToOneProperty property) {
-				visitRelationship(property);
-
-				// add PKs for flattened tables in flattened path
-				ObjRelationship rel = property.getRelationship();
-				for(int i=0; i<rel.getDbRelationships().size() - 1; i++) {
-					DbRelationship dbRel = rel.getDbRelationships().get(i);
-					dbRelationshipAdded(dbRel, JoinType.LEFT_OUTER, null);
-
-					// append path PK attributes
-					for(DbAttribute dba : dbRel.getTargetEntity().getPrimaryKeys()) {
-						appendColumn(columns, null, dba, attributes, dbRel.getName() + '.' + dba.getName());
-					}
-				}
-
-				return true;
-			}
-
-			private void visitRelationship(ArcProperty property) {
-				resetJoinStack();
-
-				ObjRelationship rel = property.getRelationship();
-				DbRelationship dbRel = rel.getDbRelationships().get(0);
-
-				List<DbJoin> joins = dbRel.getJoins();
-				for (DbJoin join : joins) {
-					DbAttribute src = join.getSource();
-					appendColumn(columns, null, src, attributes, null, tableAlias);
-				}
-			}
-		};
-
-		descriptor.visitAllProperties(visitor);
-
-		// stack should be reset, because all root table attributes go with "t0"
-		// table alias
-		resetJoinStack();
-
-		// add remaining needed attrs from DbEntity
-		DbEntity table = oe.getDbEntity();
-		for (DbAttribute dba : table.getPrimaryKeys()) {
-			appendColumn(columns, null, dba, attributes, null, tableAlias);
-		}
-
-		// special handling of a disjoint query...
-
-		if (query instanceof PrefetchSelectQuery) {
-
-			// for each relationship path add PK of the target entity...
-			for (String path : ((PrefetchSelectQuery<?>) query).getResultPaths()) {
-
-				ASTDbPath pathExp = (ASTDbPath) oe.translateToDbPath(ExpressionFactory.exp(path));
-
-				// add joins and find terminating element
-
-				resetJoinStack();
-
-				PathComponent<DbAttribute, DbRelationship> lastComponent = null;
-				for (PathComponent<DbAttribute, DbRelationship> component : table
-						.resolvePath(pathExp, getPathAliases())) {
-
-					if (component.getRelationship() != null) {
-						// do not invoke dbRelationshipAdded(), invoke
-						// pushJoin() instead. This is to prevent
-						// 'forcingDistinct' flipping to true, that will result
-						// in unneeded extra processing and sometimes in invalid
-						// results (see CAY-1979). Distinctness of each row is
-						// guaranteed by the prefetch query semantics - we
-						// include target ID in the result columns
-						getJoinStack().pushJoin(component.getRelationship(), component.getJoinType(), null);
-					}
-
-					lastComponent = component;
-				}
-
-				// process terminating element
-				if (lastComponent != null) {
-
-					DbRelationship relationship = lastComponent.getRelationship();
-
-					if (relationship != null) {
-
-						String labelPrefix = pathExp.getPath();
-						DbEntity targetEntity = relationship.getTargetEntity();
-
-						for (DbAttribute pk : targetEntity.getPrimaryKeys()) {
-
-							// note that we my select a source attribute, but
-							// label it as
-							// target for simplified snapshot processing
-							appendColumn(columns, null, pk, attributes, labelPrefix + '.' + pk.getName());
-						}
-					}
-				}
-			}
-		}
-
-		// handle joint prefetches directly attached to this query...
-		if (query.getPrefetchTree() != null) {
-			// 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 : query.getPrefetchTree().getChildren()) {
-				prefetch.setEntityName(oe.getName());
-			}
-
-			for (PrefetchTreeNode prefetch : query.getPrefetchTree().adjacentJointNodes()) {
-
-				// for each prefetch add all joins plus columns from the target
-				// entity
-				Expression prefetchExp = ExpressionFactory.exp(prefetch.getPath());
-				ASTDbPath dbPrefetch = (ASTDbPath) oe.translateToDbPath(prefetchExp);
-
-				resetJoinStack();
-				DbRelationship r = null;
-				for (PathComponent<DbAttribute, DbRelationship> component :
-						table.resolvePath(dbPrefetch, getPathAliases())) {
-					r = component.getRelationship();
-					dbRelationshipAdded(r, JoinType.LEFT_OUTER, null);
-				}
-
-				if (r == null) {
-					throw new CayenneRuntimeException("Invalid joint prefetch '%s' for entity: %s"
-							, prefetch, oe.getName());
-				}
-
-				// add columns from the target entity, including those that are matched
-				// against the FK of the source entity.
-				// This is needed to determine whether optional relationships are null
-
-				// go via target OE to make sure that Java types are mapped correctly...
-				ObjRelationship targetRel = (ObjRelationship) prefetchExp.evaluate(oe);
-				ObjEntity targetEntity = targetRel.getTargetEntity();
-
-				String labelPrefix = dbPrefetch.getPath();
-
-				PropertyVisitor prefetchVisitor = new PropertyVisitor() {
-					public boolean visitAttribute(AttributeProperty property) {
-						ObjAttribute oa = property.getAttribute();
-						Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator();
-						while (dbPathIterator.hasNext()) {
-							Object pathPart = dbPathIterator.next();
-
-							if (pathPart == null) {
-								throw new CayenneRuntimeException("ObjAttribute has no component: %s", oa.getName());
-							} else if (pathPart instanceof DbRelationship) {
-								DbRelationship rel = (DbRelationship) pathPart;
-								dbRelationshipAdded(rel, JoinType.INNER, null);
-							} else if (pathPart instanceof DbAttribute) {
-								DbAttribute dbAttr = (DbAttribute) pathPart;
-
-								appendColumn(columns, oa, dbAttr, attributes, labelPrefix + '.' + dbAttr.getName());
-							}
-						}
-						return true;
-					}
-
-					public boolean visitToMany(ToManyProperty property) {
-						return true;
-					}
-
-					public boolean visitToOne(ToOneProperty property) {
-						return true;
-					}
-				};
-
-				ClassDescriptor prefetchClassDescriptor = entityResolver.getClassDescriptor(targetEntity.getName());
-				prefetchClassDescriptor.visitAllProperties(prefetchVisitor);
-
-				// append remaining target attributes such as keys
-				DbEntity targetDbEntity = r.getTargetEntity();
-				for (DbAttribute attribute : targetDbEntity.getAttributes()) {
-					appendColumn(columns, null, attribute, attributes, labelPrefix + '.' + attribute.getName());
-				}
-			}
-		}
-
-		return columns;
-	}
-
-	<T> List<ColumnDescriptor> appendIdColumns(final List<ColumnDescriptor> columns, ObjEntity objEntity) {
-
-		Set<ColumnTracker> skipSet = new HashSet<>();
-
-		DbEntity dbEntity = objEntity.getDbEntity();
-		for (ObjAttribute attribute : objEntity.getPrimaryKeys()) {
-
-			// synthetic objattributes can't reliably lookup their DbAttribute,
-			// so do it manually..
-			DbAttribute dbAttribute = dbEntity.getAttribute(attribute.getDbAttributeName());
-			appendColumn(columns, attribute, dbAttribute, skipSet, null);
-		}
-
-		return columns;
-	}
-
-	private void appendColumn(List<ColumnDescriptor> columns, ObjAttribute objAttribute, DbAttribute attribute,
-							  Set<ColumnTracker> skipSet, String label) {
-		appendColumn(columns, objAttribute, attribute, skipSet, label, null);
-	}
-
-	private void appendColumn(List<ColumnDescriptor> columns, ObjAttribute objAttribute, DbAttribute attribute,
-			Set<ColumnTracker> skipSet, String label, String alias) {
-
-		if(alias == null) {
-			alias = getCurrentAlias();
-		}
-
-		if (skipSet.add(new ColumnTracker(alias, attribute))) {
-
-			ColumnDescriptor column = (objAttribute != null) ? new ColumnDescriptor(objAttribute, attribute, alias)
-					: new ColumnDescriptor(attribute, alias);
-
-			if (label != null) {
-				column.setDataRowKey(label);
-			}
-
-			columns.add(column);
-
-			// TODO: andrus, 5/7/2006 - replace 'columns' collection with this map, as it is redundant
-			defaultAttributesByColumn.put(column, objAttribute);
-		} else if (objAttribute != null) {
-
-			// record ObjAttribute override
-			for (ColumnDescriptor column : columns) {
-				if (attribute.getName().equals(column.getName())) {
-
-					if (attributeOverrides == null) {
-						attributeOverrides = new HashMap<>();
-					}
-
-					// kick out the original attribute
-					ObjAttribute original = defaultAttributesByColumn.remove(column);
-
-					if (original != null) {
-						attributeOverrides.put(original, column);
-					}
-
-					attributeOverrides.put(objAttribute, column);
-					column.setJavaClass(Void.TYPE.getName());
-
-					break;
-				}
-			}
-		}
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public void resetJoinStack() {
-		getJoinStack().resetStack();
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public void dbRelationshipAdded(DbRelationship relationship, JoinType joinType, String joinSplitAlias) {
-		if (relationship.isToMany()) {
-			forcingDistinct = true;
-		}
-
-		getJoinStack().pushJoin(relationship, joinType, joinSplitAlias);
-		if(joinListener != null) {
-			joinListener.joinAdded();
-		}
-	}
-
-	/**
-	 * Always returns true.
-	 */
-	@Override
-	public boolean supportsTableAliases() {
-		return true;
-	}
-
-	@Override
-	public String getAliasForExpression(Expression exp) {
-		Collection<BaseProperty<?>> columns = getSelectQuery().getColumns();
-		if(columns == null) {
-			return null;
-		}
-		for(BaseProperty<?> property : columns) {
-			if(property.getExpression().equals(exp)) {
-				return property.getAlias();
-			}
-		}
-
-		return null;
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	public boolean hasJoins() {
-		return joinStack != null && joinStack.size() > 0;
-	}
-
-	static final class ColumnTracker {
-
-		private DbAttribute attribute;
-		private String alias;
-
-		ColumnTracker(String alias, DbAttribute attribute) {
-			this.attribute = attribute;
-			this.alias = alias;
-		}
-
-		@Override
-		public boolean equals(Object object) {
-			if (!(object instanceof ColumnTracker)) {
-				return false;
-			}
-
-			ColumnTracker other = (ColumnTracker) object;
-			return new EqualsBuilder().append(alias, other.alias).append(attribute, other.attribute).isEquals();
-		}
-
-		@Override
-		public int hashCode() {
-			return new HashCodeBuilder(31, 5).append(alias).append(attribute).toHashCode();
-		}
-
-	}
-
-	static final class AccumulatingBindingListener implements AddBindingListener {
-		private List<DbAttributeBinding> bindings = new ArrayList<>();
-
-		@Override
-		public void onAdd(DbAttributeBinding binding) {
-			bindings.add(binding);
-		}
-
-		public void reset() {
-			bindings.clear();
-		}
-
-		public List<DbAttributeBinding> getBindings() {
-			return new ArrayList<>(bindings);
-		}
-	}
-
-	interface AddJoinListener {
-		void joinAdded();
-	}
-
-	private static class JointPrefetchChecker implements PrefetchProcessor {
-		private boolean haveJointNode;
-
-		public JointPrefetchChecker() {
-		}
-
-		public boolean haveJointNode(PrefetchTreeNode prefetchTree) {
-			haveJointNode = false;
-			prefetchTree.traverse(this);
-			return haveJointNode;
-		}
-
-		@Override
-        public boolean startPhantomPrefetch(PrefetchTreeNode node) {
-            return true;
-        }
-
-		@Override
-        public boolean startDisjointPrefetch(PrefetchTreeNode node) {
-            return true;
-        }
-
-		@Override
-        public boolean startDisjointByIdPrefetch(PrefetchTreeNode prefetchTreeNode) {
-            return true;
+public class DefaultSelectTranslator implements SelectTranslator {
+
+    private static final TranslationStage[] TRANSLATION_STAGES = {
+            new ColumnExtractorStage(),
+            new PrefetchNodeStage(),
+            new OrderingStage(),
+            new QualifierTranslationStage(),
+            new HavingTranslationStage(),
+            new GroupByStage(),
+            new DistinctStage(),
+            new LimitOffsetStage(),
+            new ColumnDescriptorStage(),
+            new TableTreeStage(),
+            new SQLGenerationStage()
+    };
+
+    private final TranslatorContext context;
+
+    /**
+     * Constructor for the subquery case
+     */
+    DefaultSelectTranslator(TranslatableQueryWrapper query, TranslatorContext parentContext) {
+        Objects.requireNonNull(query, "Query is null");
+        Objects.requireNonNull(parentContext, "Parent context is null");
+        this.context = new TranslatorContext(query, parentContext.getAdapter(), parentContext.getResolver(), parentContext);
+        // skip SQL translation stage for nested translators, it should be performed by root context only
+        this.context.setSkipSQLGeneration(true);
+    }
+
+    /**
+     * Constructor for the root query case
+     */
+    DefaultSelectTranslator(TranslatableQueryWrapper query, DbAdapter adapter, EntityResolver entityResolver) {
+        Objects.requireNonNull(query, "Query is null");
+        Objects.requireNonNull(adapter, "DbAdapter is null");
+        Objects.requireNonNull(entityResolver, "EntityResolver is null");
+        this.context = new TranslatorContext(query, adapter, entityResolver, null);
+    }
+
+    public DefaultSelectTranslator(SelectQuery<?> query, DbAdapter adapter, EntityResolver entityResolver) {
+        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);
+    }
+
+    TranslatorContext getContext() {
+        return context;
+    }
+
+    void translate() {
+        for(TranslationStage stage : TRANSLATION_STAGES) {
+            stage.perform(context);
         }
+    }
+
+    @Override
+    public String getSql() {
+        translate();
+        return context.getFinalSQL();
+    }
+
+    @Override
+    public DbAttributeBinding[] getBindings() {
+        return context.getBindings().toArray(new DbAttributeBinding[0]);
+    }
+
+    @Override
+    public Map<ObjAttribute, ColumnDescriptor> getAttributeOverrides() {
+        return Collections.emptyMap();
+    }
+
+    @Override
+    public ColumnDescriptor[] getResultColumns() {
+        return context.getColumnDescriptors().toArray(new ColumnDescriptor[0]);
+    }
+
+    @Override
+    public boolean isSuppressingDistinct() {
+        return context.isDistinctSuppression();
+    }
+
+    @Override
+    public boolean hasJoins() {
+        return context.getTableCount() > 1;
+    }
 
-		@Override
-        public boolean startJointPrefetch(PrefetchTreeNode node) {
-            haveJointNode = true;
-            return false;
-        }
-
-		@Override
-        public boolean startUnknownPrefetch(PrefetchTreeNode node) {
-            return true;
-        }
-
-		@Override
-        public void finishPrefetch(PrefetchTreeNode node) {
-        }
-	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
new file mode 100644
index 0000000..5bcff0f
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
@@ -0,0 +1,140 @@
+/*****************************************************************
+ *   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.HashSet;
+import java.util.Set;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.reflect.AttributeProperty;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.reflect.PropertyVisitor;
+import org.apache.cayenne.reflect.ToManyProperty;
+import org.apache.cayenne.reflect.ToOneProperty;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.table;
+
+/**
+ * @since 4.2
+ */
+class DescriptorColumnExtractor extends BaseColumnExtractor implements PropertyVisitor {
+
+    private static final String PREFETCH_PREFIX = "p:";
+
+    private final ClassDescriptor descriptor;
+    private final PathTranslator pathTranslator;
+
+    private String prefix;
+    private Set<String> columnTracker = new HashSet<>();
+
+    DescriptorColumnExtractor(TranslatorContext context, ClassDescriptor descriptor) {
+        super(context);
+        this.descriptor = descriptor;
+        this.pathTranslator = context.getPathTranslator();
+    }
+
+    public void extract(String prefix) {
+        this.prefix = prefix;
+        String labelPrefix = prefix;
+        TranslatorContext.DescriptorType type = TranslatorContext.DescriptorType.OTHER;
+        if(prefix != null && prefix.length() > 2 && prefix.startsWith(PREFETCH_PREFIX)) {
+            type = TranslatorContext.DescriptorType.PREFETCH;
+            labelPrefix = prefix.substring(2);
+        } else if(descriptor.getEntity().getDbEntity() == context.getRootDbEntity()) {
+            type = TranslatorContext.DescriptorType.ROOT;
+        }
+        context.markDescriptorStart(type);
+
+        descriptor.visitAllProperties(this);
+
+        // add remaining needed attrs from DbEntity
+        DbEntity table = descriptor.getEntity().getDbEntity();
+        String alias = context.getTableTree().aliasForPath(prefix);
+        for (DbAttribute dba : table.getPrimaryKeys()) {
+            String columnUniqueName = alias + '.' + dba.getName();
+            if(columnTracker.add(columnUniqueName)) {
+                addDbAttribute(prefix, labelPrefix, dba);
+            }
+        }
+        context.markDescriptorEnd(type);
+    }
+
+    @Override
+    public boolean visitAttribute(AttributeProperty property) {
+        ObjAttribute oa = property.getAttribute();
+        PathTranslationResult result = pathTranslator.translatePath(oa.getEntity(), property.getName(), prefix);
+
+        int count = result.getDbAttributes().size();
+        for(int i=0; i<count; i++) {
+            ResultNodeDescriptor resultNodeDescriptor = processTranslationResult(result, i);
+            if(resultNodeDescriptor != null && i == count - 1) {
+                resultNodeDescriptor.setJavaType(oa.getType());
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean visitToOne(ToOneProperty property) {
+        ObjRelationship rel = property.getRelationship();
+        PathTranslationResult result = pathTranslator.translatePath(rel.getSourceEntity(), property.getName(), prefix);
+
+        int count = result.getDbAttributes().size();
+        for(int i=0; i<count; i++) {
+            processTranslationResult(result, i);
+        }
+
+        return true;
+    }
+
+    private ResultNodeDescriptor processTranslationResult(PathTranslationResult result, int i) {
+        String path = result.getAttributePaths().get(i);
+        String alias = context.getTableTree().aliasForPath(path);
+        DbAttribute attribute = result.getDbAttributes().get(i);
+
+        String columnUniqueName = alias + '.' + attribute.getName();
+        if(columnTracker.add(columnUniqueName)) {
+            String columnLabelPrefix = path;
+            if(columnLabelPrefix.length() > 2) {
+                if(columnLabelPrefix.startsWith(PREFETCH_PREFIX)) {
+                    columnLabelPrefix = columnLabelPrefix.substring(2);
+                }
+            }
+            String attributeName = columnLabelPrefix.isEmpty()
+                    ? attribute.getName()
+                    : columnLabelPrefix + '.' + attribute.getName();
+
+            Node columnNode = table(alias).column(attribute).build();
+            return context.addResultNode(columnNode, attributeName).setDbAttribute(attribute);
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean visitToMany(ToManyProperty property) {
+        return true;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DistinctStage.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DistinctStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DistinctStage.java
new file mode 100644
index 0000000..5a5febc
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DistinctStage.java
@@ -0,0 +1,71 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * @since 4.2
+ */
+class DistinctStage implements TranslationStage {
+
+    private static final int[] UNSUPPORTED_DISTINCT_TYPES = {
+            Types.BLOB,
+            Types.CLOB,
+            Types.NCLOB,
+            Types.LONGVARCHAR,
+            Types.LONGNVARCHAR,
+            Types.LONGVARBINARY
+    };
+
+    static boolean isUnsupportedForDistinct(int type) {
+        for (int unsupportedDistinctType : UNSUPPORTED_DISTINCT_TYPES) {
+            if (unsupportedDistinctType == type) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public void perform(TranslatorContext context) {
+        // explicit suppressing of distinct
+        if(context.getMetadata().isSuppressingDistinct()) {
+            context.setDistinctSuppression(true);
+            return;
+        }
+
+        // query forcing distinct or query have joins (qualifier or prefetch)
+        if(!context.getQuery().isDistinct() && context.getTableTree().getNodeCount() <= 1) {
+            return;
+        }
+
+        // unsuitable jdbc type for distinct clause
+        for(ResultNodeDescriptor node : context.getResultNodeList()) {
+            // TODO: make it per adapter rather than one-for-all
+            if(isUnsupportedForDistinct(node.getJdbcType())) {
+                context.setDistinctSuppression(true);
+                return;
+            }
+        }
+        context.getSelectBuilder().distinct();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/GroupByStage.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/GroupByStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/GroupByStage.java
new file mode 100644
index 0000000..db05011
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/GroupByStage.java
@@ -0,0 +1,54 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * @since 4.2
+ */
+class GroupByStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        if (!hasAggregate(context)) {
+            return;
+        }
+
+        for(ResultNodeDescriptor resultNode : context.getResultNodeList()) {
+            if(resultNode.isAggregate()) {
+                continue;
+            }
+            context.getSelectBuilder().groupBy(resultNode.getNode().deepCopy());
+        }
+    }
+
+    private boolean hasAggregate(TranslatorContext context) {
+        if(context.getQuery().getHavingQualifier() != null) {
+            return true;
+        }
+
+        for(ResultNodeDescriptor resultNode : context.getResultNodeList()) {
+            if(resultNode.isAggregate()) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/HavingTranslationStage.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/HavingTranslationStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/HavingTranslationStage.java
new file mode 100644
index 0000000..e0fdd8b
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/HavingTranslationStage.java
@@ -0,0 +1,40 @@
+/*****************************************************************
+ *   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.Node;
+import org.apache.cayenne.exp.Expression;
+
+/**
+ * @since 4.2
+ */
+class HavingTranslationStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        Expression expression = context.getQuery().getHavingQualifier();
+        QualifierTranslator translator = context.getQualifierTranslator();
+        Node qualifier = translator.translate(expression);
+        if(qualifier != null) {
+            context.getSelectBuilder().having(qualifier);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/IdColumnExtractor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/IdColumnExtractor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/IdColumnExtractor.java
new file mode 100644
index 0000000..a718a04
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/IdColumnExtractor.java
@@ -0,0 +1,49 @@
+/*****************************************************************
+ *   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.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * @since 4.2
+ */
+class IdColumnExtractor extends BaseColumnExtractor {
+
+    private final DbEntity dbEntity;
+
+    IdColumnExtractor(TranslatorContext context, DbEntity dbEntity) {
+        super(context);
+        this.dbEntity = dbEntity;
+    }
+
+    IdColumnExtractor(TranslatorContext context, ObjEntity objEntity) {
+        this(context, objEntity.getDbEntity());
+    }
+
+    @Override
+    public void extract(String prefix) {
+        for (DbAttribute dba : dbEntity.getPrimaryKeys()) {
+            addDbAttribute(prefix, prefix, dba);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java
deleted file mode 100644
index 11b57fc..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java
+++ /dev/null
@@ -1,219 +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.List;
-import java.util.function.Function;
-
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.parser.ASTDbPath;
-import org.apache.cayenne.exp.parser.ASTPath;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbJoin;
-import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.Entity;
-import org.apache.cayenne.map.JoinType;
-import org.apache.cayenne.map.ObjEntity;
-
-/**
- * Encapsulates join reuse/split logic used in SelectQuery processing. All
- * expression path's that exist in the query (in the qualifier, etc.) are
- * processed to produce a combined join tree.
- * 
- * @since 3.0
- */
-public class JoinStack {
-
-	protected JoinTreeNode rootNode;
-	protected JoinTreeNode topNode;
-	private QuotingStrategy quotingStrategy;
-
-	private int aliasCounter;
-
-	/**
-	 * Helper class to process DbEntity qualifiers
-	 */
-	private QualifierTranslator qualifierTranslator;
-
-	protected JoinStack(DbAdapter dbAdapter, QueryAssembler assembler) {
-		this.rootNode = new JoinTreeNode(this);
-		this.rootNode.setTargetTableAlias(newAlias());
-
-		this.quotingStrategy = dbAdapter.getQuotingStrategy();
-		this.qualifierTranslator = dbAdapter.getQualifierTranslator(assembler);
-
-		resetStack();
-	}
-
-	String getCurrentAlias() {
-		return topNode.getTargetTableAlias();
-	}
-
-	/**
-	 * Returns the number of configured joins.
-	 */
-	protected int size() {
-		// do not count root as a join
-		return rootNode.size() - 1;
-	}
-
-	void appendRootWithQuoteSqlIdentifiers(StringBuilder out, DbEntity rootEntity) {
-
-		out.append(quotingStrategy.quotedFullyQualifiedName(rootEntity));
-		out.append(' ');
-		out.append(quotingStrategy.quotedIdentifier(rootEntity, rootNode.getTargetTableAlias()));
-	}
-
-	/**
-	 * Appends all configured joins to the provided output object.
-	 */
-	protected void appendJoins(StringBuilder out) {
-
-		// skip root, recursively append its children
-		for (JoinTreeNode child : rootNode.getChildren()) {
-			appendJoinSubtree(out, child);
-		}
-	}
-
-	protected void appendJoinSubtree(StringBuilder out, JoinTreeNode node) {
-
-		DbRelationship relationship = node.getRelationship();
-
-		DbEntity targetEntity = relationship.getTargetEntity();
-		String srcAlias = node.getSourceTableAlias();
-		String targetAlias = node.getTargetTableAlias();
-
-		switch (node.getJoinType()) {
-		case INNER:
-			out.append(" JOIN");
-			break;
-		case LEFT_OUTER:
-			out.append(" LEFT JOIN");
-			break;
-		default:
-			throw new IllegalArgumentException("Unsupported join type: " + node.getJoinType());
-		}
-
-		out.append(' ');
-		out.append(quotingStrategy.quotedFullyQualifiedName(targetEntity));
-
-		out.append(' ');
-		out.append(quotingStrategy.quotedIdentifier(targetEntity, targetAlias));
-		out.append(" ON (");
-
-		List<DbJoin> joins = relationship.getJoins();
-		int len = joins.size();
-		for (int i = 0; i < len; i++) {
-			DbJoin join = joins.get(i);
-			if (i > 0) {
-				out.append(" AND ");
-			}
-
-			out.append(quotingStrategy.quotedIdentifier(relationship.getSourceEntity(), srcAlias, join.getSourceName()));
-			out.append(" = ");
-			out.append(quotingStrategy.quotedIdentifier(targetEntity, targetAlias, join.getTargetName()));
-		}
-
-		/*
-		 * Attaching root Db entity's qualifier
-		 */
-		Expression dbQualifier = targetEntity.getQualifier();
-		if (dbQualifier != null) {
-			dbQualifier = dbQualifier.transform(new JoinedDbEntityQualifierTransformer(node));
-
-			if (len > 0) {
-				out.append(" AND ");
-			}
-			qualifierTranslator.setOut(out);
-			qualifierTranslator.doAppendPart(dbQualifier);
-		}
-
-		out.append(')');
-
-		for (JoinTreeNode child : node.getChildren()) {
-			appendJoinSubtree(out, child);
-		}
-	}
-
-	/**
-	 * Append join information to the qualifier - the part after "WHERE".
-	 */
-	protected void appendQualifier(StringBuilder out, boolean firstQualifierElement) {
-		// nothing as standard join is performed before "WHERE"
-	}
-
-	/**
-	 * Pops the stack all the way to the root node.
-	 */
-	void resetStack() {
-		topNode = rootNode;
-	}
-
-	/**
-	 * Finds or creates a JoinTreeNode for the given arguments and sets it as
-	 * the next current join.
-	 */
-	void pushJoin(DbRelationship relationship, JoinType joinType, String alias) {
-		topNode = topNode.findOrCreateChild(relationship, joinType, alias);
-	}
-
-	protected String newAlias() {
-		return "t" + aliasCounter++;
-	}
-
-	/**
-	 * Class to translate *joined* DB Entity qualifiers annotation to *current*
-	 * Obj-entity qualifiers annotation This is done by changing all Obj-paths
-	 * to concatenated Db-paths to root entity and rejecting all original
-	 * Db-paths
-	 */
-	class JoinedDbEntityQualifierTransformer implements Function<Object, Object> {
-
-		StringBuilder pathToRoot;
-
-		JoinedDbEntityQualifierTransformer(JoinTreeNode node) {
-			pathToRoot = new StringBuilder();
-			while (node != null && node.getRelationship() != null) {
-				String relName = node.getRelationship().getName();
-
-				/*
-				 * We must be in the same join as 'node', otherwise incorrect
-				 * join statement like JOIN t1 ... ON (t0.id=t1.id AND
-				 * t2.qualifier=0) could be generated
-				 */
-				if (node.getJoinType() == JoinType.LEFT_OUTER) {
-					relName += Entity.OUTER_JOIN_INDICATOR;
-				}
-				relName += ObjEntity.PATH_SEPARATOR;
-
-				pathToRoot.insert(0, relName);
-				node = node.getParent();
-			}
-		}
-
-		public Object apply(Object input) {
-			if (input instanceof ASTPath) {
-				return new ASTDbPath(pathToRoot.toString() + ((ASTPath) input).getPath());
-			}
-			return input;
-		}
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinTreeNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinTreeNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinTreeNode.java
deleted file mode 100644
index fc80fa3..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinTreeNode.java
+++ /dev/null
@@ -1,145 +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 org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.JoinType;
-import org.apache.cayenne.util.Util;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-
-/**
- * @since 3.0
- */
-public final class JoinTreeNode {
-
-    private String targetTableAlias;
-    private String sourceTableAlias;
-    private DbRelationship relationship;
-    private String alias;
-    private JoinType joinType;
-    private Collection<JoinTreeNode> children;
-    private JoinStack joinProcessor;
-    
-    /**
-     * Parent join
-     */
-    private JoinTreeNode parent;
-
-    JoinTreeNode(JoinStack joinProcessor) {
-        this.joinProcessor = joinProcessor;
-    }
-
-    JoinTreeNode(JoinStack joinProcessor, DbRelationship relationship,
-            JoinType joinType, String alias, JoinTreeNode parent) {
-        this(joinProcessor);
-        this.relationship = relationship;
-        this.alias = alias;
-        this.joinType = joinType;
-        this.parent = parent;
-    }
-
-    int size() {
-        int i = 1;
-
-        if (children != null) {
-            for (JoinTreeNode child : children) {
-                i += child.size();
-            }
-        }
-
-        return i;
-    }
-
-    public Collection<JoinTreeNode> getChildren() {
-        return children != null ? children : Collections.<JoinTreeNode> emptyList();
-    }
-
-    JoinTreeNode findOrCreateChild(
-            DbRelationship relationship,
-            JoinType joinType,
-            String alias) {
-
-        if (children == null) {
-            children = new ArrayList<>(4);
-        }
-        else {
-            for (JoinTreeNode child : children) {
-
-                if (child.equals(relationship, joinType, alias)) {
-                    return child;
-                }
-            }
-        }
-
-        JoinTreeNode child = new JoinTreeNode(
-                joinProcessor,
-                relationship,
-                joinType,
-                alias,
-                this);
-        child.setSourceTableAlias(this.targetTableAlias);
-        child.setTargetTableAlias(joinProcessor.newAlias());
-        children.add(child);
-        return child;
-    }
-
-    private boolean equals(DbRelationship relationship, JoinType joinType, String alias) {
-        return this.relationship == relationship
-                && this.joinType == joinType
-                && Util.nullSafeEquals(this.alias, alias);
-    }
-
-    public String getTargetTableAlias() {
-        return targetTableAlias;
-    }
-
-    void setTargetTableAlias(String targetTableAlias) {
-        this.targetTableAlias = targetTableAlias;
-    }
-
-    public String getSourceTableAlias() {
-        return sourceTableAlias;
-    }
-
-    void setSourceTableAlias(String sourceTableAlias) {
-        this.sourceTableAlias = sourceTableAlias;
-    }
-
-    public DbRelationship getRelationship() {
-        return relationship;
-    }
-
-    String getAlias() {
-        return alias;
-    }
-
-    public JoinType getJoinType() {
-        return joinType;
-    }
-    
-    /**
-     * @return parent join
-     */
-    public JoinTreeNode getParent() {
-        return parent;
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/LimitOffsetStage.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/LimitOffsetStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/LimitOffsetStage.java
new file mode 100644
index 0000000..3d6faa3
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/LimitOffsetStage.java
@@ -0,0 +1,37 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * @since 4.2
+ */
+class LimitOffsetStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        if(context.isDistinctSuppression()) {
+            return;
+        }
+        int offset = context.getMetadata().getFetchOffset();
+        int limit = context.getMetadata().getFetchLimit();
+
+        context.getSelectBuilder().limitOffset(limit, offset);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjPathProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjPathProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjPathProcessor.java
new file mode 100644
index 0000000..111bfc4
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjPathProcessor.java
@@ -0,0 +1,132 @@
+/*****************************************************************
+ *   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.map.DbRelationship;
+import org.apache.cayenne.map.EmbeddedAttribute;
+import org.apache.cayenne.map.JoinType;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+
+/**
+ * @since 4.2
+ */
+class ObjPathProcessor extends PathProcessor<ObjEntity> {
+
+    private ObjAttribute attribute;
+    private EmbeddedAttribute embeddedAttribute;
+
+    ObjPathProcessor(TranslatorContext context, ObjEntity entity, String parentPath) {
+        super(context, entity);
+        if(parentPath != null) {
+            currentDbPath.append(parentPath);
+        }
+    }
+
+    ObjAttribute getAttribute() {
+        return attribute;
+    }
+
+    @Override
+    protected void processNormalAttribute(String next) {
+        attribute = fetchAttribute(next);
+        if(attribute != null) {
+            processAttribute(attribute);
+            return;
+        }
+
+        ObjRelationship relationship = entity.getRelationship(next);
+        if(relationship != null) {
+            processRelationship(relationship);
+            return;
+        }
+
+        throw new IllegalStateException("Unable to resolve path: " + currentDbPath.toString()
+                + " (unknown '" + next + "' component)");
+    }
+
+    @Override
+    protected void processAliasedAttribute(String next, String alias) {
+        ObjRelationship relationship = entity.getRelationship(alias);
+        if(relationship == null) {
+            throw new IllegalStateException("Non-relationship aliased path part: " + alias);
+        }
+
+        processRelationship(relationship);
+    }
+
+    protected ObjAttribute fetchAttribute(String name) {
+        if(embeddedAttribute != null) {
+            ObjAttribute attribute = embeddedAttribute.getAttribute(name);
+            embeddedAttribute = null;
+            return attribute;
+        } else {
+            return entity.getAttribute(name);
+        }
+    }
+
+    protected void processAttribute(ObjAttribute attribute) {
+        if(attribute instanceof EmbeddedAttribute) {
+            embeddedAttribute = (EmbeddedAttribute)attribute;
+            return;
+        }
+
+        PathTranslationResult result = context.getPathTranslator()
+                .translatePath(entity.getDbEntity(), attribute.getDbAttributePath(), currentDbPath.toString());
+        attributes.addAll(result.getDbAttributes());
+        attributePaths.addAll(result.getAttributePaths());
+        relationship = result.getDbRelationship().orElse(relationship);
+        currentDbPath.delete(0, currentDbPath.length());
+        currentDbPath.append(result.getFinalPath());
+    }
+
+    protected void processRelationship(ObjRelationship relationship) {
+        if (lastComponent) {
+            // if this is a last relationship in the path, it needs special handling
+            processRelTermination(relationship);
+        } else {
+            entity = relationship.getTargetEntity();
+            // find and add joins ....
+            int count = relationship.getDbRelationships().size();
+            for (int i=0; i<count; i++) {
+                DbRelationship dbRel = relationship.getDbRelationships().get(i);
+                appendCurrentPath(dbRel.getName());
+                context.getTableTree().addJoinTable(currentDbPath.toString(), dbRel,
+                        isOuterJoin ? JoinType.LEFT_OUTER : JoinType.INNER);
+            }
+        }
+    }
+
+    protected void processRelTermination(ObjRelationship rel) {
+        String path = currentAlias != null ? currentAlias : rel.getDbRelationshipPath();
+        if(isOuterJoin) {
+            path += OUTER_JOIN_INDICATOR;
+        }
+        PathTranslationResult result = context.getPathTranslator()
+                .translatePath(entity.getDbEntity(), path, currentDbPath.toString());
+        attributes.addAll(result.getDbAttributes());
+        attributePaths.addAll(result.getAttributePaths());
+        relationship = result.getDbRelationship().orElse(relationship);
+        currentDbPath.delete(0, currentDbPath.length());
+        currentDbPath.append(result.getFinalPath());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjectSelectWrapper.java
----------------------------------------------------------------------
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/ObjectSelectWrapper.java
new file mode 100644
index 0000000..272bf83
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjectSelectWrapper.java
@@ -0,0 +1,84 @@
+/*****************************************************************
+ *   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.ObjectSelect;
+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 {
+
+    private final ObjectSelect<?> selectQuery;
+
+    public ObjectSelectWrapper(ObjectSelect<?> selectQuery) {
+        this.selectQuery = Objects.requireNonNull(selectQuery);
+    }
+
+    @Override
+    public boolean isDistinct() {
+        return false;
+    }
+
+    @Override
+    public QueryMetadata getMetaData(EntityResolver resolver) {
+        return selectQuery.getMetaData(resolver);
+    }
+
+    @Override
+    public PrefetchTreeNode getPrefetchTree() {
+        return selectQuery.getPrefetches();
+    }
+
+    @Override
+    public Expression getQualifier() {
+        return selectQuery.getWhere();
+    }
+
+    @Override
+    public Collection<Ordering> getOrderings() {
+        return selectQuery.getOrderings();
+    }
+
+    @Override
+    public Collection<BaseProperty<?>> getColumns() {
+        return null;
+    }
+
+    @Override
+    public Expression getHavingQualifier() {
+        return null;
+    }
+
+    @Override
+    public Select<?> unwrap() {
+        return selectQuery;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingStage.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingStage.java
new file mode 100644
index 0000000..46b4919
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingStage.java
@@ -0,0 +1,81 @@
+/*****************************************************************
+ *   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.NodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.OrderingNodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTAggregateFunctionCall;
+import org.apache.cayenne.query.Ordering;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
+
+/**
+ * @since 4.2
+ */
+class OrderingStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        if(context.getQuery().getOrderings() == null) {
+            return;
+        }
+
+        QualifierTranslator qualifierTranslator = context.getQualifierTranslator();
+        for(Ordering ordering : context.getQuery().getOrderings()) {
+            processOrdering(qualifierTranslator, context, ordering);
+        }
+    }
+
+    private void processOrdering(QualifierTranslator qualifierTranslator, TranslatorContext context, Ordering ordering) {
+        Expression exp = ordering.getSortSpec();
+        Node translatedNode = qualifierTranslator.translate(exp);
+
+        NodeBuilder nodeBuilder = node(translatedNode);
+        if(ordering.isCaseInsensitive()) {
+            nodeBuilder = function("UPPER", nodeBuilder);
+        }
+
+        // If query is DISTINCT then we need to add all ORDER BY clauses as result columns
+        if(shouldAddToResult(context, exp)) {
+            // TODO: need to check duplicates?
+            // need UPPER() function here too, as some DB expect exactly the same expression in select and in ordering
+            context.addResultNode(nodeBuilder.build().deepCopy());
+        }
+
+        OrderingNodeBuilder orderingNodeBuilder = order(nodeBuilder);
+        if(ordering.isDescending()) {
+            orderingNodeBuilder.desc();
+        }
+        context.getSelectBuilder().orderBy(orderingNodeBuilder);
+    }
+
+    private boolean shouldAddToResult(TranslatorContext context, Expression exp) {
+        if(context.isDistinctSuppression()) {
+            return false;
+        }
+        if(exp instanceof ASTAggregateFunctionCall) {
+            return false;
+        }
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingTranslator.java
deleted file mode 100644
index 59602f0..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingTranslator.java
+++ /dev/null
@@ -1,127 +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 org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.query.Ordering;
-import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.SelectQuery;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-/**
- * Translates query ordering to SQL.
- */
-public class OrderingTranslator extends QueryAssemblerHelper {
-
-	protected List<String> orderByColumnList = new ArrayList<>();
-
-	public OrderingTranslator(QueryAssembler queryAssembler) {
-		super(queryAssembler);
-	}
-
-	/**
-	 * Translates query Ordering list to SQL ORDER BY clause. Ordering list is
-	 * obtained from <code>queryAssembler</code>'s query object. In a process of
-	 * building of ORDER BY clause, <code>queryAssembler</code> is notified when
-	 * a join needs to be added.
-	 * 
-	 * @since 3.0
-	 */
-	@Override
-	protected void doAppendPart() {
-
-		Query q = queryAssembler.getQuery();
-
-		// only select queries can have ordering...
-		if (q == null || !(q instanceof SelectQuery)) {
-			return;
-		}
-
-		Iterator<Ordering> it = ((SelectQuery<?>) q).getOrderings().iterator();
-
-		StringBuilder mainBuffer = this.out;
-
-		try {
-			while (it.hasNext()) {
-				Ordering ord = it.next();
-
-				// reset buffer to collect SQL for the single column, that we'll
-				// be reusing
-				this.out = new StringBuilder();
-
-				if (ord.isCaseInsensitive()) {
-					out.append("UPPER(");
-				}
-
-				Expression exp = ord.getSortSpec();
-
-				if (exp.getType() == Expression.OBJ_PATH) {
-					appendObjPath(exp);
-				} else if (exp.getType() == Expression.DB_PATH) {
-					appendDbPath(exp);
-				} else if (exp.getType() == Expression.FUNCTION_CALL) {
-					appendFunction(exp);
-				} else {
-					throw new CayenneRuntimeException("Unsupported ordering expression: %s", exp);
-				}
-
-				// Close UPPER() modifier
-				if (ord.isCaseInsensitive()) {
-					out.append(")");
-				}
-
-				String columnSQL = out.toString();
-				mainBuffer.append(columnSQL);
-				orderByColumnList.add(columnSQL);
-
-				// "ASC" is a noop, omit it from the query
-				if (!ord.isAscending()) {
-					mainBuffer.append(" DESC");
-				}
-
-				if (it.hasNext()) {
-					mainBuffer.append(", ");
-				}
-			}
-		} finally {
-			this.out = mainBuffer;
-		}
-	}
-
-	protected void appendFunction(Expression exp) {
-		QualifierTranslator qualifierTranslator = queryAssembler.getAdapter().getQualifierTranslator(queryAssembler);
-		qualifierTranslator.setQualifier(exp);
-		StringBuilder builder = qualifierTranslator.appendPart(new StringBuilder());
-		out.append(builder.toString());
-	}
-
-	/**
-	 * Returns the column expressions (not Expressions) used in the order by
-	 * clause. E.g., in the case of an case-insensitive order by, an element of
-	 * the list would be <code>UPPER(&lt;column reference&gt;)</code>
-	 */
-	public List<String> getOrderByColumnList() {
-		return orderByColumnList;
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathComponents.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathComponents.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathComponents.java
new file mode 100644
index 0000000..0d1d93c
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathComponents.java
@@ -0,0 +1,95 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * @since 4.2
+ */
+class PathComponents {
+
+    private final String path;
+    private String parent;
+    private String[] pathComponents;
+
+    PathComponents(String path) {
+        this.path = path;
+        initPath();
+    }
+
+    int size() {
+        return pathComponents.length;
+    }
+
+    String getLast() {
+        return pathComponents[pathComponents.length - 1];
+    }
+
+    String getParent() {
+        return parent;
+    }
+
+    String[] getAll() {
+        return pathComponents;
+    }
+
+    String getPath() {
+        return path;
+    }
+
+    private void initPath() {
+        int count = 1;
+        int last = 0;
+
+        // quick scan to check for path separator in path
+        for(int i=0; i<path.length(); i++) {
+            if(path.charAt(i) == '.') {
+                count++;
+                last = i;
+            }
+        }
+
+        // fast path, simple path
+        if(count == 1) {
+            pathComponents = new String[]{path};
+            parent = "";
+            return;
+        }
+
+        // two parts path, can be fast too
+        pathComponents = new String[count];
+        parent = path.substring(0, last);
+        if(count == 2) {
+            pathComponents[0] = path.substring(0, last);
+            pathComponents[1] = path.substring(last + 1);
+            return;
+        }
+
+        // additional full scan
+        last = 0;
+        int idx = 0;
+        for(int i=0; i<path.length(); i++) {
+            if(path.charAt(i) == '.') {
+                pathComponents[idx++] = path.substring(last, i);
+                last = i + 1;
+            }
+        }
+        pathComponents[idx] = path.substring(last);
+    }
+}


[06/11] cayenne git commit: CAY-2465 New SelectTranslator implementation

Posted by nt...@apache.org.
http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java
new file mode 100644
index 0000000..5082c4e
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java
@@ -0,0 +1,109 @@
+/*****************************************************************
+ *   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.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.JoinType;
+import org.apache.cayenne.util.Util;
+
+/**
+ * @since 4.2
+ */
+class TableTree {
+    /**
+     * Tables mapped by db path it's spawned by.
+     * Can be following:
+     * - query root table
+     * - tables from flattened attributes (including all intermediate tables)
+     * - tables from attributes used in expressions (WHERE, HAVING, ORDER BY)
+     * - tables from prefetches
+     */
+    private final Map<String, TableTreeNode> tableNodes;
+    private final TableTree parentTree;
+
+    private TableTreeNode rootNode;
+
+    private int tableAliasSequence;
+
+    TableTree(DbEntity root, TableTree parentTree) {
+        this.tableAliasSequence = 0;
+        this.parentTree = parentTree;
+        this.tableNodes = new LinkedHashMap<>();
+        this.rootNode = new TableTreeNode(root, nextTableAlias());
+    }
+
+    void addJoinTable(String path, DbRelationship relationship, JoinType joinType) {
+        if (tableNodes.get(path) != null) {
+            return;
+        }
+
+        TableTreeNode node = new TableTreeNode(path, relationship, nextTableAlias(), joinType);
+        tableNodes.put(path, node);
+    }
+
+    String aliasForPath(String attributePath) {
+        if(Util.isEmptyString(attributePath)) {
+            return rootNode.getTableAlias();
+        }
+        TableTreeNode node = tableNodes.get(attributePath);
+        if (node == null) {
+            throw new CayenneRuntimeException("No table for attribute '%s' found", attributePath);
+        }
+        return node.getTableAlias();
+    }
+
+    String aliasForAttributePath(String attributePath) {
+        int lastSeparator = attributePath.lastIndexOf('.');
+        if (lastSeparator == -1) {
+            return rootNode.getTableAlias();
+        }
+        String table = attributePath.substring(0, lastSeparator);
+        return aliasForPath(table);
+    }
+
+    String nextTableAlias() {
+        // delegate actual generation to parent if any
+        if(parentTree != null) {
+            return parentTree.nextTableAlias();
+        }
+        return 't' + String.valueOf(tableAliasSequence++);
+    }
+
+    public int getNodeCount() {
+        return tableNodes.size() + 1;
+    }
+
+    public void visit(TableNodeVisitor visitor) {
+        visitor.visit(rootNode);
+        for(TableTreeNode node : tableNodes.values()) {
+            visitor.visit(node);
+        }
+    }
+
+    @FunctionalInterface
+    interface TableNodeVisitor {
+        void visit(TableTreeNode node);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeNode.java
new file mode 100644
index 0000000..f197b4b
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeNode.java
@@ -0,0 +1,75 @@
+/*****************************************************************
+ *   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.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.JoinType;
+
+/**
+ * @since 4.2
+ */
+class TableTreeNode {
+
+    // path that spawned this node
+    private final PathComponents attributePath;
+    private final DbEntity entity;
+    private final String tableAlias;
+    private final JoinType joinType;
+
+    // relationship that connects this node with parent (or null if this is root)
+    private final DbRelationship relationship;
+
+    TableTreeNode(DbEntity entity, String tableAlias) {
+        this.attributePath = new PathComponents("");
+        this.entity = entity;
+        this.tableAlias = tableAlias;
+        this.relationship = null;
+        this.joinType = null;
+    }
+
+    TableTreeNode(String path, DbRelationship relationship, String tableAlias, JoinType joinType) {
+        this.attributePath = new PathComponents(path);
+        this.entity = relationship.getTargetEntity();
+        this.tableAlias = tableAlias;
+        this.relationship = relationship;
+        this.joinType = joinType;
+    }
+
+    public PathComponents getAttributePath() {
+        return attributePath;
+    }
+
+    public DbEntity getEntity() {
+        return entity;
+    }
+
+    public String getTableAlias() {
+        return tableAlias;
+    }
+
+    public JoinType getJoinType() {
+        return joinType;
+    }
+
+    public DbRelationship getRelationship() {
+        return relationship;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java
new file mode 100644
index 0000000..9ad1588
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java
@@ -0,0 +1,104 @@
+/*****************************************************************
+ *   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.List;
+
+import org.apache.cayenne.access.sqlbuilder.ExpressionNodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.JoinNodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.NodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.exp.parser.ASTPath;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbJoin;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
+
+/**
+ * @since 4.2
+ */
+class TableTreeStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        context.getTableTree().visit(node -> {
+            NodeBuilder tableNode = table(node.getEntity().getFullyQualifiedName()).as(node.getTableAlias());
+            if(node.getRelationship() != null) {
+                tableNode = getJoin(node, tableNode).on(getJoinExpression(context, node));
+            }
+
+            context.getSelectBuilder().from(tableNode);
+        });
+    }
+
+    private JoinNodeBuilder getJoin(TableTreeNode node, NodeBuilder table) {
+        switch (node.getJoinType()) {
+            case INNER:
+                return join(table);
+            case LEFT_OUTER:
+                return leftJoin(table);
+            default:
+                throw new IllegalArgumentException("Unsupported join type: " + node.getJoinType());
+        }
+    }
+
+    private NodeBuilder getJoinExpression(TranslatorContext context, TableTreeNode node) {
+        List<DbJoin> joins = node.getRelationship().getJoins();
+
+        ExpressionNodeBuilder expressionNodeBuilder = null;
+        String sourceAlias = context.getTableTree().aliasForPath(node.getAttributePath().getParent());
+        for (DbJoin dbJoin : joins) {
+            DbAttribute src = dbJoin.getSource();
+            DbAttribute dst = dbJoin.getTarget();
+            ExpressionNodeBuilder joinExp = table(sourceAlias).column(src)
+                    .eq(table(node.getTableAlias()).column(dst));
+
+            if (expressionNodeBuilder != null) {
+                expressionNodeBuilder = expressionNodeBuilder.and(joinExp);
+            } else {
+                expressionNodeBuilder = joinExp;
+            }
+        }
+
+        expressionNodeBuilder = attachTargetQualifier(context, node, expressionNodeBuilder);
+
+        return expressionNodeBuilder;
+    }
+
+    private ExpressionNodeBuilder attachTargetQualifier(TranslatorContext context, TableTreeNode node, ExpressionNodeBuilder expressionNodeBuilder) {
+        Expression dbQualifier = node.getRelationship().getTargetEntity().getQualifier();
+        if (dbQualifier != null) {
+            String pathToRoot = node.getAttributePath().getPath();
+            dbQualifier = dbQualifier.transform(input -> input instanceof ASTPath
+                    ? new ASTDbPath(pathToRoot + '.' + ((ASTPath) input).getPath())
+                    : input
+            );
+            Node translatedQualifier = context.getQualifierTranslator().translate(dbQualifier);
+            if (expressionNodeBuilder != null) {
+                expressionNodeBuilder = expressionNodeBuilder.and(node(translatedQualifier));
+            } else {
+                expressionNodeBuilder = exp(node(translatedQualifier));
+            }
+        }
+        return expressionNodeBuilder;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatableQueryWrapper.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..6bd2c41
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatableQueryWrapper.java
@@ -0,0 +1,57 @@
+/*****************************************************************
+ *   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 org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.Property;
+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;
+
+/**
+ * This interface allows transparently use different queries (namely SelectQuery, ObjectSelect and ColumnSelect)
+ * in translator and as subqueries.
+ *
+ * @since 4.2
+ */
+public interface TranslatableQueryWrapper {
+
+    boolean isDistinct();
+
+    QueryMetadata getMetaData(EntityResolver resolver);
+
+    PrefetchTreeNode getPrefetchTree();
+
+    Expression getQualifier();
+
+    Collection<Ordering> getOrderings();
+
+    Collection<BaseProperty<?>> getColumns();
+
+    Expression getHavingQualifier();
+
+    Select<?> unwrap();
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslationStage.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslationStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslationStage.java
new file mode 100644
index 0000000..2518759
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslationStage.java
@@ -0,0 +1,27 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * @since 4.2
+ */
+interface TranslationStage {
+    void perform(TranslatorContext context);
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
new file mode 100644
index 0000000..ba03b72
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
@@ -0,0 +1,245 @@
+/*****************************************************************
+ *   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.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.access.sqlbuilder.SQLGenerationContext;
+import org.apache.cayenne.access.sqlbuilder.SelectBuilder;
+import org.apache.cayenne.access.sqlbuilder.SQLBuilder;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.exp.property.BaseProperty;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.QueryMetadata;
+
+/**
+ * Context that holds all data necessary for query translation as well as a result of that translation.
+ *
+ * @since 4.2
+ */
+public class TranslatorContext implements SQLGenerationContext {
+
+    private final TableTree tableTree;
+
+    /**
+     * Result columns, can be following:
+     * - root object attributes (including flattened)
+     * - root object additional db attributes (PKs and FKs)
+     * - flattened attributes additional PKs
+     * - prefetched objects attributes and additional db attributes (PKs and FKs)
+     * - order by expressions if query is distinct?
+     */
+    private final List<ColumnDescriptor> columnDescriptors;
+
+
+    /**
+     * Scalar values bindings in order of appearance in final SQL,
+     * may be should be filled by SQL node visitor.
+     * <p>
+     * Can be from expressions encountered in:
+     * - attributes
+     * - order by expressions
+     * - where expression (including qualifiers from all used DbEntities and ObjEntities)
+     */
+    private final Collection<DbAttributeBinding> bindings;
+
+    // Translated query
+    private final TranslatableQueryWrapper query;
+    private final QueryMetadata metadata;
+
+    private final EntityResolver resolver;
+    private final DbAdapter adapter;
+    private final QuotingStrategy quotingStrategy;
+
+    // Select builder that builds final SQL tree
+    private final SelectBuilder selectBuilder;
+    private final QualifierTranslator qualifierTranslator;
+    private final PathTranslator pathTranslator;
+    // Parent context will be not null in case of a nested query
+    private final TranslatorContext parentContext;
+    // List of SQL tree nodes that describe resulting rows of this query
+    private final List<ResultNodeDescriptor> resultNodeList;
+
+    // if true SQL generation stage will be skipped, needed for nested queries translation
+    private boolean skipSQLGeneration;
+    // translated SQL string
+    private String finalSQL;
+    // suppress DISTINCT clause
+    private boolean distinctSuppression;
+
+    // index of a last result node of a root entity
+    private int rootSegmentEnd;
+    // should current entity be linked to root object
+    // (prefetch entity should, while unrelated entity in a column select should't)
+    // this flag can be removed if logic that converts result row into an object tree allow random order of columns if a row.
+    private boolean appendResultToRoot;
+
+    TranslatorContext(TranslatableQueryWrapper query, DbAdapter adapter, EntityResolver resolver, TranslatorContext parentContext) {
+        this.query = query;
+        this.adapter = adapter;
+        this.resolver = resolver;
+        this.metadata = query.getMetaData(resolver);
+        this.parentContext = parentContext;
+        this.tableTree = new TableTree(metadata.getDbEntity(), parentContext == null ? null : parentContext.getTableTree());
+        this.columnDescriptors = new ArrayList<>();
+        this.bindings = new ArrayList<>(4);
+        this.selectBuilder = SQLBuilder.select();
+        this.pathTranslator = new PathTranslator(this);
+        this.qualifierTranslator = new QualifierTranslator(this);
+        this.quotingStrategy = adapter.getQuotingStrategy();
+        this.resultNodeList = new LinkedList<>();
+    }
+
+    /**
+     * Mark start of a new class descriptor, to be able to process result columns properly.
+     * @param type of a descriptor
+     * @see #addResultNode(Node, boolean, BaseProperty, String)
+     */
+    void markDescriptorStart(DescriptorType type) {
+        if(type == DescriptorType.PREFETCH) {
+            appendResultToRoot = true;
+        }
+    }
+
+    void markDescriptorEnd(DescriptorType type) {
+        if(type == DescriptorType.ROOT) {
+            rootSegmentEnd = resultNodeList.size() - 1;
+        } else if(type == DescriptorType.PREFETCH) {
+            appendResultToRoot = false;
+        }
+    }
+
+    SelectBuilder getSelectBuilder() {
+        return selectBuilder;
+    }
+
+    Collection<ColumnDescriptor> getColumnDescriptors() {
+        return columnDescriptors;
+    }
+
+    public Collection<DbAttributeBinding> getBindings() {
+        return bindings;
+    }
+
+    TableTree getTableTree() {
+        return tableTree;
+    }
+
+    QualifierTranslator getQualifierTranslator() {
+        return qualifierTranslator;
+    }
+
+    PathTranslator getPathTranslator() {
+        return pathTranslator;
+    }
+
+    int getTableCount() {
+        return tableTree.getNodeCount();
+    }
+
+    TranslatableQueryWrapper getQuery() {
+        return query;
+    }
+
+    QueryMetadata getMetadata() {
+        return metadata;
+    }
+
+    EntityResolver getResolver() {
+        return resolver;
+    }
+
+    public DbAdapter getAdapter() {
+        return adapter;
+    }
+
+    DbEntity getRootDbEntity() {
+        return metadata.getDbEntity();
+    }
+
+    public QuotingStrategy getQuotingStrategy() {
+        return quotingStrategy;
+    }
+
+    void setDistinctSuppression(boolean distinctSuppression) {
+        this.distinctSuppression = distinctSuppression;
+    }
+
+    boolean isDistinctSuppression() {
+        return distinctSuppression;
+    }
+
+    void setFinalSQL(String SQL) {
+        this.finalSQL = SQL;
+    }
+
+    String getFinalSQL() {
+        return finalSQL;
+    }
+
+    List<ResultNodeDescriptor> getResultNodeList() {
+        return resultNodeList;
+    }
+
+    ResultNodeDescriptor addResultNode(Node node) {
+        return addResultNode(node, false, null, null);
+    }
+
+    ResultNodeDescriptor addResultNode(Node node, String dataRowKey) {
+        return addResultNode(node, true, null, dataRowKey);
+    }
+
+    ResultNodeDescriptor addResultNode(Node node, boolean inDataRow, BaseProperty<?> property, String dataRowKey) {
+        ResultNodeDescriptor resultNode = new ResultNodeDescriptor(node, inDataRow, property, dataRowKey);
+        if(appendResultToRoot) {
+            resultNodeList.add(rootSegmentEnd + 1, resultNode);
+        } else {
+            resultNodeList.add(resultNode);
+        }
+        return resultNode;
+    }
+
+    TranslatorContext getParentContext() {
+        return parentContext;
+    }
+
+    void setSkipSQLGeneration(boolean skipSQLGeneration) {
+        this.skipSQLGeneration = skipSQLGeneration;
+    }
+
+    boolean isSkipSQLGeneration() {
+        return skipSQLGeneration;
+    }
+
+    enum DescriptorType {
+        ROOT,
+        PREFETCH,
+        OTHER
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TrimmingQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TrimmingQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TrimmingQualifierTranslator.java
deleted file mode 100644
index 31eb2c9..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TrimmingQualifierTranslator.java
+++ /dev/null
@@ -1,92 +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.sql.Types;
-
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.map.DbAttribute;
-
-/**
- * QualifierTranslator that allows translation of qualifiers that perform
- * comparison with CHAR columns. Some databases require trimming the values for
- * this to work.
- * 
- */
-public class TrimmingQualifierTranslator extends QualifierTranslator {
-
-	protected String trimFunction;
-
-	/**
-	 * Constructor for TrimmingQualifierTranslator.
-	 */
-	public TrimmingQualifierTranslator(QueryAssembler queryAssembler, String trimFunction) {
-		super(queryAssembler);
-		this.trimFunction = trimFunction;
-	}
-
-	/**
-	 * Adds special handling of CHAR columns.
-	 */
-	@Override
-	protected void processColumn(DbAttribute dbAttr) {
-		if (dbAttr.getType() == Types.CHAR) {
-			out.append(trimFunction).append("(");
-			super.processColumn(dbAttr);
-			out.append(')');
-		} else {
-			super.processColumn(dbAttr);
-		}
-	}
-
-	/**
-	 * Adds special handling of CHAR columns.
-	 */
-	@Override
-	protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) {
-
-		if (dbAttr.getType() == Types.CHAR) {
-			out.append(trimFunction).append("(");
-			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-			out.append(')');
-		} else {
-			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-		}
-	}
-
-	/**
-	 * Returns the trimFunction.
-	 * 
-	 * @return String
-	 */
-	public String getTrimFunction() {
-		return trimFunction;
-	}
-
-	/**
-	 * Sets the trimFunction.
-	 * 
-	 * @param trimFunction
-	 *            The trimFunction to set
-	 */
-	public void setTrimFunction(String trimFunction) {
-		this.trimFunction = trimFunction;
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
----------------------------------------------------------------------
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 44f1b25..fffd1c1 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
@@ -21,10 +21,9 @@ package org.apache.cayenne.dba;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+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.ExtendedTypeMap;
 import org.apache.cayenne.di.Provider;
@@ -40,6 +39,7 @@ import org.apache.cayenne.query.SelectQuery;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.util.Collection;
+import java.util.function.Function;
 
 /**
  * A DbAdapter that automatically detects the kind of database it is running on
@@ -111,8 +111,8 @@ public class AutoAdapter implements DbAdapter {
 	}
 
 	@Override
-	public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-		return getAdapter().getQualifierTranslator(queryAssembler);
+	public Function<Node, Node> getSqlTreeProcessor() {
+		return getAdapter().getSqlTreeProcessor();
 	}
 
 	@Override

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java
----------------------------------------------------------------------
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 ae8073e..629ef13 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
@@ -19,10 +19,9 @@
 package org.apache.cayenne.dba;
 
 import org.apache.cayenne.access.DataNode;
+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.ExtendedTypeMap;
 import org.apache.cayenne.map.DbAttribute;
@@ -36,6 +35,7 @@ import org.apache.cayenne.query.SelectQuery;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.util.Collection;
+import java.util.function.Function;
 
 /**
  * A Cayenne extension point that abstracts the differences between specifics of
@@ -60,7 +60,11 @@ public interface DbAdapter {
 	 */
 	SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver);
 
-	QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler);
+	/**
+	 * @since 4.2
+	 * @return {@link Function} that can adjust SQL tree to specific database flavour
+	 */
+	Function<Node, Node> getSqlTreeProcessor();
 
 	/**
 	 * Returns an instance of SQLAction that should handle the query.

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/DefaultQuotingStrategy.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/DefaultQuotingStrategy.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/DefaultQuotingStrategy.java
index e961f07..f7122ff 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/DefaultQuotingStrategy.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/DefaultQuotingStrategy.java
@@ -18,6 +18,9 @@
  ****************************************************************/
 package org.apache.cayenne.dba;
 
+import java.io.IOException;
+
+import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
@@ -29,8 +32,8 @@ import org.apache.cayenne.map.Entity;
  */
 public class DefaultQuotingStrategy implements QuotingStrategy {
 
-    private String endQuote;
-    private String startQuote;
+    private final String endQuote;
+    private final String startQuote;
 
     public DefaultQuotingStrategy(String startQuote, String endQuote) {
         this.startQuote = startQuote;
@@ -59,37 +62,44 @@ public class DefaultQuotingStrategy implements QuotingStrategy {
         return quotedIdentifier(dataMap, join.getTargetName());
     }
 
+    /**
+     * @since 4.2
+     */
     @Override
-    public String quotedIdentifier(Entity entity, String... identifierParts) {
-        return quotedIdentifier(entity.getDataMap(), identifierParts);
+    public void quotedIdentifier(DataMap dataMap, CharSequence identifier, Appendable appender) {
+        if (identifier == null) {
+            return;
+        }
+
+        boolean quoting = dataMap != null && dataMap.isQuotingSQLIdentifiers();
+        try {
+            if (quoting) {
+                appender.append(startQuote).append(identifier).append(endQuote);
+            } else {
+                appender.append(identifier);
+            }
+        } catch (IOException ex) {
+            throw new CayenneRuntimeException("Failed to append quoted identifier", ex);
+        }
     }
 
     @Override
     public String quotedIdentifier(DataMap dataMap, String... identifierParts) {
-
-        String startQuote, endQuote;
-
-        if (dataMap != null && dataMap.isQuotingSQLIdentifiers()) {
-            startQuote = this.startQuote;
-            endQuote = this.endQuote;
-        } else {
-            startQuote = "";
-            endQuote = "";
-        }
-
+        boolean quoting = dataMap != null && dataMap.isQuotingSQLIdentifiers();
         StringBuilder buffer = new StringBuilder();
 
         for (String part : identifierParts) {
-
             if (part == null) {
                 continue;
             }
-
             if (buffer.length() > 0) {
                 buffer.append('.');
             }
-
-            buffer.append(startQuote).append(part).append(endQuote);
+            if(quoting) {
+                buffer.append(startQuote).append(part).append(endQuote);
+            } else {
+                buffer.append(part);
+            }
         }
 
         return buffer.toString();

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
----------------------------------------------------------------------
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 4db2b38..db3c5ad 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
@@ -21,14 +21,13 @@ package org.apache.cayenne.dba;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.batch.BatchTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
-import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
-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.translator.select.DefaultSelectTranslator;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
@@ -52,12 +51,12 @@ import org.apache.cayenne.util.Util;
 
 import java.net.URL;
 import java.sql.PreparedStatement;
-import java.sql.SQLException;
 import java.sql.Types;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * A generic DbAdapter implementation. Can be used as a default adapter or as a
@@ -516,16 +515,6 @@ public class JdbcAdapter implements DbAdapter {
     }
 
     /**
-     * Creates and returns a default implementation of a qualifier translator.
-     */
-    @Override
-    public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-        QualifierTranslator translator = new QualifierTranslator(queryAssembler);
-        translator.setCaseInsensitive(caseInsensitiveCollations);
-        return translator;
-    }
-
-    /**
      * Uses JdbcActionBuilder to create the right action.
      *
      * @since 1.2
@@ -541,9 +530,13 @@ public class JdbcAdapter implements DbAdapter {
     }
 
     @Override
+    public Function<Node, Node> getSqlTreeProcessor() {
+        return Function.identity();
+    }
+
     @SuppressWarnings("unchecked")
-    public void bindParameter(PreparedStatement statement, ParameterBinding binding)
-            throws SQLException, Exception {
+    @Override
+    public void bindParameter(PreparedStatement statement, ParameterBinding binding) throws Exception {
 
         if (binding.getValue() == null) {
             statement.setNull(binding.getStatementPosition(), binding.getJdbcType());

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/QuotingStrategy.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/QuotingStrategy.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/QuotingStrategy.java
index a94dd72..4f4dfa3 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/QuotingStrategy.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/QuotingStrategy.java
@@ -31,7 +31,7 @@ public interface QuotingStrategy {
 
     /**
      * Builds a fully qualified name from catalog, schema, name parts of
-     * DbEntity, inclosing them in quotations according to this strategy
+     * DbEntity, enclosing them in quotations according to this strategy
      * algorithm. Analog of "quotedIdentifier(entity.getCatalog(),
      * entity.getSchema(), entity.getName())".
      * 
@@ -58,10 +58,26 @@ public interface QuotingStrategy {
     /**
      * @since 4.0
      */
-    String quotedIdentifier(Entity entity, String... identifierParts);
+    default String quotedIdentifier(Entity entity, String... identifierParts) {
+        return quotedIdentifier(entity.getDataMap(), identifierParts);
+    }
     
     /**
      * @since 4.0
      */
     String quotedIdentifier(DataMap dataMap, String... identifierParts);
+
+    /**
+     * Append quoted identifier to provided appender
+     * @since 4.2
+     */
+    default void quotedIdentifier(Entity entity, CharSequence identifier, Appendable appender) {
+        quotedIdentifier(entity.getDataMap(), identifier, appender);
+    }
+
+    /**
+     * Append quoted identifier to provided appender
+     * @since 4.2
+     */
+    void quotedIdentifier(DataMap dataMap, CharSequence identifier, Appendable appender);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2ActionBuilder.java
----------------------------------------------------------------------
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 c9a7d8c..f22b8f2 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
@@ -22,6 +22,7 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.dba.JdbcActionBuilder;
 import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.SQLAction;
+import org.apache.cayenne.query.SelectQuery;
 
 /**
  * @since 3.1
@@ -36,4 +37,9 @@ public class DB2ActionBuilder extends JdbcActionBuilder {
     public SQLAction procedureAction(ProcedureQuery query) {
         return new DB2ProcedureAction(query, dataNode);
     }
+
+    @Override
+    public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
+        return new DB2SelectAction(query, dataNode);
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
index c6df839..d76d62c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
@@ -22,14 +22,14 @@ package org.apache.cayenne.dba.db2;
 import java.sql.PreparedStatement;
 import java.sql.Types;
 import java.util.List;
+import java.util.function.Function;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
+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.ejbql.JdbcEJBQLTranslatorFactory;
-import org.apache.cayenne.access.translator.select.QualifierTranslator;
-import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.types.BooleanType;
 import org.apache.cayenne.access.types.ByteArrayType;
 import org.apache.cayenne.access.types.CharType;
@@ -145,13 +145,11 @@ public class DB2Adapter extends JdbcAdapter {
     }
 
     /**
-     * Returns a trimming translator.
+     * @since 4.2
      */
     @Override
-    public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-        QualifierTranslator translator = new DB2QualifierTranslator(queryAssembler, TRIM_FUNCTION);
-        translator.setCaseInsensitive(caseInsensitiveCollations);
-        return translator;
+    public Function<Node, Node> getSqlTreeProcessor() {
+        return new DB2SQLTreeProcessor();
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java
deleted file mode 100644
index 99e019f..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java
+++ /dev/null
@@ -1,174 +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.db2;
-
-import java.io.IOException;
-import java.sql.Types;
-
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.access.translator.select.TrimmingQualifierTranslator;
-import org.apache.cayenne.dba.TypesMapping;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.parser.ASTEqual;
-import org.apache.cayenne.exp.parser.ASTExtract;
-import org.apache.cayenne.exp.parser.ASTFunctionCall;
-import org.apache.cayenne.exp.parser.ASTNotEqual;
-import org.apache.cayenne.exp.parser.SimpleNode;
-import org.apache.cayenne.map.DbAttribute;
-
-public class DB2QualifierTranslator extends TrimmingQualifierTranslator {
-
-	public DB2QualifierTranslator(QueryAssembler queryAssembler, String trimFunction) {
-		super(queryAssembler, trimFunction);
-	}
-
-	@Override
-	protected void appendLiteralDirect(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
-
-		boolean castNeeded = false;
-
-		if (parentExpression != null) {
-			int type = parentExpression.getType();
-
-			castNeeded = attr != null
-					&& (type == Expression.LIKE || type == Expression.LIKE_IGNORE_CASE || type == Expression.NOT_LIKE || type == Expression.NOT_LIKE_IGNORE_CASE);
-		}
-
-		if (castNeeded) {
-			out.append("CAST (");
-		}
-
-		super.appendLiteralDirect(val, attr, parentExpression);
-
-		if (castNeeded) {
-			int jdbcType = attr.getType();
-			int len = attr.getMaxLength();
-
-			// determine CAST type
-
-			// LIKE on CHAR may produce unpredictible results
-			// LIKE on LONVARCHAR doesn't seem to be supported
-			if (jdbcType == Types.CHAR || jdbcType == Types.LONGVARCHAR) {
-				jdbcType = Types.VARCHAR;
-
-				// length is required for VARCHAR
-				if (len <= 0) {
-					len = 254;
-				}
-			}
-
-			out.append(" AS ");
-			String[] types = queryAssembler.getAdapter().externalTypesForJdbcType(jdbcType);
-
-			if (types == null || types.length == 0) {
-				throw new CayenneRuntimeException("Can't find database type for JDBC type '%s'"
-						, TypesMapping.getSqlNameByType(jdbcType));
-			}
-
-			out.append(types[0]);
-			if (len > 0 && queryAssembler.getAdapter().typeSupportsLength(jdbcType)) {
-				out.append("(");
-				out.append(String.valueOf(len));
-				out.append(")");
-			}
-
-			out.append(")");
-		}
-	}
-
-	@Override
-	protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) {
-
-		SimpleNode parent = null;
-		if (pathExp instanceof SimpleNode) {
-			parent = (SimpleNode) ((SimpleNode) pathExp).jjtGetParent();
-		}
-
-		// problem in db2 : Comparisons between CLOB and CLOB are not supported.
-		// we need do it by casting the Clob to VARCHAR.
-		if (parent != null && (parent instanceof ASTEqual || parent instanceof ASTNotEqual)
-				&& dbAttr.getType() == Types.CLOB && parent.getOperandCount() == 2
-				&& parent.getOperand(1) instanceof String) {
-			Integer size = parent.getOperand(1).toString().length() + 1;
-
-			out.append("CAST(");
-			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-			out.append(" AS VARCHAR(" + size + "))");
-		} else {
-			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-		}
-	}
-
-    /**
-     * @since 4.0
-     */
-	@Override
-	protected void appendFunction(ASTFunctionCall functionExpression) {
-		if("SUBSTRING".equals(functionExpression.getFunctionName())) {
-			out.append("SUBSTR");
-		} else if(!"CONCAT".equals(functionExpression.getFunctionName())) {
-			super.appendFunction(functionExpression);
-		}
-	}
-
-    /**
-     * @since 4.0
-     */
-	@Override
-	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
-		if("CONCAT".equals(functionExpression.getFunctionName())) {
-			out.append(" || ");
-		} else {
-			super.appendFunctionArgDivider(functionExpression);
-		}
-	}
-
-    /**
-     * @since 4.0
-     */
-	@Override
-	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
-		if("CONCAT".equals(functionExpression.getFunctionName())) {
-			out.delete(out.length() - " || ".length(), out.length());
-		} else {
-			super.clearLastFunctionArgDivider(functionExpression);
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	protected void appendExtractFunction(ASTExtract functionExpression) {
-		switch (functionExpression.getPart()) {
-			case DAY_OF_MONTH:
-				out.append("DAY");
-				break;
-			case DAY_OF_WEEK:
-			case DAY_OF_YEAR:
-				// db2 variants are without '_'
-				out.append(functionExpression.getPart().name().replace("_", ""));
-				break;
-			default:
-				appendFunction(functionExpression);
-		}
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2SQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2SQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2SQLTreeProcessor.java
new file mode 100644
index 0000000..7b31a2c
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2SQLTreeProcessor.java
@@ -0,0 +1,85 @@
+/*****************************************************************
+ *   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.db2;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
+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.ValueNode;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TrimmingColumnNode;
+import org.apache.cayenne.dba.derby.sqltree.DerbyValueNode;
+import org.apache.cayenne.dba.mysql.sqltree.MysqlLimitOffsetNode;
+
+/**
+ * @since 4.2
+ */
+public class DB2SQLTreeProcessor extends BaseSQLTreeProcessor {
+
+
+    @Override
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+        Node replacement = new MysqlLimitOffsetNode(child.getLimit(), child.getOffset());
+        replaceChild(parent, index, replacement, false);
+    }
+
+    @Override
+    protected void onColumnNode(Node parent, ColumnNode child, int index) {
+        TrimmingColumnNode replacement = new TrimmingColumnNode(child);
+        replaceChild(parent, index, replacement);
+    }
+
+    @Override
+    protected void onValueNode(Node parent, ValueNode child, int index) {
+        Node replacement = new DerbyValueNode(child.getValue(), child.isArray(), child.getAttribute());
+        replaceChild(parent, index, replacement, false);
+    }
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        Node replacement = getReplacementForFunction(child);
+        if(replacement != null) {
+            replaceChild(parent, index, replacement);
+        }
+    }
+
+    private Node getReplacementForFunction(FunctionNode child) {
+        switch (child.getFunctionName()) {
+            case "SUBSTRING":
+                return new FunctionNode("SUBSTR", child.getAlias(), true);
+            case "DAY_OF_MONTH":
+                return new FunctionNode("DAY", child.getAlias(), true);
+            case "DAY_OF_WEEK":
+                return new FunctionNode("DAYOFWEEK", child.getAlias(), true);
+            case "DAY_OF_YEAR":
+                return new FunctionNode("DAYOFYEAR", child.getAlias(), true);
+            case "CURRENT_DATE":
+            case "CURRENT_TIME":
+            case "CURRENT_TIMESTAMP":
+                return new FunctionNode(child.getFunctionName(), child.getAlias(), false);
+            case "CONCAT":
+                return new OpExpressionNode("||");
+        }
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2SelectAction.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..d0f8fd1
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2SelectAction.java
@@ -0,0 +1,39 @@
+/*****************************************************************
+ *   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.db2;
+
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.jdbc.SelectAction;
+import org.apache.cayenne.query.SelectQuery;
+
+/**
+ * @since 4.1
+ */
+public class DB2SelectAction extends SelectAction {
+
+    public DB2SelectAction(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/derby/DerbyActionBuilder.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..b0b67ff
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyActionBuilder.java
@@ -0,0 +1,40 @@
+/*****************************************************************
+ *   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.derby;
+
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.dba.JdbcActionBuilder;
+import org.apache.cayenne.query.SQLAction;
+import org.apache.cayenne.query.SelectQuery;
+
+/**
+ * @since 4.1
+ */
+public class DerbyActionBuilder extends JdbcActionBuilder {
+
+    public DerbyActionBuilder(DataNode dataNode) {
+        super(dataNode);
+    }
+
+    @Override
+    public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
+        return new DerbySelectAction(query, dataNode);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java
index 7dd23a3..da19f09 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java
@@ -20,11 +20,11 @@
 package org.apache.cayenne.dba.derby;
 
 import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.DataNode;
+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.ejbql.JdbcEJBQLTranslatorFactory;
-import org.apache.cayenne.access.translator.select.QualifierTranslator;
-import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.types.ByteType;
 import org.apache.cayenne.access.types.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
@@ -38,13 +38,15 @@ import org.apache.cayenne.dba.JdbcAdapter;
 import org.apache.cayenne.dba.PkGenerator;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.resource.ResourceLocator;
 
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.sql.Types;
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * DbAdapter implementation for the <a href="http://db.apache.org/derby/"> Derby RDBMS
@@ -91,6 +93,11 @@ public class DerbyAdapter extends JdbcAdapter {
         return new DerbyPkGenerator(this);
     }
 
+    @Override
+    public SQLAction getAction(Query query, DataNode node) {
+        return query.createSQLAction(new DerbyActionBuilder(node));
+    }
+
     /**
      * Installs appropriate ExtendedTypes as converters for passing values between JDBC
      * and Java layers.
@@ -163,15 +170,11 @@ public class DerbyAdapter extends JdbcAdapter {
     }
 
     /**
-     * Returns a trimming translator.
+     * @since 4.2
      */
     @Override
-    public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-        QualifierTranslator translator = new DerbyQualifierTranslator(
-                queryAssembler,
-                "RTRIM");
-        translator.setCaseInsensitive(caseInsensitiveCollations);
-        return translator;
+    public Function<Node, Node> getSqlTreeProcessor() {
+        return new DerbySQLTreeProcessor();
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java
deleted file mode 100644
index 4b4cc51..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java
+++ /dev/null
@@ -1,141 +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.derby;
-
-import java.io.IOException;
-import java.sql.Types;
-
-import org.apache.cayenne.CayenneRuntimeException;
-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.ASTEqual;
-import org.apache.cayenne.exp.parser.ASTExtract;
-import org.apache.cayenne.exp.parser.ASTFunctionCall;
-import org.apache.cayenne.exp.parser.ASTNotEqual;
-import org.apache.cayenne.exp.parser.SimpleNode;
-import org.apache.cayenne.map.DbAttribute;
-
-public class DerbyQualifierTranslator extends TrimmingQualifierTranslator {
-
-	public DerbyQualifierTranslator(QueryAssembler queryAssembler, String trimFunction) {
-		super(queryAssembler, trimFunction);
-	}
-
-	@Override
-	protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) {
-
-		SimpleNode parent = null;
-		if (pathExp instanceof SimpleNode) {
-			parent = (SimpleNode) ((SimpleNode) pathExp).jjtGetParent();
-		}
-
-		// problem in derby : Comparisons between 'CLOB (UCS_BASIC)' and 'CLOB
-		// (UCS_BASIC)' are not supported.
-		// we need do it by casting the Clob to VARCHAR.
-		if (parent != null && (parent instanceof ASTEqual || parent instanceof ASTNotEqual)
-				&& dbAttr.getType() == Types.CLOB && parent.getOperandCount() == 2
-				&& parent.getOperand(1) instanceof String) {
-			Integer size = parent.getOperand(1).toString().length() + 1;
-
-			out.append("CAST(");
-			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-			out.append(" AS VARCHAR(").append(size).append("))");
-		} else {
-			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	protected void appendFunction(ASTFunctionCall functionExpression) {
-		if("SUBSTRING".equals(functionExpression.getFunctionName())) {
-			out.append("SUBSTR");
-		} else if("CONCAT".equals(functionExpression.getFunctionName())) {
-			out.append("");
-		} else {
-			super.appendFunction(functionExpression);
-		}
-	}
-
-	/**
-	 * A little bit ugly code that wraps String scalars to CAST(? AS VARCHAR(length))
-	 * because otherwise derby don't know what type will be at the placeholder and
-	 * use LONG VARCHAR that isn't comparable what leads to statement preparation failure.
-	 *
-	 * @since 4.0
-	 */
-	protected void appendFunctionArg(Object value, ASTFunctionCall functionExpression) throws IOException {
-		if("CONCAT".equals(functionExpression.getFunctionName())) {
-			if(value instanceof String) {
-				out.append("CAST(");
-			}
-			super.appendFunctionArg(value, functionExpression);
-			if(value instanceof String) {
-				clearLastFunctionArgDivider(functionExpression);
-				out.append(" AS VARCHAR(").append(((String)value).length()).append("))");
-				appendFunctionArgDivider(functionExpression);
-			}
-		} else {
-			super.appendFunctionArg(value, functionExpression);
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
-		if("CONCAT".equals(functionExpression.getFunctionName())) {
-			out.append(" || ");
-		} else {
-			super.appendFunctionArgDivider(functionExpression);
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
-		if("CONCAT".equals(functionExpression.getFunctionName())) {
-			out.delete(out.length() - 4, out.length());
-		} else {
-			super.clearLastFunctionArgDivider(functionExpression);
-		}
-	}
-
-	@Override
-	protected void appendExtractFunction(ASTExtract functionExpression) {
-		switch (functionExpression.getPart()) {
-			case DAY_OF_MONTH:
-				out.append("DAY");
-				break;
-			case DAY_OF_WEEK:
-			case DAY_OF_YEAR:
-			case WEEK:
-				throw new CayenneRuntimeException("Function %s() is unsupported in Derby."
-						, functionExpression.getPartCamelCaseName());
-			default:
-				super.appendExtractFunction(functionExpression);
-		}
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbySQLTreeProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbySQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbySQLTreeProcessor.java
new file mode 100644
index 0000000..b00212c
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbySQLTreeProcessor.java
@@ -0,0 +1,81 @@
+/*****************************************************************
+ *   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.derby;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+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.OffsetFetchNextNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ValueNode;
+import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TrimmingColumnNode;
+import org.apache.cayenne.dba.derby.sqltree.DerbyValueNode;
+
+/**
+ * @since 4.2
+ */
+public class DerbySQLTreeProcessor extends BaseSQLTreeProcessor {
+
+    @Override
+    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+        replaceChild(parent, index, new OffsetFetchNextNode(child), false);
+    }
+
+    @Override
+    protected void onColumnNode(Node parent, ColumnNode child, int index) {
+        replaceChild(parent, index, new TrimmingColumnNode(child));
+    }
+
+    @Override
+    protected void onValueNode(Node parent, ValueNode child, int index) {
+        replaceChild(parent, index, new DerbyValueNode(child.getValue(), child.isArray(), child.getAttribute()));
+    }
+
+    @Override
+    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+        Node replacement = getReplacementForFunction(child);
+        if(replacement != null) {
+            replaceChild(parent, index, replacement);
+        }
+    }
+
+    private Node getReplacementForFunction(FunctionNode child) {
+        switch (child.getFunctionName()) {
+            case "SUBSTRING":
+                return new FunctionNode("SUBSTR", child.getAlias(), true);
+            case "DAY_OF_MONTH":
+                return new FunctionNode("DAY", child.getAlias(), true);
+            case "WEEK":
+            case "DAY_OF_WEEK":
+            case "DAY_OF_YEAR":
+                throw new CayenneRuntimeException("Function %s() is unsupported in Derby.", child.getFunctionName());
+            case "CURRENT_DATE":
+            case "CURRENT_TIME":
+            case "CURRENT_TIMESTAMP":
+                return new FunctionNode(child.getFunctionName(), child.getAlias(), false);
+            case "CONCAT":
+                return new OpExpressionNode("||");
+        }
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbySelectAction.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..6a1c921
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbySelectAction.java
@@ -0,0 +1,39 @@
+/*****************************************************************
+ *   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.derby;
+
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.jdbc.SelectAction;
+import org.apache.cayenne.query.SelectQuery;
+
+/**
+ * @since 4.1
+ */
+public class DerbySelectAction extends SelectAction {
+
+    public DerbySelectAction(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/derby/sqltree/DerbyValueNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/sqltree/DerbyValueNode.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/sqltree/DerbyValueNode.java
new file mode 100644
index 0000000..cb8348e
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/sqltree/DerbyValueNode.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   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.derby.sqltree;
+
+import java.sql.Types;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ValueNode;
+import org.apache.cayenne.map.DbAttribute;
+
+/**
+ * @since 4.2
+ */
+public class DerbyValueNode extends ValueNode {
+
+    public DerbyValueNode(Object value, boolean isArray, DbAttribute attribute) {
+        super(value, isArray, attribute);
+    }
+
+    protected void appendStringValue(QuotingAppendable buffer, CharSequence value) {
+        if(getAttribute() == null || (getAttribute() != null && getAttribute().getType() == Types.CLOB)) {
+            buffer.append(" CAST(? AS VARCHAR(").append(value.length()).append("))");
+        } else {
+            buffer.append(" ?");
+        }
+        addValueBinding(buffer, value);
+    }
+
+    @Override
+    public Node copy() {
+        return new DerbyValueNode(getValue(), isArray(), getAttribute());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdActionBuilder.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..92f606c
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdActionBuilder.java
@@ -0,0 +1,41 @@
+/*****************************************************************
+ *   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.firebird;
+
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.dba.JdbcActionBuilder;
+import org.apache.cayenne.query.SQLAction;
+import org.apache.cayenne.query.SelectQuery;
+
+/**
+ * @since 4.1
+ */
+public class FirebirdActionBuilder extends JdbcActionBuilder {
+
+    public FirebirdActionBuilder(DataNode dataNode) {
+        super(dataNode);
+    }
+
+
+    @Override
+    public <T> SQLAction objectSelectAction(SelectQuery<T> query) {
+        return new FirebirdSelectAction(query, dataNode);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdAdapter.java
index a878be2..8a5e0b0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdAdapter.java
@@ -20,9 +20,9 @@
 package org.apache.cayenne.dba.firebird;
 
 import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
 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.types.ByteArrayType;
 import org.apache.cayenne.access.types.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
@@ -34,9 +34,12 @@ import org.apache.cayenne.configuration.RuntimeProperties;
 import org.apache.cayenne.dba.JdbcAdapter;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.resource.ResourceLocator;
 
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * DbAdapter implementation for <a href="http://www.firebirdsql.org">FirebirdSQL
@@ -105,14 +108,22 @@ public class FirebirdAdapter extends JdbcAdapter {
 
         sqlBuffer.append(column.isMandatory() ? " NOT NULL" : "");
     }
-    
+
+    /**
+     * @since 4.2
+     */
     @Override
-    public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-        return new FirebirdQualifierTranslator(queryAssembler);
+    public Function<Node, Node> getSqlTreeProcessor() {
+        return new FirebirdSQLTreeProcessor();
     }
 
     @Override
     public EJBQLTranslatorFactory getEjbqlTranslatorFactory() {
         return new FirebirdEJBQLTranslatorFactory();
     }
+
+    @Override
+    public SQLAction getAction(Query query, DataNode node) {
+        return query.createSQLAction(new FirebirdActionBuilder(node));
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f6b2dac9/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java
deleted file mode 100644
index eae42f7..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java
+++ /dev/null
@@ -1,181 +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.firebird;
-
-import org.apache.cayenne.access.translator.select.QualifierTranslator;
-import org.apache.cayenne.access.translator.select.QueryAssembler;
-import org.apache.cayenne.dba.oracle.OracleQualifierTranslator;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.parser.ASTExtract;
-import org.apache.cayenne.exp.parser.ASTFunctionCall;
-
-import java.io.IOException;
-
-public class FirebirdQualifierTranslator extends QualifierTranslator {
-
-	private int substringArg = 0;
-
-	public FirebirdQualifierTranslator(QueryAssembler queryAssembler) {
-		super(queryAssembler);
-	}
-
-	@Override
-	protected void doAppendPart(Expression rootNode) {
-		if (rootNode == null) {
-			return;
-		}
-		// IN statements with more than 1500 values are denied in Firebird
-		// so we need to split one big statement on few smaller ones
-		rootNode = rootNode.transform(new OracleQualifierTranslator.INTrimmer());
-		rootNode.traverse(this);
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	protected void appendFunction(ASTFunctionCall functionExpression) {
-		switch (functionExpression.getFunctionName()) {
-			case "CONCAT":
-				// noop
-				break;
-			case "LENGTH":
-				out.append("CHAR_LENGTH");
-				break;
-			case "LOCATE":
-				out.append("POSITION");
-				break;
-			case "SUBSTRING":
-				substringArg = 0;
-			default:
-				super.appendFunction(functionExpression);
-		}
-	}
-
-	/**
-	 * A little bit ugly code that wraps String scalars to CAST(? AS VARCHAR(length))
-	 * because otherwise derby don't know what type will be at the placeholder and
-	 * use LONG VARCHAR that isn't comparable what leads to statement preparation failure.
-	 *
-	 * @since 4.0
-	 */
-	protected void appendFunctionArg(Object value, ASTFunctionCall functionExpression) throws IOException {
-		if("CONCAT".equals(functionExpression.getFunctionName())) {
-			if(value instanceof String) {
-				out.append("CAST(");
-			}
-			super.appendFunctionArg(value, functionExpression);
-			if(value instanceof String) {
-				clearLastFunctionArgDivider(functionExpression);
-				out.append(" AS VARCHAR(").append(((String)value).length()).append("))");
-				appendFunctionArgDivider(functionExpression);
-			}
-		} else if("SUBSTRING".equals(functionExpression.getFunctionName())) {
-			out.append("CAST(");
-			super.appendFunctionArg(value, functionExpression);
-			clearLastFunctionArgDivider(functionExpression);
-			substringArg--;
-			out.append(" AS INTEGER)");
-			appendFunctionArgDivider(functionExpression);
-		} else {
-			super.appendFunctionArg(value, functionExpression);
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
-		switch (functionExpression.getFunctionName()) {
-			case "CONCAT":
-				out.append(" || ");
-				break;
-			case "SUBSTRING":
-				switch (substringArg++) {
-					case 0:
-						out.append(" FROM ");
-						break;
-					case 1:
-						out.append(" FOR ");
-						break;
-				}
-				break;
-			default:
-				super.appendFunctionArgDivider(functionExpression);
-		}
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
-		switch (functionExpression.getFunctionName()) {
-			case "CONCAT":
-				out.delete(out.length() - 4, out.length());
-				break;
-			case "SUBSTRING":
-				if(substringArg == 1) {
-					out.delete(out.length() - " FROM ".length(), out.length());
-				} else if(substringArg == 2) {
-					out.delete(out.length() - " FOR ".length(), 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("EXTRACT(");
-		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("YEARDAY");
-				break;
-			default:
-				out.append(functionExpression.getPartCamelCaseName());
-		}
-
-		out.append(" FROM ");
-	}
-}