CAY-2863 DbEntity qualifiers are no longer applied to JOIN conditions
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 17a1dda..78ccd1c 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -24,6 +24,7 @@
 
 CAY-2701 MySQL DST-related LocalDateTime issues
 CAY-2836 ObjectSelect.selectCount() throws if a query contains ordering
+CAY-2863 DbEntity qualifiers are no longer applied to JOIN conditions
 CAY-2871 QualifierTranslator breaks on a relationship with a compound FK
 CAY-2872 CayenneModeler "Documentation" link is broken
 CAY-2876 Memory leak in the ObjectStore
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
index 615c9cc..d6369e1 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
@@ -36,6 +36,7 @@
 import org.apache.cayenne.exp.parser.ASTFunctionCall;
 import org.apache.cayenne.exp.parser.ASTNotExists;
 import org.apache.cayenne.exp.parser.ASTObjPath;
+import org.apache.cayenne.exp.parser.ASTPath;
 import org.apache.cayenne.exp.parser.ASTScalar;
 import org.apache.cayenne.exp.parser.ASTSubquery;
 import org.apache.cayenne.exp.parser.PatternMatchNode;
@@ -295,6 +296,9 @@
             return new EmptyNode();
         } else {
             String alias = context.getTableTree().aliasForPath(result.getLastAttributePath());
+            if(TableTree.CURRENT_ALIAS.equals(alias)) {
+                alias = node.getPathAliases().get(TableTree.CURRENT_ALIAS);
+            }
             return table(alias).column(result.getLastAttribute()).build();
         }
     }
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java
index 87ab9bc..fe573f6 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java
@@ -35,6 +35,9 @@
  * @since 4.2
  */
 class TableTree {
+
+    public static final String CURRENT_ALIAS = "__current_table_alias__";
+
     /**
      * Tables mapped by db path it's spawned by.
      * Can be following:
@@ -61,6 +64,10 @@
     }
 
     void addJoinTable(CayennePath path, DbRelationship relationship, JoinType joinType, Expression additionalQualifier) {
+        // skip adding new node if we are resolving table tree itself
+        if(path.marker() == CayennePath.TABLE_TREE_MARKER) {
+            return;
+        }
         TableTreeNode treeNode = tableNodes.get(path);
         if (treeNode != null) {
             return;
@@ -71,6 +78,10 @@
     }
 
     String aliasForPath(CayennePath attributePath) {
+        // should be resolved dynamically by the caller
+        if(attributePath.marker() == CayennePath.TABLE_TREE_MARKER) {
+            return CURRENT_ALIAS;
+        }
         if(attributePath.isEmpty()) {
             return rootNode.getTableAlias();
         }
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTreeQualifierStage.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTreeQualifierStage.java
index 313582c..ca26d6c 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTreeQualifierStage.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTreeQualifierStage.java
@@ -21,9 +21,6 @@
 
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
 import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.parser.ASTDbPath;
-import org.apache.cayenne.exp.parser.ASTPath;
-import org.apache.cayenne.exp.path.CayennePath;
 
 /**
  * @since 4.2
@@ -33,8 +30,11 @@
     @Override
     public void perform(TranslatorContext context) {
         context.getTableTree().visit(node -> {
-            appendQualifier(context, node, node.getEntity().getQualifier());
-            appendQualifier(context, node, node.getAdditionalQualifier());
+            if(node.getRelationship() == null) {
+                // translate only root qualifier here, joined tables are processed in the `TableTreeStage`
+                appendQualifier(context, node, node.getEntity().getQualifier());
+                appendQualifier(context, node, node.getAdditionalQualifier());
+            }
         });
 
         if(context.getQualifierNode() != null) {
@@ -46,14 +46,7 @@
         if (dbQualifier == null) {
             return;
         }
-
-        CayennePath pathToRoot = node.getAttributePath();
-        dbQualifier = dbQualifier.transform(input ->
-                // here we are not only marking path as prefetch, but changing ObjPath to DB (without )
-                input instanceof ASTPath
-                        ? new ASTDbPath(pathToRoot.dot(((ASTPath) input).getPath()).withMarker(CayennePath.PREFETCH_MARKER))
-                        : input
-        );
+        dbQualifier = TableTreeStage.translateToDbPath(node, dbQualifier);
         Node translatedQualifier = context.getQualifierTranslator().translate(dbQualifier);
         context.appendQualifierNode(translatedQualifier);
     }
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java
index ff0eb0f..c1a9e4c 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java
@@ -20,10 +20,16 @@
 package org.apache.cayenne.access.translator.select;
 
 import java.util.List;
+import java.util.Map;
 
 import org.apache.cayenne.access.sqlbuilder.ExpressionNodeBuilder;
 import org.apache.cayenne.access.sqlbuilder.JoinNodeBuilder;
 import org.apache.cayenne.access.sqlbuilder.NodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.exp.parser.ASTPath;
+import org.apache.cayenne.exp.path.CayennePath;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbJoin;
 
@@ -74,6 +80,36 @@
             }
         }
 
+        // append entity qualifiers
+        expressionNodeBuilder = appendQualifier(expressionNodeBuilder, context, node, node.getEntity().getQualifier());
+        expressionNodeBuilder = appendQualifier(expressionNodeBuilder, context, node, node.getAdditionalQualifier());
         return expressionNodeBuilder;
     }
+
+    private static ExpressionNodeBuilder appendQualifier(ExpressionNodeBuilder joinBuilder,
+                                        TranslatorContext context,
+                                        TableTreeNode node,
+                                        Expression dbQualifier) {
+        if (dbQualifier == null) {
+            return joinBuilder;
+        }
+
+        dbQualifier = translateToDbPath(node, dbQualifier);
+        Node translatedQualifier = context.getQualifierTranslator().translate(dbQualifier);
+        return joinBuilder.and(() -> translatedQualifier);
+    }
+
+    static Expression translateToDbPath(TableTreeNode node, Expression dbQualifier) {
+        CayennePath pathToRoot = node.getAttributePath();
+        dbQualifier = dbQualifier.transform(input -> {
+            // here we are not only marking path, but changing ObjPath to DB
+            if (input instanceof ASTPath) {
+                ASTDbPath dbPath = new ASTDbPath(pathToRoot.dot(((ASTPath) input).getPath()).withMarker(CayennePath.TABLE_TREE_MARKER));
+                dbPath.setPathAliases(Map.of(TableTree.CURRENT_ALIAS, node.getTableAlias()));
+                return dbPath;
+            }
+            return input;
+        });
+        return dbQualifier;
+    }
 }
diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/path/CayennePath.java b/cayenne/src/main/java/org/apache/cayenne/exp/path/CayennePath.java
index 78d4c33..562db04 100644
--- a/cayenne/src/main/java/org/apache/cayenne/exp/path/CayennePath.java
+++ b/cayenne/src/main/java/org/apache/cayenne/exp/path/CayennePath.java
@@ -54,11 +54,15 @@
 
     /**
      * Prefetch path marker
-     * TODO: this marker used only for prefetch processing
      */
     int PREFETCH_MARKER = 1;
 
     /**
+     * Marker denotes paths inside tree resolution logic
+     */
+    int TABLE_TREE_MARKER = 2;
+
+    /**
      * Constant value for an empty path
      */
     CayennePath EMPTY_PATH = new EmptyCayennePath(NO_MARKER);
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java
index 5c11dc8..4bd1d92 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslatorIT.java
@@ -149,14 +149,18 @@
 			// do some simple assertions to make sure all parts are in
 			assertNotNull(generatedSql);
 			assertTrue(generatedSql.startsWith("SELECT "));
-			assertTrue(generatedSql.indexOf(" FROM ") > 0);
-			if (generatedSql.contains("RTRIM")) {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME) =") > generatedSql.indexOf("RTRIM("));
-			} else if (generatedSql.contains("TRIM")) {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME) =") > generatedSql.indexOf("TRIM("));
-			} else {
-				assertTrue(generatedSql.indexOf("ARTIST_NAME =") > 0);
-			}
+
+			int iFrom = generatedSql.indexOf(" FROM ");
+			int iPaintingTable = generatedSql.indexOf(" PAINTING ");
+			int iArtistTable = generatedSql.indexOf(" ARTIST ");
+			int iName = generatedSql.indexOf("ARTIST_NAME =");
+			int iOrder = generatedSql.indexOf(" ORDER");
+
+			assertTrue(iFrom > 0);
+			assertTrue(iPaintingTable > iFrom);
+			assertTrue(iArtistTable > iPaintingTable);
+			assertTrue(iName > iArtistTable);
+			assertTrue(iOrder > iName);
 
 		} finally {
 			entity.setQualifier(null);
@@ -854,4 +858,31 @@
 		int totalJoins = translator.getContext().getTableCount() - 1;
 		assertEquals(4, totalJoins);
 	}
+
+	@Test
+	public void testDbEntityQualifier_JoinQuery() throws Exception {
+
+		final DbEntity entity = context.getEntityResolver().getDbEntity("ARTIST");
+		entity.setQualifier(ExpressionFactory.exp("ARTIST_NAME = 'Should be on JOIN condition and not WHERE'"));
+
+		ObjectSelect<Painting> q = ObjectSelect.query(Painting.class)
+				.where
+						(
+								Painting.TO_ARTIST.dot(Artist.DATE_OF_BIRTH).eq(new java.sql.Date(1, 0, 1))
+										.orExp(Painting.TO_GALLERY.dot(Gallery.GALLERY_NAME).like("G%"))
+						);
+
+		// If the DbEntity qualifier is set on the WHERE condition then the OR expression will fail to find matches
+
+		SelectTranslator transl = new DefaultSelectTranslator(q, dataNode.getAdapter(), dataNode.getEntityResolver());
+		try {
+			String generatedSql = transl.getSql();
+			int whereNdx = generatedSql.indexOf(" WHERE ");
+			int joinNdx = generatedSql.indexOf(" JOIN ARTIST ");
+			assertTrue(generatedSql.substring(joinNdx, whereNdx).indexOf("ARTIST_NAME") > 0); // Should be in JOIN condition
+			assertTrue(generatedSql.indexOf("ARTIST_NAME", whereNdx) < 0); // Should not be part of WHERE
+		} finally {
+			entity.setQualifier(null);
+		}
+	}
 }