Skip to content

Commit 20152fe

Browse files
Adding FieldValue.numericAdd()
1 parent 8ccb305 commit 20152fe

29 files changed

+942
-48
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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 expectLocalAndRemoteValue(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 expectLocalAndRemoteValue(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 createDocumentWithIncrement() {
86+
waitFor(docRef.set(map("sum", FieldValue.numericAdd(1337))));
87+
expectLocalAndRemoteValue(1337L);
88+
}
89+
90+
@Test
91+
public void integerIncrementExistingInteger() {
92+
writeInitialData(map("sum", 1337L));
93+
waitFor(docRef.update("sum", FieldValue.numericAdd(1)));
94+
expectLocalAndRemoteValue(1338L);
95+
}
96+
97+
@Test
98+
public void doubleIncrementWithExistingDouble() {
99+
writeInitialData(map("sum", 13.37D));
100+
waitFor(docRef.update("sum", FieldValue.numericAdd(0.1)));
101+
expectLocalAndRemoteValue(13.47D);
102+
}
103+
104+
@Test
105+
public void integerIncrementExistingDouble() {
106+
writeInitialData(map("sum", 13.37D));
107+
waitFor(docRef.update("sum", FieldValue.numericAdd(1)));
108+
expectLocalAndRemoteValue(14.37D);
109+
}
110+
111+
@Test
112+
public void doubleIncrementExistingInteger() {
113+
writeInitialData(map("sum", 1337L));
114+
waitFor(docRef.update("sum", FieldValue.numericAdd(0.1)));
115+
expectLocalAndRemoteValue(1337.1D);
116+
}
117+
118+
@Test
119+
public void integerIncrementExistingString() {
120+
writeInitialData(map("sum", "overwrite"));
121+
waitFor(docRef.update("sum", FieldValue.numericAdd(1337)));
122+
expectLocalAndRemoteValue(1337L);
123+
}
124+
125+
@Test
126+
public void doubleIncrementExistingString() {
127+
writeInitialData(map("sum", "overwrite"));
128+
waitFor(docRef.update("sum", FieldValue.numericAdd(13.37)));
129+
expectLocalAndRemoteValue(13.37D);
130+
}
131+
132+
@Test
133+
public void multipleDoubleIncrements() throws ExecutionException, InterruptedException {
134+
writeInitialData(map("sum", 0.0D));
135+
136+
Tasks.await(docRef.getFirestore().disableNetwork());
137+
138+
docRef.update("sum", FieldValue.numericAdd(0.1D));
139+
docRef.update("sum", FieldValue.numericAdd(0.01D));
140+
docRef.update("sum", FieldValue.numericAdd(0.001D));
141+
142+
DocumentSnapshot snap = accumulator.awaitLocalEvent();
143+
assertEquals(0.1D, snap.getDouble("sum"), DOUBLE_EPSILON);
144+
snap = accumulator.awaitLocalEvent();
145+
assertEquals(0.11D, snap.getDouble("sum"), DOUBLE_EPSILON);
146+
snap = accumulator.awaitLocalEvent();
147+
assertEquals(0.111D, snap.getDouble("sum"), DOUBLE_EPSILON);
148+
149+
Tasks.await(docRef.getFirestore().enableNetwork());
150+
151+
snap = accumulator.awaitRemoteEvent();
152+
assertEquals(0.111D, snap.getDouble("sum"), DOUBLE_EPSILON);
153+
}
154+
}

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 Firestore emulator instead of the
63+
// Production environment. Note that the Android Emulator treats "10.0.2.2" as its 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:%d", 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:%d", 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: 55 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 operand;
89+
90+
NumericAddFieldValue(Number operand) {
91+
this.operand = operand;
92+
}
93+
94+
@Override
95+
String getMethodName() {
96+
return "FieldValue.numericAdd";
97+
}
98+
99+
Number getOperand() {
100+
return operand;
83101
}
84102
}
85103

@@ -134,4 +152,39 @@ 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+
/**
157+
* Returns a special value that can be used with set() or update() that tells the server to add
158+
* the given value to the field's current value.
159+
*
160+
* <p>If the current field value is an integer, possible integer overflows are resolved to
161+
* Long.MAX_VALUE or Long.MIN_VALUE respectively. If the current field value is a double, both
162+
* values will be interpreted as doubles and the arithmetic will follow IEEE 754 semantics.
163+
*
164+
* <p>If field is not an integer or double, or if the field does not yet exist, the transformation
165+
* will set the field to the given value.
166+
*
167+
* @return The FieldValue sentinel for use in a call to set() or update().
168+
*/
169+
@NonNull
170+
@PublicApi
171+
public static FieldValue numericAdd(long l) {
172+
return new NumericAddFieldValue(l);
173+
}
174+
175+
/**
176+
* Returns a special value that can be used with set() or update() that tells the server to add
177+
* the given value to the field's current value.
178+
*
179+
* <p>If the current value is an integer or a double, both the current and the given value will be
180+
* interpreted as doubles and all arithmetic will follow IEEE 754 semantics. Otherwise, the
181+
* transformation will set the field to the given value.
182+
*
183+
* @return The FieldValue sentinel for use in a call to set() or update().
184+
*/
185+
@NonNull
186+
@PublicApi
187+
public static FieldValue numericAdd(double l) {
188+
return new NumericAddFieldValue(l);
189+
}
137190
}

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 operand = (NumberValue) parseQueryValue(numericAddFieldValue.getOperand());
358+
NumericAddTransformOperation numericAdd = new NumericAddTransformOperation(operand);
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/LocalSerializer.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ com.google.firebase.firestore.proto.WriteBatch encodeMutationBatch(MutationBatch
160160

161161
result.setBatchId(batch.getBatchId());
162162
result.setLocalWriteTime(rpcSerializer.encodeTimestamp(batch.getLocalWriteTime()));
163+
for (Mutation mutation : batch.getBaseMutations()) {
164+
result.addBaseWrites(rpcSerializer.encodeMutation(mutation));
165+
}
163166
for (Mutation mutation : batch.getMutations()) {
164167
result.addWrites(rpcSerializer.encodeMutation(mutation));
165168
}
@@ -171,13 +174,17 @@ MutationBatch decodeMutationBatch(com.google.firebase.firestore.proto.WriteBatch
171174
int batchId = batch.getBatchId();
172175
Timestamp localWriteTime = rpcSerializer.decodeTimestamp(batch.getLocalWriteTime());
173176

174-
int count = batch.getWritesCount();
175-
List<Mutation> mutations = new ArrayList<>(count);
176-
for (int i = 0; i < count; i++) {
177+
int baseMutationsCount = batch.getBaseWritesCount();
178+
List<Mutation> baseMutations = new ArrayList<>(baseMutationsCount);
179+
for (int i = 0; i < baseMutationsCount; i++) {
180+
baseMutations.add(rpcSerializer.decodeMutation(batch.getBaseWrites(i)));
181+
}
182+
int mutationsCount = batch.getWritesCount();
183+
List<Mutation> mutations = new ArrayList<>(mutationsCount);
184+
for (int i = 0; i < mutationsCount; i++) {
177185
mutations.add(rpcSerializer.decodeMutation(batch.getWrites(i)));
178186
}
179-
180-
return new MutationBatch(batchId, localWriteTime, mutations);
187+
return new MutationBatch(batchId, localWriteTime, baseMutations, mutations);
181188
}
182189

183190
com.google.firebase.firestore.proto.Target encodeQueryData(QueryData queryData) {

0 commit comments

Comments
 (0)