Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/main/java/org/datanucleus/store/types/SCOUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -857,13 +857,16 @@ public static boolean updateListWithListElements(List list, List elements)
}
}

// Update position of elements in the list to match the new order
// Update position of elements in the list to match the new order.
// Take a snapshot of the current order via iteration rather than using list.get(position) directly,
// to avoid inconsistencies between delegate and backing store when collection caching is disabled.
java.util.ArrayList currentOrder = new java.util.ArrayList(list);
elementsIter = elements.iterator();
int position = 0;
while (elementsIter.hasNext())
{
Object element = elementsIter.next();
Object currentElement = list.get(position);
Object currentElement = currentOrder.get(position);
boolean updatePosition = false;
if ((element == null && currentElement != null) || (element != null && currentElement == null))
{
Expand All @@ -879,6 +882,7 @@ else if (element != null && currentElement != null && !currentElement.equals(ele
{
// Update the position, taking care not to have dependent-field deletes taking place
((SCOList) list).set(position, element, false);
currentOrder.set(position, element);
updated = true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1035,10 +1035,8 @@ public E set(int index, E element, boolean allowDependentField)

makeDirty();

if (useCache)
{
loadFromStore();
}
// Always load the delegate, even when useCache=false, since delegate.set() requires it to be populated
loadFromStore();

E delegateReturn = delegate.set(index, element);
if (backingStore != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1059,10 +1059,8 @@ public E set(int index, E element, boolean allowDependentField)

makeDirty();

if (useCache)
{
loadFromStore();
}
// Always load the delegate, even when useCache=false, since delegate.set() requires it to be populated
loadFromStore();

E delegateReturn = delegate.set(index, element);
if (backingStore != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1033,10 +1033,8 @@ public E set(int index, E element, boolean allowDependentField)

makeDirty();

if (useCache)
{
loadFromStore();
}
// Always load the delegate, even when useCache=false, since delegate.set() requires it to be populated
loadFromStore();

// Update the delegate since updating this can cause removal of the original object
E delegateReturn = delegate.set(index, element);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1116,10 +1116,8 @@ public E set(int index, E element, boolean allowDependentField)

makeDirty();

if (useCache)
{
loadFromStore();
}
// Always load the delegate, even when useCache=false, since delegate.set() requires it to be populated
loadFromStore();

E delegateReturn = delegate.set(index, element);
if (backingStore != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1131,10 +1131,8 @@ public E set(int index, E element, boolean allowDependentField)

makeDirty();

if (useCache)
{
loadFromStore();
}
// Always load the delegate, even when useCache=false, since delegate.set() requires it to be populated
loadFromStore();

E delegateReturn = delegate.set(index, element);
if (backingStore != null)
Expand Down
101 changes: 101 additions & 0 deletions src/test/java/org/datanucleus/store/types/sco/SCOUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@

import junit.framework.TestCase;

import org.datanucleus.FetchPlanState;
import org.datanucleus.metadata.AbstractMemberMetaData;
import org.datanucleus.state.DNStateManager;
import org.datanucleus.store.StoreManager;
import org.datanucleus.store.types.SCOList;
import org.datanucleus.store.types.SCOUtils;
import org.datanucleus.store.types.scostore.CollectionStore;

Expand Down Expand Up @@ -42,6 +44,105 @@ public void testToArrayCollectionStoreStateManagerObjectArray()
assertEquals(arr[1],"TEST2");
}

/**
* Test that updateListWithListElements handles reordering when an element is inserted at the beginning.
* Reproduces the scenario from GitHub issue #526.
* @see <a href="https://github.com/datanucleus/datanucleus-core/issues/526">GitHub #526</a>
* @see <a href="https://github.com/datanucleus/tests/pull/92">Integration test</a>
*/
public void testUpdateListWithListElementsReorder()
{
TestSCOList list = new TestSCOList();
list.add("A");
list.add("B");
list.add("C");

// Desired order: insert "D" at beginning
java.util.List<String> elements = new java.util.ArrayList<>();
elements.add("D");
elements.add("A");
elements.add("B");
elements.add("C");

boolean updated = SCOUtils.updateListWithListElements(list, elements);

assertTrue("List should be marked as updated", updated);
assertEquals(4, list.size());
assertEquals("D", list.get(0));
assertEquals("A", list.get(1));
assertEquals("B", list.get(2));
assertEquals("C", list.get(3));
}

/**
* Test that updateListWithListElements handles combined add, remove, and reorder.
*/
public void testUpdateListWithListElementsAddRemoveReorder()
{
TestSCOList list = new TestSCOList();
list.add("A");
list.add("B");
list.add("C");

// Remove "B", add "D", reorder to [C, D, A]
java.util.List<String> elements = new java.util.ArrayList<>();
elements.add("C");
elements.add("D");
elements.add("A");

boolean updated = SCOUtils.updateListWithListElements(list, elements);

assertTrue("List should be marked as updated", updated);
assertEquals(3, list.size());
assertEquals("C", list.get(0));
assertEquals("D", list.get(1));
assertEquals("A", list.get(2));
}

/**
* Test that updateListWithListElements returns false when nothing changes.
*/
public void testUpdateListWithListElementsNoChange()
{
TestSCOList list = new TestSCOList();
list.add("A");
list.add("B");

java.util.List<String> elements = new java.util.ArrayList<>();
elements.add("A");
elements.add("B");

boolean updated = SCOUtils.updateListWithListElements(list, elements);
assertFalse("List should not be marked as updated when nothing changes", updated);
}

/**
* Minimal SCOList implementation backed by an ArrayList, for testing updateListWithListElements.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private static class TestSCOList extends java.util.ArrayList implements SCOList
{
public Object set(int index, Object element, boolean allowDependentField)
{
return super.set(index, element);
}

public void updateEmbeddedElement(Object element, int fieldNumber, Object value, boolean makeDirty) {}
public boolean remove(Object element, boolean allowCascadeDelete) { return super.remove(element); }
public void load() {}
public boolean isLoaded() { return true; }
public void setValue(Object value) {}
public void initialise(Object value) {}
public void initialise() {}
public void initialise(Object newValue, Object oldValue) {}
public String getFieldName() { return null; }
public Object getOwner() { return null; }
public void unsetOwner() {}
public Object getValue() { return this; }
public Object detachCopy(FetchPlanState state) { return null; }
public void attachCopy(Object value) {}
}

private static class BackingStore implements CollectionStore
{
Collection elm;
Expand Down