Bug 578262: Bulk update queries reuse the same version locking value

Signed-off-by: Will Dazey <dazeydev.3@gmail.com>
diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/TimestampLockingPolicy.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/TimestampLockingPolicy.java
index 0328762..064cef3 100644
--- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/TimestampLockingPolicy.java
+++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/TimestampLockingPolicy.java
@@ -1,5 +1,6 @@
 /*
- * 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) 2022 IBM Corporation. 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
@@ -206,7 +207,7 @@
      */
     @Override
     public Expression getWriteLockUpdateExpression(ExpressionBuilder builder, AbstractSession session) {
-        return builder.value(getInitialWriteValue(session));
+        return builder.currentTimeStamp();
     }
 
     /**
diff --git a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/TestTimestampVersionLocking.java b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/TestTimestampVersionLocking.java
index 3783ab2..f41c2c0 100644
--- a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/TestTimestampVersionLocking.java
+++ b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/TestTimestampVersionLocking.java
@@ -1,6 +1,6 @@
 /*
- * Copyright (c) 2015, 2021 Oracle and/or its affiliates. All rights reserved.
- * Copyright (c) 2015 IBM Corporation. All rights reserved.
+ * Copyright (c) 2015, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2022 IBM Corporation. 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
@@ -18,6 +18,9 @@
 
 import jakarta.persistence.EntityManager;
 import jakarta.persistence.EntityManagerFactory;
+import jakarta.persistence.Query;
+
+import java.sql.Timestamp;
 
 import org.eclipse.persistence.descriptors.TimestampLockingPolicy;
 import org.eclipse.persistence.internal.jpa.EntityManagerImpl;
@@ -26,6 +29,7 @@
 import org.eclipse.persistence.jpa.test.framework.EmfRunner;
 import org.eclipse.persistence.jpa.test.framework.Property;
 import org.eclipse.persistence.jpa.test.locking.model.ClassDescriptorCustomizer;
+import org.eclipse.persistence.jpa.test.locking.model.EcallRegistration;
 import org.eclipse.persistence.jpa.test.locking.model.TimestampDog;
 import org.eclipse.persistence.queries.DatabaseQuery;
 import org.eclipse.persistence.sessions.Session;
@@ -59,6 +63,9 @@
             @Property(name = "eclipselink.descriptor.customizer.TimestampDog", value = "org.eclipse.persistence.jpa.test.locking.model.ClassDescriptorCustomizer") })
     private EntityManagerFactory emfFalseCustomized;
 
+    @Emf(name = "emf2", createTables = DDLGen.DROP_CREATE, classes = { EcallRegistration.class })
+    private EntityManagerFactory emf2;
+
     /**
      * Check that setting the property "true" will get the local system time
      * instead of the default behavior of contacting the server.
@@ -204,6 +211,200 @@
     }
 
     /**
+     * Test bulk update queries do not reuse the same timestamp value across multiple executions
+     */
+    @Test
+    public void testUpdateAllQueryWithTimestampLocking() {
+
+        int flag1 = 0;
+        int flag2 = 1;
+        String pk1 = "11004";
+        String pk2 = "11005";
+
+        // Populate entities with initial timestamp version values
+        EntityManager em = emf2.createEntityManager();
+        try {
+            em.getTransaction().begin();
+
+            EcallRegistration e1 = new EcallRegistration(pk1, flag1);
+            EcallRegistration e2 = new EcallRegistration(pk2, flag2);
+
+            em.persist(e1);
+            em.persist(e2);
+
+            em.getTransaction().commit();
+        } finally {
+            if(em.isOpen()) {
+                em.clear();
+                em.close();
+            }
+        }
+
+        try {
+            Thread.sleep(2000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        /*
+         * Update flag1 value via bulk Update query.
+         * This should update the version locking timestamp value for entity1.
+         */
+        em = emf2.createEntityManager();
+        try {
+            flag1 = flag1++;
+
+            em.getTransaction().begin();
+
+            Query query = em.createNamedQuery("updateActiveEcallAvailableFlag", EcallRegistration.class);
+            query.setParameter("flag", flag1);
+            query.setParameter("pk", pk1);
+
+            query.executeUpdate();
+
+            em.getTransaction().commit();
+        } finally {
+            if(em.isOpen()) {
+                em.clear();
+                em.close();
+            }
+        }
+
+        try {
+            Thread.sleep(2000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        /*
+         * Update flag2 value via bulk Update query.
+         * This should update the version locking timestamp value for entity2.
+         */
+        em = emf2.createEntityManager();
+        try {
+            flag2 = flag2++;
+
+            em.getTransaction().begin();
+
+            Query query = em.createNamedQuery("updateActiveEcallAvailableFlag", EcallRegistration.class);
+            query.setParameter("flag", flag2);
+            query.setParameter("pk", pk2);
+
+            query.executeUpdate();
+
+            em.getTransaction().commit();
+        } finally {
+            if(em.isOpen()) {
+                em.clear();
+                em.close();
+            }
+        }
+
+        try {
+            Thread.sleep(2000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        /*
+         *  Validate that even though both bulk updates used the same UpdateAllQuery, both entities have 
+         *  different version locking timestamps in the database.
+         */
+        em = emf2.createEntityManager();
+        try {
+            em.getTransaction().begin();
+
+            EcallRegistration e1 = em.find(EcallRegistration.class, pk1);
+            EcallRegistration e2 = em.find(EcallRegistration.class, pk2);
+
+            Assert.assertNotNull(e1);
+            Assert.assertNotNull(e2);
+
+            Assert.assertNotEquals(e1.getSysUpdateTimestamp(), e2.getSysUpdateTimestamp());
+            Assert.assertTrue("Expected entity2.sysUpdateTimestamp [" + e2.getSysUpdateTimestamp() + "] "
+                    + "to be after entity1.sysUpdateTimestamp [" + e1.getSysUpdateTimestamp() + "]", 
+                    e2.getSysUpdateTimestamp().after(e1.getSysUpdateTimestamp()));
+
+            em.getTransaction().commit();
+        } finally {
+            if(em.isOpen()) {
+                em.clear();
+                em.close();
+            }
+        }
+    }
+
+    /**
+     * Test that bulk update queries do not update the managed entities version 
+     * values as documented in the specification
+     * 
+     * JPA Spec section 4.10: Bulk Update and Delete Operations
+     * 
+     * Bulk update maps directly to a database update operation, bypassing optimistic locking checks. 
+     * Portable applications must manually update the value of the version column, if desired, and/or 
+     * manually validate the value of the version column.
+     */
+    @Test
+    public void testTimestampLockingUpdateWithUpdateAllQuery() {
+
+        int flag1 = 0;
+        String pk1 = "11006";
+
+        EntityManager em = emf2.createEntityManager();
+        try {
+            // Populate an entity
+            em.getTransaction().begin();
+
+            EcallRegistration persist1 = new EcallRegistration(pk1, flag1);
+
+            em.persist(persist1);
+
+            em.getTransaction().commit();
+
+            // Find managed instance, record current timestamp version value
+            em.getTransaction().begin();
+
+            EcallRegistration find1 = em.find(EcallRegistration.class, pk1);
+            Assert.assertNotNull(find1);
+
+            Timestamp ver1 = find1.getSysUpdateTimestamp();
+            Assert.assertNotNull(ver1);
+
+            em.getTransaction().commit();
+
+            // Execute UpdateAllQuery to update version locking field in db
+            flag1 = flag1++;
+
+            em.getTransaction().begin();
+
+            Query query = em.createNamedQuery("updateActiveEcallAvailableFlag", EcallRegistration.class);
+            query.setParameter("flag", flag1);
+            query.setParameter("pk", pk1);
+
+            query.executeUpdate();
+
+            em.getTransaction().commit();
+
+            // Verify that the UpdateAllQuery didn't update the managed instance
+            em.getTransaction().begin();
+
+            EcallRegistration find2 = em.find(EcallRegistration.class, pk1);
+            Assert.assertNotNull(find2);
+
+            Timestamp ver2 = find2.getSysUpdateTimestamp();
+            Assert.assertNotNull(ver2);
+            Assert.assertEquals(ver1, ver2);
+
+            em.getTransaction().commit();
+        } finally {
+            if(em.isOpen()) {
+                em.clear();
+                em.close();
+            }
+        }
+    }
+
+    /**
      * Add this to Session Event Manager to listen for query executions. This
      * class will listen for the execution of a specified query and track it.
      */
diff --git a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/model/EcallRegistration.java b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/model/EcallRegistration.java
new file mode 100644
index 0000000..0fd8655
--- /dev/null
+++ b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/model/EcallRegistration.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022 IBM Corporation. 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
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ */
+package org.eclipse.persistence.jpa.test.locking.model;
+
+import java.sql.Timestamp;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.NamedQuery;
+import jakarta.persistence.Version;
+
+@Entity
+@NamedQuery(name="updateActiveEcallAvailableFlag", query="UPDATE EcallRegistration e SET e.ecallAvailableFlag = :flag WHERE e.pk = :pk")
+public class EcallRegistration {
+
+	@Id private String pk;
+
+	private int ecallAvailableFlag;
+
+	@Version @Column(name = "sys_update_timestamp")
+	private Timestamp sysUpdateTimestamp;
+
+	public EcallRegistration() { }
+	
+	public EcallRegistration(String pk, int ecallAvailableFlag) {
+		this.pk = pk;
+		this.ecallAvailableFlag = ecallAvailableFlag;
+	}
+
+	public String getPk() {
+		return pk;
+	}
+
+	public void setPk(String pk) {
+		this.pk = pk;
+	}
+
+	public int getEcallAvailableFlag() {
+		return ecallAvailableFlag;
+	}
+
+	public void setEcallAvailableFlag(int ecallAvailableFlag) {
+		this.ecallAvailableFlag = ecallAvailableFlag;
+	}
+
+	public Timestamp getSysUpdateTimestamp() {
+		return this.sysUpdateTimestamp;
+	}
+	
+	public void setSysUpdateTimestamp(Timestamp sysUpdateTimestamp) {
+		this.sysUpdateTimestamp = sysUpdateTimestamp;
+	}
+}