Skip to content

Commit ef38ef7

Browse files
author
Brian Chen
authored
Add verify support (#1107)
1 parent 29f561d commit ef38ef7

File tree

7 files changed

+146
-53
lines changed

7 files changed

+146
-53
lines changed

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java

Lines changed: 32 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import java.util.ArrayList;
3535
import java.util.Arrays;
3636
import java.util.List;
37-
import java.util.Map;
3837
import java.util.concurrent.CountDownLatch;
3938
import java.util.concurrent.atomic.AtomicInteger;
4039
import org.junit.After;
@@ -203,19 +202,6 @@ public void tearDown() {
203202
IntegrationTestUtil.tearDown();
204203
}
205204

206-
@Test
207-
public void testGetDocuments() {
208-
FirebaseFirestore firestore = testFirestore();
209-
DocumentReference doc = firestore.collection("spaces").document();
210-
Map<String, Object> value = map("foo", 1, "desc", "Stuff", "owner", "Jonny");
211-
waitFor(doc.set(value));
212-
Exception e = waitForException(firestore.runTransaction(transaction -> transaction.get(doc)));
213-
// We currently require every document read to also be written.
214-
// TODO: Fix this check once we drop that requirement.
215-
assertEquals(Code.INVALID_ARGUMENT, ((FirebaseFirestoreException) e).getCode());
216-
assertEquals("Every document read in a transaction must also be written.", e.getMessage());
217-
}
218-
219205
@Test
220206
public void testRunsTransactionsAfterGettingExistingDoc() {
221207
FirebaseFirestore firestore = testFirestore();
@@ -513,38 +499,32 @@ public void testUpdatePOJOTransactionally() {
513499
}
514500

515501
@Test
516-
public void testHandleReadingOneDocAndWritingAnother() {
502+
public void testRetriesWhenDocumentThatWasReadWithoutBeingWrittenChanges() {
517503
FirebaseFirestore firestore = testFirestore();
518504
DocumentReference doc1 = firestore.collection("counters").document();
519505
DocumentReference doc2 = firestore.collection("counters").document();
520506
CountDownLatch latch = new CountDownLatch(2);
521507
waitFor(doc1.set(map("count", 15)));
522-
Exception e =
523-
waitForException(
524-
firestore.runTransaction(
525-
transaction -> {
526-
latch.countDown();
527-
// Get the first doc.
528-
transaction.get(doc1);
529-
// Do a write outside of the transaction. The first time the
530-
// transaction is tried, this will bump the version, which
531-
// will cause the write to doc2 to fail. The second time, it
532-
// will be a no-op and not bump the version.
533-
waitFor(doc1.set(map("count", 1234)));
534-
// Now try to update the other doc from within the transaction.
535-
// This should fail once, because we read 15 earlier.
536-
transaction.set(doc2, map("count", 16));
537-
return null;
538-
}));
539-
// We currently require every document read to also be written.
540-
// TODO: Add this check back once we drop that.
541-
// Document snapshot = waitFor(doc1.get());
542-
// assertEquals(0, tries.getCount());
543-
// assertEquals(1234, snapshot.getDouble("count"));
544-
// snapshot = waitFor(doc2.get());
545-
// assertEquals(16, snapshot.getDouble("count"));
546-
assertEquals(Code.INVALID_ARGUMENT, ((FirebaseFirestoreException) e).getCode());
547-
assertEquals("Every document read in a transaction must also be written.", e.getMessage());
508+
waitFor(
509+
firestore.runTransaction(
510+
transaction -> {
511+
latch.countDown();
512+
// Get the first doc.
513+
transaction.get(doc1);
514+
// Do a write outside of the transaction. The first time the
515+
// transaction is tried, this will bump the version, which
516+
// will cause the write to doc2 to fail. The second time, it
517+
// will be a no-op and not bump the version.
518+
waitFor(doc1.set(map("count", 1234)));
519+
// Now try to update the other doc from within the transaction.
520+
// This should fail once, because we read 15 earlier.
521+
transaction.set(doc2, map("count", 16));
522+
return null;
523+
}));
524+
DocumentSnapshot snapshot = waitFor(doc1.get());
525+
assertEquals(0, latch.getCount());
526+
assertTrue(snapshot.exists());
527+
assertEquals(1234L, snapshot.getData().get("count"));
548528
}
549529

550530
@Test
@@ -616,17 +596,20 @@ public void testReadAndUpdateNonExistentDocumentWithExternalWrite() {
616596
}
617597

618598
@Test
619-
public void testCannotHaveAGetWithoutMutations() {
599+
public void testCanHaveGetsWithoutMutations() {
620600
FirebaseFirestore firestore = testFirestore();
621601
DocumentReference doc = firestore.collection("foo").document();
602+
DocumentReference doc2 = firestore.collection("foo").document();
622603
waitFor(doc.set(map("foo", "bar")));
623-
624-
Exception e = waitForException(firestore.runTransaction(transaction -> transaction.get(doc)));
625-
// We currently require every document read to also be written.
626-
// TODO: Add this check back once we drop that.
627-
// assertEquals("bar", snapshot.getString("foo"));
628-
assertEquals(Code.INVALID_ARGUMENT, ((FirebaseFirestoreException) e).getCode());
629-
assertEquals("Every document read in a transaction must also be written.", e.getMessage());
604+
waitFor(
605+
firestore.runTransaction(
606+
transaction -> {
607+
transaction.get(doc2);
608+
return transaction.get(doc);
609+
}));
610+
DocumentSnapshot snapshot = waitFor(doc.get());
611+
assertTrue(snapshot.exists());
612+
assertEquals(map("foo", "bar"), snapshot.getData());
630613
}
631614

632615
@Test

firebase-firestore/src/main/java/com/google/firebase/firestore/core/Transaction.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.google.firebase.firestore.model.mutation.DeleteMutation;
3333
import com.google.firebase.firestore.model.mutation.Mutation;
3434
import com.google.firebase.firestore.model.mutation.Precondition;
35+
import com.google.firebase.firestore.model.mutation.VerifyMutation;
3536
import com.google.firebase.firestore.remote.Datastore;
3637
import com.google.firebase.firestore.util.Executors;
3738
import java.util.ArrayList;
@@ -136,10 +137,9 @@ public Task<Void> commit() {
136137
for (Mutation mutation : mutations) {
137138
unwritten.remove(mutation.getKey());
138139
}
139-
if (unwritten.size() > 0) {
140-
return Tasks.forException(
141-
new FirebaseFirestoreException(
142-
"Every document read in a transaction must also be written.", Code.INVALID_ARGUMENT));
140+
// For each document that was read but not written to, we want to perform a `verify` operation.
141+
for (DocumentKey key : unwritten) {
142+
mutations.add(new VerifyMutation(key, precondition(key)));
143143
}
144144
committed = true;
145145
return datastore
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.model.mutation;
16+
17+
import androidx.annotation.Nullable;
18+
import com.google.firebase.Timestamp;
19+
import com.google.firebase.firestore.model.DocumentKey;
20+
import com.google.firebase.firestore.model.MaybeDocument;
21+
import com.google.firebase.firestore.model.value.ObjectValue;
22+
import com.google.firebase.firestore.util.Assert;
23+
24+
/**
25+
* A mutation that verifies the existence of the document at the given key with the provided
26+
* precondition.
27+
*
28+
* <p>The `verify` operation is only used in Transactions, and this class serves primarily to
29+
* facilitate serialization into protos.
30+
*/
31+
public final class VerifyMutation extends Mutation {
32+
33+
public VerifyMutation(DocumentKey key, Precondition precondition) {
34+
super(key, precondition);
35+
}
36+
37+
@Override
38+
public boolean equals(Object o) {
39+
if (this == o) {
40+
return true;
41+
}
42+
if (o == null || getClass() != o.getClass()) {
43+
return false;
44+
}
45+
46+
VerifyMutation that = (VerifyMutation) o;
47+
return hasSameKeyAndPrecondition(that);
48+
}
49+
50+
@Override
51+
public int hashCode() {
52+
return keyAndPreconditionHashCode();
53+
}
54+
55+
@Override
56+
public String toString() {
57+
return "VerifyMutation{" + keyAndPreconditionToString() + "}";
58+
}
59+
60+
@Override
61+
public MaybeDocument applyToRemoteDocument(
62+
@Nullable MaybeDocument maybeDoc, MutationResult mutationResult) {
63+
throw Assert.fail("VerifyMutation should only be used in Transactions.");
64+
}
65+
66+
@Nullable
67+
@Override
68+
public MaybeDocument applyToLocalView(
69+
@Nullable MaybeDocument maybeDoc, @Nullable MaybeDocument baseDoc, Timestamp localWriteTime) {
70+
throw Assert.fail("VerifyMutation should only be used in Transactions.");
71+
}
72+
73+
@Nullable
74+
@Override
75+
public ObjectValue extractBaseValue(@Nullable MaybeDocument maybeDoc) {
76+
throw Assert.fail("VerifyMutation should only be used in Transactions.");
77+
}
78+
}

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import com.google.firebase.firestore.model.mutation.SetMutation;
5252
import com.google.firebase.firestore.model.mutation.TransformMutation;
5353
import com.google.firebase.firestore.model.mutation.TransformOperation;
54+
import com.google.firebase.firestore.model.mutation.VerifyMutation;
5455
import com.google.firebase.firestore.model.value.ArrayValue;
5556
import com.google.firebase.firestore.model.value.BlobValue;
5657
import com.google.firebase.firestore.model.value.BooleanValue;
@@ -443,6 +444,8 @@ public com.google.firestore.v1.Write encodeMutation(Mutation mutation) {
443444
builder.setTransform(transformBuilder);
444445
} else if (mutation instanceof DeleteMutation) {
445446
builder.setDelete(encodeKey(mutation.getKey()));
447+
} else if (mutation instanceof VerifyMutation) {
448+
builder.setVerify(encodeKey(mutation.getKey()));
446449
} else {
447450
throw fail("unknown mutation type %s", mutation.getClass());
448451
}
@@ -489,6 +492,9 @@ public Mutation decodeMutation(com.google.firestore.v1.Write mutation) {
489492
return new TransformMutation(
490493
decodeKey(mutation.getTransform().getDocument()), fieldTransforms);
491494

495+
case VERIFY:
496+
return new VerifyMutation(decodeKey(mutation.getVerify()), precondition);
497+
492498
default:
493499
throw fail("Unknown mutation operation: %d", mutation.getOperationCase());
494500
}

firebase-firestore/src/proto/google/firestore/v1/write.proto

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ message Write {
4242
// `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
4343
string delete = 2;
4444

45+
// The name of a document on which to verify the `current_document`
46+
// precondition.
47+
// This only requires read access to the document.
48+
string verify = 5;
49+
4550
// Applies a tranformation to a document.
4651
// At most one `transform` per document is allowed in a given request.
4752
// An `update` cannot follow a `transform` on the same document in a given

firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static com.google.firebase.firestore.testutil.TestUtil.ref;
2828
import static com.google.firebase.firestore.testutil.TestUtil.setMutation;
2929
import static com.google.firebase.firestore.testutil.TestUtil.transformMutation;
30+
import static com.google.firebase.firestore.testutil.TestUtil.verifyMutation;
3031
import static com.google.firebase.firestore.testutil.TestUtil.wrap;
3132
import static java.util.Arrays.asList;
3233
import static org.junit.Assert.assertEquals;
@@ -317,6 +318,21 @@ public void testEncodeDeleteMutation() {
317318
assertRoundTripForMutation(mutation, expected);
318319
}
319320

321+
@Test
322+
public void testEncodeVerifyMutation() {
323+
Mutation mutation = verifyMutation("docs/1", 4);
324+
325+
com.google.firestore.v1.Write expected =
326+
Write.newBuilder()
327+
.setVerify("projects/p/databases/d/documents/docs/1")
328+
.setCurrentDocument(
329+
Precondition.newBuilder()
330+
.setUpdateTime(Timestamp.newBuilder().setNanos(4000).build())
331+
.build())
332+
.build();
333+
assertRoundTripForMutation(mutation, expected);
334+
}
335+
320336
@Test
321337
public void testEncodeSetMutation() {
322338
Mutation mutation = setMutation("docs/1", map("key", "value"));

firebase-firestore/src/testUtil/java/com/google/firebase/firestore/testutil/TestUtil.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import com.google.firebase.firestore.model.mutation.Precondition;
6363
import com.google.firebase.firestore.model.mutation.SetMutation;
6464
import com.google.firebase.firestore.model.mutation.TransformMutation;
65+
import com.google.firebase.firestore.model.mutation.VerifyMutation;
6566
import com.google.firebase.firestore.model.value.FieldValue;
6667
import com.google.firebase.firestore.model.value.ObjectValue;
6768
import com.google.firebase.firestore.remote.RemoteEvent;
@@ -506,6 +507,10 @@ public static DeleteMutation deleteMutation(String path) {
506507
return new DeleteMutation(key(path), Precondition.NONE);
507508
}
508509

510+
public static VerifyMutation verifyMutation(String path, int micros) {
511+
return new VerifyMutation(key(path), Precondition.updateTime(version(micros)));
512+
}
513+
509514
/**
510515
* Creates a TransformMutation by parsing any FieldValue sentinels in the provided data. The data
511516
* is expected to use dotted-notation for nested fields (i.e. { "foo.bar": FieldValue.foo() } and

0 commit comments

Comments
 (0)