Cannot set FOR UPDATE in conjunction with ORDER BY and / or LIMIT (OracleDB & Derby) - bugfix + unit test (#1390)


Signed-off-by: Radek Felcman <radek.felcman@oracle.com>
diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/expressions/SQLSelectStatement.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/expressions/SQLSelectStatement.java
index 52448a5..b4edcfd 100644
--- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/expressions/SQLSelectStatement.java
+++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/expressions/SQLSelectStatement.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -1725,69 +1725,102 @@
     public Vector<DatabaseField> printSQL(ExpressionSQLPrinter printer) {
         try {
             Vector<DatabaseField> selectFields = null;
-            printer.setRequiresDistinct(shouldDistinctBeUsed());
-
-            if (hasUnionExpressions()) {
-                // Ensure union order using brackets.
-                int size = getUnionExpressions().size();
-                for (int index = 0; index < size; index++) {
-                    printer.printString("(");
-                }
-            }
-            printer.printString("SELECT ");
-
-            if (getHintString() != null) {
-                printer.printString(getHintString());
-                printer.printString(" ");
-            }
-
-            if (shouldDistinctBeUsed()) {
-                printer.printString("DISTINCT ");
-            }
-
-            selectFields = writeFieldsIn(printer);
-            //fix bug:6070214: turn off unique field aliases after fields are written
-            setUseUniqueFieldAliases(false);
-
-            appendFromClauseToWriter(printer);
-
-            if (!(getWhereClause() == null)) {
-                printer.printString(" WHERE ");
-                printer.printExpression(getWhereClause());
-            }
-
-            if (hasHierarchicalQueryExpressions()) {
-                appendHierarchicalQueryClauseToWriter(printer);
-            }
-
-            if (hasGroupByExpressions()) {
-                appendGroupByClauseToWriter(printer);
-            }
-            if (hasHavingExpression()) {
-                //appendHavingClauseToWriter(printer);
-                printer.printString(" HAVING ");
-                printer.printExpression(getHavingExpression());
-            }
-
-            if (hasOrderByExpressions()) {
-                appendOrderClauseToWriter(printer);
-            }
-
-            if(printer.getPlatform().shouldPrintLockingClauseAfterWhereClause() && printer.getPlatform().shouldPrintForUpdateClause()) {
-                // For pessimistic locking.
-                appendForUpdateClause(printer);
-            }
-
-            if (hasUnionExpressions()) {
-                appendUnionClauseToWriter(printer);
-            }
-
+            selectFields = printSQLSelect(printer);
+            printSQLWhereKeyWord(printer);
+            printSQLWhereClause(printer);
+            printSQLHierarchicalQueryClause(printer);
+            printSQLGroupByClause(printer);
+            printSQLHavingClause(printer);
+            printSQLOrderByClause(printer);
+            printSQLForUpdateClause(printer);
+            printSQLUnionClause(printer);
             return selectFields;
         } catch (IOException exception) {
             throw ValidationException.fileError(exception);
         }
     }
 
+    public Vector<DatabaseField> printSQLSelect(ExpressionSQLPrinter printer) throws IOException {
+        Vector<DatabaseField> selectFields = null;
+        printer.setRequiresDistinct(shouldDistinctBeUsed());
+
+        if (hasUnionExpressions()) {
+            // Ensure union order using brackets.
+            int size = getUnionExpressions().size();
+            for (int index = 0; index < size; index++) {
+                printer.printString("(");
+            }
+        }
+        printer.printString("SELECT ");
+
+        if (getHintString() != null) {
+            printer.printString(getHintString());
+            printer.printString(" ");
+        }
+
+        if (shouldDistinctBeUsed()) {
+            printer.printString("DISTINCT ");
+        }
+
+        selectFields = writeFieldsIn(printer);
+        //fix bug:6070214: turn off unique field aliases after fields are written
+        setUseUniqueFieldAliases(false);
+
+        appendFromClauseToWriter(printer);
+        return selectFields;
+    }
+
+    public void printSQLWhereKeyWord(ExpressionSQLPrinter printer) throws IOException {
+        if (!(getWhereClause() == null)) {
+            printer.printString(" WHERE ");
+        }
+    }
+
+    public void printSQLWhereClause(ExpressionSQLPrinter printer) throws IOException {
+        if (!(getWhereClause() == null)) {
+            printer.printExpression(getWhereClause());
+        }
+    }
+
+    public void printSQLHierarchicalQueryClause(ExpressionSQLPrinter printer) throws IOException {
+        if (hasHierarchicalQueryExpressions()) {
+            appendHierarchicalQueryClauseToWriter(printer);
+        }
+    }
+
+    public void printSQLGroupByClause(ExpressionSQLPrinter printer) throws IOException {
+        if (hasGroupByExpressions()) {
+            appendGroupByClauseToWriter(printer);
+        }
+    }
+
+    public void printSQLHavingClause(ExpressionSQLPrinter printer) throws IOException {
+        if (hasHavingExpression()) {
+            //appendHavingClauseToWriter(printer);
+            printer.printString(" HAVING ");
+            printer.printExpression(getHavingExpression());
+        }
+    }
+
+    public void printSQLOrderByClause(ExpressionSQLPrinter printer) throws IOException {
+        if (hasOrderByExpressions()) {
+            appendOrderClauseToWriter(printer);
+        }
+    }
+
+    public void printSQLForUpdateClause(ExpressionSQLPrinter printer) throws IOException {
+        if(printer.getPlatform().shouldPrintLockingClauseAfterWhereClause() && printer.getPlatform().shouldPrintForUpdateClause()) {
+            // For pessimistic locking.
+            appendForUpdateClause(printer);
+        }
+    }
+
+    public void printSQLUnionClause(ExpressionSQLPrinter printer) throws IOException {
+        if (hasUnionExpressions()) {
+            appendUnionClauseToWriter(printer);
+        }
+    }
+
     /**
      * Rebuild the expressions with the correct expression builder if using a different one.
      */
diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/platform/database/OraclePlatform.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/platform/database/OraclePlatform.java
index 3924c14..73c55fc 100644
--- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/platform/database/OraclePlatform.java
+++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/platform/database/OraclePlatform.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved.
  * Copyright (c) 1998, 2021 IBM Corporation. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -952,13 +952,15 @@
     protected String MIN_ROW = ") WHERE rnum > ";
     // Bug #453208
     protected String LOCK_START_PREFIX = " AND (";
+    protected String LOCK_START_PREFIX_WHERE = " WHERE (";
     protected String LOCK_START_SUFFIX = ") IN (";
-    protected String LOCK_END = ") FOR UPDATE";
+    protected String LOCK_END = " FOR UPDATE";
     protected String SELECT_ID_PREFIX = "SELECT ";
     protected String SELECT_ID_SUFFIX = " FROM (SELECT ";
     protected String FROM_ID = ", ROWNUM rnum  FROM (";
     protected String END_FROM_ID = ") ";
     protected String ORDER_BY_ID = " ORDER BY ";
+    protected String BRACKET_END = " ) ";
 
     /**
      * INTERNAL:
@@ -989,7 +991,7 @@
                     // Workaround can exist for this specific case
                     Vector fields = new Vector();
                     statement.enableFieldAliasesCaching();
-                    String queryString = printOmittingForUpdate(statement, printer, fields);
+                    String queryString = printOmittingOrderByForUpdateUnion(statement, printer, fields);
                     duplicateCallParameters(call);
                     call.setFields(fields);
 
@@ -1024,6 +1026,13 @@
                     printer.printParameter(DatabaseCall.MAXROW_FIELD);
                     printer.printString(MIN_ROW);
                     printer.printParameter(DatabaseCall.FIRSTRESULT_FIELD);
+                    printer.printString(BRACKET_END);
+                    try {
+                        statement.printSQLOrderByClause(printer);
+                        statement.printSQLUnionClause(printer);
+                    } catch (IOException exception) {
+                        throw ValidationException.fileError(exception);
+                    }
                     printer.printString(LOCK_END);
                 } else {
                     throw new UnsupportedOperationException(ExceptionLocalization.buildMessage("ora_pessimistic_locking_with_rownum"));
@@ -1067,14 +1076,25 @@
     }
 
     @SuppressWarnings("unchecked")
-    private String printOmittingForUpdate(SQLSelectStatement statement, ExpressionSQLPrinter printer, Vector fields) {
+    private String printOmittingOrderByForUpdateUnion(SQLSelectStatement statement, ExpressionSQLPrinter printer, Vector fields) {
         boolean originalShouldPrintForUpdate = this.shouldPrintForUpdateClause;
         Writer originalWriter = printer.getWriter();
+        Vector selectFields = null;
 
         this.shouldPrintForUpdateClause = false;
         printer.setWriter(new StringWriter());
 
-        fields.addAll(statement.printSQL(printer));
+        try {
+            selectFields = statement.printSQLSelect(printer);
+            statement.printSQLWhereKeyWord(printer);
+            statement.printSQLWhereClause(printer);
+            statement.printSQLHierarchicalQueryClause(printer);
+            statement.printSQLGroupByClause(printer);
+            statement.printSQLHavingClause(printer);
+        } catch (IOException exception) {
+            throw ValidationException.fileError(exception);
+        }
+        fields.addAll(selectFields);
         String query = printer.getWriter().toString();
 
         this.shouldPrintForUpdateClause = originalShouldPrintForUpdate;
@@ -1084,7 +1104,11 @@
     }
 
     private void printLockStartWithPrimaryKeyFields(SQLSelectStatement statement, ExpressionSQLPrinter printer) {
-        printer.printString(LOCK_START_PREFIX);
+        if (statement.getWhereClause() == null) {
+            printer.printString(LOCK_START_PREFIX_WHERE);
+        } else {
+            printer.printString(LOCK_START_PREFIX);
+        }
 
         Iterator<DatabaseField> iterator = statement.getQuery().getDescriptor().getPrimaryKeyFields().iterator();
         while (iterator.hasNext()) {
diff --git a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/criteria/TestCriteriaBuilder.java b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/criteria/TestCriteriaBuilder.java
index 0312ae4..ce3167b 100644
--- a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/criteria/TestCriteriaBuilder.java
+++ b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/criteria/TestCriteriaBuilder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2022 Oracle and/or its affiliates. All rights reserved.
  * Copyright (c) 2018, 2021 IBM Corporation. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -21,6 +21,7 @@
 
 import jakarta.persistence.EntityManager;
 import jakarta.persistence.EntityManagerFactory;
+import jakarta.persistence.LockModeType;
 import jakarta.persistence.Query;
 import jakarta.persistence.TypedQuery;
 import jakarta.persistence.criteria.CompoundSelection;
@@ -42,23 +43,21 @@
 import org.eclipse.persistence.jpa.test.framework.Emf;
 import org.eclipse.persistence.jpa.test.framework.EmfRunner;
 import org.eclipse.persistence.platform.database.DatabasePlatform;
-import org.junit.Assert;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
 @RunWith(EmfRunner.class)
 public class TestCriteriaBuilder {
 
     @Emf(createTables = DDLGen.DROP_CREATE, classes = { L1.class, L2.class, CriteriaCar.class })
     private EntityManagerFactory emf;
 
-    /**
-     * Merging ElementCollections on Oracle fails when EclipseLink generates 
-     * a DELETE SQL statement with a WHERE clause containing a CLOB.
-     *
-     */
-    @Test
-    public void testCriteriaCompoundSelectionModel() throws Exception {
+    @Before
+    public void setup() {
         //Populate the database
         EntityManager em = emf.createEntityManager();
         try {
@@ -69,10 +68,10 @@
             L1 l1 = new L1(1, "L1-1", l2);
             L1 l1_2 = new L1(2, "L1-2", l2_2);
 
-            em.persist(l2);
-            em.persist(l2_2);
-            em.persist(l1);
-            em.persist(l1_2);
+            em.merge(l2);
+            em.merge(l2_2);
+            em.merge(l1);
+            em.merge(l1_2);
 
             em.getTransaction().commit();
             em.clear();
@@ -84,8 +83,16 @@
                 em.close();
             }
         }
+    }
 
-        em = emf.createEntityManager();
+    /**
+     * Merging ElementCollections on Oracle fails when EclipseLink generates 
+     * a DELETE SQL statement with a WHERE clause containing a CLOB.
+     *
+     */
+    @Test
+    public void testCriteriaCompoundSelectionModel() throws Exception {
+        EntityManager em = emf.createEntityManager();
         try {
             //Test CriteriaBuilder
             final CriteriaBuilder builder = em.getCriteriaBuilder();
@@ -100,7 +107,7 @@
             List<L1Model> l1List = q.getResultList();
             if (l1List != null && !l1List.isEmpty()) {
                 for (L1Model l1m : l1List) {
-                    Assert.assertNotNull(l1m.getL2());
+                    assertNotNull(l1m.getL2());
                 }
             }
         } finally {
@@ -205,6 +212,35 @@
         }
     }
 
+    @Test
+    public void testCriteriaBuilder_WhereOrderByResultLimitPesimisticWriteClause() throws Exception {
+        EntityManager em = emf.createEntityManager();
+        try {
+            em.getTransaction().begin();
+            //"SELECT OBJECT(l1) FROM L1 l1 WHERE l1.id > 0 ORDER BY l1.id FOR UPDATE"
+            //with row limit firstResult=1 maxResults=10
+            final CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
+            final CriteriaQuery<L1> criteriaQuery = criteriaBuilder.createQuery(L1.class);
+            Root<L1> root = criteriaQuery.from(L1.class);
+            criteriaQuery.where(criteriaBuilder.greaterThan(root.get("id"), 0));
+            criteriaQuery.orderBy(criteriaBuilder.asc(root.get(L1_.id)));
+            final TypedQuery<L1> query = em.createQuery(criteriaQuery);
+            query.setFirstResult(1);
+            query.setMaxResults(10);
+            query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
+            final List<L1> results = query.getResultList();
+            assertNotNull(results);
+            assertEquals(1, results.size());
+        } finally {
+            if (em.getTransaction().isActive()) {
+                em.getTransaction().rollback();
+            }
+            if(em.isOpen()) {
+                em.close();
+            }
+        }
+    }
+
     private DatabasePlatform getPlatform(EntityManagerFactory emf) {
         return ((EntityManagerFactoryImpl)emf).getServerSession().getPlatform();
     }