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

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


The following commit(s) were added to refs/heads/STABLE-4.2 by this push:
     new f1291235d CAY-2866 DefaultDataDomainFlushAction breaks on circular 
relationship update
f1291235d is described below

commit f1291235d2dd40b2b47ceb141772e256a5b9da26
Author: Nikita Timofeev <stari...@gmail.com>
AuthorDate: Tue Aug 13 17:26:08 2024 +0400

    CAY-2866 DefaultDataDomainFlushAction breaks on circular relationship update
---
 RELEASE-NOTES.txt                                  | 10 ++++
 .../org/apache/cayenne/access/DataRowStore.java    |  2 +-
 .../access/flush/DefaultDataDomainFlushAction.java | 48 +++++++++++++++--
 .../org/apache/cayenne/CircularDependencyIT.java   | 61 +++++++++++++++++++++-
 4 files changed, 115 insertions(+), 6 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 6446db64a..36dd5af24 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -7,6 +7,16 @@ https://cayenne.apache.org/
 To browse individual bug reports check out project issue tracker:
 https://issues.apache.org/jira/browse/CAY
 
+----------------------------------
+Release: 4.2.2
+Date:
+----------------------------------
+
+Bug Fixes:
+
+CAY-2866 DefaultDataDomainFlushAction breaks on circular relationship update
+
+
 ----------------------------------
 Release: 4.2.1
 Date:
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowStore.java 
b/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowStore.java
index f2fe18ad2..e730b6c98 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowStore.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowStore.java
@@ -336,7 +336,7 @@ public class DataRowStore implements Serializable {
         if (deletedSnapshotIds.isEmpty()
                 && invalidatedSnapshotIds.isEmpty()
                 && updatedSnapshots.isEmpty()
-                && indirectlyModifiedIds.isEmpty()) {
+                && (indirectlyModifiedIds == null || 
indirectlyModifiedIds.isEmpty())) {
             logger.warn("postSnapshotsChangeEvent.. bogus call... no 
changes.");
             return;
         }
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
index 3d99e797f..d67ed20be 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
@@ -41,6 +41,8 @@ import 
org.apache.cayenne.access.flush.operation.DbRowOpMerger;
 import org.apache.cayenne.access.flush.operation.DbRowOpSorter;
 import org.apache.cayenne.access.flush.operation.DbRowOp;
 import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
+import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
 import org.apache.cayenne.access.flush.operation.OpIdFactory;
 import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
 import org.apache.cayenne.graph.CompoundDiff;
@@ -89,7 +91,9 @@ public class DefaultDataDomainFlushAction implements 
DataDomainFlushAction {
         List<? extends Query> queries = createQueries(sortedOps);
         executeQueries(queries);
         createReplacementIds(objectStore, afterCommitDiff, sortedOps);
-        postprocess(context, objectStoreGraphDiff, afterCommitDiff, sortedOps);
+        // note: we are using here not filtered operations, but the original 
ones,
+        // as we need them all for the postprocessing
+        postprocess(context, objectStoreGraphDiff, afterCommitDiff, 
deduplicatedOps);
 
         return afterCommitDiff;
     }
@@ -139,8 +143,25 @@ public class DefaultDataDomainFlushAction implements 
DataDomainFlushAction {
     }
 
     protected List<DbRowOp> filterOps(List<DbRowOp> dbRowOps) {
-        // clear phantom update (this can be from insert/delete of arc with 
transient object)
-        dbRowOps.forEach(row -> row.accept(PhantomDbRowOpCleaner.INSTANCE));
+        List<DbRowOp> opsToRemove = null;
+        for (DbRowOp row : dbRowOps) {
+            // clear phantom update (this can be from insert/delete of arc 
with transient object)
+            row.accept(PhantomDbRowOpCleaner.INSTANCE);
+            // check for an empty update that we may need to filter out
+            if (row.accept(EmptyRowChecker.INSTANCE)) {
+                if (opsToRemove == null) {
+                    opsToRemove = new ArrayList<>();
+                }
+                opsToRemove.add(row);
+            }
+        }
+
+        if(opsToRemove != null && !opsToRemove.isEmpty()) {
+            // make a duplicate, to keep original operations intact
+            dbRowOps = new ArrayList<>(dbRowOps);
+            dbRowOps.removeAll(opsToRemove);
+        }
+
         return dbRowOps;
     }
 
@@ -224,11 +245,30 @@ public class DefaultDataDomainFlushAction implements 
DataDomainFlushAction {
 
         @Override
         public Void visitUpdate(UpdateDbRowOp dbRow) {
-            //
             if(dbRow.getChangeId().isTemporary() && 
!dbRow.getChangeId().isReplacementIdAttached()) {
                 dbRow.getValues().clear();
             }
             return null;
         }
     }
+
+    protected static class EmptyRowChecker implements DbRowOpVisitor<Boolean> {
+
+        protected static final EmptyRowChecker INSTANCE = new 
EmptyRowChecker();
+
+        @Override
+        public Boolean visitDelete(DeleteDbRowOp dbRow) {
+            return false;
+        }
+
+        @Override
+        public Boolean visitInsert(InsertDbRowOp dbRow) {
+            return false;
+        }
+
+        @Override
+        public Boolean visitUpdate(UpdateDbRowOp dbRow) {
+            return dbRow.getValues().isEmpty();
+        }
+    }
 }
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java 
b/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java
index 6cfc68f7c..1a7923b51 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java
@@ -20,13 +20,20 @@
 package org.apache.cayenne;
 
 import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
 import org.apache.cayenne.testdo.relationships.E1;
 import org.apache.cayenne.testdo.relationships.E2;
+import org.apache.cayenne.testdo.relationships.ReflexiveAndToOne;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.After;
 import org.junit.Test;
 
+import java.sql.SQLException;
+import java.sql.Types;
+
 import static org.junit.Assert.*;
 
 @UseServerRuntime(CayenneProjects.RELATIONSHIPS_PROJECT)
@@ -34,7 +41,28 @@ public class CircularDependencyIT extends ServerCase {
 
     @Inject
     private ObjectContext context;
-    
+
+    @Inject
+    private DBHelper dbHelper;
+
+    @After
+    public void cleanUp() throws SQLException {
+        // manually cleanup circular references
+        TableHelper e1 = new TableHelper(dbHelper, "CYCLE_E1", "id", "e2_id", 
"text");
+        e1.setColumnTypes(Types.INTEGER, Types.INTEGER, Types.VARCHAR);
+        TableHelper e2 = new TableHelper(dbHelper, "CYCLE_E2", "id", "e1_id", 
"text");
+        e2.setColumnTypes(Types.INTEGER, Types.INTEGER, Types.VARCHAR);
+        TableHelper reflexive = new TableHelper(dbHelper, 
"REFLEXIVE_AND_TO_ONE", "REFLEXIVE_AND_TO_ONE_ID", "NAME", "PARENT_ID");
+
+        e1.update().set("e2_id", null, Types.INTEGER).execute();
+        e2.update().set("e1_id", null, Types.INTEGER).execute();
+        e1.deleteAll();
+        e2.deleteAll();
+
+        reflexive.update().set("PARENT_ID", null, Types.INTEGER).execute();
+        reflexive.deleteAll();
+    }
+
     @Test()
     public void testCycle() {
         E1 e1 = context.newObject(E1.class);
@@ -55,4 +83,35 @@ public class CircularDependencyIT extends ServerCase {
         }
 
     }
+
+    @Test
+    public void testUpdate() {
+        E1 e1 = context.newObject(E1.class);
+        E2 e2 = context.newObject(E2.class);
+
+        e1.setText("e1 #" + 1);
+        e2.setText("e2 #" + 2);
+        context.commitChanges();
+
+        e1.setE2(e2);
+        context.commitChanges();
+
+        e2.setE1(e1);
+        context.commitChanges();
+    }
+
+    @Test
+    public void testUpdateSelfRelationship() {
+        ReflexiveAndToOne e1 = context.newObject(ReflexiveAndToOne.class);
+        ReflexiveAndToOne e2 = context.newObject(ReflexiveAndToOne.class);
+
+        e1.setName("e1 #" + 1);
+        e2.setName("e2 #" + 2);
+
+        e1.setToParent(e2);
+        context.commitChanges();
+
+        e2.setToParent(e1);
+        context.commitChanges();
+    }
 }

Reply via email to