Improve duplicate select column detection when using order by
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 1dd7aa3..1c01a9d 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -102,5 +102,6 @@
 CAY-2841 Multi column ColumnSelect with SHARED_CACHE fails after 1st select
 CAY-2844 Joint prefetch doesn't use ObjEntity qualifier
 CAY-2842 Prevent duplicate select columns when using distinct with order by
+CAY-2847 Improve duplicate select column detection when using order by
 CAY-2850 Query using Clob comparison with empty String fails
 CAY-2851 Replace Existing OneToOne From New Object
\ No newline at end of file
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/OrderingAbstractStage.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/OrderingAbstractStage.java
index 101f779..4ef9060 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/OrderingAbstractStage.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/OrderingAbstractStage.java
@@ -21,20 +21,21 @@
 
 import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
 
-import java.util.function.Predicate;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.cayenne.access.sqlbuilder.NodeBuilder;
-import org.apache.cayenne.access.sqlbuilder.SQLGenerationVisitor;
-import org.apache.cayenne.access.sqlbuilder.StringBuilderAppendable;
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.SQLGenerationContext;
+import org.apache.cayenne.access.sqlbuilder.sqltree.AliasedNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
-import org.apache.cayenne.access.sqlbuilder.sqltree.NodeType;
-import org.apache.cayenne.access.sqlbuilder.sqltree.SelectResultNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.SimpleNodeTreeVisitor;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.parser.ASTAggregateFunctionCall;
-import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.query.Ordering;
 
 abstract class OrderingAbstractStage implements TranslationStage {
@@ -57,51 +58,154 @@
         }
     }
 
-    private DbAttribute getOrderDbAttribute(Node translatedOrderNode)
+    private boolean orderColumnAbsent(TranslatorContext context, NodeBuilder nodeBuilder) 
     {
-        DbAttribute[] orderDbAttribute = {null};
-        translatedOrderNode.visit(new SimpleNodeTreeVisitor() {
-            @Override
-            public boolean onNodeStart(Node node) {
-                if (node.getType() == NodeType.COLUMN) {
-                    orderDbAttribute[0] = ((ColumnNode) node).getAttribute();
-                    return false;
-                }
-                return true;
-            }
-        });
-        return orderDbAttribute[0];
-    }
-
-    private boolean orderColumnAbsent(TranslatorContext context, NodeBuilder nodeBuilder)
-    {
-        var orderDbAttribute = getOrderDbAttribute(nodeBuilder.build());
-        if (orderDbAttribute == null) return false; // Alias ?
-
-        var orderEntity = orderDbAttribute.getEntity().getName();
-        var orderColumn = orderDbAttribute.getName();
-
-        Predicate<DbAttribute> columnAndEntity = dba -> dba != null
-                                    && orderColumn.equals(dba.getName())
-                                    && orderEntity.equals(dba.getEntity().getName());
-
-        var orderStr = getSqlString(order(nodeBuilder));
+        OrderNodeVisitor orderVisitor = new OrderNodeVisitor();
+        nodeBuilder.build().visit( orderVisitor );
+        List<CharSequence> orderParts = orderVisitor.getParts();
 
         return context.getResultNodeList().stream()
-            .filter( result -> columnAndEntity.test(result.getDbAttribute()) )
-            .noneMatch( result -> getSqlString(node(result.getNode())).equals(orderStr) );
+            .noneMatch( result -> {
+                ResultNodeVisitor resultVisitor = new ResultNodeVisitor(orderParts);
+                // Visitor aborts as soon as there's a mismatch with orderParts
+                result.getNode().visit(resultVisitor);
+                return resultVisitor.matches();
+            });
     }
 
-    private String getSqlString(NodeBuilder nb) {
-        var node = nb.build();
-        if (node instanceof FunctionNode && ((FunctionNode) node).getAlias() != null)
-        {
-            // Wrap in result node otherwise content isn't generated, only alias
-            node = new SelectResultNode().addChild(node.deepCopy());
+    private class OrderNodeVisitor extends AppendableVisitor // see below
+    {
+        @Override
+        public boolean onNodeStart(Node node) {
+            node.append( this );
+            node.appendChildrenStart(this);
+            return true;
         }
-        var strBuilder = new StringBuilderAppendable();
-        var sqlVisitor = new SQLGenerationVisitor(strBuilder);
-        node.visit(sqlVisitor);
-        return strBuilder.append(' ').toString();
+
+        @Override
+        public void onChildNodeEnd(Node parent, Node child, int index, boolean hasMore) {
+            if (hasMore && parent != null) {
+                parent.appendChildrenSeparator(this, index);
+            }
+        }
+
+        @Override
+        public void onNodeEnd(Node node) {
+            node.appendChildrenEnd(this);
+        }
+
+        List<CharSequence> getParts() {
+            return Collections.unmodifiableList(partList);
+        }
+    }
+
+    private class ResultNodeVisitor extends AppendableVisitor // see below
+    {
+        private List<CharSequence> orderItemParts;
+        private boolean itemsMatch = true;
+        private int lastIndex = 0;
+
+        ResultNodeVisitor(List<CharSequence> orderParts) {
+            orderItemParts = orderParts;
+        }
+
+        @Override
+        public boolean onNodeStart(Node node) {
+            node.append(this);
+            if (node instanceof ColumnNode && ((ColumnNode) node).getAlias() != null) {
+                partList.removeLast(); // Remove appended alias
+            }
+            if (!(node.getParent() instanceof AliasedNode)) {
+                // Prevent appending opening bracket
+                node.appendChildrenStart(this);
+            }
+            return isEqualSoFar();
+        }
+
+        @Override
+        public void onChildNodeEnd(Node parent, Node child, int index, boolean hasMore) {
+            if (hasMore && parent != null) {
+                parent.appendChildrenSeparator(this, index);
+                isEqualSoFar();
+            }
+        }
+
+        @Override
+        public void onNodeEnd(Node node) {
+            // Prevent appending alias or closing bracket
+            if (!(node instanceof AliasedNode || node.getParent() instanceof AliasedNode)) {
+                node.appendChildrenEnd(this);
+                if (node instanceof FunctionNode && ((FunctionNode) node).getAlias() != null) {
+                    if (partList.getLast().equals(((FunctionNode) node).getAlias())) {
+                        partList.removeLast(); // Remove appended alias
+                    }
+                }
+                isEqualSoFar();
+            }
+        }
+
+        private boolean isEqualSoFar() {
+            int currentSize = partList.size();
+            if (currentSize == lastIndex) return itemsMatch;
+            if (currentSize > orderItemParts.size()) itemsMatch = false;
+            if (itemsMatch) {
+                // In reverse to fail fast by hopefully comparing column names first
+                for (int x = currentSize-1; x >= lastIndex; x--) {
+                    if (!partList.get(x).equals(orderItemParts.get(x))) {
+                        itemsMatch = false;
+                        break;
+                    }
+                }
+            }
+            lastIndex = partList.size();
+            return itemsMatch;
+        }
+
+        boolean matches() {
+            return isEqualSoFar();
+        }
+    }
+
+    private class AppendableVisitor extends SimpleNodeTreeVisitor implements QuotingAppendable
+    {
+        protected final LinkedList<CharSequence> partList = new LinkedList<>();
+
+        @Override
+        public QuotingAppendable append(CharSequence csq) {
+            partList.add(csq);
+            return this;
+        }
+
+        @Override
+        public QuotingAppendable append(CharSequence csq, int start, int end) {
+            return this;
+        }
+
+        @Override
+        public QuotingAppendable append(char c) {
+            if (c != '.' && c != ' ') partList.add(String.valueOf(c));
+            return this;
+        }
+
+        @Override
+        public QuotingAppendable append(int c) {
+            return this;
+        }
+
+        @Override
+        public QuotingAppendable appendQuoted(CharSequence csq) {
+            partList.add(csq);
+            return this;
+        }
+
+        @Override
+        public SQLGenerationContext getContext() {
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            return partList.stream().collect( Collectors.joining("|") );
+        }
     }
 }
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/translator/select/OrderingStageTest.java b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/OrderingStageTest.java
index 2d15f42..c360fd2 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/translator/select/OrderingStageTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/OrderingStageTest.java
@@ -115,7 +115,7 @@
         ColumnExtractorStage columnStage = new ColumnExtractorStage();
         columnStage.perform(context);
 
-        OrderingStage orderingStage = new OrderingStage();
+        OrderingDistictStage orderingStage = new OrderingDistictStage();
         orderingStage.perform(context);
 
         assertTrue(context.getQuery().isDistinct());