You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2019/04/24 14:53:59 UTC
[cayenne] 02/06: CAY-2571 DataDomainFlushAction redesign tests
This is an automated email from the ASF dual-hosted git repository.
ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit 99175f9042524a9314c64eab4664865a12750415
Author: Nikita Timofeev <st...@gmail.com>
AuthorDate: Mon Apr 22 18:02:20 2019 +0300
CAY-2571 DataDomainFlushAction redesign
tests
---
.../access/flush/ArcValuesCreationHandlerTest.java | 210 ++++++++++++++++++++
.../flush/DefaultDataDomainFlushActionTest.java | 158 +++++++++++++++
.../access/flush/DefaultDbRowOpSorterTest.java | 215 +++++++++++++++++++++
.../access/flush/operation/BaseDbRowOpTest.java | 125 ++++++++++++
.../access/flush/operation/DbRowOpMergerTest.java | 177 +++++++++++++++++
.../access/flush/operation/QualifierTest.java | 171 ++++++++++++++++
.../cayenne/access/flush/operation/ValuesTest.java | 109 +++++++++++
7 files changed, 1165 insertions(+)
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/ArcValuesCreationHandlerTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/ArcValuesCreationHandlerTest.java
new file mode 100644
index 0000000..73a7afb
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/ArcValuesCreationHandlerTest.java
@@ -0,0 +1,210 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.ObjectDiff;
+import org.apache.cayenne.access.ObjectStore;
+import org.apache.cayenne.access.flush.operation.DbRowOpType;
+import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
+import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
+import org.apache.cayenne.access.flush.operation.Values;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @since 4.2
+ */
+public class ArcValuesCreationHandlerTest {
+
+ private ArcValuesCreationHandler handler;
+ private DbRowOpFactory factory;
+ private InsertDbRowOp dbRowOp;
+ private Values values;
+
+ @SuppressWarnings("unchecked")
+ @Before
+ public void setup() {
+ factory = mock(DbRowOpFactory.class);
+ handler = new ArcValuesCreationHandler(factory, DbRowOpType.INSERT);
+ dbRowOp = mock(InsertDbRowOp.class);
+ values = new Values(dbRowOp, false);
+
+ ObjectDiff diff = mock(ObjectDiff.class);
+ ClassDescriptor descriptor = mock(ClassDescriptor.class);
+ ObjEntity entity = mock(ObjEntity.class);
+ ObjRelationship relationship = mock(ObjRelationship.class);
+ DbRelationship dbRelationship = mock(DbRelationship.class);
+ ObjectStore store = mock(ObjectStore.class);
+ Persistent object = mock(Persistent.class);
+
+ when(relationship.getDbRelationships()).thenReturn(Collections.singletonList(dbRelationship));
+ when(entity.getRelationship(anyString())).thenReturn(relationship);
+ when(descriptor.getEntity()).thenReturn(entity);
+ when(dbRowOp.accept(any(DbRowOpVisitor.class))).thenCallRealMethod();
+ when(dbRowOp.getValues()).thenReturn(values);
+ when(factory.getDiff()).thenReturn(diff);
+ when(factory.getDescriptor()).thenReturn(descriptor);
+ when(factory.getStore()).thenReturn(store);
+ when(factory.getObject()).thenReturn(object);
+ when(factory.getOrCreate(isNull(), any(ObjectId.class), any(DbRowOpType.class))).thenReturn(dbRowOp);
+ }
+
+ @Test
+ public void processRelationshipPkPkMaster() {
+ ObjectId srcId = ObjectId.of("test1", "id1", 1);
+ ObjectId targetId = ObjectId.of("test2", "id2", 2);
+
+ DbRelationship relationship = DbRelBuilder.of("id1", "id2")
+ .withToDepPk().withDstPk().withSrcPk().build();
+
+ handler.processRelationship(relationship, srcId, targetId, true);
+
+ assertNotNull(handler);
+ verify(factory).getOrCreate(isNull(), eq(targetId), eq(DbRowOpType.UPDATE));
+ assertTrue(targetId.isReplacementIdAttached());
+ assertEquals(1, targetId.getReplacementIdMap().size());
+ assertEquals(1, targetId.getReplacementIdMap().get("id2"));
+ assertFalse(srcId.isReplacementIdAttached());
+ }
+
+ @Test
+ public void processRelationshipPkPkDependent() {
+ ObjectId srcId = ObjectId.of("test1", "id1", 1);
+ ObjectId targetId = ObjectId.of("test2", "id2", 2);
+
+ DbRelationship relationship = DbRelBuilder.of("id1", "id2")
+ .withDstPk().withSrcPk().build();
+
+ handler.processRelationship(relationship, srcId, targetId, true);
+
+ assertNotNull(handler);
+ verify(factory).getOrCreate(isNull(), eq(srcId), eq(DbRowOpType.INSERT));
+ assertTrue(srcId.isReplacementIdAttached());
+ assertEquals(1, srcId.getReplacementIdMap().size());
+ assertEquals(2, srcId.getReplacementIdMap().get("id1"));
+ assertFalse(targetId.isReplacementIdAttached());
+ }
+
+ @Test
+ public void processRelationshipPkFkMaster() {
+ ObjectId srcId = ObjectId.of("test1", "pk", 1);
+ ObjectId targetId = ObjectId.of("test2", "id2", 2);
+
+ DbRelationship relationship = DbRelBuilder.of("pk", "fk")
+ .withSrcPk().build();
+
+ handler.processRelationship(relationship, srcId, targetId, true);
+
+ assertNotNull(handler);
+ verify(factory).getOrCreate(isNull(), eq(targetId), eq(DbRowOpType.UPDATE));
+ assertFalse(srcId.isReplacementIdAttached());
+ assertFalse(targetId.isReplacementIdAttached());
+
+ verify(dbRowOp).getValues();
+ Map<String, Object> snapshot = values.getSnapshot();
+ assertEquals(1, snapshot.size());
+ assertEquals(1, snapshot.get("fk"));
+ }
+
+ @Test
+ public void processRelationshipFkPkDependent() {
+ ObjectId srcId = ObjectId.of("test1", "id1", 1);
+ ObjectId targetId = ObjectId.of("test2", "pk", 2);
+
+ DbRelationship relationship = DbRelBuilder.of("fk", "pk")
+ .withDstPk().build();
+
+ handler.processRelationship(relationship, srcId, targetId, true);
+
+ assertNotNull(handler);
+ verify(factory).getOrCreate(isNull(), eq(srcId), eq(DbRowOpType.INSERT));
+ assertFalse(srcId.isReplacementIdAttached());
+ assertFalse(targetId.isReplacementIdAttached());
+
+ verify(dbRowOp).getValues();
+ Map<String, Object> snapshot = values.getSnapshot();
+ assertEquals(1, snapshot.size());
+ assertEquals(2, snapshot.get("fk"));
+ }
+
+ final static class DbRelBuilder {
+ private String srcName;
+ private String dstName;
+ private boolean srcPk;
+ private boolean dstPk;
+ private boolean toDepPk;
+
+ static DbRelBuilder of(String srcName, String dstName) {
+ DbRelBuilder builder = new DbRelBuilder();
+ builder.srcName = srcName;
+ builder.dstName = dstName;
+ return builder;
+ }
+
+ DbRelBuilder withSrcPk() {
+ srcPk = true;
+ return this;
+ }
+
+ DbRelBuilder withDstPk() {
+ dstPk = true;
+ return this;
+ }
+
+ DbRelBuilder withToDepPk() {
+ toDepPk = true;
+ return this;
+ }
+
+ DbRelationship build() {
+ DbRelationship relationship = mock(DbRelationship.class);
+ when(relationship.isToDependentPK()).thenReturn(toDepPk);
+ DbJoin join = mock(DbJoin.class);
+ DbAttribute src = new DbAttribute(srcName);
+ src.setPrimaryKey(srcPk);
+ DbAttribute target = new DbAttribute(dstName);
+ target.setPrimaryKey(dstPk);
+ when(join.getSource()).thenReturn(src);
+ when(join.getSourceName()).thenReturn(src.getName());
+ when(join.getTarget()).thenReturn(target);
+ when(join.getTargetName()).thenReturn(target.getName());
+ when(relationship.getJoins()).thenReturn(Collections.singletonList(join));
+
+ DbRelationship mockRel = mock(DbRelationship.class);
+ when(relationship.getReverseRelationship()).thenReturn(mockRel);
+ return relationship;
+ }
+ }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionTest.java
new file mode 100644
index 0000000..69a927f
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionTest.java
@@ -0,0 +1,158 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.flush.DefaultDataDomainFlushAction;
+import org.apache.cayenne.access.flush.operation.BaseDbRowOp;
+import org.apache.cayenne.access.flush.operation.DbRowOp;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
+import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
+import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.query.DeleteBatchQuery;
+import org.apache.cayenne.query.InsertBatchQuery;
+import org.apache.cayenne.query.Query;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * @since 4.2
+ */
+public class DefaultDataDomainFlushActionTest {
+
+ @Test
+ public void mergeSameObjectIds() {
+ ObjectId id1 = ObjectId.of("test2", "id", 1);
+ ObjectId id2 = ObjectId.of("test", "id", 2);
+ ObjectId id3 = ObjectId.of("test", "id", 2);
+ ObjectId id4 = ObjectId.of("test", "id", 3);
+ ObjectId id5 = ObjectId.of("test2", "id", 4);
+ ObjectId id6 = ObjectId.of("test", "id", 5);
+ ObjectId id7 = ObjectId.of("test", "id", 6);
+ ObjectId id8 = ObjectId.of("test2", "id", 3);
+ ObjectId id9 = ObjectId.of("test2", "id", 4);
+ ObjectId id10 = ObjectId.of("test", "id", 6);
+
+ DbEntity test = mockEntity("test");
+ DbEntity test2 = mockEntity("test2");
+ BaseDbRowOp[] op = new BaseDbRowOp[10];
+ op[0] = new InsertDbRowOp(mockObject(id1), test2, id1); // +
+ op[1] = new InsertDbRowOp(mockObject(id2), test, id2); // -
+ op[2] = new DeleteDbRowOp(mockObject(id3), test, id3); // -
+ op[3] = new UpdateDbRowOp(mockObject(id4), test, id4); // +
+ op[4] = new InsertDbRowOp(mockObject(id5), test2, id5); // -
+ op[5] = new DeleteDbRowOp(mockObject(id6), test, id6); // +
+ op[6] = new InsertDbRowOp(mockObject(id7), test, id7); // -
+ op[7] = new UpdateDbRowOp(mockObject(id8), test2, id8); // +
+ op[8] = new DeleteDbRowOp(mockObject(id9), test2, id9); // -
+ op[9] = new DeleteDbRowOp(mockObject(id10), test, id10);// -
+
+ DefaultDataDomainFlushAction action = mock(DefaultDataDomainFlushAction.class);
+ when(action.mergeSameObjectIds((List<DbRowOp>) any(List.class))).thenCallRealMethod();
+
+ Collection<DbRowOp> merged = action.mergeSameObjectIds(new ArrayList<>(Arrays.asList(op)));
+ assertEquals(7, merged.size());
+ assertThat(merged, hasItems(op[0], op[3], op[5], op[7]));
+ assertThat(merged, not(hasItem(sameInstance(op[1]))));
+ assertThat(merged, not(hasItem(sameInstance(op[2]))));
+ assertThat(merged, not(hasItem(sameInstance(op[4]))));
+ assertThat(merged, not(hasItem(sameInstance(op[6]))));
+ assertThat(merged, not(hasItem(sameInstance(op[8]))));
+ assertThat(merged, not(hasItem(sameInstance(op[9]))));
+ }
+
+ @Test
+ public void createQueries() {
+ ObjectId id1 = ObjectId.of("test", "id", 1);
+ ObjectId id2 = ObjectId.of("test", "id", 2);
+ ObjectId id3 = ObjectId.of("test2", "id", 3);
+ ObjectId id4 = ObjectId.of("test2", "id", 4);
+ ObjectId id5 = ObjectId.of("test", "id", 5);
+ ObjectId id6 = ObjectId.of("test2", "id", 6);
+ ObjectId id7 = ObjectId.of("test", "id", 7);
+
+ DbEntity test = mockEntity("test");
+ DbEntity test2 = mockEntity("test2");
+
+ List<DbRowOp> ops = new ArrayList<>();
+ ops.add(new InsertDbRowOp(mockObject(id1), test, id1));
+ ops.add(new InsertDbRowOp(mockObject(id2), test, id2));
+ ops.add(new InsertDbRowOp(mockObject(id3), test2, id5));
+ ops.add(new InsertDbRowOp(mockObject(id4), test2, id7));
+ ops.add(new UpdateDbRowOp(mockObject(id5), test, id3));
+ ops.add(new DeleteDbRowOp(mockObject(id6), test2, id6));
+ ops.add(new DeleteDbRowOp(mockObject(id7), test, id4));
+
+ DefaultDataDomainFlushAction action = mock(DefaultDataDomainFlushAction.class);
+ when(action.createQueries((List<DbRowOp>) any(List.class))).thenCallRealMethod();
+
+ List<? extends Query> queries = action.createQueries(ops);
+ assertEquals(4, queries.size());
+ assertThat(queries.get(0), instanceOf(InsertBatchQuery.class));
+ InsertBatchQuery insert1 = (InsertBatchQuery)queries.get(0);
+ assertSame(test, insert1.getDbEntity());
+ assertEquals(2, insert1.getRows().size());
+
+ assertThat(queries.get(1), instanceOf(InsertBatchQuery.class));
+ InsertBatchQuery insert2 = (InsertBatchQuery)queries.get(1);
+ assertSame(test2, insert2.getDbEntity());
+ assertEquals(2, insert2.getRows().size());
+
+ assertThat(queries.get(2), instanceOf(DeleteBatchQuery.class));
+ DeleteBatchQuery delete1 = (DeleteBatchQuery)queries.get(2);
+ assertSame(test2, delete1.getDbEntity());
+ assertEquals(1, delete1.getRows().size());
+
+ assertThat(queries.get(3), instanceOf(DeleteBatchQuery.class));
+ DeleteBatchQuery delete2 = (DeleteBatchQuery)queries.get(3);
+ assertSame(test, delete2.getDbEntity());
+ assertEquals(1, delete2.getRows().size());
+ }
+
+ private Persistent mockObject(ObjectId id) {
+ Persistent persistent = mock(Persistent.class);
+ when(persistent.getObjectId()).thenReturn(id);
+ when(persistent.getPersistenceState()).thenReturn(PersistenceState.MODIFIED);
+ return persistent;
+ }
+
+ private DbEntity mockEntity(String name) {
+ DbAttribute attribute1 = new DbAttribute("id");
+ attribute1.setPrimaryKey(true);
+ DbAttribute attribute2 = new DbAttribute("attr");
+ DbEntity testEntity = new DbEntity(name);
+ testEntity.addAttribute(attribute1);
+ testEntity.addAttribute(attribute2);
+ return testEntity;
+ }
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/DefaultDbRowOpSorterTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/DefaultDbRowOpSorterTest.java
new file mode 100644
index 0000000..b332d03
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/DefaultDbRowOpSorterTest.java
@@ -0,0 +1,215 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.DataDomain;
+import org.apache.cayenne.access.flush.operation.DefaultDbRowOpSorter;
+import org.apache.cayenne.access.flush.operation.BaseDbRowOp;
+import org.apache.cayenne.access.flush.operation.DbRowOp;
+import org.apache.cayenne.access.flush.operation.DbRowOpSorter;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
+import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
+import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.EntitySorter;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @since 4.2
+ */
+public class DefaultDbRowOpSorterTest {
+
+ private EntitySorter entitySorter;
+ private DbRowOpSorter sorter;
+
+ @Before
+ public void createSorter() {
+ entitySorter = mock(EntitySorter.class);
+ EntityResolver entityResolver = mock(EntityResolver.class);
+
+ when(entitySorter.getDbEntityComparator())
+ .thenReturn(Comparator.comparing(DbEntity::getName));
+ when(entitySorter.isReflexive(argThat(ent -> ent.getName().equals("reflexive"))))
+ .thenReturn(true);
+
+ DataDomain dataDomain = mock(DataDomain.class);
+ when(dataDomain.getEntitySorter()).thenReturn(entitySorter);
+ when(dataDomain.getEntityResolver()).thenReturn(entityResolver);
+
+ sorter = new DefaultDbRowOpSorter(() -> dataDomain);
+ }
+
+ @Test
+ public void sortEmptyList() {
+ List<DbRowOp> rows = new ArrayList<>();
+ List<DbRowOp> sorted = sorter.sort(rows);
+ assertTrue(sorted.isEmpty());
+ }
+
+ @Test
+ public void sortByOpType() {
+ ObjectId id1 = ObjectId.of("test", "id", 1);
+ ObjectId id2 = ObjectId.of("test", "id", 2);
+ ObjectId id3 = ObjectId.of("test", "id", 3);
+ ObjectId id4 = ObjectId.of("test", "id", 4);
+ ObjectId id5 = ObjectId.of("test", "id", 5);
+ ObjectId id6 = ObjectId.of("test", "id", 6);
+
+ DbEntity test = mockEntity("test");
+ DbRowOp op1 = new InsertDbRowOp(mockObject(id1), test, id1);
+ DbRowOp op2 = new UpdateDbRowOp(mockObject(id2), test, id2);
+ DbRowOp op3 = new DeleteDbRowOp(mockObject(id3), test, id3);
+ DbRowOp op4 = new InsertDbRowOp(mockObject(id4), test, id4);
+ DbRowOp op5 = new UpdateDbRowOp(mockObject(id5), test, id5);
+ DbRowOp op6 = new DeleteDbRowOp(mockObject(id6), test, id6);
+
+ List<DbRowOp> rows = Arrays.asList(op1, op2, op3, op4, op5, op6);
+ List<DbRowOp> expected = Arrays.asList(op1, op4, op2, op5, op3, op6);
+
+ List<DbRowOp> sorted = sorter.sort(rows);
+ assertEquals(expected, sorted);
+ }
+
+ @Test
+ public void sortByOpEntity() {
+ ObjectId id1 = ObjectId.of("test4", "id", 1);
+ ObjectId id2 = ObjectId.of("test2", "id", 2);
+ ObjectId id3 = ObjectId.of("test3", "id", 3);
+ ObjectId id4 = ObjectId.of("test1", "id", 4);
+
+ DbRowOp op1 = new InsertDbRowOp(mockObject(id1), mockEntity("test4"), id1);
+ DbRowOp op2 = new InsertDbRowOp(mockObject(id2), mockEntity("test2"), id2);
+ DbRowOp op3 = new InsertDbRowOp(mockObject(id3), mockEntity("test3"), id3);
+ DbRowOp op4 = new InsertDbRowOp(mockObject(id4), mockEntity("test1"), id4);
+
+ List<DbRowOp> rows = Arrays.asList(op1, op2, op3, op4);
+ List<DbRowOp> expected = Arrays.asList(op4, op2, op3, op1);
+
+ List<DbRowOp> sorted = sorter.sort(rows);
+ assertEquals(expected, sorted);
+ }
+
+ @Test
+ public void sortById() {
+ ObjectId id1 = ObjectId.of("test", "id", 1);
+ ObjectId id2 = ObjectId.of("test", "id", 2);
+ ObjectId id3 = ObjectId.of("test", "id", 2);
+ ObjectId id4 = ObjectId.of("test", "id", 3);
+
+ DbEntity test = mockEntity("test");
+ InsertDbRowOp op1 = new InsertDbRowOp(mockObject(id1), test, id1);
+ InsertDbRowOp op2 = new InsertDbRowOp(mockObject(id2), test, id2);
+ DeleteDbRowOp op3 = new DeleteDbRowOp(mockObject(id3), test, id3);
+ DeleteDbRowOp op4 = new DeleteDbRowOp(mockObject(id4), test, id4);
+
+ List<DbRowOp> rows = Arrays.asList(op1, op2, op3, op4);
+ List<DbRowOp> expected = Arrays.asList(op1, op2, op3, op4);
+
+ List<DbRowOp> sorted = sorter.sort(rows);
+ assertEquals(expected, sorted);
+ }
+
+ @Test
+ public void sortByIdDifferentEntities() {
+ ObjectId id1 = ObjectId.of("test2", "id", 1);
+ ObjectId id2 = ObjectId.of("test", "id", 2);
+ ObjectId id3 = ObjectId.of("test", "id", 2);
+ ObjectId id4 = ObjectId.of("test", "id", 3);
+ ObjectId id5 = ObjectId.of("test2", "id", 4);
+ ObjectId id6 = ObjectId.of("test", "id", 5);
+ ObjectId id7 = ObjectId.of("test", "id", 8);
+ ObjectId id8 = ObjectId.of("test2", "id", 7);
+ ObjectId id9 = ObjectId.of("test2", "id", 4);
+ ObjectId id10 = ObjectId.of("test", "id", 8);
+
+ DbEntity test = mockEntity("test");
+ DbEntity test2 = mockEntity("test2");
+ BaseDbRowOp[] op = new BaseDbRowOp[10];
+ op[0] = new InsertDbRowOp(mockObject(id1), test2, id1);
+ op[1] = new InsertDbRowOp(mockObject(id2), test, id2);
+ op[2] = new DeleteDbRowOp(mockObject(id3), test, id3);
+ op[3] = new UpdateDbRowOp(mockObject(id4), test, id4);
+ op[4] = new InsertDbRowOp(mockObject(id5), test2, id5);
+ op[5] = new DeleteDbRowOp(mockObject(id6), test, id6);
+ op[6] = new InsertDbRowOp(mockObject(id7), test, id7);
+ op[7] = new UpdateDbRowOp(mockObject(id8), test2, id8);
+ op[8] = new DeleteDbRowOp(mockObject(id9), test2, id9);
+ op[9] = new DeleteDbRowOp(mockObject(id10), test, id10);
+
+ List<DbRowOp> expected = Arrays.asList(op[1], op[6], op[0], op[4], op[3], op[7], op[8], op[2], op[5], op[9]);
+ List<DbRowOp> sorted = sorter.sort(Arrays.asList(op));
+
+ assertEquals(expected, sorted);
+ }
+
+ @Test
+ public void sortReflexive() {
+ ObjectId id1 = ObjectId.of("reflexive", "id", 1);
+ ObjectId id2 = ObjectId.of("reflexive", "id", 2);
+ ObjectId id3 = ObjectId.of("reflexive", "id", 3);
+ ObjectId id4 = ObjectId.of("reflexive", "id", 4);
+
+ DbEntity reflexive = mockEntity("reflexive");
+ DbRowOp op1 = new InsertDbRowOp(mockObject(id1), reflexive, id1);
+ DbRowOp op2 = new InsertDbRowOp(mockObject(id2), reflexive, id2);
+ DbRowOp op3 = new InsertDbRowOp(mockObject(id3), reflexive, id3);
+ DbRowOp op4 = new InsertDbRowOp(mockObject(id4), reflexive, id4);
+
+ List<DbRowOp> rows = Arrays.asList(op1, op2, op3, op4);
+ List<DbRowOp> expected = Arrays.asList(op1, op2, op3, op4);
+
+ List<DbRowOp> sorted = sorter.sort(rows);
+ assertEquals(expected, sorted); // no actual sorting is done
+ verify(entitySorter) // should call entity sorter
+ .sortObjectsForEntity(isNull(), any(List.class), eq(false));
+ }
+
+ private Persistent mockObject(ObjectId id) {
+ Persistent persistent = mock(Persistent.class);
+ when(persistent.getObjectId()).thenReturn(id);
+ when(persistent.getPersistenceState()).thenReturn(PersistenceState.MODIFIED);
+ return persistent;
+ }
+
+ private DbEntity mockEntity(String name) {
+ DbAttribute attribute1 = new DbAttribute("id");
+ attribute1.setPrimaryKey(true);
+ DbAttribute attribute2 = new DbAttribute("attr");
+ DbEntity testEntity = new DbEntity(name);
+ testEntity.addAttribute(attribute1);
+ testEntity.addAttribute(attribute2);
+ return testEntity;
+ }
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/BaseDbRowOpTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/BaseDbRowOpTest.java
new file mode 100644
index 0000000..e0353ad
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/BaseDbRowOpTest.java
@@ -0,0 +1,125 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @since 4.2
+ */
+public class BaseDbRowOpTest {
+
+ @Test
+ public void testEquals_SameId() {
+ ObjectId id = ObjectId.of("test");
+
+ DbRowOp row1 = new InsertDbRowOp(mockObject(id), mockEntity(), id);
+ DbRowOp row2 = new InsertDbRowOp(mockObject(id), mockEntity(), id);
+
+ assertEquals(row1, row2);
+ assertEquals(row2, row1);
+ }
+
+ @Test
+ public void testEquals_EqualId() {
+ ObjectId id1 = ObjectId.of("test", "id", 1);
+ ObjectId id2 = ObjectId.of("test", "id", 1);
+
+ DbRowOp row1 = new InsertDbRowOp(mockObject(id1), mockEntity(), id1);
+ DbRowOp row2 = new InsertDbRowOp(mockObject(id2), mockEntity(), id2);
+
+ assertEquals(row1, row2);
+ assertEquals(row2, row1);
+ }
+
+ @Test
+ public void testNotEquals_EqualId() {
+ ObjectId id1 = ObjectId.of("test", "id", 1);
+ ObjectId id2 = ObjectId.of("test", "id", 1);
+
+ DbRowOp row1 = new InsertDbRowOp(mockObject(id1), mockEntity(), id1);
+ DbRowOp row2 = new DeleteDbRowOp(mockObject(id2), mockEntity(), id2);
+
+ assertNotEquals(row1, row2);
+ assertNotEquals(row2, row1);
+ }
+
+ @Test
+ public void testEqualsInsertUpdate_EqualId() {
+ ObjectId id1 = ObjectId.of("test", "id", 1);
+ ObjectId id2 = ObjectId.of("test", "id", 1);
+
+ DbRowOp row1 = new InsertDbRowOp(mockObject(id1), mockEntity(), id1);
+ DbRowOp row2 = new UpdateDbRowOp(mockObject(id2), mockEntity(), id2);
+
+ assertEquals(row1, row2);
+ assertEquals(row2, row1);
+ }
+
+ @Test
+ public void testEqualsUpdateDelete_EqualId() {
+ ObjectId id1 = ObjectId.of("test", "id", 1);
+ ObjectId id2 = ObjectId.of("test", "id", 1);
+
+ DbRowOp row1 = new DeleteDbRowOp(mockObject(id1), mockEntity(), id1);
+ DbRowOp row2 = new UpdateDbRowOp(mockObject(id2), mockEntity(), id2);
+
+ assertEquals(row1, row2);
+ assertEquals(row2, row1);
+ }
+
+ @Test
+ public void testNotEquals_NotEqualId() {
+ ObjectId id1 = ObjectId.of("test", "id", 1);
+ ObjectId id2 = ObjectId.of("test", "id", 2);
+
+ DbRowOp row1 = new InsertDbRowOp(mockObject(id1), mockEntity(), id1);
+ DbRowOp row2 = new InsertDbRowOp(mockObject(id2), mockEntity(), id2);
+
+ assertNotEquals(row1, row2);
+ assertNotEquals(row2, row1);
+ }
+
+ private Persistent mockObject(ObjectId id) {
+ Persistent persistent = mock(Persistent.class);
+ when(persistent.getObjectId()).thenReturn(id);
+ when(persistent.getPersistenceState()).thenReturn(PersistenceState.MODIFIED);
+ return persistent;
+ }
+
+ private DbEntity mockEntity() {
+ DbAttribute attribute1 = new DbAttribute("id");
+ attribute1.setPrimaryKey(true);
+ DbAttribute attribute2 = new DbAttribute("attr");
+ DbEntity testEntity = new DbEntity("TEST");
+ testEntity.addAttribute(attribute1);
+ testEntity.addAttribute(attribute2);
+ return testEntity;
+ }
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/DbRowOpMergerTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/DbRowOpMergerTest.java
new file mode 100644
index 0000000..0499704
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/DbRowOpMergerTest.java
@@ -0,0 +1,177 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import java.util.Map;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @since 4.2
+ */
+public class DbRowOpMergerTest {
+
+ @Test
+ public void testMergeUpdateDelete() {
+ ObjectId id = ObjectId.of("test");
+
+ UpdateDbRowOp row1 = new UpdateDbRowOp(mockObject(id), mockEntity(), id);
+ DeleteDbRowOp row2 = new DeleteDbRowOp(mockObject(id), mockEntity(), id);
+
+ {
+ DbRowOpMerger merger = new DbRowOpMerger();
+ DbRowOp row = merger.apply(row1, row2);
+ assertSame(row2, row);
+ }
+
+ {
+ DbRowOpMerger merger = new DbRowOpMerger();
+ DbRowOp row = merger.apply(row2, row1);
+ assertSame(row2, row);
+ }
+ }
+
+ @Test
+ public void testMergeInsertDelete() {
+ ObjectId id = ObjectId.of("test");
+
+ InsertDbRowOp row1 = new InsertDbRowOp(mockObject(id), mockEntity(), id);
+ DeleteDbRowOp row2 = new DeleteDbRowOp(mockObject(id), mockEntity(), id);
+
+ {
+ DbRowOpMerger merger = new DbRowOpMerger();
+ DbRowOp row = merger.apply(row1, row2);
+ assertSame(row2, row);
+ }
+ }
+
+ @Test
+ public void testMergeUpdateInsert() {
+ ObjectId id = ObjectId.of("test");
+
+ UpdateDbRowOp row1 = new UpdateDbRowOp(mockObject(id), mockEntity(), id);
+ InsertDbRowOp row2 = new InsertDbRowOp(mockObject(id), mockEntity(), id);
+
+ {
+ DbRowOpMerger merger = new DbRowOpMerger();
+ DbRowOp row = merger.apply(row1, row2);
+ assertSame(row2, row);
+ }
+
+ {
+ DbRowOpMerger merger = new DbRowOpMerger();
+ DbRowOp row = merger.apply(row2, row1);
+ assertSame(row1, row);
+ }
+ }
+
+ @Test
+ public void testMergeInsertInsert() {
+ ObjectId id = ObjectId.of("test");
+
+ DbAttribute attr1 = new DbAttribute("attr1");
+ DbAttribute attr2 = new DbAttribute("attr2");
+
+ InsertDbRowOp row1 = new InsertDbRowOp(mockObject(id), mockEntity(), id);
+ row1.getValues().addValue(attr1, 1);
+ InsertDbRowOp row2 = new InsertDbRowOp(mockObject(id), mockEntity(), id);
+ row2.getValues().addValue(attr2, 2);
+
+ {
+ DbRowOpMerger merger = new DbRowOpMerger();
+ DbRowOp row = merger.apply(row1, row2);
+ assertSame(row2, row);
+ Map<String, Object> snapshot = ((InsertDbRowOp) row).getValues().getSnapshot();
+ assertEquals(2, snapshot.size());
+ assertEquals(1, snapshot.get("attr1"));
+ assertEquals(2, snapshot.get("attr2"));
+ }
+
+ {
+ DbRowOpMerger merger = new DbRowOpMerger();
+ DbRowOp row = merger.apply(row2, row1);
+ assertSame(row1, row);
+ Map<String, Object> snapshot = ((InsertDbRowOp) row).getValues().getSnapshot();
+ assertEquals(2, snapshot.size());
+ assertEquals(1, snapshot.get("attr1"));
+ assertEquals(2, snapshot.get("attr2"));
+ }
+ }
+
+ @Test
+ public void testMergeUpdateUpdate() {
+ ObjectId id = ObjectId.of("test");
+
+ DbAttribute attr1 = new DbAttribute("attr1");
+ DbAttribute attr2 = new DbAttribute("attr2");
+
+ UpdateDbRowOp row1 = new UpdateDbRowOp(mockObject(id), mockEntity(), id);
+ row1.getValues().addValue(attr1, 1);
+ UpdateDbRowOp row2 = new UpdateDbRowOp(mockObject(id), mockEntity(), id);
+ row2.getValues().addValue(attr2, 2);
+
+ {
+ DbRowOpMerger merger = new DbRowOpMerger();
+ DbRowOp row = merger.apply(row1, row2);
+ assertSame(row2, row);
+ Map<String, Object> snapshot = ((UpdateDbRowOp) row).getValues().getSnapshot();
+ assertEquals(2, snapshot.size());
+ assertEquals(1, snapshot.get("attr1"));
+ assertEquals(2, snapshot.get("attr2"));
+ }
+
+ {
+ DbRowOpMerger merger = new DbRowOpMerger();
+ DbRowOp row = merger.apply(row2, row1);
+ assertSame(row1, row);
+ Map<String, Object> snapshot = ((UpdateDbRowOp) row).getValues().getSnapshot();
+ assertEquals(2, snapshot.size());
+ assertEquals(1, snapshot.get("attr1"));
+ assertEquals(2, snapshot.get("attr2"));
+ }
+ }
+
+ private Persistent mockObject(ObjectId id) {
+ Persistent persistent = mock(Persistent.class);
+ when(persistent.getObjectId()).thenReturn(id);
+ when(persistent.getPersistenceState()).thenReturn(PersistenceState.MODIFIED);
+ return persistent;
+ }
+
+ private DbEntity mockEntity() {
+ DbAttribute attribute1 = new DbAttribute("id");
+ attribute1.setPrimaryKey(true);
+ DbAttribute attribute2 = new DbAttribute("attr");
+ DbEntity testEntity = new DbEntity("TEST");
+ testEntity.addAttribute(attribute1);
+ testEntity.addAttribute(attribute2);
+ return testEntity;
+ }
+
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/QualifierTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/QualifierTest.java
new file mode 100644
index 0000000..4eca80f
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/QualifierTest.java
@@ -0,0 +1,171 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @since 4.2
+ */
+public class QualifierTest {
+
+ @Test
+ public void testScalarObjectIdQualifier() {
+ ObjectId id = ObjectId.of("test", "id", 123);
+ Persistent persistent = mockObject(id);
+ DbRowOp row = mockRow(persistent);
+
+ Qualifier qualifier = new Qualifier(row);
+ Map<String, Object> qualifierSnapshot = qualifier.getSnapshot();
+
+ assertEquals(Collections.singletonMap("id", 123), qualifierSnapshot);
+ assertFalse(qualifier.isUsingOptimisticLocking());
+
+ qualifierSnapshot = qualifier.getSnapshot();
+ assertEquals(Collections.singletonMap("id", 123), qualifierSnapshot);
+ }
+
+ @Test
+ public void testMapObjectIdQualifier() {
+ Map<String, Object> idMap = new HashMap<>();
+ idMap.put("id1", 123);
+ idMap.put("id2", 321);
+ ObjectId id = ObjectId.of("test", idMap);
+
+ Persistent persistent = mockObject(id);
+ DbRowOp row = mockRow(persistent);
+
+ Qualifier qualifier = new Qualifier(row);
+ Map<String, Object> qualifierSnapshot = qualifier.getSnapshot();
+ assertEquals(idMap, qualifierSnapshot);
+
+ qualifierSnapshot = qualifier.getSnapshot();
+ assertEquals(idMap, qualifierSnapshot);
+ }
+
+ @Test
+ public void testAdditionalQualifier() {
+ ObjectId id = ObjectId.of("test", "id", 123);
+ Persistent persistent = mockObject(id);
+ DbRowOp row = mockRow(persistent);
+
+ Qualifier qualifier = new Qualifier(row);
+ qualifier.addAdditionalQualifier(new DbAttribute("attr"), 42, true);
+
+ Map<String, Object> qualifierSnapshot = qualifier.getSnapshot();
+
+ Map<String, Object> expectedSnapshot = new HashMap<>();
+ expectedSnapshot.put("id", 123);
+ expectedSnapshot.put("attr", 42);
+
+ assertEquals(expectedSnapshot, qualifierSnapshot);
+ assertTrue(qualifier.isUsingOptimisticLocking());
+
+ qualifierSnapshot = qualifier.getSnapshot();
+ assertEquals(expectedSnapshot, qualifierSnapshot);
+ }
+
+ @Test
+ public void testOptimisticQualifier() {
+ ObjectId id = ObjectId.of("test", "id", 123);
+ Persistent persistent = mockObject(id);
+ DbRowOp row = mockRow(persistent);
+
+ Qualifier qualifier = new Qualifier(row);
+ qualifier.addAdditionalQualifier(new DbAttribute("attr"), 42, true);
+
+ Map<String, Object> qualifierSnapshot = qualifier.getSnapshot();
+
+ Map<String, Object> expectedSnapshot = new HashMap<>();
+ expectedSnapshot.put("id", 123);
+ expectedSnapshot.put("attr", 42);
+
+ assertEquals(expectedSnapshot, qualifierSnapshot);
+ assertTrue(qualifier.isUsingOptimisticLocking());
+
+ qualifierSnapshot = qualifier.getSnapshot();
+ assertEquals(expectedSnapshot, qualifierSnapshot);
+ }
+
+ @Test
+ public void testSameBatch() {
+ ObjectId id1 = ObjectId.of("test", "id", 123);
+ Persistent persistent1 = mockObject(id1);
+ DbRowOp row1 = mockRow(persistent1);
+
+ Qualifier qualifier1 = new Qualifier(row1);
+
+ ObjectId id2 = ObjectId.of("test", "id", 321);
+ Persistent persistent2 = mockObject(id2);
+ DbRowOp row2 = mockRow(persistent2);
+
+ Qualifier qualifier2 = new Qualifier(row2);
+
+ assertTrue(qualifier1.isSameBatch(qualifier2));
+
+ ObjectId id3 = ObjectId.of("test", "id", 321);
+ Persistent persistent3 = mockObject(id3);
+ DbRowOp row3 = mockRow(persistent3);
+
+ Qualifier qualifier3 = new Qualifier(row3);
+ qualifier3.addAdditionalQualifier(new DbAttribute("attr"), 42);
+
+ assertFalse(qualifier1.isSameBatch(qualifier3));
+ }
+
+ private DbRowOp mockRow(Persistent persistent) {
+ DbRowOp row = mock(DbRowOp.class);
+ ObjectId objectId = persistent.getObjectId();
+ when(row.getChangeId()).thenReturn(objectId);
+ when(row.getObject()).thenReturn(persistent);
+ when(row.getEntity()).thenReturn(mockEntity());
+ return row;
+ }
+
+ private Persistent mockObject(ObjectId id) {
+ Persistent persistent = mock(Persistent.class);
+ when(persistent.getObjectId()).thenReturn(id);
+ when(persistent.getPersistenceState()).thenReturn(PersistenceState.MODIFIED);
+ return persistent;
+ }
+
+ private DbEntity mockEntity() {
+ DbAttribute attribute1 = new DbAttribute("id");
+ attribute1.setPrimaryKey(true);
+ DbAttribute attribute2 = new DbAttribute("attr");
+ DbEntity testEntity = new DbEntity("TEST");
+ testEntity.addAttribute(attribute1);
+ testEntity.addAttribute(attribute2);
+ return testEntity;
+ }
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/ValuesTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/ValuesTest.java
new file mode 100644
index 0000000..fe22066
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/ValuesTest.java
@@ -0,0 +1,109 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import java.util.Collections;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @since 4.2
+ */
+public class ValuesTest {
+
+ @Test
+ public void testEmptyValues() {
+ ObjectId id = ObjectId.of("test", "id", 123);
+ Persistent persistent = mockObject(id);
+ DbRowOp row = mockRow(persistent);
+
+ Values values = new Values(row, false);
+
+ assertTrue(values.getUpdatedAttributes().isEmpty());
+ assertTrue(values.getSnapshot().isEmpty());
+ assertTrue(values.getFlattenedIds().isEmpty());
+ assertTrue(values.isEmpty());
+ }
+
+ @Test
+ public void testValuesWithId() {
+ ObjectId id = ObjectId.of("test", "id", 123);
+ Persistent persistent = mockObject(id);
+ DbRowOp row = mockRow(persistent);
+
+ Values values = new Values(row, true);
+
+ assertTrue(values.getUpdatedAttributes().isEmpty());
+ assertTrue(values.getFlattenedIds().isEmpty());
+ assertEquals(Collections.singletonMap("id", 123), values.getSnapshot());
+ assertFalse(values.isEmpty());
+ }
+
+ @Test
+ public void testValuesWithUpdatedAttributes() {
+ ObjectId id = ObjectId.of("test", "id", 123);
+ Persistent persistent = mockObject(id);
+ DbRowOp row = mockRow(persistent);
+
+ Values values = new Values(row, false);
+ DbAttribute attr1 = new DbAttribute("attr1");
+ values.addValue(attr1, 32);
+
+ assertEquals(Collections.singletonList(attr1), values.getUpdatedAttributes());
+ assertEquals(Collections.singletonMap("attr1", 32), values.getSnapshot());
+ assertTrue(values.getFlattenedIds().isEmpty());
+ assertFalse(values.isEmpty());
+ }
+
+ private DbRowOp mockRow(Persistent persistent) {
+ DbRowOp row = mock(DbRowOp.class);
+ ObjectId objectId = persistent.getObjectId();
+ when(row.getChangeId()).thenReturn(objectId);
+ when(row.getObject()).thenReturn(persistent);
+ when(row.getEntity()).thenReturn(mockEntity());
+ return row;
+ }
+
+ private Persistent mockObject(ObjectId id) {
+ Persistent persistent = mock(Persistent.class);
+ when(persistent.getObjectId()).thenReturn(id);
+ when(persistent.getPersistenceState()).thenReturn(PersistenceState.MODIFIED);
+ return persistent;
+ }
+
+ private DbEntity mockEntity() {
+ DbAttribute attribute1 = new DbAttribute("id");
+ attribute1.setPrimaryKey(true);
+ DbAttribute attribute2 = new DbAttribute("attr");
+ DbEntity testEntity = new DbEntity("TEST");
+ testEntity.addAttribute(attribute1);
+ testEntity.addAttribute(attribute2);
+ return testEntity;
+ }
+}
\ No newline at end of file