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

[cayenne] branch master updated (45877e8 -> 9199293)

This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git.


    from 45877e8  CAY-2576 Ant cgen task is broken
     new d72999e  Do not use TRIM() on Sybase to keep 4.1 compatibility
     new 932037f  Add DbEntity qualifiers to main query qualifier instead of join clause
     new 9ef7776  Do not use TRIM() in JOIN clauses and inside other function calls
     new 9199293  CAY-2573 DI field injection is triggered when creating sql Driver

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 RELEASE-NOTES.txt                                  |  1 +
 .../configuration/DriverDataSourceFactory.java     |  2 +-
 .../sqlbuilder/sqltree/TrimmingColumnNode.java     | 13 ++-
 .../translator/select/DefaultSelectTranslator.java |  1 +
 .../select/QualifierTranslationStage.java          | 21 +----
 .../access/translator/select/TableTree.java        | 14 +++-
 .../translator/select/TableTreeQualifierStage.java | 67 +++++++++++++++
 .../access/translator/select/TableTreeStage.java   | 25 ------
 .../translator/select/TranslatorContext.java       | 10 +++
 .../server/PropertyDataSourceFactory.java          |  2 +-
 .../server/XMLPoolingDataSourceFactory.java        |  2 +-
 .../cayenne/dba/sqlserver/SQLServerAdapter.java    |  4 +
 .../dba/sqlserver/SQLServerTreeProcessor.java      | 96 +---------------------
 .../apache/cayenne/dba/sybase/SybaseAdapter.java   |  7 +-
 .../SybaseSQLTreeProcessor.java}                   | 21 +++--
 .../select/QualifierTranslationStageTest.java      | 10 +--
 .../exp/parser/ASTFunctionCallStringIT.java        |  2 +-
 17 files changed, 132 insertions(+), 166 deletions(-)
 create mode 100644 cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeQualifierStage.java
 copy cayenne-server/src/main/java/org/apache/cayenne/dba/{sqlserver/SQLServerTreeProcessor.java => sybase/SybaseSQLTreeProcessor.java} (90%)


[cayenne] 04/04: CAY-2573 DI field injection is triggered when creating sql Driver

Posted by nt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 91992935b9de28f9b5739b0b763b90520675c77e
Author: Nikita Timofeev <st...@gmail.com>
AuthorDate: Sat May 4 18:00:42 2019 +0300

    CAY-2573 DI field injection is triggered when creating sql Driver
---
 RELEASE-NOTES.txt                                                       | 1 +
 .../cayenne/dbsync/reverse/configuration/DriverDataSourceFactory.java   | 2 +-
 .../apache/cayenne/configuration/server/PropertyDataSourceFactory.java  | 2 +-
 .../cayenne/configuration/server/XMLPoolingDataSourceFactory.java       | 2 +-
 4 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 1cce2d2..aee19d7 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -51,6 +51,7 @@ CAY-2553 Wrong disjoint prefetch query qualifier
 CAY-2559 Modeler: Warning dialog shows wrong information after changing target entity in dbRelationship
 CAY-2561 Modeler: cgen type combobox doesn't set templates
 CAY-2572 Queries are not sorted by name in data map XML
+CAY-2573 DI field injection is triggered when creating sql Driver
 CAY-2575 Select translator: Wrong translation of IN Expression
 CAY-2576 Ant cgen task is broken
 
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/DriverDataSourceFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/DriverDataSourceFactory.java
index d24f367..a124c13 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/DriverDataSourceFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/DriverDataSourceFactory.java
@@ -47,7 +47,7 @@ public class DriverDataSourceFactory implements DataSourceFactory {
 			throw new IllegalArgumentException("'nodeDescriptor' contains no datasource descriptor");
 		}
 
-		Driver driver = objectFactory.newInstance(Driver.class, properties.getJdbcDriver());
+		Driver driver = (Driver)objectFactory.getJavaClass(properties.getJdbcDriver()).newInstance();
 		return new DriverDataSource(driver, properties.getDataSourceUrl(), properties.getUserName(),
 				properties.getPassword());
 	}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/PropertyDataSourceFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/PropertyDataSourceFactory.java
index d3fc1ac..365e875 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/PropertyDataSourceFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/PropertyDataSourceFactory.java
@@ -71,7 +71,7 @@ public class PropertyDataSourceFactory implements DataSourceFactory {
 				UnmanagedPoolingDataSource.MAX_QUEUE_WAIT_DEFAULT);
 		String validationQuery = properties.get(Constants.JDBC_VALIDATION_QUERY_PROPERTY);
 
-		Driver driver = objectFactory.newInstance(Driver.class, driverClass);
+		Driver driver = (Driver)objectFactory.getJavaClass(driverClass).newInstance();
 		return DataSourceBuilder.url(url).driver(driver).userName(username).password(password)
 				.pool(minConnections, maxConnections).maxQueueWaitTime(maxQueueWaitTime)
 				.validationQuery(validationQuery).build();
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/XMLPoolingDataSourceFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/XMLPoolingDataSourceFactory.java
index e99d78d..d0d07af 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/XMLPoolingDataSourceFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/XMLPoolingDataSourceFactory.java
@@ -67,7 +67,7 @@ public class XMLPoolingDataSourceFactory implements DataSourceFactory {
 		long maxQueueWaitTime = properties.getLong(Constants.JDBC_MAX_QUEUE_WAIT_TIME,
 				UnmanagedPoolingDataSource.MAX_QUEUE_WAIT_DEFAULT);
 
-		Driver driver = objectFactory.newInstance(Driver.class, descriptor.getJdbcDriver());
+		Driver driver = (Driver)objectFactory.getJavaClass(descriptor.getJdbcDriver()).newInstance();
 
 		return DataSourceBuilder.url(descriptor.getDataSourceUrl()).driver(driver).userName(descriptor.getUserName())
 				.password(descriptor.getPassword())


[cayenne] 03/04: Do not use TRIM() in JOIN clauses and inside other function calls

Posted by nt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 9ef777667da5bb33e308b80a2d27443e574e2788
Author: Nikita Timofeev <st...@gmail.com>
AuthorDate: Sat May 4 17:15:28 2019 +0300

    Do not use TRIM() in JOIN clauses and inside other function calls
---
 .../access/sqlbuilder/sqltree/TrimmingColumnNode.java       | 13 ++++++++++++-
 .../apache/cayenne/exp/parser/ASTFunctionCallStringIT.java  |  2 +-
 2 files changed, 13 insertions(+), 2 deletions(-)

diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TrimmingColumnNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TrimmingColumnNode.java
index c4c252b..b57e536 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TrimmingColumnNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TrimmingColumnNode.java
@@ -38,7 +38,7 @@ public class TrimmingColumnNode extends Node {
     public QuotingAppendable append(QuotingAppendable buffer) {
         boolean isResult = isResultNode();
         if(columnNode.getAlias() == null || isResult) {
-            if(isCharType()) {
+            if(isCharType() && isAllowedForTrimming()) {
                 appendRtrim(buffer);
                 appendAlias(buffer, isResult);
             } else if(isComparisionWithClob()) {
@@ -72,6 +72,17 @@ public class TrimmingColumnNode extends Node {
                 && columnNode.getAttribute().getType() == Types.CHAR;
     }
 
+    protected boolean isAllowedForTrimming() {
+        Node parent = getParent();
+        while(parent != null) {
+            if(parent.getType() == NodeType.JOIN || parent.getType() == NodeType.FUNCTION) {
+                return false;
+            }
+            parent = parent.getParent();
+        }
+        return true;
+    }
+
     protected boolean isResultNode() {
         Node parent = getParent();
         while(parent != null) {
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java
index 3a8742d..361c5b6 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java
@@ -88,7 +88,7 @@ public class ASTFunctionCallStringIT extends ServerCase {
     public void testASTConcat() throws Exception {
         Artist a1 = createArtist("Pablo");
         Artist a2 = ObjectSelect.query(Artist.class)
-                .where(Artist.ARTIST_NAME.concat(" ", "Picasso").eq("Pablo Picasso")).selectOne(context);
+                .where(Artist.ARTIST_NAME.trim().concat(" ", "Picasso").eq("Pablo Picasso")).selectOne(context);
         assertEquals(a1, a2);
     }
 


[cayenne] 01/04: Do not use TRIM() on Sybase to keep 4.1 compatibility

Posted by nt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit d72999e4b78cf2db70dfe4d79fd7953e21bca34b
Author: Nikita Timofeev <st...@gmail.com>
AuthorDate: Sat May 4 17:13:21 2019 +0300

    Do not use TRIM() on Sybase to keep 4.1 compatibility
---
 .../cayenne/dba/sqlserver/SQLServerAdapter.java    |  4 +
 .../dba/sqlserver/SQLServerTreeProcessor.java      | 96 +---------------------
 .../apache/cayenne/dba/sybase/SybaseAdapter.java   |  7 +-
 .../SybaseSQLTreeProcessor.java}                   | 21 +++--
 4 files changed, 21 insertions(+), 107 deletions(-)

diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
index 6a99dcb..ed0619b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
@@ -74,6 +74,10 @@ import org.apache.cayenne.resource.ResourceLocator;
  */
 public class SQLServerAdapter extends SybaseAdapter {
 
+	/**
+	 * @deprecated since 4.2 unused
+	 */
+	@Deprecated
 	public static final String TRIM_FUNCTION = "RTRIM";
 
 	public SQLServerAdapter(@Inject RuntimeProperties runtimeProperties,
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTreeProcessor.java
index 2681a53..2d697cb 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTreeProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTreeProcessor.java
@@ -20,109 +20,17 @@
 package org.apache.cayenne.dba.sqlserver;
 
 import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.EmptyNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
-import org.apache.cayenne.access.sqlbuilder.sqltree.NodeType;
-import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.TextNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.TopNode;
-import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
 import org.apache.cayenne.dba.sqlserver.sqltree.SQLServerColumnNode;
+import org.apache.cayenne.dba.sybase.SybaseSQLTreeProcessor;
 
 /**
  * @since 4.2
  */
-public class SQLServerTreeProcessor extends BaseSQLTreeProcessor {
-
-    @Override
-    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
-        // SQLServer uses "SELECT DISTINCT TOP N" or "SELECT TOP N" instead of LIMIT
-        // Offset will be calculated in-memory
-        replaceChild(parent, index, new EmptyNode(), false);
-        if(child.getLimit() > 0) {
-            int limit = child.getLimit() + child.getOffset();
-            // we have root actually as input for processor, but it's better to keep processor stateless
-            // root shouldn't be far from limit's parent (likely it will be parent itself)
-            Node root = getRoot(parent);
-            int idx = 0;
-            if(root.getChild(0).getType() == NodeType.DISTINCT) {
-                idx = 1;
-            }
-            root.addChild(idx, new TopNode(limit));
-        }
-    }
-
-    private Node getRoot(Node node) {
-        while(node.getParent() != null) {
-            node = node.getParent();
-        }
-        return node;
-    }
+public class SQLServerTreeProcessor extends SybaseSQLTreeProcessor {
 
     @Override
     protected void onColumnNode(Node parent, ColumnNode child, int index) {
         replaceChild(parent, index,  new SQLServerColumnNode(child));
     }
-
-    @Override
-    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
-        String functionName = child.getFunctionName();
-        Node replacement = null;
-        switch (functionName) {
-            case "LENGTH":
-                replacement = new FunctionNode("LEN", child.getAlias(), true);
-                break;
-            case "LOCATE":
-                replacement = new FunctionNode("CHARINDEX", child.getAlias(), true);
-                break;
-            case "MOD":
-                replacement = new OpExpressionNode("%");
-                break;
-            case "TRIM":
-                Node rtrim = new FunctionNode("RTRIM", null, true);
-                replacement = new FunctionNode("LTRIM", child.getAlias(), true);
-                for(int i=0; i<child.getChildrenCount(); i++) {
-                    rtrim.addChild(child.getChild(i));
-                }
-                replacement.addChild(rtrim);
-                parent.replaceChild(index, replacement);
-                return;
-            case "CURRENT_DATE":
-                replacement = new FunctionNode("{fn CURDATE()}", child.getAlias(), false);
-                break;
-            case "CURRENT_TIME":
-                replacement = new FunctionNode("{fn CURTIME()}", child.getAlias(), false);
-                break;
-            case "CURRENT_TIMESTAMP":
-                replacement = new FunctionNode("CURRENT_TIMESTAMP", child.getAlias(), false);
-                break;
-
-            case "YEAR":
-            case "MONTH":
-            case "WEEK":
-            case "DAY_OF_YEAR":
-            case "DAY":
-            case "DAY_OF_MONTH":
-            case "DAY_OF_WEEK":
-            case "HOUR":
-            case "MINUTE":
-            case "SECOND":
-                replacement = new FunctionNode("DATEPART", child.getAlias(), true);
-                if("DAY_OF_MONTH".equals(functionName)) {
-                    functionName = "DAY";
-                } else if("DAY_OF_WEEK".equals(functionName)) {
-                    functionName = "WEEKDAY";
-                } else if("DAY_OF_YEAR".equals(functionName)) {
-                    functionName = "DAYOFYEAR";
-                }
-                replacement.addChild(new TextNode(functionName));
-                break;
-        }
-
-        if(replacement != null) {
-            replaceChild(parent, index, replacement);
-        }
-    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
index 2aaff9d..6226f2c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
@@ -20,7 +20,6 @@
 package org.apache.cayenne.dba.sybase;
 
 import java.sql.PreparedStatement;
-import java.sql.SQLException;
 import java.sql.Types;
 import java.util.List;
 import java.util.function.Function;
@@ -42,7 +41,6 @@ import org.apache.cayenne.dba.DefaultQuotingStrategy;
 import org.apache.cayenne.dba.JdbcAdapter;
 import org.apache.cayenne.dba.PkGenerator;
 import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dba.sqlserver.SQLServerTreeProcessor;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.resource.ResourceLocator;
@@ -81,7 +79,7 @@ public class SybaseAdapter extends JdbcAdapter {
      */
     @Override
     public Function<Node, Node> getSqlTreeProcessor() {
-        return new SQLServerTreeProcessor();
+        return new SybaseSQLTreeProcessor();
     }
 
     /**
@@ -124,8 +122,7 @@ public class SybaseAdapter extends JdbcAdapter {
     }
 
     @Override
-    public void bindParameter(PreparedStatement statement, ParameterBinding binding)
-            throws SQLException, Exception {
+    public void bindParameter(PreparedStatement statement, ParameterBinding binding) throws Exception {
 
         // Sybase driver doesn't like CLOBs and BLOBs as parameters
         if (binding.getValue() == null) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseSQLTreeProcessor.java
similarity index 90%
copy from cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTreeProcessor.java
copy to cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseSQLTreeProcessor.java
index 2681a53..8393298 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTreeProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseSQLTreeProcessor.java
@@ -17,7 +17,7 @@
  *  under the License.
  ****************************************************************/
 
-package org.apache.cayenne.dba.sqlserver;
+package org.apache.cayenne.dba.sybase;
 
 import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.EmptyNode;
@@ -34,7 +34,7 @@ import org.apache.cayenne.dba.sqlserver.sqltree.SQLServerColumnNode;
 /**
  * @since 4.2
  */
-public class SQLServerTreeProcessor extends BaseSQLTreeProcessor {
+public class SybaseSQLTreeProcessor extends BaseSQLTreeProcessor {
 
     @Override
     protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
@@ -110,12 +110,16 @@ public class SQLServerTreeProcessor extends BaseSQLTreeProcessor {
             case "MINUTE":
             case "SECOND":
                 replacement = new FunctionNode("DATEPART", child.getAlias(), true);
-                if("DAY_OF_MONTH".equals(functionName)) {
-                    functionName = "DAY";
-                } else if("DAY_OF_WEEK".equals(functionName)) {
-                    functionName = "WEEKDAY";
-                } else if("DAY_OF_YEAR".equals(functionName)) {
-                    functionName = "DAYOFYEAR";
+                switch (functionName) {
+                    case "DAY_OF_MONTH":
+                        functionName = "DAY";
+                        break;
+                    case "DAY_OF_WEEK":
+                        functionName = "WEEKDAY";
+                        break;
+                    case "DAY_OF_YEAR":
+                        functionName = "DAYOFYEAR";
+                        break;
                 }
                 replacement.addChild(new TextNode(functionName));
                 break;
@@ -125,4 +129,5 @@ public class SQLServerTreeProcessor extends BaseSQLTreeProcessor {
             replaceChild(parent, index, replacement);
         }
     }
+
 }


[cayenne] 02/04: Add DbEntity qualifiers to main query qualifier instead of join clause

Posted by nt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 932037f80adade89f49df5e58f3f482a48b32bc4
Author: Nikita Timofeev <st...@gmail.com>
AuthorDate: Sat May 4 17:15:00 2019 +0300

    Add DbEntity qualifiers to main query qualifier instead of join clause
---
 .../translator/select/DefaultSelectTranslator.java |  1 +
 .../select/QualifierTranslationStage.java          | 21 +------
 .../access/translator/select/TableTree.java        | 14 ++++-
 .../translator/select/TableTreeQualifierStage.java | 67 ++++++++++++++++++++++
 .../access/translator/select/TableTreeStage.java   | 25 --------
 .../translator/select/TranslatorContext.java       | 10 ++++
 .../select/QualifierTranslationStageTest.java      | 10 +---
 7 files changed, 94 insertions(+), 54 deletions(-)

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 1213c38..fee28d8 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
@@ -48,6 +48,7 @@ public class DefaultSelectTranslator implements SelectTranslator {
             new DistinctStage(),
             new LimitOffsetStage(),
             new ColumnDescriptorStage(),
+            new TableTreeQualifierStage(),
             new TableTreeStage(),
             new SQLResultStage(),
             new SQLGenerationStage()
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java
index c4d2ba1..386c0e3 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslationStage.java
@@ -49,26 +49,7 @@ class QualifierTranslationStage implements TranslationStage {
             }
         }
 
-        // Attaching root Db entity's qualifier
-        DbEntity dbEntity = context.getMetadata().getDbEntity();
-        if (dbEntity != null) {
-            Expression dbQualifier = dbEntity.getQualifier();
-            if (dbQualifier != null) {
-                dbQualifier = dbQualifier.transform(node -> {
-                    if (node instanceof ASTObjPath) {
-                        return new ASTDbPath(((SimpleNode) node).getOperand(0));
-                    }
-                    return node;
-                });
-
-                expression = expression == null ? dbQualifier : expression.andExp(dbQualifier);
-            }
-        }
-
         Node qualifierNode = translator.translate(expression);
-
-        if(qualifierNode != null) {
-            context.getSelectBuilder().where(qualifierNode);
-        }
+        context.setQualifierNode(qualifierNode);
     }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java
index 5082c4e..6eb7a8d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTree.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.access.translator.select;
 
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.map.DbEntity;
@@ -97,8 +98,17 @@ class TableTree {
 
     public void visit(TableNodeVisitor visitor) {
         visitor.visit(rootNode);
-        for(TableTreeNode node : tableNodes.values()) {
-            visitor.visit(node);
+
+        // as we can spawn new nodes while processing existing,
+        // we need multiple iterations until all rows are processed
+        int initialSize = 0;
+        int currentSize = tableNodes.size();
+        while(initialSize != currentSize) {
+            tableNodes.values().stream().skip(initialSize)
+                    .collect(Collectors.toList()) // copy collection in case of concurrent modification in visitor
+                    .forEach(visitor::visit);
+            initialSize = currentSize;
+            currentSize = tableNodes.size();
         }
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeQualifierStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeQualifierStage.java
new file mode 100644
index 0000000..1d00543
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeQualifierStage.java
@@ -0,0 +1,67 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.select;
+
+import org.apache.cayenne.access.sqlbuilder.NodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.exp.parser.ASTPath;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.exp;
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.node;
+
+/**
+ * @since 4.2
+ */
+class TableTreeQualifierStage implements TranslationStage {
+
+    @Override
+    public void perform(TranslatorContext context) {
+        context.getTableTree().visit(node -> {
+            Expression dbQualifier = node.getEntity().getQualifier();
+            if (dbQualifier != null) {
+                String pathToRoot = node.getAttributePath().getPath();
+                dbQualifier = dbQualifier.transform(input -> {
+                    if (input instanceof ASTPath) {
+                        String path = ((ASTPath) input).getPath();
+                        if(!pathToRoot.isEmpty()) {
+                            path = pathToRoot + '.' + path;
+                        }
+                        return new ASTDbPath(path);
+                    }
+                    return input;
+                });
+                Node rootQualifier = context.getQualifierNode();
+                Node translatedQualifier = context.getQualifierTranslator().translate(dbQualifier);
+                if (rootQualifier != null) {
+                    NodeBuilder expressionNodeBuilder = exp(node(rootQualifier)).and(node(translatedQualifier));
+                    context.setQualifierNode(expressionNodeBuilder.build());
+                } else {
+                    context.setQualifierNode(translatedQualifier);
+                }
+            }
+        });
+
+        if(context.getQualifierNode() != null) {
+            context.getSelectBuilder().where(context.getQualifierNode());
+        }
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java
index 9ad1588..c4ee949 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TableTreeStage.java
@@ -24,10 +24,6 @@ import java.util.List;
 import org.apache.cayenne.access.sqlbuilder.ExpressionNodeBuilder;
 import org.apache.cayenne.access.sqlbuilder.JoinNodeBuilder;
 import org.apache.cayenne.access.sqlbuilder.NodeBuilder;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.parser.ASTDbPath;
-import org.apache.cayenne.exp.parser.ASTPath;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbJoin;
 
@@ -45,7 +41,6 @@ class TableTreeStage implements TranslationStage {
             if(node.getRelationship() != null) {
                 tableNode = getJoin(node, tableNode).on(getJoinExpression(context, node));
             }
-
             context.getSelectBuilder().from(tableNode);
         });
     }
@@ -79,26 +74,6 @@ class TableTreeStage implements TranslationStage {
             }
         }
 
-        expressionNodeBuilder = attachTargetQualifier(context, node, expressionNodeBuilder);
-
-        return expressionNodeBuilder;
-    }
-
-    private ExpressionNodeBuilder attachTargetQualifier(TranslatorContext context, TableTreeNode node, ExpressionNodeBuilder expressionNodeBuilder) {
-        Expression dbQualifier = node.getRelationship().getTargetEntity().getQualifier();
-        if (dbQualifier != null) {
-            String pathToRoot = node.getAttributePath().getPath();
-            dbQualifier = dbQualifier.transform(input -> input instanceof ASTPath
-                    ? new ASTDbPath(pathToRoot + '.' + ((ASTPath) input).getPath())
-                    : input
-            );
-            Node translatedQualifier = context.getQualifierTranslator().translate(dbQualifier);
-            if (expressionNodeBuilder != null) {
-                expressionNodeBuilder = expressionNodeBuilder.and(node(translatedQualifier));
-            } else {
-                expressionNodeBuilder = exp(node(translatedQualifier));
-            }
-        }
         return expressionNodeBuilder;
     }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
index bf89ceb..25c1687 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
@@ -87,6 +87,8 @@ public class TranslatorContext implements SQLGenerationContext {
     // List of SQL tree nodes that describe resulting rows of this query
     private final List<ResultNodeDescriptor> resultNodeList;
 
+    // resulting qualifier for this query ('where' qualifier and qualifiers from entities)
+    private Node qualifierNode;
     // if true SQL generation stage will be skipped, needed for nested queries translation
     private boolean skipSQLGeneration;
     // translated SQL string
@@ -256,6 +258,14 @@ public class TranslatorContext implements SQLGenerationContext {
         return rootEntityResult;
     }
 
+    void setQualifierNode(Node qualifierNode) {
+        this.qualifierNode = qualifierNode;
+    }
+
+    Node getQualifierNode() {
+        return qualifierNode;
+    }
+
     enum DescriptorType {
         ROOT,
         PREFETCH,
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslationStageTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslationStageTest.java
index 9b56c81..cc628f8 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslationStageTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslationStageTest.java
@@ -62,19 +62,15 @@ public class QualifierTranslationStageTest {
         QualifierTranslationStage stage = new QualifierTranslationStage();
         stage.perform(context);
 
-        Node select = context.getSelectBuilder().build();
+        assertNotNull(context.getQualifierNode());
 
-        // Content of "select" node:
+        // Content of "Qualifier" node:
         //
-        //      Where
-        //        |
         //   OpExpression
         //    /        \
         // Column     Value
 
-        assertEquals(1, select.getChildrenCount());
-        assertThat(select.getChild(0), instanceOf(WhereNode.class));
-        Node op = select.getChild(0).getChild(0);
+        Node op = context.getQualifierNode();
         assertThat(op, instanceOf(OpExpressionNode.class));
         assertEquals(">=", ((OpExpressionNode)op).getOp());
         assertEquals(2, op.getChildrenCount());