Skip to content

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

Merged
merged 14 commits into from
Sep 22, 2021
Merged

Conversation

wu-hui
Copy link
Contributor

@wu-hui wu-hui commented Sep 8, 2021

No description provided.

@googlebot googlebot added the cla: yes Override cla label Sep 8, 2021
@google-oss-bot
Copy link
Contributor

google-oss-bot commented Sep 8, 2021

Coverage Report

Affected SDKs

No changes between base commit (cf7680d) and head commit (aa3208b8).

Test Logs

Notes

HTML coverage reports can be produced locally with ./gradlew <product>:checkCoverage.
Report files are located at <product-build-dir>/reports/jacoco/.

Head commit (aa3208b8) is created by Prow via merging commits: cf7680d 95e2542.

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Sep 8, 2021

Binary Size Report

Affected SDKs

  • firebase-firestore

    Type Base (cf7680d) Head (aa3208b8) Diff
    aar 1.20 MB 1.20 MB +1.13 kB (+0.1%)
    apk (release) 3.24 MB 3.24 MB +316 B (+0.0%)

Test Logs

Notes

Head commit (aa3208b8) is created by Prow via merging commits: cf7680d 95e2542.

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a 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.

Copy link
Contributor Author

@wu-hui wu-hui left a 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.

assertEquals(doc, original);
}

// TODO(overlay): This test fails because deleting nested fields like bar.baz leaves an empty bar
Copy link
Contributor Author

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.

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian Sep 9, 2021

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.

@wu-hui wu-hui changed the title A third way to squash. Squash mutation by materializing field transforms Sep 9, 2021
@wu-hui wu-hui changed the base branch from mrschmidt/squashdraft to master September 14, 2021 14:03
@wu-hui wu-hui changed the title Squash mutation by materializing field transforms Squash by applying mutations and keep track of mask Sep 15, 2021
@wu-hui wu-hui changed the title Squash by applying mutations and keep track of mask Squash by applying mutations and keeping track of mask Sep 15, 2021
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a 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.

Comment on lines 86 to 94
protected ObjectValue getValue() {
return null;
}

@Nullable
protected FieldMask getMask() {
return null;
}
;
Copy link
Contributor

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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,

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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:

  1. ALL_FIELDS as currently used by Set and Delete.
  2. 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.

Copy link
Contributor

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.

Copy link
Contributor

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).

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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() {
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test.

Comment on lines 421 to 422
ObjectValue docValue = doc.getData();
ObjectValue objectValue = new ObjectValue();
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 427 to 435
// 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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`.

Copy link
Contributor Author

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));
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian Sep 15, 2021

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

@wu-hui wu-hui left a 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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 86 to 94
protected ObjectValue getValue() {
return null;
}

@Nullable
protected FieldMask getMask() {
return null;
}
;
Copy link
Contributor Author

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);
Copy link
Contributor Author

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:

  1. ALL_FIELDS as currently used by Set and Delete.
  2. 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
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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);
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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(
Copy link
Contributor Author

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.

Comment on lines 427 to 435
// 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.
Copy link
Contributor Author

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.

@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Sep 16, 2021
@wu-hui wu-hui changed the title Squash by applying mutations and keeping track of mask Overlay on top of mutation list Sep 16, 2021
@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Sep 20, 2021
@wu-hui
Copy link
Contributor Author

wu-hui commented Sep 20, 2021

Pushed a new commit trying to fix the version issue.

Note I have to change the test testAppliesPatchWithMergeToDocuments to use deletedDoc(version:None) to make it pass. This fails without the new logic added in this PR if the version is 1, because post mutation version for NoDocument is always 0.

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a 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?

wu-hui and others added 3 commits September 22, 2021 09:20
…/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
@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Sep 22, 2021
@wu-hui
Copy link
Contributor Author

wu-hui commented Sep 22, 2021

Can you rebase against master?

Done..and thanks for the fix!

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a 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.

@wu-hui wu-hui merged commit 9815e45 into master Sep 22, 2021
@wu-hui wu-hui deleted the wuandy/SquashBasedOnSebastianDraft branch September 22, 2021 16:28
@firebase firebase locked and limited conversation to collaborators Oct 23, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants