Skip to content

Commit 0d26c0e

Browse files
Adding FieldValue.numericAdd()
1 parent 8ccb305 commit 0d26c0e

30 files changed

+1029
-69
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright 2018 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;
16+
17+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testDocument;
18+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor;
19+
import static com.google.firebase.firestore.testutil.TestUtil.map;
20+
import static junit.framework.Assert.assertEquals;
21+
import static junit.framework.Assert.assertFalse;
22+
23+
import com.google.android.gms.tasks.Tasks;
24+
import com.google.firebase.firestore.testutil.EventAccumulator;
25+
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
26+
import java.util.Map;
27+
import java.util.concurrent.ExecutionException;
28+
import org.junit.After;
29+
import org.junit.Before;
30+
import org.junit.Ignore;
31+
import org.junit.Test;
32+
33+
@Ignore("Not yet available in production")
34+
public class NumericTransformsTest {
35+
private static final double DOUBLE_EPSILON = 0.000001;
36+
37+
// A document reference to read and write to.
38+
private DocumentReference docRef;
39+
40+
// Accumulator used to capture events during the test.
41+
private EventAccumulator<DocumentSnapshot> accumulator;
42+
43+
// Listener registration for a listener maintained during the course of the test.
44+
private ListenerRegistration listenerRegistration;
45+
46+
@Before
47+
public void setUp() {
48+
docRef = testDocument();
49+
accumulator = new EventAccumulator<>();
50+
listenerRegistration =
51+
docRef.addSnapshotListener(MetadataChanges.INCLUDE, accumulator.listener());
52+
53+
// Wait for initial null snapshot to avoid potential races.
54+
DocumentSnapshot initialSnapshot = accumulator.await();
55+
assertFalse(initialSnapshot.exists());
56+
}
57+
58+
@After
59+
public void tearDown() {
60+
listenerRegistration.remove();
61+
IntegrationTestUtil.tearDown();
62+
}
63+
64+
/** Writes some initialData and consumes the events generated. */
65+
private void writeInitialData(Map<String, Object> initialData) {
66+
waitFor(docRef.set(initialData));
67+
accumulator.awaitRemoteEvent();
68+
}
69+
70+
private void expectLocalAndRemoteEvent(double expectedSum) {
71+
DocumentSnapshot snap = accumulator.awaitLocalEvent();
72+
assertEquals(expectedSum, snap.getDouble("sum"), DOUBLE_EPSILON);
73+
snap = accumulator.awaitRemoteEvent();
74+
assertEquals(expectedSum, snap.getDouble("sum"), DOUBLE_EPSILON);
75+
}
76+
77+
private void expectLocalAndRemoteEvent(long expectedSum) {
78+
DocumentSnapshot snap = accumulator.awaitLocalEvent();
79+
assertEquals(expectedSum, (long) snap.getLong("sum"));
80+
snap = accumulator.awaitRemoteEvent();
81+
assertEquals(expectedSum, (long) snap.getLong("sum"));
82+
}
83+
84+
@Test
85+
public void createDocumentWithIntegerIncrement() {
86+
waitFor(docRef.set(map("sum", FieldValue.numericAdd(1337))));
87+
expectLocalAndRemoteEvent(1337L);
88+
}
89+
90+
@Test
91+
public void createDocumentWithDoubleIncrement() {
92+
waitFor(docRef.set(map("sum", FieldValue.numericAdd(13.37))));
93+
expectLocalAndRemoteEvent(13.37D);
94+
}
95+
96+
@Test
97+
public void integerIncrement() {
98+
writeInitialData(map("sum", 1337L));
99+
waitFor(docRef.update("sum", FieldValue.numericAdd(1)));
100+
expectLocalAndRemoteEvent(1338L);
101+
}
102+
103+
@Test
104+
public void doubleIncrement() {
105+
writeInitialData(map("sum", 13.37D));
106+
waitFor(docRef.update("sum", FieldValue.numericAdd(0.1)));
107+
expectLocalAndRemoteEvent(13.47D);
108+
}
109+
110+
@Test
111+
public void integerIncrementExistingDouble() {
112+
writeInitialData(map("sum", 13.37D));
113+
waitFor(docRef.update("sum", FieldValue.numericAdd(1)));
114+
expectLocalAndRemoteEvent(14.37D);
115+
}
116+
117+
@Test
118+
public void doubleIncrementExistingInteger() {
119+
writeInitialData(map("sum", 1337L));
120+
waitFor(docRef.update("sum", FieldValue.numericAdd(0.1)));
121+
expectLocalAndRemoteEvent(1337.1D);
122+
}
123+
124+
@Test
125+
public void integerIncrementExistingString() {
126+
writeInitialData(map("sum", "overwrite"));
127+
waitFor(docRef.update("sum", FieldValue.numericAdd(1337)));
128+
expectLocalAndRemoteEvent(1337L);
129+
}
130+
131+
@Test
132+
public void doubleIncrementExistingString() {
133+
writeInitialData(map("sum", "overwrite"));
134+
waitFor(docRef.update("sum", FieldValue.numericAdd(13.37)));
135+
expectLocalAndRemoteEvent(13.37D);
136+
}
137+
138+
@Test
139+
public void multipleDoubleIncrements() throws ExecutionException, InterruptedException {
140+
writeInitialData(map("sum", 0.0D));
141+
Tasks.await(docRef.getFirestore().disableNetwork());
142+
docRef.update("sum", FieldValue.numericAdd(0.1D));
143+
docRef.update("sum", FieldValue.numericAdd(0.01D));
144+
docRef.update("sum", FieldValue.numericAdd(0.001D));
145+
146+
DocumentSnapshot snap = accumulator.awaitLocalEvent();
147+
assertEquals(0.1D, snap.getDouble("sum"), DOUBLE_EPSILON);
148+
snap = accumulator.awaitLocalEvent();
149+
assertEquals(0.11D, snap.getDouble("sum"), DOUBLE_EPSILON);
150+
snap = accumulator.awaitLocalEvent();
151+
assertEquals(0.111D, snap.getDouble("sum"), DOUBLE_EPSILON);
152+
153+
Tasks.await(docRef.getFirestore().enableNetwork());
154+
snap = accumulator.awaitRemoteEvent();
155+
assertEquals(0.111D, snap.getDouble("sum"), DOUBLE_EPSILON);
156+
}
157+
}

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static junit.framework.Assert.fail;
2121

2222
import android.content.Context;
23+
import android.net.SSLCertificateSocketFactory;
2324
import android.support.test.InstrumentationRegistry;
2425
import com.google.android.gms.tasks.Task;
2526
import com.google.android.gms.tasks.TaskCompletionSource;
@@ -38,10 +39,12 @@
3839
import com.google.firebase.firestore.local.Persistence;
3940
import com.google.firebase.firestore.local.SQLitePersistence;
4041
import com.google.firebase.firestore.model.DatabaseId;
42+
import com.google.firebase.firestore.remote.Datastore;
4143
import com.google.firebase.firestore.testutil.provider.FirestoreProvider;
4244
import com.google.firebase.firestore.util.AsyncQueue;
4345
import com.google.firebase.firestore.util.Logger;
4446
import com.google.firebase.firestore.util.Logger.Level;
47+
import io.grpc.okhttp.OkHttpChannelBuilder;
4548
import java.io.File;
4649
import java.util.ArrayList;
4750
import java.util.HashMap;
@@ -56,6 +59,13 @@
5659
/** A set of helper methods for tests */
5760
public class IntegrationTestUtil {
5861

62+
// Whether the integration tests should run against a local emulator instead of the Production
63+
// environment. Note that the Android Emulator treats "10.0.2.2" as the host machine.
64+
private static final boolean CONNECT_TO_EMULATOR = false;
65+
66+
private static final String EMULATOR_HOST = "10.0.2.2";
67+
private static final int EMULATOR_PORT = 8081;
68+
5969
// Alternate project ID for creating "bad" references. Doesn't actually need to work.
6070
public static final String BAD_PROJECT_ID = "test-project-2";
6171

@@ -80,11 +90,19 @@ public static FirestoreProvider provider() {
8090
}
8191

8292
public static DatabaseInfo testEnvDatabaseInfo() {
83-
return new DatabaseInfo(
84-
DatabaseId.forProject(provider.projectId()),
85-
"test-persistenceKey",
86-
provider.firestoreHost(),
87-
/*sslEnabled=*/ true);
93+
if (CONNECT_TO_EMULATOR) {
94+
return new DatabaseInfo(
95+
DatabaseId.forProject(provider.projectId()),
96+
"test-persistenceKey",
97+
String.format("%s:%s", EMULATOR_HOST, EMULATOR_PORT),
98+
/*sslEnabled=*/ true);
99+
} else {
100+
return new DatabaseInfo(
101+
DatabaseId.forProject(provider.projectId()),
102+
"test-persistenceKey",
103+
provider.firestoreHost(),
104+
/*sslEnabled=*/ true);
105+
}
88106
}
89107

90108
public static FirebaseFirestoreSettings newTestSettings() {
@@ -93,11 +111,33 @@ public static FirebaseFirestoreSettings newTestSettings() {
93111

94112
public static FirebaseFirestoreSettings newTestSettingsWithSnapshotTimestampsEnabled(
95113
boolean enabled) {
96-
return new FirebaseFirestoreSettings.Builder()
97-
.setHost(provider.firestoreHost())
98-
.setPersistenceEnabled(true)
99-
.setTimestampsInSnapshotsEnabled(enabled)
100-
.build();
114+
FirebaseFirestoreSettings.Builder settings = new FirebaseFirestoreSettings.Builder();
115+
116+
if (CONNECT_TO_EMULATOR) {
117+
settings.setHost(String.format("%s:%s", EMULATOR_HOST, EMULATOR_PORT));
118+
119+
// Disable SSL and hostname verification
120+
OkHttpChannelBuilder channelBuilder =
121+
new OkHttpChannelBuilder(EMULATOR_HOST, EMULATOR_PORT) {
122+
@Override
123+
protected String checkAuthority(String authority) {
124+
return authority;
125+
}
126+
};
127+
channelBuilder.hostnameVerifier((hostname, session) -> true);
128+
SSLCertificateSocketFactory insecureFactory =
129+
(SSLCertificateSocketFactory) SSLCertificateSocketFactory.getInsecure(0, null);
130+
channelBuilder.sslSocketFactory(insecureFactory);
131+
132+
Datastore.overrideChannelBuilder(() -> channelBuilder);
133+
} else {
134+
settings.setHost(provider.firestoreHost());
135+
}
136+
137+
settings.setPersistenceEnabled(true);
138+
settings.setTimestampsInSnapshotsEnabled(enabled);
139+
140+
return settings.build();
101141
}
102142

103143
/** Initializes a new Firestore instance that uses the default project. */

firebase-firestore/src/main/java/com/google/firebase/firestore/FieldValue.java

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ String getMethodName() {
6161
}
6262

6363
List<Object> getElements() {
64-
return this.elements;
64+
return elements;
6565
}
6666
}
6767

@@ -79,7 +79,25 @@ String getMethodName() {
7979
}
8080

8181
List<Object> getElements() {
82-
return this.elements;
82+
return elements;
83+
}
84+
}
85+
86+
/* FieldValue class for numericAdd() transforms. */
87+
static class NumericAddFieldValue extends FieldValue {
88+
private final Number summand;
89+
90+
NumericAddFieldValue(Number summand) {
91+
this.summand = summand;
92+
}
93+
94+
@Override
95+
String getMethodName() {
96+
return "FieldValue.numericAdd";
97+
}
98+
99+
Number getSummand() {
100+
return summand;
83101
}
84102
}
85103

@@ -134,4 +152,51 @@ public static FieldValue arrayUnion(@NonNull Object... elements) {
134152
public static FieldValue arrayRemove(@NonNull Object... elements) {
135153
return new ArrayRemoveFieldValue(Arrays.asList(elements));
136154
}
155+
156+
// Adds the given value to the field's current value.
157+
//
158+
// This must be an integer or a double value.
159+
// If the field is not an integer or double, or if the field does not yet
160+
// exist, the transformation will set the field to the given value.
161+
// If either of the given value or the current field value are doubles,
162+
// both values will be interpreted as doubles. Double arithmetic and
163+
// representation of double values follow IEEE 754 semantics.
164+
// If there is positive/negative integer overflow, the field is resolved
165+
// to the largest magnitude positive/negative integer.
166+
167+
/**
168+
* Returns a special value that can be used with set() or update() that tells the server to add
169+
* the given value to the field's current value.
170+
*
171+
* <p>If the current field value is an integer, possible integer overflows are resolved to the
172+
* largest maximum positive/negative integer value (Long.MAX_VALUE or Long.MIN_VALUE
173+
* respectively). If current field value is a double, both values will be interpreted as doubles
174+
* and the arithmetic will follow IEEE 754 semantics.
175+
*
176+
* <p>If field is not an integer or double, or if the field does not yet exist, the transformation
177+
* will set the field to the given value.
178+
*
179+
* @return The FieldValue sentinel for use in a call to set() or update().
180+
*/
181+
@NonNull
182+
@PublicApi
183+
public static FieldValue numericAdd(long l) {
184+
return new NumericAddFieldValue(l);
185+
}
186+
187+
/**
188+
* Returns a special value that can be used with set() or update() that tells the server to add
189+
* the given value to the field's current value.
190+
*
191+
* <p>If the current value is an integer or a double, both the current and the given value will be
192+
* interpreted as doubles and all arithmetic will follow IEEE 754 semantics. Otherwise, the
193+
* transformation will set the field to the given value.
194+
*
195+
* @return The FieldValue sentinel for use in a call to set() or update().
196+
*/
197+
@NonNull
198+
@PublicApi
199+
public static FieldValue numericAdd(double l) {
200+
return new NumericAddFieldValue(l);
201+
}
137202
}

firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataConverter.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.google.firebase.firestore.model.FieldPath;
3232
import com.google.firebase.firestore.model.mutation.ArrayTransformOperation;
3333
import com.google.firebase.firestore.model.mutation.FieldMask;
34+
import com.google.firebase.firestore.model.mutation.NumericAddTransformOperation;
3435
import com.google.firebase.firestore.model.mutation.ServerTimestampOperation;
3536
import com.google.firebase.firestore.model.value.ArrayValue;
3637
import com.google.firebase.firestore.model.value.BlobValue;
@@ -40,6 +41,7 @@
4041
import com.google.firebase.firestore.model.value.GeoPointValue;
4142
import com.google.firebase.firestore.model.value.IntegerValue;
4243
import com.google.firebase.firestore.model.value.NullValue;
44+
import com.google.firebase.firestore.model.value.NumberValue;
4345
import com.google.firebase.firestore.model.value.ObjectValue;
4446
import com.google.firebase.firestore.model.value.ReferenceValue;
4547
import com.google.firebase.firestore.model.value.StringValue;
@@ -349,6 +351,13 @@ private void parseSentinelFieldValue(
349351
ArrayTransformOperation arrayRemove = new ArrayTransformOperation.Remove(parsedElements);
350352
context.addToFieldTransforms(context.getPath(), arrayRemove);
351353

354+
} else if (value instanceof com.google.firebase.firestore.FieldValue.NumericAddFieldValue) {
355+
com.google.firebase.firestore.FieldValue.NumericAddFieldValue numericAddFieldValue =
356+
(com.google.firebase.firestore.FieldValue.NumericAddFieldValue) value;
357+
NumberValue summand = (NumberValue) parseQueryValue(numericAddFieldValue.getSummand());
358+
NumericAddTransformOperation numericAdd = new NumericAddTransformOperation(summand);
359+
context.addToFieldTransforms(context.getPath(), numericAdd);
360+
352361
} else {
353362
throw Assert.fail("Unknown FieldValue type: %s", Util.typeName(value));
354363
}

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,7 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollection
135135

136136
DocumentKey key = mutation.getKey();
137137
MaybeDocument baseDoc = results.get(key);
138-
MaybeDocument mutatedDoc =
139-
mutation.applyToLocalView(baseDoc, baseDoc, batch.getLocalWriteTime());
138+
MaybeDocument mutatedDoc = mutation.applyToLocalView(baseDoc, batch.getLocalWriteTime());
140139
if (mutatedDoc instanceof Document) {
141140
results = results.insert(key, (Document) mutatedDoc);
142141
} else {

0 commit comments

Comments
 (0)