Skip to content

Commit f2bc5ac

Browse files
authored
fix(datastore): merge incoming mutation with existing update mutation (#1379)
* chore(datastore): merge incoming mutation with existing update pending mutation in the queue * chore(datastore): address PR comments * add comments * move anotation inline * rename saveIncomingAndNotify to saveAndNotify
1 parent b72c7eb commit f2bc5ac

File tree

3 files changed

+111
-5
lines changed

3 files changed

+111
-5
lines changed

aws-datastore/src/main/java/com/amplifyframework/datastore/syncengine/PersistentMutationOutbox.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.amplifyframework.core.Amplify;
2424
import com.amplifyframework.core.model.Model;
2525
import com.amplifyframework.core.model.ModelSchema;
26+
import com.amplifyframework.core.model.SerializedModel;
2627
import com.amplifyframework.core.model.query.Where;
2728
import com.amplifyframework.core.model.query.predicate.QueryPredicate;
2829
import com.amplifyframework.core.model.query.predicate.QueryPredicates;
@@ -329,13 +330,29 @@ private Completable handleIncomingUpdate() {
329330
// we're simply performing the create (with the updated item item contents)
330331
return overwriteExistingAndNotify(PendingMutation.Type.CREATE, QueryPredicates.all());
331332
case UPDATE:
333+
// If the incoming update does not have a condition, we want to delete any
334+
// existing mutations for the modelId before saving the incoming one.
332335
if (QueryPredicates.all().equals(incoming.getPredicate())) {
333-
// If the incoming update does not have a condition, we want to delete any
334-
// existing mutations for the modelId before saving the incoming one.
335-
return removeNotLocking(existing.getMutationId()).andThen(saveIncomingAndNotify());
336+
// If the incoming & existing update is of type serializedModel
337+
// then merge the existing model data into incoming.
338+
if (incoming.getMutatedItem() instanceof SerializedModel
339+
&& existing.getMutatedItem() instanceof SerializedModel) {
340+
SerializedModel mergedSerializedModel = SerializedModel.merge(
341+
(SerializedModel) incoming.getMutatedItem(),
342+
(SerializedModel) existing.getMutatedItem(),
343+
incoming.getModelSchema());
344+
@SuppressWarnings("unchecked") // cast SerializedModel to Model
345+
PendingMutation<T> mergedPendingMutation = (PendingMutation<T>) PendingMutation.update(
346+
mergedSerializedModel,
347+
incoming.getModelSchema());
348+
return removeNotLocking(existing.getMutationId())
349+
.andThen(saveAndNotify(mergedPendingMutation));
350+
} else {
351+
return removeNotLocking(existing.getMutationId()).andThen(saveAndNotify(incoming));
352+
}
336353
} else {
337354
// If it has a condition, we want to just add it to the queue
338-
return saveIncomingAndNotify();
355+
return saveAndNotify(incoming);
339356
}
340357
case DELETE:
341358
// Incoming update after a delete -> throw exception
@@ -381,7 +398,7 @@ private Completable overwriteExistingAndNotify(@NonNull PendingMutation.Type typ
381398
.andThen(notifyContentAvailable());
382399
}
383400

384-
private Completable saveIncomingAndNotify() {
401+
private Completable saveAndNotify(PendingMutation<T> incoming) {
385402
return save(incoming)
386403
.andThen(notifyContentAvailable());
387404
}

aws-datastore/src/test/java/com/amplifyframework/datastore/syncengine/PersistentMutationOutboxTest.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.amplifyframework.AmplifyException;
1919
import com.amplifyframework.core.model.Model;
2020
import com.amplifyframework.core.model.ModelSchema;
21+
import com.amplifyframework.core.model.SerializedModel;
2122
import com.amplifyframework.core.model.query.Where;
2223
import com.amplifyframework.core.model.query.predicate.QueryPredicates;
2324
import com.amplifyframework.datastore.DataStoreException;
@@ -630,6 +631,70 @@ public void existingUpdateIncomingUpdateWithoutConditionRewritesExistingMutation
630631
);
631632
}
632633

634+
/**
635+
* When there is an existing SerializedModel update mutation, and a new SerializedModel update mutation comes in,
636+
* then we need to merge any existing mutations for that modelId and create the new one.
637+
* @throws AmplifyException On failure to find the serializedModel difference.
638+
* @throws InterruptedException If interrupted while awaiting terminal result in test observer
639+
*/
640+
@Test
641+
public void existingSerializedModelUpdateIncomingUpdateWithoutConditionMergesWithExistingMutation()
642+
throws AmplifyException, InterruptedException {
643+
// Arrange an existing update mutation
644+
BlogOwner modelInSqlLite = BlogOwner.builder()
645+
.name("Papa Tony")
646+
.wea("Something")
647+
.build();
648+
649+
BlogOwner initialUpdate = BlogOwner.builder()
650+
.name("Tony Jr")
651+
.id(modelInSqlLite.getId())
652+
.build();
653+
654+
PendingMutation<SerializedModel> initialUpdatePendingMutation =
655+
PendingMutation.update(SerializedModel.difference(initialUpdate, modelInSqlLite, schema), schema);
656+
String existingUpdateId = initialUpdatePendingMutation.getMutationId().toString();
657+
mutationOutbox.enqueue(initialUpdatePendingMutation).blockingAwait();
658+
659+
// Act: try to enqueue a new update mutation when there already is one
660+
BlogOwner incomingUpdatedModel = BlogOwner.builder()
661+
.name("Papa Tony")
662+
.wea("something else")
663+
.id(modelInSqlLite.getId())
664+
.build();
665+
PendingMutation<SerializedModel> incomingUpdate = PendingMutation.update(
666+
SerializedModel.difference(incomingUpdatedModel, modelInSqlLite, schema),
667+
schema);
668+
String incomingUpdateId = incomingUpdate.getMutationId().toString();
669+
TestObserver<Void> enqueueObserver = mutationOutbox.enqueue(incomingUpdate).test();
670+
671+
// Assert: OK. The new mutation is accepted
672+
enqueueObserver.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
673+
enqueueObserver.assertComplete();
674+
675+
// Assert: the existing mutation has been removed
676+
assertRecordCountForMutationId(existingUpdateId, 0);
677+
678+
// And the new one has been added to the queue
679+
assertRecordCountForMutationId(incomingUpdateId, 0);
680+
681+
List<PersistentRecord> pendingMutationsFromStorage = getAllPendingMutationRecordFromStorage();
682+
for (PersistentRecord record : pendingMutationsFromStorage) {
683+
if (!record.getContainedModelId().equals(incomingUpdate.getMutatedItem().getId())) {
684+
pendingMutationsFromStorage.remove(record);
685+
}
686+
}
687+
// Ensure the new one is in storage.
688+
PendingMutation<SerializedModel> storedMutation =
689+
converter.fromRecord(pendingMutationsFromStorage.get(0));
690+
// This is the name from the second model, not the first!!
691+
assertEquals(initialUpdate.getName(),
692+
storedMutation.getMutatedItem().getSerializedData().get("name"));
693+
// wea got merged from existing model!!
694+
assertEquals(incomingUpdatedModel.getWea(),
695+
storedMutation.getMutatedItem().getSerializedData().get("wea"));
696+
}
697+
633698
/**
634699
* When there is an existing creation mutation, and an update comes in,
635700
* the exiting creation should be updated with the contents of the incoming
@@ -1032,4 +1097,8 @@ private void assertRecordCountForMutationId(String mutationId, int expectedCount
10321097
private List<PersistentRecord> getPendingMutationRecordFromStorage(String mutationId) throws DataStoreException {
10331098
return storage.query(PersistentRecord.class, Where.id(mutationId));
10341099
}
1100+
1101+
private List<PersistentRecord> getAllPendingMutationRecordFromStorage() throws DataStoreException {
1102+
return storage.query(PersistentRecord.class, Where.matchesAll());
1103+
}
10351104
}

core/src/main/java/com/amplifyframework/core/model/SerializedModel.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,26 @@ public static <T extends Model> SerializedModel difference(T updated, T original
8989
.build();
9090
}
9191

92+
/**
93+
* Merge the serialized data from existing to incoming model.
94+
* @param incoming the incoming Model to which serialized data fields will be added.
95+
* @param existing the original Model to compare against.
96+
* @param modelSchema ModelSchema for the Models between compared.
97+
* @return a SerializedModel, containing the values from the incoming Model and existing Model.
98+
*/
99+
public static SerializedModel merge(SerializedModel incoming, SerializedModel existing, ModelSchema modelSchema) {
100+
Map<String, Object> mergedSerializedData = new HashMap<>(incoming.serializedData);
101+
for (String key : existing.getSerializedData().keySet()) {
102+
if (!mergedSerializedData.containsKey(key)) {
103+
mergedSerializedData.put(key, existing.getSerializedData().get(key));
104+
}
105+
}
106+
return SerializedModel.builder()
107+
.serializedData(mergedSerializedData)
108+
.modelSchema(modelSchema)
109+
.build();
110+
}
111+
92112
/**
93113
* Return a builder of {@link SerializedModel}.
94114
* @return A serialized model builder

0 commit comments

Comments
 (0)