=== modified file 'src/com/goldencode/p2j/persist/AdaptiveQuery.java'
--- src/com/goldencode/p2j/persist/AdaptiveQuery.java	2020-09-24 17:51:18 +0000
+++ src/com/goldencode/p2j/persist/AdaptiveQuery.java	2020-10-01 13:56:04 +0000
@@ -4,7 +4,7 @@
 **
 ** Copyright (c) 2004-2019, Golden Code Development Corporation.
 **
-** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
+** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
 ** 001 ECF 20060721   @28133 Created initial version. Query which adapts
 **                           its retrieval type from preselect to dynamic,
 **                           based upon DMO state change notifications.
@@ -308,6 +308,7 @@
 ** 112 SVL 20191218          Handled case when REPOSITION TO ROWID fails.
 ** 113 ECF 20200215          New ORM implementation.
 ** 114 CA  20200924          Replaced DMO property access from reflection to property's data handler.
+**     OM  20201001          Dropped redundant ORDER BY elements in multi-table queries.
 */
 
 /*
@@ -3151,32 +3152,21 @@
    }
    
    /**
-    * Assemble the <code>order by</code> clause for an HQL query by combining
-    * the sort criteria from each query component sequentially.  Uses the
-    * form:
-    * <pre>
-    *   order by {{dmo.property} {asc/desc}} [, ...]
-    * </pre>
-    * <p>
-    * Any text-based sort criteria are augmented with the necessary upper()
-    * and rtrim() functions.
-    *
-    * @param   buf
-    *          String buffer into which clause is assembled.
-    *
-    * @return  List of <code>SortCriterion</code> objects which describe the
-    *          sort behavior for this query.
+    * Assemble the {@code order by} clause for an HQL query by combining the sort criteria from each query
+    * component sequentially.
+    * Any text-based sort criteria are augmented with the necessary upper() and rtrim() functions.
+    *
+    * @return  List of {@code SortCriterion} objects which describe the sort behavior for this query.
     *
     * @throws  PersistenceException
-    *          if the sort clause of any query component contains any
-    *          unqualified property names;  if a sort clause contains a
-    *          reference to an unrecognized record buffer.
+    *          if the sort clause of any query component contains any unqualified property names;  if a sort
+    *          clause contains a reference to an unrecognized record buffer.
     * 
     * @throws  PersistenceException
-    *          if there is an error creating the <code>HQLPreprocessor</code>
-    *          from which setup data is required.
+    *          if there is an error creating the {@code HQLPreprocessor} from which setup data is required.
     */
-   protected List<SortCriterion> assembleOrderByClause(StringBuilder buf)
+   @Override
+   protected List<SortCriterion> assembleOrderByClause()
    throws PersistenceException
    {
       List<SortCriterion> all = new ArrayList<>();
@@ -3202,16 +3192,7 @@
          List<SortCriterion> sortCriteria = next.getSortCriteria();
          all.addAll(sortCriteria);
          
-         if (i == 0)
-         {
-            buf.append("order by ");
-         }
-         else
-         {
-            buf.append(", ");
-         }
-         
-         assembleOrderByClauseNoPreamble(buf, sortCriteria);
+         assembleOrderByClause(sortCriteria);
       }
       
       return all;

=== modified file 'src/com/goldencode/p2j/persist/HQLPreprocessor.java'
--- src/com/goldencode/p2j/persist/HQLPreprocessor.java	2020-09-18 11:12:02 +0000
+++ src/com/goldencode/p2j/persist/HQLPreprocessor.java	2020-10-01 14:01:33 +0000
@@ -4,7 +4,7 @@
 **
 ** Copyright (c) 2004-2020, Golden Code Development Corporation.
 **
-** -#- -I- --Date-- --JPRM-- -----------------------------Description-----------------------------
+** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
 ** 001 ECF 20060317   @25230 Created initial version. Identifies certain
 **                           patterns in a where clause string, which
 **                           indicate unsupported HQL "shortcut" syntax,
@@ -247,6 +247,7 @@
 ** 086 CA  20191009          Added where and sort clause translation in case of bound buffers.
 ** 087 ECF 20200906          New ORM implementation.
 ** 088 AIL 20200831          Skipped parenthesis inside simplifyUnknowns.
+** 089 OM  20201001          Dropped redundant ORDER BY elements in multi-table queries.
 */
 
 /*
@@ -307,8 +308,10 @@
 import java.io.*;
 import java.lang.reflect.*;
 import java.util.*;
+import java.util.List;
 import java.util.function.*;
 import java.util.logging.*;
+import antlr.collections.*;
 import com.goldencode.ast.*;
 import com.goldencode.util.*;
 import com.goldencode.p2j.persist.dialect.*;
@@ -605,6 +608,11 @@
    private Map<String, RecordBuffer> bufferMap = null;
    
    /**
+    * The substitution pairs. They are optionally computed in {@code preprocess} if {@code replacementAliases}
+    * are present.*/
+   private List<PropertyPair> substPairs = null;
+   
+   /**
     * Factory method which accepts an unprocessed where clause, the database
     * instance and the database dialect associated with the enclosing query,
     * the DMO implementation class, and the substitution parameters, if any,
@@ -1124,6 +1132,16 @@
    }
    
    /**
+    * Obtain the substitution pair list, if any.
+    * 
+    * @return  the substitution pair list or {@code null} if none was detected.
+    */
+   public List<PropertyPair> getSubstPairs()
+   {
+      return substPairs;
+   }
+   
+   /**
     * Check if the preprocessed query is a simple RECID/ROWID lookup query.
     * <p>
     * Generally, we can easily find such records using persistent database or dirty database for
@@ -1388,7 +1406,7 @@
       {
          if (referenceSubs != null)
          {
-            inlineReferenceSubs(root, referenceSubs);
+            substPairs = inlineReferenceSubs(root, referenceSubs);
          }
          
          // Annotate query substitution parameter nodes with their data types.
@@ -1941,23 +1959,25 @@
    }
    
    /**
-    * Given an array containing one or more strings and zero or more nulls, replace those query
-    * substitution parameters in the where clause AST which positionally coincide with the
-    * non-null strings in the array. Each non-null string in the array represents a field
-    * reference to a specific DMO property. The SUBST AST node whose index coincides with that of
-    * a field reference in the array is replaced with a an ALIAS node which has a PROPERTY child.
-    * If the field reference represents an extent field element, the PROPERTY node has an
-    * LBRACKET child and an INDEX grandchild.
+    * Given an array containing one or more strings and zero or more nulls, replace those query substitution
+    * parameters in the where clause AST which positionally coincide with the non-null strings in the array.
+    * Each non-null string in the array represents a field reference to a specific DMO property. The SUBST
+    * AST node whose index coincides with that of a field reference in the array is replaced with a an ALIAS
+    * node which has a PROPERTY child. If the field reference represents an extent field element, the PROPERTY
+    * node has an LBRACKET child and an INDEX grandchild.
     * 
     * @param   root
     *          HQL AST root node.
     * @param   referenceSubs
     *          Array of field references and nulls.
+    *
+    * @return  A list of pairs of properties (belonging to different tables) whose equality is guaranteed by
+    *          the where predicate. These will be used to simplify the sort order when the SQL query is
+    *          composed.
     */
-   private void inlineReferenceSubs(HQLAst root, String[] referenceSubs)
+   private List<PropertyPair> inlineReferenceSubs(HQLAst root, String[] referenceSubs)
    {
       int len = referenceSubs.length;
-      
       Map<HQLAst, String> replacements = new HashMap<>();
       
       Iterator<Aast> iter = root.iterator();
@@ -1988,6 +2008,12 @@
          }
       }
       
+      if (replacements.isEmpty())
+      {
+         return null;
+      }
+      
+      List<PropertyPair> ret = new ArrayList<>(replacements.size());
       for (Map.Entry<HQLAst, String> next : replacements.entrySet())
       {
          HQLAst aliasAst = next.getKey();
@@ -2025,9 +2051,9 @@
          }
          
          // re-parent ALIAS with upper function if needed
+         HQLAst parent = (HQLAst) aliasAst.getParent();
          if (upper)
          {
-            HQLAst parent = (HQLAst) aliasAst.getParent();
             int idxPos = aliasAst.getIndexPos();
             
             HQLAst upperAst = createAstNode(FUNCTION, "upper", null);
@@ -2035,8 +2061,51 @@
             aliasAst.remove();
             parent.graftAt(upperAst, idxPos);
             upperAst.graft(aliasAst);
+            aliasAst = upperAst; 
+         }
+         
+         if (parent == null)
+         {
+            return null;
+         }
+         
+         // check if all the parent's are AND (and parenthesis) nodes
+         Aast it = parent.getParent();
+         while (it != null)
+         {
+            if (it.getType() == AND || it.getType() == LPARENS)
+            {
+               it = it.getParent();
+            }
+            else
+            {
+               break;
+            }
+         }
+         
+         // if [it] isn't null then it represents the node different from AND and LPARENS in the parent's path
+         if (it == null)
+         {
+            // analyze the left side of the operator
+            AST refNode = parent.getFirstChild();
+            if (refNode == aliasAst)
+            {
+               // handle [A op B] and [B op A] cases
+               refNode = refNode.getNextSibling();
+            }
+            while (refNode.getType() == FUNCTION && 
+                   (refNode.getText().equals("upper") || refNode.getText().equals("rtrim")))
+            {
+               refNode = refNode.getFirstChild();
+            }
+            // TODO: need to check whether refNode.type == ALIAS and refNode.firstChild.type == PROPERTY ? 
+            
+            ret.add(new PropertyPair(
+                  refNode.getText(), refNode.getFirstChild().getText(), alias, property, parent.getText()));
          }
       }
+      
+      return ret;
    }
    
    /**
@@ -5985,8 +6054,73 @@
          result = 37 * result + dropAlias.hashCode();
          result = 37 * result + replacementAlias.hashCode();
          result = 37 * result + (informational ? 0 : 1);
-      
+         
          return result;
       }
    }
+   
+   /** A public container for storing a pair of properties. It is immutable, used for transfering data. */
+   public static final class PropertyPair
+   {
+      /** Operator used for joining the properties. */
+      private final String operator;
+      
+      /** The full name of the first (left) property. */
+      private final String name1;
+      
+      /** The full name of the second (right) property. */
+      private final String name2;
+      
+      /**
+       *  The constructor initializes all the fields.
+       * 
+       * @param   alias1
+       *          The alias (table) of the first (left) property.
+       * @param   property1
+       *          The name of the property from first (left) table.
+       * @param   alias2
+       *          The alias (table) of the second (right) property.
+       * @param   property2
+       *          The name of the property from second (right) table.
+       * @param   operator
+       *          The operator which creates a relations between the two fields. We are interested only in
+       *          "=" operator, but this leave room for other usages of this class.
+       */
+      public PropertyPair(String alias1, String property1, String alias2, String property2, String operator)
+      {
+         this.operator = operator;
+         this.name1 = alias1 + "." + property1;
+         this.name2 = alias2 + "." + property2;
+      }
+      
+      /**
+       * Obtain the full name of the first (left) property.
+       *
+       * @return  the full name of the first (left) property.
+       */
+      public String getLeft()
+      {
+         return name1;
+      }
+      
+      /**
+       * Obtain the full name of the second (right) property.
+       *
+       * @return  the full name of the second (right) property.
+       */
+      public String getRight()
+      {
+         return name2;
+      }
+      
+      /**
+       * Obtain the operator used for joining the properties..
+       *
+       * @return the operator used for joining the properties.
+       */
+      public String getOperator()
+      {
+         return operator;
+      }
+   }
 }

=== modified file 'src/com/goldencode/p2j/persist/PreselectQuery.java'
--- src/com/goldencode/p2j/persist/PreselectQuery.java	2020-09-19 22:17:50 +0000
+++ src/com/goldencode/p2j/persist/PreselectQuery.java	2020-10-01 14:15:33 +0000
@@ -4,7 +4,7 @@
 **
 ** Copyright (c) 2004-2020, Golden Code Development Corporation.
 **
-** -#- -I- --Date-- --JPRM-- --------------------------Description-------------------------------
+** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
 ** 001 ECF 20051115   @23396 Created initial version, based on prototype
 **                           CursoredQuery. Backing support for queries
 **                           which must support the Progress, row-level
@@ -560,6 +560,7 @@
 ** 176 AIL 20200908          Reset offSet flag when adding new row through createResultListEntry.
 **     AIL 20200910          Restrict resetting offSet flag only for browsed queries.
 **     ECF 20200919          Persistence.load API signature change.
+**     OM  20201001          Dropped redundant ORDER BY elements in multi-table queries.
 */
 
 /*
@@ -4409,27 +4410,25 @@
    }
    
    /**
-    * Assemble the from clause for an HQL query. The class name of each
-    * DMO is decapitalized to produce an alias for that object. Form of
-    * the clause is:
+    * Assemble the from clause for an HQL query. The class name of each DMO is decapitalized to produce an
+    * alias for that object. Form of the clause is:
     * <pre>
     *    from {Buffer0} as {buffer0} [, {Buffer1} as {buffer1} [, ...]]
     * </pre>
     * <p>
     * <b>Known Limitation:</b>
-    * At this time, only theta-style joins are supported and as such, only
-    * inner joins are supported (there is no way to specify an outer join
-    * using theta-join syntax in HQL).
+    * At this time, only theta-style joins are supported and as such, only inner joins are supported (there
+    * is no way to specify an outer join using theta-join syntax in HQL).
     *
     * @param   buf
     *          String buffer into which clause is assembled.
     * @param   sortCriteria
-    *          List of <code>SortCriterion</code> objects which describe the
-    *          sort behavior for this query.
+    *          All known sort criteria. If a JOIN term is detected in the preprocessed query this list is
+    *          simplified to allow a cleaner ORDER BY clause and let the query planner chose a better plan.
     * 
     * @throws  PersistenceException
-    *          if there is an error creating the <code>HQLPreprocessor</code>
-    *          from which the composite join data are gathered.
+    *          if there is an error creating the {@code HQLPreprocessor} from which the composite join data
+    *          are gathered.
     */
    protected void assembleFromClause(StringBuilder buf, List<SortCriterion> sortCriteria)
    throws PersistenceException
@@ -4475,6 +4474,68 @@
             buf.append(" ");
             joins.add(aJoin);
          }
+         
+         // now simplify the [sortCriteria] is a server-side JOIN was detected while preprocessing the hql
+         List<HQLPreprocessor.PropertyPair> joinPairs = comp.getHQLPreprocessor().getSubstPairs();
+         if (joinPairs != null)
+         {
+            int delCount = 0;
+            SortCriterion[] criteria = new SortCriterion[sortCriteria.size()]; 
+            sortCriteria.toArray(criteria);
+            
+            for (int i = 0; i < criteria.length; i++)
+            {
+               if (criteria[i] == null)
+               {
+                  continue; // already dropped
+               }
+               
+               for (HQLPreprocessor.PropertyPair pair : joinPairs)
+               {
+                  if (!pair.getOperator().equals("="))
+                  {
+                     continue; // ignore properties not joined with = operator 
+                  }
+                  
+                  if (criteria[i].getOriginalName().equals(pair.getLeft()))
+                  {
+                     for (int j = i + 1; j < criteria.length; j++)
+                     {
+                        if (criteria[j].getOriginalName().equals(pair.getRight()))
+                        {
+                           criteria[j] = null;
+                           ++ delCount;
+                           break; //j
+                        }
+                     }
+                  }
+                  else if (criteria[i].getOriginalName().equals(pair.getRight()))
+                  {
+                     for (int j = i + 1; j < criteria.length; j++)
+                     {
+                        if (criteria[j].getOriginalName().equals(pair.getLeft()))
+                        {
+                           criteria[j] = null;
+                           ++ delCount;
+                           break; //j
+                        }
+                     }
+                  }
+               }
+            }
+            
+            // check the remaining criteria
+            if (delCount != 0)
+            {
+               for (int i = sortCriteria.size() - 1; i >= 0; i--)
+               {
+                  if (criteria[i] == null)
+                  {
+                     sortCriteria.remove(i);
+                  }
+               }
+            }
+         }
       }
    }
    
@@ -4612,27 +4673,17 @@
    }
    
    /**
-    * Assemble the <code>order by</code> clause for an HQL query.  Uses the
-    * form:
-    * <pre>
-    *   order by {{dmo.property} {asc/desc}} [, ...]
-    * </pre>
+    * Assemble the {@code order by} clause for an HQL query.
     * <p>
-    * Any text-based sort criteria are augmented with the necessary upper()
-    * and rtrim() functions.
-    *
-    * @param   buf
-    *          String buffer into which clause is assembled.
-    *
-    * @return  List of <code>SortCriterion</code> objects which describe the
-    *          sort behavior for this query.
+    * Any text-based sort criteria are augmented with the necessary upper() and rtrim() functions.
+    *
+    * @return  List of {@code SortCriterion} objects which describe the sort behavior for this query.
     *
     * @throws  PersistenceException
-    *          if the raw sort clause provided at construction contains any
-    *          unqualified property names;  if the raw sort clause contains
-    *          a reference to an unrecognized record buffer.
+    *          if the raw sort clause provided at construction contains any unqualified property names;
+    *          if the raw sort clause contains a reference to an unrecognized record buffer.
     */
-   protected List<SortCriterion> assembleOrderByClause(StringBuilder buf)
+   protected List<SortCriterion> assembleOrderByClause()
    throws PersistenceException
    {
       if (sort == null && dynamicSorts == null)
@@ -4641,7 +4692,6 @@
       }
       
       List<SortCriterion> ret = new ArrayList<>();
-      buf.append("order by ");
       
       if (dynamicSorts != null)
       {
@@ -4654,58 +4704,74 @@
                dynSort.append(", ");
             }
          }
-         ret.addAll(assembleOrderByClauseNoPreamble(buf, dynSort.toString()));
+         ret.addAll(assembleOrderByClause(dynSort.toString()));
          
          if (sort == null)
          {
             return ret;
          }
-         else
-         {
-            buf.append(", ");
-         }
       }
       
-      ret.addAll(assembleOrderByClauseNoPreamble(buf, sort));
+      ret.addAll(assembleOrderByClause(sort));
       return ret;
    }
    
    /**
-    * Assemble the <code>order by</code> clause for an HQL query, sans the
-    * &quot;order by&quot; preamble.  Uses the
-    * form:
+    * Generates the {@code order by} clause starting from the list of criteria, including the "order by"
+    * keyword. Uses the form:
     * <pre>
-    *   {{dmo.property} {asc/desc}} [, ...]
+    *   order by {{dmo.property} {asc/desc}} [, ...]
     * </pre>
     * <p>
-    * Any text-based sort criteria are augmented with the necessary upper()
-    * and rtrim() functions.
     *
+    * @param   sortCriteria
+    *          The list of criteria to be added.
     * @param   buf
-    *          String buffer into which clause is assembled.
+    *          The string builder to add the result.
+    */
+   private void generateOrderBy(List<SortCriterion> sortCriteria, StringBuilder buf)
+   {
+      boolean orderBy = true;
+      for (SortCriterion criterion : sortCriteria)
+      {
+         if (orderBy)
+         {
+            buf.append(" order by ");
+            orderBy = false;
+         }
+         else
+         {
+            buf.append(", ");
+         }
+         buf.append(criterion.toString());
+      }
+   }
+   
+   /**
+    * Assemble the {@code order by} clause for an HQL query.
+    * <p>
+    * Any text-based sort criteria are augmented with the necessary upper() and rtrim() functions.
+    *
     * @param   sort
     *          Raw sort clause to parse and augment if necessary.
     *
-    * @return  List of <code>SortCriterion</code> objects which describe the
-    *          sort behavior for this query.
+    * @return  List of {@code SortCriterion} objects which describe the sort behavior for this query.
     *
     * @throws  PersistenceException
-    *          if <code>sort</code> contains any unqualified property names;
-    *          if <code>sort</code> clause contains a reference to an
-    *          unrecognized record buffer.
+    *          if {@code sort} contains any unqualified property names;
+    *          if {@code sort} clause contains a reference to an unrecognized record buffer.
     */
-   protected List<SortCriterion> assembleOrderByClauseNoPreamble(StringBuilder buf, String sort)
+   protected List<SortCriterion> assembleOrderByClause(String sort)
    throws PersistenceException
    {
       List<SortCriterion> sortCriteria = buildSortCriteria(sort);
       
-      // Assemble the order by clause, given the list of SortCriterion objects.
-      return assembleOrderByClauseNoPreamble(buf, sortCriteria);
+      // assemble the order by clause, given the list of SortCriterion objects.
+      return assembleOrderByClause(sortCriteria);
    }
    
    /**
-    * Returns a list <code>SortCriterion</code> objects build from the given
-    * sort clause.
+    * Returns a list <code>SortCriterion</code> objects build from the given sort clause.
     *
     * @param   sort
     *          Raw sort clause to parse and augment if necessary.
@@ -4721,8 +4787,7 @@
    protected List<SortCriterion> buildSortCriteria(String sort)
    throws PersistenceException
    {
-      // Map record buffers used in the sort clause to their identifying
-      // aliases.
+      // map record buffers used in the sort clause to their identifying aliases
       Map<String, RecordBuffer> aliases = new HashMap<>();
       Iterator<QueryComponent> iter = components();
       while (iter.hasNext())
@@ -4797,31 +4862,20 @@
    }
    
    /**
-    * Assemble the <code>order by</code> clause for an HQL query, sans the
-    * &quot;order by&quot; preamble.  Uses the
-    * form:
-    * <pre>
-    *   {{dmo.property} {asc/desc}} [, ...]
-    * </pre>
+    * Assemble the {@code order by} clause for an HQL query.
     * <p>
-    * Any text-based sort criteria are augmented with the necessary upper()
-    * and rtrim() functions.
+    * Any text-based sort criteria are augmented with the necessary upper() and rtrim() functions.
     *
-    * @param   buf
-    *          String buffer into which clause is assembled.
     * @param   sortCriteria
-    *          A list of <code>SortCriterion</code> objects which describe
-    *          the sorting to be applied.
+    *          A list of {@code SortCriterion} objects which describe the sorting to be applied.
     *
-    * @return  List of <code>SortCriterion</code> objects which describe the
-    *          sort behavior for this query.
+    * @return  List of {@code SortCriterion} objects which describe the sort behavior for this query.
     */
-   protected List<SortCriterion> assembleOrderByClauseNoPreamble(StringBuilder buf,
-                                                                 List<SortCriterion> sortCriteria)
+   protected List<SortCriterion> assembleOrderByClause(List<SortCriterion> sortCriteria)
    {
       RecordBuffer buffer = getRecordBuffers()[0];
       boolean multiplex = (buffer.getMultiplexID() != null);
-      List<SortCriterion> workList = null;
+      List<SortCriterion> workList;
       
       if (sortCriteria == null)
       {
@@ -4834,32 +4888,20 @@
             return null;
          }
       }
-      
-      if (workList == null)
+      else
       {
          workList = new ArrayList<>(sortCriteria.size() + 1);
       }
       
-      workList.addAll(sortCriteria);
-      
       if (multiplex)
       {
-//         String schema = buffer.getSchema();
          Class<? extends DataModelObject> dmoIface = buffer.getDMOInterface();
          Class<? extends Record> dmoClass = buffer.getDMOImplementationClass();
+         String text = buffer.getDMOAlias() + "." + TemporaryBuffer.MULTIPLEX_FIELD_NAME + " asc";
          
-         StringBuilder sb = new StringBuilder();
-         sb.append(buffer.getDMOAlias());
-         sb.append(".");
-         sb.append(TemporaryBuffer.MULTIPLEX_FIELD_NAME);
-         sb.append(" asc");
          try
          {
-            SortCriterion multiCrit = new SortCriterion(buffer,
-                                                        sb.toString(),
-                                                        dmoIface,
-                                                        dmoClass);
-            workList.add(0, multiCrit);
+            workList.add(new SortCriterion(buffer, text, dmoIface, dmoClass));
          }
          catch (PersistenceException exc)
          {
@@ -4867,20 +4909,12 @@
          }
       }
       
-      Iterator<SortCriterion> iter = workList.iterator();
-      for (int i = 0; iter.hasNext(); i++)
+      if (sortCriteria != null)
       {
-         SortCriterion crit = iter.next();
-         
-         if (i > 0)
-         {
-            buf.append(", ");
-         }
-         
-         buf.append(crit.toSortExpression(false));
+         workList.addAll(sortCriteria);
       }
       
-      return sortCriteria;
+      return workList;
    }
    
    /**
@@ -6331,18 +6365,15 @@
    private String assembleHQL()
    throws PersistenceException
    {
+      // need to put together order by clause first, as the sort criteria determined in this analysis may be
+      // needed when composing from clause and where clause
+      List<SortCriterion> sortCriteria = assembleOrderByClause();
+      
       StringBuilder buf = new StringBuilder();
-      
-      // Need to put together order by clause first, as the sort criteria
-      // determined in this analysis may be needed when composing from clause
-      // and where clause.
-      List<SortCriterion> sortCriteria = assembleOrderByClause(buf);
-      String orderBy = buf.toString();
-      buf.setLength(0);
       assembleSelectClause(buf);
       assembleFromClause(buf, sortCriteria);
       buf.append(assembleWhereClause(sortCriteria).toFinalExpression());
-      buf.append(orderBy);
+      generateOrderBy(sortCriteria, buf);
       
       return buf.toString().trim();
    }

=== modified file 'src/com/goldencode/p2j/persist/PresortQuery.java'
--- src/com/goldencode/p2j/persist/PresortQuery.java	2020-09-06 23:15:41 +0000
+++ src/com/goldencode/p2j/persist/PresortQuery.java	2020-10-01 13:48:40 +0000
@@ -5,7 +5,7 @@
 **
 ** Copyright (c) 2004-2020, Golden Code Development Corporation.
 **
-** -#- -I- --Date-- --JPRM-- --------------------------Description---------------------------
+** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
 ** 001 ECF 20060103   @23795 Created initial version. A specialized
 **                           preselect query which uses client-side record
 **                           sorting to presort query results before loop
@@ -88,6 +88,7 @@
 ** 030 OM  20180515          Added dynamic filters and dynamic sort.
 ** 031 ECF 20181106          Added runtime support for {FIRST|LAST}-OF methods.
 ** 032 ECF 20200906          New ORM implementation.
+** 033 OM  20201001          Dropped redundant ORDER BY elements in multi-table queries.
 */
 
 /*
@@ -1473,31 +1474,19 @@
       
       buf.append(" ");
    }
-
+   
    /**
-    * Assemble the <code>order by</code> clause for an HQL query.  Uses the
-    * form (ordered only by primary keys):
-    * <pre>
-    *   order by {dmo.id asc} [, ...]
-    * </pre>
-    * <p>
-    *
-    * @param   buf
-    *          String buffer into which clause is assembled.
-    *
-    * @return  List of <code>SortCriterion</code> objects which describe the
-    *          sort behavior for this query.
+    * Assemble the {@code order by} clause for an HQL query.
+    *
+    * @return  List of <code>SortCriterion</code> objects which describe the sort behavior for this query.
     *
     * @throws  PersistenceException
     *          never.
     */
-   protected List<SortCriterion> assembleOrderByClause(StringBuilder buf)
+   protected List<SortCriterion> assembleOrderByClause()
    throws PersistenceException
    {
-      buf.append("order by ");
-      
-      List<SortCriterion> sortCriteria =
-         (sort == null) ? new ArrayList<>() : super.buildSortCriteria(sort);
+      List<SortCriterion> sortCriteria = (sort == null) ? new ArrayList<>() : super.buildSortCriteria(sort);
       
       Iterator<QueryComponent> iter = components();
       while (iter.hasNext())
@@ -1514,6 +1503,6 @@
          sortCriteria.add(crit);
       }
       
-      return assembleOrderByClauseNoPreamble(buf, sortCriteria);
+      return assembleOrderByClause(sortCriteria);
    }
 }

=== modified file 'src/com/goldencode/p2j/persist/SortCriterion.java'
--- src/com/goldencode/p2j/persist/SortCriterion.java	2020-09-06 23:15:41 +0000
+++ src/com/goldencode/p2j/persist/SortCriterion.java	2020-10-01 13:53:57 +0000
@@ -4,7 +4,7 @@
 **
 ** Copyright (c) 2004-2020, Golden Code Development Corporation.
 **
-** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------
+** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
 ** 001 ECF 20060802   @28346 Created initial version. Based upon an inner
 **                           class previously within HQLHelper.
 ** 002 ECF 20060831   @29089 Added support for sorting by an indexed
@@ -88,6 +88,7 @@
 **     CA  20200306          Changed H033 to rely on 'WHERE_WITH_NULLS' system property, to 
 **                           activate the 'withNull in toWhereExpression.
 ** 035 ECF 20200906          New ORM implementation.
+** 036 OM  20201001          Dropped redundant ORDER BY elements in multi-table queries.
 */
 
 /*
@@ -270,8 +271,6 @@
     *          Record buffer.
     * @param   text
     *          Text representation of sort component, as provided by query.
-//    * @param   schema
-//    *          Name of database schema.
     * @param   dmoIface
     *          DMO interface.
     * @param   dmoClass
@@ -283,12 +282,11 @@
     */
    SortCriterion(RecordBuffer buffer,
                  String text,
-                /* String schema,*/
                  Class<? extends DataModelObject> dmoIface,
                  Class<? extends Record> dmoClass)
    throws PersistenceException
    {
-      this(buffer.getDialect(), buffer.getDatabase(), text, /*schema,*/ dmoIface, dmoClass, false);
+      this(buffer.getDialect(), buffer.getDatabase(), text, dmoIface, dmoClass, false);
    }
    
    /**
@@ -310,8 +308,6 @@
     *          The target database.
     * @param   text
     *          Text representation of sort component, as provided by query.
-//    * @param   schema
-//    *          Name of database schema.
     * @param   dmoIface
     *          DMO interface.
     * @param   dmoClass
@@ -324,12 +320,11 @@
    SortCriterion(Dialect dialect,
                  Database database,
                  String text,
-                 /*String schema,*/
                  Class<? extends DataModelObject> dmoIface,
                  Class<? extends Record> dmoClass)
    throws PersistenceException
    {
-      this(dialect, database, text, /*schema,*/ dmoIface, dmoClass, true);
+      this(dialect, database, text, dmoIface, dmoClass, true);
    }
    
    /**
@@ -346,8 +341,6 @@
     *          The target database.
     * @param   text
     *          Text representation of sort component, as provided by query.
-//    * @param   schema
-//    *          Name of database schema.
     * @param   dmoIface
     *          DMO interface.
     * @param   dmoClass
@@ -362,7 +355,6 @@
    SortCriterion(Dialect dialect,
                  Database database,
                  String text,
-                 /*String schema,*/
                  Class<? extends DataModelObject> dmoIface,
                  Class<? extends Record> dmoClass,
                  boolean internStrings)

