cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ntimof...@apache.org
Subject [02/11] cayenne git commit: CAY-2465 New SelectTranslator implementation
Date Wed, 09 Jan 2019 10:46:53 GMT
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


Mime
View raw message