-
Notifications
You must be signed in to change notification settings - Fork 626
Overlay on top of mutation list #2962
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Coverage ReportAffected SDKsNo changes between base commit (cf7680d) and head commit (aa3208b8). Test Logs
NotesHTML coverage reports can be produced locally with Head commit (aa3208b8) is created by Prow via merging commits: cf7680d 95e2542. |
Binary Size ReportAffected SDKs
Test Logs
NotesHead commit (aa3208b8) is created by Prow via merging commits: cf7680d 95e2542. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for playin with this. Some more ideas for simplification - some or all of which might not work.
firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/Mutation.java
Outdated
Show resolved
Hide resolved
...base-firestore/src/main/java/com/google/firebase/firestore/model/mutation/PatchMutation.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/Mutation.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I am adding tests back to validate this impl...and cleaning it up.
...base-firestore/src/main/java/com/google/firebase/firestore/model/mutation/PatchMutation.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/Mutation.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/Mutation.java
Outdated
Show resolved
Hide resolved
assertEquals(doc, original); | ||
} | ||
|
||
// TODO(overlay): This test fails because deleting nested fields like bar.baz leaves an empty bar |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@schmidt-sebastian This currently fails, but I do not think this is an issue, we should discuss this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is an issue (thanks for adding the test!). This changes the behavior - we should be left with an empty map. I don't (yet) know how to solve this though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good! Most of the comments are nits about a repeated pattern that we should be able to fix easily.
protected ObjectValue getValue() { | ||
return null; | ||
} | ||
|
||
@Nullable | ||
protected FieldMask getMask() { | ||
return null; | ||
} | ||
; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are not needed anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -68,11 +70,26 @@ public void applyToRemoteDocument(MutableDocument document, MutationResult mutat | |||
} | |||
|
|||
@Override | |||
public void applyToLocalView(MutableDocument document, Timestamp localWriteTime) { | |||
public FieldMask applyToLocalView( | |||
MutableDocument document, Timestamp localWriteTime, FieldMask mask) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
previousMask? You probably also want to re-order the arguments and move localWriteTime
last. The first two arguments then together describe the previous state,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
} | ||
|
||
public static FieldMask emptyMask() { | ||
return new FieldMask(new HashSet<FieldPath>(), Scope.NONE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None+[] should behave similarly to All_Fields+[]. We should be able to reduce the two field mask kinds to set and unset, which we currently represent as null
or FieldMask
. Is it possible to use a nullable FieldMask again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null
could potentially have two meaning:
- ALL_FIELDS as currently used by Set and Delete.
- NONE, as the initial mask to specify that no mutations have been applied. If a mutation's precondition failed to match, the resulting mask stays unchanged.
I personally find isAllFields
checks carry more meaning then null checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am still trying to get it to work without this change (will continue tomorrow).
Reasons I advocate against it:
- It exposes a lot of state on the FieldMask class, some of which is mutually exclusive and only sensical if the consumer understand the underlying states. I wish we could somehow redesign this so that the mask type is hidden from the API.
- It breaks
getMask()
- It breaks
covers()
(which can be fixed)
We would also need to fix equals+hashCode.
I will report back tomorrow if I can come up with a solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Follow up PR: #2983
Let me know what you think.
Note that the changes I made uncover a versioning issue that exists in this PR as well (there is a comment in the code changes explaining it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM for the follow up, and thanks for doing it!
Re: version issue, this could be fixed by having a version update in the final mutation. We do need to serialize this as well. Thoughts?
@@ -183,11 +187,13 @@ static SnapshotVersion getPostMutationVersion(MutableDocument document) { | |||
* result of applying a transform) for use when applying a transform locally. | |||
* | |||
* @param localWriteTime The local time of the mutation (used to generate ServerTimestampValues). | |||
* @param mutableDocument The current state of the document after applying all previous mutations. | |||
* @param mutableDocument The document to apply transforms on. | |||
* @param mutation The mutation to be applied to {@code mutableDocument} to get field values on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field values come from mutableDocument
, don't they?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is residual I forgot to remove from previous iterations.
@@ -198,6 +204,14 @@ static SnapshotVersion getPostMutationVersion(MutableDocument document) { | |||
return transformResults; | |||
} | |||
|
|||
protected List<FieldPath> getFieldTransformPaths() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can live in PatchMutation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
if (doc.isNoDocument()) { | ||
squashed = new DeleteMutation(doc.getKey(), Precondition.NONE); | ||
} else { | ||
squashed = new SetMutation(doc.getKey(), doc.getData(), Precondition.NONE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to handle the case where all preconditions fail to apply? I think we don't but we should verify via tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a test.
ObjectValue docValue = doc.getData(); | ||
ObjectValue objectValue = new ObjectValue(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you improve the names here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
// If it is deleting a nested field, we take the immediate parent as the mask used to | ||
// construct resulting mutation. | ||
// Justification: Nested fields can create parent fields implicitly, if they are | ||
// deleted in later mutations, there is no trace that the empty parent fields should | ||
// still remain (which is expected). | ||
// Consider mutation (foo.bar 1), then mutation (foo.bar delete()). | ||
// This leaves the final result (foo, {}). Despite `doc` has the correct result, | ||
// `foo` is not in `mask`, and the resulting mutation will miss `foo` completely without | ||
// this logic. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// If it is deleting a nested field, we take the immediate parent as the mask used to | |
// construct resulting mutation. | |
// Justification: Nested fields can create parent fields implicitly, if they are | |
// deleted in later mutations, there is no trace that the empty parent fields should | |
// still remain (which is expected). | |
// Consider mutation (foo.bar 1), then mutation (foo.bar delete()). | |
// This leaves the final result (foo, {}). Despite `doc` has the correct result, | |
// `foo` is not in `mask`, and the resulting mutation will miss `foo` completely without | |
// this logic. | |
// If we are deleting a nested field, we take the immediate parent as the mask used to | |
// construct resulting mutation. | |
// Justification: Nested fields can create parent fields implicitly. If only a leaf entry | |
// deleted in later mutations, the parent field should still remain, but we may have | |
// lost this information. | |
// Consider mutation (foo.bar 1), then mutation (foo.bar delete()). | |
// This leaves the final result (foo, {}). Despite the fact that `doc` has the correct result, | |
// `foo` is not in `mask`, and the resulting mutation would then miss `foo`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Manually applied because code moved.
|
||
@Test | ||
public void testSquashFieldDeletionOfNestedField() { | ||
MutableDocument doc = doc("collection/key", 0, map("bar.baz", 1)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not created a nested object. Rather it creates an object with a single key of bar.baz
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I realized it, I can see it is confusing to leave it, changed to foo.
path = path.popLast(); | ||
} | ||
objectValue.set(path, docValue.get(path)); | ||
maskSet.add(path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code snippet should be unit tested explicitly once moved to production code.
I also just figured out why my original proposal was working: ObjectValue.set('a.b', delete())
creates field 'a', which it should not, but it made my original implementation work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I think the problem that caused my test to fail was that the both the original and the new doc did not have an entry for "bar". Not sure how this happened, but this points to another thing we need to do - we should convert most test to compare against a hard-coded end state rather than a computed end state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree on explicit unit test.
Test against hard coded end state is hard here because of the tests are generated.
With current approach (most of applyToLocalDocument
is unchanged, only added mask logic), we have good confidence that overly mutation works as original logic intended.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review!
@@ -68,11 +70,26 @@ public void applyToRemoteDocument(MutableDocument document, MutationResult mutat | |||
} | |||
|
|||
@Override | |||
public void applyToLocalView(MutableDocument document, Timestamp localWriteTime) { | |||
public FieldMask applyToLocalView( | |||
MutableDocument document, Timestamp localWriteTime, FieldMask mask) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
protected ObjectValue getValue() { | ||
return null; | ||
} | ||
|
||
@Nullable | ||
protected FieldMask getMask() { | ||
return null; | ||
} | ||
; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
} | ||
|
||
public static FieldMask emptyMask() { | ||
return new FieldMask(new HashSet<FieldPath>(), Scope.NONE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null
could potentially have two meaning:
- ALL_FIELDS as currently used by Set and Delete.
- NONE, as the initial mask to specify that no mutations have been applied. If a mutation's precondition failed to match, the resulting mask stays unchanged.
I personally find isAllFields
checks carry more meaning then null checks.
@@ -183,11 +187,13 @@ static SnapshotVersion getPostMutationVersion(MutableDocument document) { | |||
* result of applying a transform) for use when applying a transform locally. | |||
* | |||
* @param localWriteTime The local time of the mutation (used to generate ServerTimestampValues). | |||
* @param mutableDocument The current state of the document after applying all previous mutations. | |||
* @param mutableDocument The document to apply transforms on. | |||
* @param mutation The mutation to be applied to {@code mutableDocument} to get field values on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is residual I forgot to remove from previous iterations.
* @return A map of fields to transform results. | ||
*/ | ||
protected Map<FieldPath, Value> localTransformResults( | ||
Timestamp localWriteTime, MutableDocument mutableDocument) { | ||
Timestamp localWriteTime, MutableDocument mutableDocument, Mutation mutation) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
path = path.popLast(); | ||
} | ||
objectValue.set(path, docValue.get(path)); | ||
maskSet.add(path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree on explicit unit test.
Test against hard coded end state is hard here because of the tests are generated.
With current approach (most of applyToLocalDocument
is unchanged, only added mask logic), we have good confidence that overly mutation works as original logic intended.
return builder.toString(); | ||
} | ||
|
||
private <E> List<List<E>> generatePermutations(List<E> mutations) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did not know Collections2! Thanks!
/** Tests MutationSquasher */ | ||
@RunWith(RobolectricTestRunner.class) | ||
@Config(manifest = Config.NONE) | ||
public class MutationSquashTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@Test | ||
public void testSquashOneSetMutation() { | ||
Map<String, Object> data = map("foo", "foo-value", "baz", "baz-value"); | ||
squashRoundTrips( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think with current implementation, overlay
is a better name.
// If it is deleting a nested field, we take the immediate parent as the mask used to | ||
// construct resulting mutation. | ||
// Justification: Nested fields can create parent fields implicitly, if they are | ||
// deleted in later mutations, there is no trace that the empty parent fields should | ||
// still remain (which is expected). | ||
// Consider mutation (foo.bar 1), then mutation (foo.bar delete()). | ||
// This leaves the final result (foo, {}). Despite `doc` has the correct result, | ||
// `foo` is not in `mask`, and the resulting mutation will miss `foo` completely without | ||
// this logic. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Manually applied because code moved.
Pushed a new commit trying to fix the version issue. Note I have to change the test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you rebase against master
?
firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java
Outdated
Show resolved
Hide resolved
…/model/mutation/MutationTest.java Co-authored-by: Sebastian Schmidt <[email protected]>
# Conflicts: # firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/DeleteMutation.java # firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/Mutation.java # firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/SetMutation.java # firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java
Done..and thanks for the fix! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for working through this! This looks great.
No description provided.