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 2017/02/03 14:33:28 UTC

cayenne git commit: CAY-2032 SelectAction: DistinctResultIterator ignores flattened relationships

Repository: cayenne
Updated Branches:
  refs/heads/master 062e85259 -> e3704c287


CAY-2032 SelectAction: DistinctResultIterator ignores flattened relationships


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

Branch: refs/heads/master
Commit: e3704c2878273d339cb3b792d085a65e1a639ec8
Parents: 062e852
Author: Nikita Timofeev <st...@gmail.com>
Authored: Fri Feb 3 17:23:25 2017 +0300
Committer: Nikita Timofeev <st...@gmail.com>
Committed: Fri Feb 3 17:23:25 2017 +0300

----------------------------------------------------------------------
 .../access/jdbc/DistinctResultIterator.java     |  28 ++---
 .../cayenne/access/jdbc/SelectAction.java       |   8 +-
 .../select/DefaultSelectTranslator.java         |  10 +-
 .../access/translator/select/JoinStack.java     |   8 +-
 .../translator/select/SelectTranslator.java     |   6 +
 .../cayenne/dba/openbase/OpenBaseJoinStack.java |   6 +-
 .../dba/openbase/OpenBaseSelectTranslator.java  |   2 +-
 .../cayenne/dba/oracle/Oracle8JoinStack.java    |   6 +-
 .../dba/oracle/Oracle8SelectTranslator.java     |   2 +-
 .../cayenne/query/PrefetchSelectQuery.java      |   6 +-
 .../org/apache/cayenne/access/Cay2032IT.java    | 115 +++++++++++++++++++
 .../select/DefaultSelectTranslatorIT.java       |   8 +-
 .../apache/cayenne/testdo/cay_2032/Team.java    |   9 ++
 .../apache/cayenne/testdo/cay_2032/User.java    |   9 ++
 .../cayenne/testdo/cay_2032/auto/_Team.java     |  35 ++++++
 .../cayenne/testdo/cay_2032/auto/_User.java     |  43 +++++++
 .../cayenne/unit/di/server/CayenneProjects.java |   1 +
 .../cayenne/unit/di/server/SchemaBuilder.java   |   2 +-
 .../src/test/resources/cay-2032.map.xml         |  37 ++++++
 .../src/test/resources/cayenne-cay-2032.xml     |   4 +
 docs/doc/src/main/resources/RELEASE-NOTES.txt   |   1 +
 21 files changed, 306 insertions(+), 40 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/DistinctResultIterator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/DistinctResultIterator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/DistinctResultIterator.java
index 4cc0bb6..850b117 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/DistinctResultIterator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/DistinctResultIterator.java
@@ -45,7 +45,7 @@ public class DistinctResultIterator<T> implements ResultIterator<T> {
 
     protected ResultIterator<T> delegate;
     protected Set<Map<String, Object>> fetchedIds;
-    protected DataRow nextDataRow;
+    protected T nextDataRow;
     protected DbEntity defaultEntity;
     protected boolean compareFullRows;
 
@@ -53,6 +53,7 @@ public class DistinctResultIterator<T> implements ResultIterator<T> {
      * Creates new DistinctResultIterator wrapping another ResultIterator.
      * 
      * @param delegate
+     *            actual result iterator, that will be decorated by this DistinctResultIterator
      * @param defaultEntity
      *            an entity needed to build ObjectIds for distinct comparison.
      */
@@ -67,7 +68,7 @@ public class DistinctResultIterator<T> implements ResultIterator<T> {
 
         this.delegate = delegate;
         this.defaultEntity = defaultEntity;
-        this.fetchedIds = new HashSet<Map<String, Object>>();
+        this.fetchedIds = new HashSet<>();
         this.compareFullRows = compareFullRows;
 
         checkNextRow();
@@ -78,7 +79,7 @@ public class DistinctResultIterator<T> implements ResultIterator<T> {
      */
     @Override
     public Iterator<T> iterator() {
-        return new ResultIteratorIterator<T>(this);
+        return new ResultIteratorIterator<>(this);
     }
 
     /**
@@ -113,9 +114,7 @@ public class DistinctResultIterator<T> implements ResultIterator<T> {
             throw new NoSuchElementException("An attempt to read uninitialized row or past the end of the iterator.");
         }
 
-        // TODO: 
-        @SuppressWarnings("unchecked")
-        T row = (T) nextDataRow;
+        T row = nextDataRow;
         checkNextRow();
         return row;
     }
@@ -132,8 +131,7 @@ public class DistinctResultIterator<T> implements ResultIterator<T> {
         checkNextRow();
     }
 
-    void checkNextRow() {
-
+    private void checkNextRow() {
         if (this.compareFullRows) {
             checkNextUniqueRow();
         } else {
@@ -141,24 +139,22 @@ public class DistinctResultIterator<T> implements ResultIterator<T> {
         }
     }
 
-    void checkNextUniqueRow() {
-
+    private void checkNextUniqueRow() {
         nextDataRow = null;
         while (delegate.hasNextRow()) {
-            DataRow next = (DataRow) delegate.nextRow();
+            T next = delegate.nextRow();
 
-            if (fetchedIds.add(next)) {
+            if (fetchedIds.add((DataRow)next)) {
                 this.nextDataRow = next;
                 break;
             }
         }
     }
 
-    void checkNextRowWithUniqueId() {
-
+    private void checkNextRowWithUniqueId() {
         nextDataRow = null;
         while (delegate.hasNextRow()) {
-            DataRow next = (DataRow) delegate.nextRow();
+            T next = delegate.nextRow();
 
             // create id map...
             // TODO: this can be optimized by creating an array with id keys
@@ -166,7 +162,7 @@ public class DistinctResultIterator<T> implements ResultIterator<T> {
 
             Map<String, Object> id = new HashMap<>();
             for (final DbAttribute pk : defaultEntity.getPrimaryKeys()) {
-                id.put(pk.getName(), next.get(pk.getName()));
+                id.put(pk.getName(), ((DataRow)next).get(pk.getName()));
             }
 
             if (fetchedIds.add(id)) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
index fc84de6..ce2a22b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
@@ -187,11 +187,11 @@ public class SelectAction extends BaseSQLAction {
 		// wrap result iterator if distinct has to be suppressed
 
 		// a joint prefetch warrants full row compare
-
 		final boolean[] compareFullRows = new boolean[1];
-		final PrefetchTreeNode rootPrefetch = queryMetadata.getPrefetchTree();
+		compareFullRows[0] = translator.hasJoins();
 
-		if (rootPrefetch != null) {
+		final PrefetchTreeNode rootPrefetch = queryMetadata.getPrefetchTree();
+		if (!compareFullRows[0] && rootPrefetch != null) {
 			rootPrefetch.traverse(new PrefetchProcessor() {
 
 				@Override
@@ -233,7 +233,7 @@ public class SelectAction extends BaseSQLAction {
 			});
 		}
 
-		return new DistinctResultIterator<T>(iterator, queryMetadata.getDbEntity(), compareFullRows[0]);
+		return new DistinctResultIterator<>(iterator, queryMetadata.getDbEntity(), compareFullRows[0]);
 	}
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/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 9adad76..affbe7a 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
@@ -117,7 +117,7 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 	}
 
 	protected JoinStack createJoinStack() {
-		return new JoinStack(getAdapter(), queryMetadata.getDataMap(), this);
+		return new JoinStack(getAdapter(), this);
 	}
 
 	@Override
@@ -737,6 +737,14 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 		return null;
 	}
 
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	public boolean hasJoins() {
+		return joinStack != null && joinStack.size() > 0;
+	}
+
 	static final class ColumnTracker {
 
 		private DbAttribute attribute;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/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
index 416f14c..5b11a3c 100644
--- 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
@@ -55,7 +55,7 @@ public class JoinStack {
 	 */
 	private QualifierTranslator qualifierTranslator;
 
-	protected JoinStack(DbAdapter dbAdapter, DataMap dataMap, QueryAssembler assembler) {
+	protected JoinStack(DbAdapter dbAdapter, QueryAssembler assembler) {
 		this.rootNode = new JoinTreeNode(this);
 		this.rootNode.setTargetTableAlias(newAlias());
 
@@ -99,7 +99,7 @@ public class JoinStack {
 
 		DbRelationship relationship = node.getRelationship();
 
-		DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
+		DbEntity targetEntity = relationship.getTargetEntity();
 		String srcAlias = node.getSourceTableAlias();
 		String targetAlias = node.getTargetTableAlias();
 
@@ -134,7 +134,7 @@ public class JoinStack {
 			out.append(quotingStrategy.quotedIdentifier(targetEntity, targetAlias, join.getTargetName()));
 		}
 
-		/**
+		/*
 		 * Attaching root Db entity's qualifier
 		 */
 		Expression dbQualifier = targetEntity.getQualifier();
@@ -196,7 +196,7 @@ public class JoinStack {
 			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

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java
index 6554565..c987f57 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SelectTranslator.java
@@ -42,4 +42,10 @@ public interface SelectTranslator {
 	ColumnDescriptor[] getResultColumns();
 
 	boolean isSuppressingDistinct();
+
+	/**
+	 * @since 4.0
+	 * @return do query has at least one join
+	 */
+	boolean hasJoins();
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/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
index a9c7579..9add9df 100644
--- 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
@@ -39,8 +39,8 @@ import org.apache.cayenne.map.DbRelationship;
  */
 class OpenBaseJoinStack extends JoinStack {
 
-	protected OpenBaseJoinStack(DbAdapter dbAdapter, DataMap dataMap, QueryAssembler assembler) {
-		super(dbAdapter, dataMap, assembler);
+	protected OpenBaseJoinStack(DbAdapter dbAdapter, QueryAssembler assembler) {
+		super(dbAdapter, assembler);
 	}
 
 	@Override
@@ -51,7 +51,7 @@ class OpenBaseJoinStack extends JoinStack {
 			return;
 		}
 
-		DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
+		DbEntity targetEntity = relationship.getTargetEntity();
 		String targetAlias = node.getTargetTableAlias();
 
 		out.append(", ").append(targetEntity.getFullyQualifiedName()).append(' ').append(targetAlias);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/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
index 342d0fd..e4ff7cc 100644
--- 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
@@ -39,7 +39,7 @@ class OpenBaseSelectTranslator extends DefaultSelectTranslator {
 
 	@Override
 	protected JoinStack createJoinStack() {
-		return new OpenBaseJoinStack(getAdapter(), queryMetadata.getDataMap(), this);
+		return new OpenBaseJoinStack(getAdapter(), this);
 	}
 
 	@Override

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/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
index e27ee90..183775a 100644
--- 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
@@ -35,8 +35,8 @@ import org.apache.cayenne.map.DbRelationship;
 // cloned from OpenBaseJoin stack... need better strategies of reuse...
 class Oracle8JoinStack extends JoinStack {
 
-	Oracle8JoinStack(DbAdapter dbAdapter, DataMap dataMap, QueryAssembler assembler) {
-		super(dbAdapter, dataMap, assembler);
+	Oracle8JoinStack(DbAdapter dbAdapter, QueryAssembler assembler) {
+		super(dbAdapter, assembler);
 	}
 
 	@Override
@@ -47,7 +47,7 @@ class Oracle8JoinStack extends JoinStack {
 			return;
 		}
 
-		DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
+		DbEntity targetEntity = relationship.getTargetEntity();
 		String targetAlias = node.getTargetTableAlias();
 
 		out.append(", ").append(targetEntity.getFullyQualifiedName()).append(' ').append(targetAlias);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/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
index 6d1b16f..6df771c 100644
--- 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
@@ -41,7 +41,7 @@ class Oracle8SelectTranslator extends OracleSelectTranslator {
 	 */
 	@Override
 	protected JoinStack createJoinStack() {
-		return new Oracle8JoinStack(getAdapter(), queryMetadata.getDataMap(), this);
+		return new Oracle8JoinStack(getAdapter(), this);
 	}
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchSelectQuery.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchSelectQuery.java b/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchSelectQuery.java
index 33aa10e..9f181ad 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchSelectQuery.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchSelectQuery.java
@@ -118,7 +118,7 @@ public class PrefetchSelectQuery extends SelectQuery<Object> {
 
     /**
      * Removes an extra result path. Note that this method doesn't check for expression
-     * invariants, as it doesn't have a proper context to do so. E.g. for the purspose of
+     * invariants, as it doesn't have a proper context to do so. E.g. for the purpose of
      * this method "db:ARTIST_NAME" and "obj:artistName" are not the same, though both
      * will resolve to the same column name.
      */
@@ -136,7 +136,7 @@ public class PrefetchSelectQuery extends SelectQuery<Object> {
     public Collection<String> getResultPaths() {
         return resultPaths != null
                 ? Collections.unmodifiableCollection(resultPaths)
-                : Collections.EMPTY_SET;
+                : Collections.<String>emptySet();
     }
 
     /**
@@ -147,7 +147,7 @@ public class PrefetchSelectQuery extends SelectQuery<Object> {
      */
     Collection<String> nonNullResultPaths() {
         if (resultPaths == null) {
-            resultPaths = new HashSet<String>();
+            resultPaths = new HashSet<>();
         }
 
         return resultPaths;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2032IT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2032IT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2032IT.java
new file mode 100644
index 0000000..7cfab34
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2032IT.java
@@ -0,0 +1,115 @@
+/*****************************************************************
+ *   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;
+
+import java.util.List;
+
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.SortOrder;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.cay_2032.Team;
+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;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @since 4.0
+ */
+@UseServerRuntime(CayenneProjects.CAY_2032)
+public class Cay2032IT extends ServerCase {
+
+    @Inject
+    private DataContext context;
+
+    @Inject
+    private DBHelper dbHelper;
+
+    @Before
+    public void createTestData() throws Exception {
+        // USERS table has field `name` BLOB to trigger suppressDistinct in translator
+        TableHelper tUser = new TableHelper(dbHelper, "USERS");
+        tUser.setColumns("user_id");
+        tUser.insert(1);
+        tUser.insert(2);
+        tUser.insert(3);
+
+        TableHelper tTeam = new TableHelper(dbHelper, "TEAM");
+        tTeam.setColumns("team_id");
+        tTeam.insert(1);
+        tTeam.insert(2);
+        tTeam.insert(3);
+        tTeam.insert(4);
+
+        TableHelper tTeamHasUser = new TableHelper(dbHelper, "USER_HAS_TEAM");
+        tTeamHasUser.setColumns("team_id", "user_id");
+        tTeamHasUser.insert(1, 2);
+        tTeamHasUser.insert(2, 1);
+        tTeamHasUser.insert(2, 2);
+        tTeamHasUser.insert(2, 3);
+        tTeamHasUser.insert(3, 1);
+        tTeamHasUser.insert(3, 3);
+    }
+
+    private void checkResult(List<Team> result) throws Exception {
+        assertNotNull(result);
+        assertEquals(4, result.size());
+        assertEquals(1, result.get(0).getTeamUsers().size());
+        assertEquals(3, result.get(1).getTeamUsers().size());
+        assertEquals(2, result.get(2).getTeamUsers().size());
+        assertEquals(0, result.get(3).getTeamUsers().size());
+    }
+
+    @Test
+    public void testPrefetchDisjoint() throws Exception {
+        List<Team> result = ObjectSelect.query(Team.class)
+                .prefetch(Team.TEAM_USERS.disjoint())
+                .orderBy("db:team_id", SortOrder.ASCENDING)
+                .select(context);
+
+        checkResult(result);
+    }
+
+    @Test
+    public void testPrefetchDisjointById() throws Exception {
+        List<Team> result = ObjectSelect.query(Team.class)
+                .prefetch(Team.TEAM_USERS.disjointById())
+                .orderBy("db:team_id", SortOrder.ASCENDING)
+                .select(context);
+
+        checkResult(result);
+    }
+
+    @Test
+    public void testPrefetchJoint() throws Exception {
+        List<Team> result = ObjectSelect.query(Team.class)
+                .prefetch(Team.TEAM_USERS.joint())
+                .orderBy("db:team_id", SortOrder.ASCENDING)
+                .select(context);
+
+        checkResult(result);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/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 e090d0c..bb2248f 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
@@ -74,11 +74,12 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 		SelectQuery<Artist> q = new SelectQuery<Artist>(Artist.class, ExpressionFactory.likeExp("artistName", "a%"));
 		q.addOrdering("dateOfBirth", SortOrder.ASCENDING);
 
-		String generatedSql = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver())
-				.getSql();
+		DefaultSelectTranslator defaultSelectTranslator = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver());
+		String generatedSql = defaultSelectTranslator.getSql();
 
 		// do some simple assertions to make sure all parts are in
 		assertNotNull(generatedSql);
+		assertFalse(defaultSelectTranslator.hasJoins());
 		assertTrue(generatedSql.startsWith("SELECT "));
 		assertTrue(generatedSql.indexOf(" FROM ") > 0);
 		assertTrue(generatedSql.indexOf(" WHERE ") > generatedSql.indexOf(" FROM "));
@@ -226,7 +227,6 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 			} else {
 				assertTrue(generatedSql.indexOf("ARTIST_NAME = ") > 0);
 			}
-
 		} finally {
 			entity.setQualifier(null);
 			middleEntity.setQualifier(null);
@@ -465,6 +465,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 
 		// assert we only have one join
 		assertEquals(2, transl.joinStack.size());
+		assertTrue(transl.hasJoins());
 	}
 
 	@Test
@@ -490,6 +491,7 @@ public class DefaultSelectTranslatorIT extends ServerCase {
 
 		// assert we have one join
 		assertEquals(1, transl.joinStack.size());
+		assertTrue(transl.hasJoins());
 	}
 
 	@Test

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/Team.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/Team.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/Team.java
new file mode 100644
index 0000000..d6f3ac4
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/Team.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.cay_2032;
+
+import org.apache.cayenne.testdo.cay_2032.auto._Team;
+
+public class Team extends _Team {
+
+    private static final long serialVersionUID = 1L; 
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/User.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/User.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/User.java
new file mode 100644
index 0000000..ab76f6a
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/User.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.cay_2032;
+
+import org.apache.cayenne.testdo.cay_2032.auto._User;
+
+public class User extends _User {
+
+    private static final long serialVersionUID = 1L; 
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/auto/_Team.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/auto/_Team.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/auto/_Team.java
new file mode 100644
index 0000000..b5007ed
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/auto/_Team.java
@@ -0,0 +1,35 @@
+package org.apache.cayenne.testdo.cay_2032.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.testdo.cay_2032.User;
+
+/**
+ * Class _Team was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _Team extends CayenneDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String TEAM_ID_PK_COLUMN = "team_id";
+
+    public static final Property<List<User>> TEAM_USERS = Property.create("teamUsers", List.class);
+
+    public void addToTeamUsers(User obj) {
+        addToManyTarget("teamUsers", obj, true);
+    }
+    public void removeFromTeamUsers(User obj) {
+        removeToManyTarget("teamUsers", obj, true);
+    }
+    @SuppressWarnings("unchecked")
+    public List<User> getTeamUsers() {
+        return (List<User>)readProperty("teamUsers");
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/auto/_User.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/auto/_User.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/auto/_User.java
new file mode 100644
index 0000000..5f40fa9
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2032/auto/_User.java
@@ -0,0 +1,43 @@
+package org.apache.cayenne.testdo.cay_2032.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.testdo.cay_2032.Team;
+
+/**
+ * Class _User was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _User extends CayenneDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String USER_ID_PK_COLUMN = "user_id";
+
+    public static final Property<byte[]> NAME = Property.create("name", byte[].class);
+    public static final Property<List<Team>> USER_TEAMS = Property.create("userTeams", List.class);
+
+    public void setName(byte[] name) {
+        writeProperty("name", name);
+    }
+    public byte[] getName() {
+        return (byte[])readProperty("name");
+    }
+
+    public void addToUserTeams(Team obj) {
+        addToManyTarget("userTeams", obj, true);
+    }
+    public void removeFromUserTeams(Team obj) {
+        removeToManyTarget("userTeams", obj, true);
+    }
+    @SuppressWarnings("unchecked")
+    public List<Team> getUserTeams() {
+        return (List<Team>)readProperty("userTeams");
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
index ee51554..d7eb2f2 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
@@ -28,6 +28,7 @@ public class CayenneProjects {
     // setup?)
     public static final String ARRAY_TYPE_PROJECT = "cayenne-array-type.xml";
     public static final String BINARY_PK_PROJECT = "cayenne-binary-pk.xml";
+    public static final String CAY_2032 = "cayenne-cay-2032.xml";
     public static final String COMPOUND_PROJECT = "cayenne-compound.xml";
     public static final String DATE_TIME_PROJECT = "cayenne-date-time.xml";
     public static final String DELETE_RULES_PROJECT = "cayenne-delete-rules.xml";

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
index 05fc392..ec5d332 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
@@ -80,7 +80,7 @@ public class SchemaBuilder {
 			"table-primitives.map.xml", "generic.map.xml", "map-db1.map.xml", "map-db2.map.xml", "embeddable.map.xml",
 			"qualified.map.xml", "quoted-identifiers.map.xml", "inheritance-single-table1.map.xml",
 			"inheritance-vertical.map.xml", "oneway-rels.map.xml", "unsupported-distinct-types.map.xml",
-			"array-type.map.xml" };
+			"array-type.map.xml", "cay-2032.map.xml" };
 
 	// hardcoded dependent entities that should be excluded
 	// if LOBs are not supported

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/test/resources/cay-2032.map.xml
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/resources/cay-2032.map.xml b/cayenne-server/src/test/resources/cay-2032.map.xml
new file mode 100644
index 0000000..9a3b380
--- /dev/null
+++ b/cayenne-server/src/test/resources/cay-2032.map.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-map xmlns="http://cayenne.apache.org/schema/9/modelMap"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/9/modelMap http://cayenne.apache.org/schema/9/modelMap.xsd"
+	 project-version="9">
+	<property name="defaultPackage" value="org.apache.cayenne.testdo.cay_2032"/>
+	<db-entity name="TEAM">
+		<db-attribute name="team_id" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+	</db-entity>
+	<db-entity name="USERS">
+		<db-attribute name="name" type="BLOB" length="255"/>
+		<db-attribute name="user_id" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+	</db-entity>
+	<db-entity name="USER_HAS_TEAM">
+		<db-attribute name="team_id" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="user_id" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+	</db-entity>
+	<obj-entity name="Team" className="org.apache.cayenne.testdo.cay_2032.Team" dbEntityName="TEAM">
+	</obj-entity>
+	<obj-entity name="User" className="org.apache.cayenne.testdo.cay_2032.User" dbEntityName="USERS">
+		<obj-attribute name="name" type="byte[]" db-attribute-path="name"/>
+	</obj-entity>
+	<db-relationship name="userHasTeam" source="TEAM" target="USER_HAS_TEAM" toMany="true">
+		<db-attribute-pair source="team_id" target="team_id"/>
+	</db-relationship>
+	<db-relationship name="userHasTeam" source="USERS" target="USER_HAS_TEAM" toMany="true">
+		<db-attribute-pair source="user_id" target="user_id"/>
+	</db-relationship>
+	<db-relationship name="team" source="USER_HAS_TEAM" target="TEAM" toMany="false">
+		<db-attribute-pair source="team_id" target="team_id"/>
+	</db-relationship>
+	<db-relationship name="user" source="USER_HAS_TEAM" target="USERS" toMany="false">
+		<db-attribute-pair source="user_id" target="user_id"/>
+	</db-relationship>
+	<obj-relationship name="teamUsers" source="Team" target="User" deleteRule="Deny" db-relationship-path="userHasTeam.user"/>
+	<obj-relationship name="userTeams" source="User" target="Team" deleteRule="Deny" db-relationship-path="userHasTeam.team"/>
+</data-map>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/cayenne-server/src/test/resources/cayenne-cay-2032.xml
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/resources/cayenne-cay-2032.xml b/cayenne-server/src/test/resources/cayenne-cay-2032.xml
new file mode 100644
index 0000000..f9427a7
--- /dev/null
+++ b/cayenne-server/src/test/resources/cayenne-cay-2032.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain project-version="9">
+	<map name="cay-2032"/>
+</domain>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/e3704c28/docs/doc/src/main/resources/RELEASE-NOTES.txt
----------------------------------------------------------------------
diff --git a/docs/doc/src/main/resources/RELEASE-NOTES.txt b/docs/doc/src/main/resources/RELEASE-NOTES.txt
index c165ab6..733956d 100644
--- a/docs/doc/src/main/resources/RELEASE-NOTES.txt
+++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt
@@ -31,6 +31,7 @@ CAY-2212 cdbimport cleanup and configuration schema refactoring
 
 Bug Fixes:
 
+CAY-2032 SelectAction: DistinctResultIterator ignores flattened relationships
 CAY-2137 When generating SQL from EJBQL, use "AND" to separate multiple join conditions
 CAY-2174 Change FK attribute name cause ObjAttribute appear after Reverse Engineering
 CAY-2175 AliasName used in EJBQLQuery is not working if it contains mixed case