Skip to content

Commit 36b3e5b

Browse files
feat(firestore): added minimum and maximum FieldValue operations (#8101)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 8e331cb commit 36b3e5b

12 files changed

Lines changed: 683 additions & 58 deletions

File tree

firebase-firestore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
- [feature] Added support for `minimum` and `maximum` FieldValue operations.
4+
35
# 26.3.0
46

57
- [feature] Added search stage support for `languageCode`, `offset`, `limit`, and `retrievalDepth`.

firebase-firestore/api.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ package com.google.firebase.firestore {
154154
method public static com.google.firebase.firestore.FieldValue delete();
155155
method public static com.google.firebase.firestore.FieldValue increment(double);
156156
method public static com.google.firebase.firestore.FieldValue increment(long);
157+
method public static com.google.firebase.firestore.FieldValue maximum(double);
158+
method public static com.google.firebase.firestore.FieldValue maximum(long);
159+
method public static com.google.firebase.firestore.FieldValue minimum(double);
160+
method public static com.google.firebase.firestore.FieldValue minimum(long);
157161
method public static com.google.firebase.firestore.FieldValue serverTimestamp();
158162
method public static com.google.firebase.firestore.VectorValue vector(double[]);
159163
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
version=26.3.1
1+
version=26.4.0
22
latestReleasedVersion=26.3.0

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

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.junit.Assert.assertEquals;
2121
import static org.junit.Assert.assertFalse;
2222
import static org.junit.Assert.assertNotNull;
23+
import static org.junit.Assert.assertTrue;
2324

2425
import androidx.test.ext.junit.runners.AndroidJUnit4;
2526
import com.google.android.gms.tasks.Tasks;
@@ -71,18 +72,31 @@ private void writeInitialData(Map<String, Object> initialData) {
7172

7273
private void expectLocalAndRemoteValue(double expectedSum) {
7374
DocumentSnapshot snap = accumulator.awaitLocalEvent();
75+
assertTrue(snap.get("sum") instanceof Double);
7476
assertEquals(expectedSum, snap.getDouble("sum"), DOUBLE_EPSILON);
7577
snap = accumulator.awaitRemoteEvent();
78+
assertTrue(snap.get("sum") instanceof Double);
7679
assertEquals(expectedSum, snap.getDouble("sum"), DOUBLE_EPSILON);
7780
}
7881

7982
private void expectLocalAndRemoteValue(long expectedSum) {
8083
DocumentSnapshot snap = accumulator.awaitLocalEvent();
84+
assertTrue(snap.get("sum") instanceof Long);
8185
assertEquals(expectedSum, (long) snap.getLong("sum"));
8286
snap = accumulator.awaitRemoteEvent();
87+
assertTrue(snap.get("sum") instanceof Long);
8388
assertEquals(expectedSum, (long) snap.getLong("sum"));
8489
}
8590

91+
private void expectLocalAndRemoteNaN() {
92+
DocumentSnapshot snap = accumulator.awaitLocalEvent();
93+
assertTrue(snap.get("sum") instanceof Double);
94+
assertTrue(Double.isNaN(snap.getDouble("sum")));
95+
snap = accumulator.awaitRemoteEvent();
96+
assertTrue(snap.get("sum") instanceof Double);
97+
assertTrue(Double.isNaN(snap.getDouble("sum")));
98+
}
99+
86100
@Test
87101
public void createDocumentWithIncrement() {
88102
waitFor(docRef.set(map("sum", FieldValue.increment(1337))));
@@ -218,4 +232,128 @@ public void serverTimestampAndIncrement() throws ExecutionException, Interrupted
218232
snap = accumulator.awaitRemoteEvent();
219233
assertEquals(1, (long) snap.getLong("val"));
220234
}
235+
236+
@Test
237+
public void createDocumentWithMinimum() {
238+
waitFor(docRef.set(map("sum", FieldValue.minimum(1337))));
239+
expectLocalAndRemoteValue(1337L);
240+
}
241+
242+
@Test
243+
public void createDocumentWithMaximum() {
244+
waitFor(docRef.set(map("sum", FieldValue.maximum(1337))));
245+
expectLocalAndRemoteValue(1337L);
246+
}
247+
248+
@Test
249+
public void minimumWithExistingInteger() {
250+
writeInitialData(map("sum", 10L));
251+
waitFor(docRef.update("sum", FieldValue.minimum(5L)));
252+
expectLocalAndRemoteValue(5L);
253+
254+
waitFor(docRef.update("sum", FieldValue.minimum(20L)));
255+
expectLocalAndRemoteValue(5L);
256+
}
257+
258+
@Test
259+
public void maximumWithExistingInteger() {
260+
writeInitialData(map("sum", 10L));
261+
waitFor(docRef.update("sum", FieldValue.maximum(5L)));
262+
expectLocalAndRemoteValue(10L);
263+
264+
waitFor(docRef.update("sum", FieldValue.maximum(20L)));
265+
expectLocalAndRemoteValue(20L);
266+
}
267+
268+
@Test
269+
public void minimumWithExistingDouble() {
270+
writeInitialData(map("sum", 10.5D));
271+
waitFor(docRef.update("sum", FieldValue.minimum(5.5D)));
272+
expectLocalAndRemoteValue(5.5D);
273+
274+
waitFor(docRef.update("sum", FieldValue.minimum(20.5D)));
275+
expectLocalAndRemoteValue(5.5D);
276+
}
277+
278+
@Test
279+
public void maximumWithExistingDouble() {
280+
writeInitialData(map("sum", 10.5D));
281+
waitFor(docRef.update("sum", FieldValue.maximum(5.5D)));
282+
expectLocalAndRemoteValue(10.5D);
283+
284+
waitFor(docRef.update("sum", FieldValue.maximum(20.5D)));
285+
expectLocalAndRemoteValue(20.5D);
286+
}
287+
288+
@Test
289+
public void mixedTypesPreserveOperandTypeForMinimum() {
290+
// field and input value of mixed types: field takes on type of smaller operand
291+
writeInitialData(map("sum", 10L));
292+
waitFor(docRef.update("sum", FieldValue.minimum(5.5D)));
293+
expectLocalAndRemoteValue(5.5D);
294+
295+
writeInitialData(map("sum", 10.5D));
296+
waitFor(docRef.update("sum", FieldValue.minimum(5L)));
297+
expectLocalAndRemoteValue(5L);
298+
}
299+
300+
@Test
301+
public void mixedTypesPreserveOperandTypeForMaximum() {
302+
// field and input value of mixed types: field takes on type of larger operand
303+
writeInitialData(map("sum", 10L));
304+
waitFor(docRef.update("sum", FieldValue.maximum(20.5D)));
305+
expectLocalAndRemoteValue(20.5D);
306+
307+
writeInitialData(map("sum", 10.5D));
308+
waitFor(docRef.update("sum", FieldValue.maximum(20L)));
309+
expectLocalAndRemoteValue(20L);
310+
}
311+
312+
@Test
313+
public void equivalentValuesDoNotChangeTypeForMinimum() {
314+
// equivalent (e.g. 3 and 3.0), field does not change type
315+
writeInitialData(map("sum", 3L));
316+
waitFor(docRef.update("sum", FieldValue.minimum(3.0D)));
317+
expectLocalAndRemoteValue(3L);
318+
319+
writeInitialData(map("sum", 3.0D));
320+
waitFor(docRef.update("sum", FieldValue.minimum(3L)));
321+
expectLocalAndRemoteValue(3.0D);
322+
}
323+
324+
@Test
325+
public void equivalentValuesDoNotChangeTypeForMaximum() {
326+
// equivalent (e.g. 3 and 3.0), field does not change type
327+
writeInitialData(map("sum", 3L));
328+
waitFor(docRef.update("sum", FieldValue.maximum(3.0D)));
329+
expectLocalAndRemoteValue(3L);
330+
331+
writeInitialData(map("sum", 3.0D));
332+
waitFor(docRef.update("sum", FieldValue.maximum(3L)));
333+
expectLocalAndRemoteValue(3.0D);
334+
}
335+
336+
@Test
337+
public void minimumWithNaN() {
338+
// If one of the values is NaN, minimum is NaN
339+
writeInitialData(map("sum", Double.NaN));
340+
waitFor(docRef.update("sum", FieldValue.minimum(5L)));
341+
expectLocalAndRemoteNaN();
342+
343+
writeInitialData(map("sum", 5L));
344+
waitFor(docRef.update("sum", FieldValue.minimum(Double.NaN)));
345+
expectLocalAndRemoteNaN();
346+
}
347+
348+
@Test
349+
public void maximumWithNaN() {
350+
// If one of the values is NaN, maximum is NaN
351+
writeInitialData(map("sum", Double.NaN));
352+
waitFor(docRef.update("sum", FieldValue.maximum(5L)));
353+
expectLocalAndRemoteNaN();
354+
355+
writeInitialData(map("sum", 5L));
356+
waitFor(docRef.update("sum", FieldValue.maximum(Double.NaN)));
357+
expectLocalAndRemoteNaN();
358+
}
221359
}

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,42 @@ Number getOperand() {
102102
}
103103
}
104104

105+
/** {@code FieldValue} class for {@link #minimum()} transforms. */
106+
static class NumericMinimumFieldValue extends FieldValue {
107+
private final Number operand;
108+
109+
NumericMinimumFieldValue(Number operand) {
110+
this.operand = operand;
111+
}
112+
113+
@Override
114+
String getMethodName() {
115+
return "FieldValue.minimum";
116+
}
117+
118+
Number getOperand() {
119+
return operand;
120+
}
121+
}
122+
123+
/** {@code FieldValue} class for {@link #maximum()} transforms. */
124+
static class NumericMaximumFieldValue extends FieldValue {
125+
private final Number operand;
126+
127+
NumericMaximumFieldValue(Number operand) {
128+
this.operand = operand;
129+
}
130+
131+
@Override
132+
String getMethodName() {
133+
return "FieldValue.maximum";
134+
}
135+
136+
Number getOperand() {
137+
return operand;
138+
}
139+
}
140+
105141
private static final DeleteFieldValue DELETE_INSTANCE = new DeleteFieldValue();
106142
private static final ServerTimestampFieldValue SERVER_TIMESTAMP_INSTANCE =
107143
new ServerTimestampFieldValue();
@@ -183,6 +219,50 @@ public static FieldValue increment(double l) {
183219
return new NumericIncrementFieldValue(l);
184220
}
185221

222+
/**
223+
* Returns a special value that can be used with {@code set()} or {@code update()} that tells the
224+
* server to set the field to the minimum of its current value and the given value.
225+
*
226+
* @return The {@code FieldValue} sentinel for use in a call to {@code set()} or {@code update()}.
227+
*/
228+
@NonNull
229+
public static FieldValue minimum(long l) {
230+
return new NumericMinimumFieldValue(l);
231+
}
232+
233+
/**
234+
* Returns a special value that can be used with {@code set()} or {@code update()} that tells the
235+
* server to set the field to the minimum of its current value and the given value.
236+
*
237+
* @return The {@code FieldValue} sentinel for use in a call to {@code set()} or {@code update()}.
238+
*/
239+
@NonNull
240+
public static FieldValue minimum(double l) {
241+
return new NumericMinimumFieldValue(l);
242+
}
243+
244+
/**
245+
* Returns a special value that can be used with {@code set()} or {@code update()} that tells the
246+
* server to set the field to the maximum of its current value and the given value.
247+
*
248+
* @return The {@code FieldValue} sentinel for use in a call to {@code set()} or {@code update()}.
249+
*/
250+
@NonNull
251+
public static FieldValue maximum(long l) {
252+
return new NumericMaximumFieldValue(l);
253+
}
254+
255+
/**
256+
* Returns a special value that can be used with {@code set()} or {@code update()} that tells the
257+
* server to set the field to the maximum of its current value and the given value.
258+
*
259+
* @return The {@code FieldValue} sentinel for use in a call to {@code set()} or {@code update()}.
260+
*/
261+
@NonNull
262+
public static FieldValue maximum(double l) {
263+
return new NumericMaximumFieldValue(l);
264+
}
265+
186266
/**
187267
* Creates a new {@link VectorValue} constructed with a copy of the given array of doubles.
188268
*

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
import com.google.firebase.firestore.FieldValue.ArrayRemoveFieldValue;
2424
import com.google.firebase.firestore.FieldValue.ArrayUnionFieldValue;
2525
import com.google.firebase.firestore.FieldValue.DeleteFieldValue;
26+
import com.google.firebase.firestore.FieldValue.NumericIncrementFieldValue;
27+
import com.google.firebase.firestore.FieldValue.NumericMaximumFieldValue;
28+
import com.google.firebase.firestore.FieldValue.NumericMinimumFieldValue;
2629
import com.google.firebase.firestore.FieldValue.ServerTimestampFieldValue;
2730
import com.google.firebase.firestore.core.UserData;
2831
import com.google.firebase.firestore.core.UserData.ParseAccumulator;
@@ -36,6 +39,8 @@
3639
import com.google.firebase.firestore.model.mutation.ArrayTransformOperation;
3740
import com.google.firebase.firestore.model.mutation.FieldMask;
3841
import com.google.firebase.firestore.model.mutation.NumericIncrementTransformOperation;
42+
import com.google.firebase.firestore.model.mutation.NumericMaximumTransformOperation;
43+
import com.google.firebase.firestore.model.mutation.NumericMinimumTransformOperation;
3944
import com.google.firebase.firestore.model.mutation.ServerTimestampOperation;
4045
import com.google.firebase.firestore.pipeline.Expression;
4146
import com.google.firebase.firestore.util.Assert;
@@ -369,16 +374,27 @@ private void parseSentinelFieldValue(
369374
ArrayTransformOperation arrayRemove = new ArrayTransformOperation.Remove(parsedElements);
370375
context.addToFieldTransforms(context.getPath(), arrayRemove);
371376

372-
} else if (value
373-
instanceof com.google.firebase.firestore.FieldValue.NumericIncrementFieldValue) {
374-
com.google.firebase.firestore.FieldValue.NumericIncrementFieldValue
375-
numericIncrementFieldValue =
376-
(com.google.firebase.firestore.FieldValue.NumericIncrementFieldValue) value;
377+
} else if (value instanceof NumericIncrementFieldValue) {
378+
NumericIncrementFieldValue numericIncrementFieldValue = (NumericIncrementFieldValue) value;
377379
Value operand = parseQueryValue(numericIncrementFieldValue.getOperand());
378380
NumericIncrementTransformOperation incrementOperation =
379381
new NumericIncrementTransformOperation(operand);
380382
context.addToFieldTransforms(context.getPath(), incrementOperation);
381383

384+
} else if (value instanceof NumericMinimumFieldValue) {
385+
NumericMinimumFieldValue numericMinimumFieldValue = (NumericMinimumFieldValue) value;
386+
Value operand = parseQueryValue(numericMinimumFieldValue.getOperand());
387+
NumericMinimumTransformOperation minimumOperation =
388+
new NumericMinimumTransformOperation(operand);
389+
context.addToFieldTransforms(context.getPath(), minimumOperation);
390+
391+
} else if (value instanceof NumericMaximumFieldValue) {
392+
NumericMaximumFieldValue numericMaximumFieldValue = (NumericMaximumFieldValue) value;
393+
Value operand = parseQueryValue(numericMaximumFieldValue.getOperand());
394+
NumericMaximumTransformOperation maximumOperation =
395+
new NumericMaximumTransformOperation(operand);
396+
context.addToFieldTransforms(context.getPath(), maximumOperation);
397+
382398
} else {
383399
throw Assert.fail("Unknown FieldValue type: %s", Util.typeName(value));
384400
}

0 commit comments

Comments
 (0)