From a1174957601eb5b0fa0418bd4548321796cbeee3 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 6 May 2026 10:44:14 -0600 Subject: [PATCH 1/8] feat(firestore): added minimum and maximum FieldValue operations --- .../firestore/NumericTransformsTest.java | 143 ++++++++++++ .../google/firebase/firestore/FieldValue.java | 80 +++++++ .../firebase/firestore/UserDataReader.java | 26 ++- .../NumericIncrementTransformOperation.java | 55 +---- .../NumericMaximumTransformOperation.java | 64 ++++++ .../NumericMinimumTransformOperation.java | 64 ++++++ .../mutation/NumericTransformOperation.java | 101 +++++++++ .../firestore/remote/RemoteSerializer.java | 24 ++ .../model/mutation/MutationTest.java | 212 ++++++++++++++++++ 9 files changed, 711 insertions(+), 58 deletions(-) create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java index af4687eeb12..b05845716ee 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java @@ -71,18 +71,31 @@ private void writeInitialData(Map initialData) { private void expectLocalAndRemoteValue(double expectedSum) { DocumentSnapshot snap = accumulator.awaitLocalEvent(); + org.junit.Assert.assertTrue(snap.get("sum") instanceof Double); assertEquals(expectedSum, snap.getDouble("sum"), DOUBLE_EPSILON); snap = accumulator.awaitRemoteEvent(); + org.junit.Assert.assertTrue(snap.get("sum") instanceof Double); assertEquals(expectedSum, snap.getDouble("sum"), DOUBLE_EPSILON); } private void expectLocalAndRemoteValue(long expectedSum) { DocumentSnapshot snap = accumulator.awaitLocalEvent(); + org.junit.Assert.assertTrue(snap.get("sum") instanceof Long); assertEquals(expectedSum, (long) snap.getLong("sum")); snap = accumulator.awaitRemoteEvent(); + org.junit.Assert.assertTrue(snap.get("sum") instanceof Long); assertEquals(expectedSum, (long) snap.getLong("sum")); } + private void expectLocalAndRemoteNaN() { + DocumentSnapshot snap = accumulator.awaitLocalEvent(); + org.junit.Assert.assertTrue(snap.get("sum") instanceof Double); + org.junit.Assert.assertTrue(Double.isNaN(snap.getDouble("sum"))); + snap = accumulator.awaitRemoteEvent(); + org.junit.Assert.assertTrue(snap.get("sum") instanceof Double); + org.junit.Assert.assertTrue(Double.isNaN(snap.getDouble("sum"))); + } + @Test public void createDocumentWithIncrement() { waitFor(docRef.set(map("sum", FieldValue.increment(1337)))); @@ -218,4 +231,134 @@ public void serverTimestampAndIncrement() throws ExecutionException, Interrupted snap = accumulator.awaitRemoteEvent(); assertEquals(1, (long) snap.getLong("val")); } + + @Test + public void createDocumentWithMinimum() { + waitFor(docRef.set(map("sum", FieldValue.minimum(1337)))); + expectLocalAndRemoteValue(1337L); + } + + @Test + public void createDocumentWithMaximum() { + waitFor(docRef.set(map("sum", FieldValue.maximum(1337)))); + expectLocalAndRemoteValue(1337L); + } + + @Test + public void minimumWithExistingInteger() { + writeInitialData(map("sum", 10L)); + waitFor(docRef.update("sum", FieldValue.minimum(5L))); + expectLocalAndRemoteValue(5L); + + waitFor(docRef.update("sum", FieldValue.minimum(20L))); + expectLocalAndRemoteValue(5L); + } + + @Test + public void maximumWithExistingInteger() { + writeInitialData(map("sum", 10L)); + waitFor(docRef.update("sum", FieldValue.maximum(5L))); + expectLocalAndRemoteValue(10L); + + waitFor(docRef.update("sum", FieldValue.maximum(20L))); + expectLocalAndRemoteValue(20L); + } + + @Test + public void minimumWithExistingDouble() { + writeInitialData(map("sum", 10.5D)); + waitFor(docRef.update("sum", FieldValue.minimum(5.5D))); + expectLocalAndRemoteValue(5.5D); + + waitFor(docRef.update("sum", FieldValue.minimum(20.5D))); + expectLocalAndRemoteValue(5.5D); + } + + @Test + public void maximumWithExistingDouble() { + writeInitialData(map("sum", 10.5D)); + waitFor(docRef.update("sum", FieldValue.maximum(5.5D))); + expectLocalAndRemoteValue(10.5D); + + waitFor(docRef.update("sum", FieldValue.maximum(20.5D))); + expectLocalAndRemoteValue(20.5D); + } + + @Test + public void mixedTypesPreserveOperandTypeForMinimum() { + // field and input value of mixed types: field takes on type of smaller operand + writeInitialData(map("sum", 10L)); + waitFor(docRef.update("sum", FieldValue.minimum(5.5D))); + // 5.5D is smaller, so sum should become 5.5D (double) + expectLocalAndRemoteValue(5.5D); + + writeInitialData(map("sum", 10.5D)); + waitFor(docRef.update("sum", FieldValue.minimum(5L))); + // 5L is smaller, so sum should become 5L (long) + expectLocalAndRemoteValue(5L); + } + + @Test + public void mixedTypesPreserveOperandTypeForMaximum() { + // field and input value of mixed types: field takes on type of larger operand + writeInitialData(map("sum", 10L)); + waitFor(docRef.update("sum", FieldValue.maximum(20.5D))); + // 20.5D is larger, so sum should become 20.5D (double) + expectLocalAndRemoteValue(20.5D); + + writeInitialData(map("sum", 10.5D)); + waitFor(docRef.update("sum", FieldValue.maximum(20L))); + // 20L is larger, so sum should become 20L (long) + expectLocalAndRemoteValue(20L); + } + + @Test + public void equivalentValuesDoNotChangeTypeForMinimum() { + // equivalent (e.g. 3 and 3.0), field does not change type + writeInitialData(map("sum", 3L)); + waitFor(docRef.update("sum", FieldValue.minimum(3.0D))); + // 3L is equivalent to 3.0D, field type/value should remain 3L (integer) + expectLocalAndRemoteValue(3L); + + writeInitialData(map("sum", 3.0D)); + waitFor(docRef.update("sum", FieldValue.minimum(3L))); + // 3.0D is equivalent to 3L, field type/value should remain 3.0D (double) + expectLocalAndRemoteValue(3.0D); + } + + @Test + public void equivalentValuesDoNotChangeTypeForMaximum() { + // equivalent (e.g. 3 and 3.0), field does not change type + writeInitialData(map("sum", 3L)); + waitFor(docRef.update("sum", FieldValue.maximum(3.0D))); + expectLocalAndRemoteValue(3L); + + writeInitialData(map("sum", 3.0D)); + waitFor(docRef.update("sum", FieldValue.maximum(3L))); + expectLocalAndRemoteValue(3.0D); + } + + @Test + public void minimumWithNaN() { + // If one of the values is NaN, minimum is NaN + writeInitialData(map("sum", Double.NaN)); + waitFor(docRef.update("sum", FieldValue.minimum(5L))); + expectLocalAndRemoteNaN(); + + writeInitialData(map("sum", 5L)); + waitFor(docRef.update("sum", FieldValue.minimum(Double.NaN))); + expectLocalAndRemoteNaN(); + } + + @Test + public void maximumWithNaN() { + // If one of the values is NaN, maximum is NaN + writeInitialData(map("sum", Double.NaN)); + waitFor(docRef.update("sum", FieldValue.maximum(5L))); + expectLocalAndRemoteNaN(); + + writeInitialData(map("sum", 5L)); + waitFor(docRef.update("sum", FieldValue.maximum(Double.NaN))); + expectLocalAndRemoteNaN(); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldValue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldValue.java index f899457acdb..871038a2813 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldValue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldValue.java @@ -102,6 +102,42 @@ Number getOperand() { } } + /** {@code FieldValue} class for {@link #minimum()} transforms. */ + static class NumericMinimumFieldValue extends FieldValue { + private final Number operand; + + NumericMinimumFieldValue(Number operand) { + this.operand = operand; + } + + @Override + String getMethodName() { + return "FieldValue.minimum"; + } + + Number getOperand() { + return operand; + } + } + + /** {@code FieldValue} class for {@link #maximum()} transforms. */ + static class NumericMaximumFieldValue extends FieldValue { + private final Number operand; + + NumericMaximumFieldValue(Number operand) { + this.operand = operand; + } + + @Override + String getMethodName() { + return "FieldValue.maximum"; + } + + Number getOperand() { + return operand; + } + } + private static final DeleteFieldValue DELETE_INSTANCE = new DeleteFieldValue(); private static final ServerTimestampFieldValue SERVER_TIMESTAMP_INSTANCE = new ServerTimestampFieldValue(); @@ -183,6 +219,50 @@ public static FieldValue increment(double l) { return new NumericIncrementFieldValue(l); } + /** + * Returns a special value that can be used with {@code set()} or {@code update()} that tells the + * server to set the field to the minimum of its current value and the given value. + * + * @return The {@code FieldValue} sentinel for use in a call to {@code set()} or {@code update()}. + */ + @NonNull + public static FieldValue minimum(long l) { + return new NumericMinimumFieldValue(l); + } + + /** + * Returns a special value that can be used with {@code set()} or {@code update()} that tells the + * server to set the field to the minimum of its current value and the given value. + * + * @return The {@code FieldValue} sentinel for use in a call to {@code set()} or {@code update()}. + */ + @NonNull + public static FieldValue minimum(double l) { + return new NumericMinimumFieldValue(l); + } + + /** + * Returns a special value that can be used with {@code set()} or {@code update()} that tells the + * server to set the field to the maximum of its current value and the given value. + * + * @return The {@code FieldValue} sentinel for use in a call to {@code set()} or {@code update()}. + */ + @NonNull + public static FieldValue maximum(long l) { + return new NumericMaximumFieldValue(l); + } + + /** + * Returns a special value that can be used with {@code set()} or {@code update()} that tells the + * server to set the field to the maximum of its current value and the given value. + * + * @return The {@code FieldValue} sentinel for use in a call to {@code set()} or {@code update()}. + */ + @NonNull + public static FieldValue maximum(double l) { + return new NumericMaximumFieldValue(l); + } + /** * Creates a new {@link VectorValue} constructed with a copy of the given array of doubles. * diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java index 20fcd9054ca..28d29f4b743 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java @@ -23,6 +23,9 @@ import com.google.firebase.firestore.FieldValue.ArrayRemoveFieldValue; import com.google.firebase.firestore.FieldValue.ArrayUnionFieldValue; import com.google.firebase.firestore.FieldValue.DeleteFieldValue; +import com.google.firebase.firestore.FieldValue.NumericIncrementFieldValue; +import com.google.firebase.firestore.FieldValue.NumericMaximumFieldValue; +import com.google.firebase.firestore.FieldValue.NumericMinimumFieldValue; import com.google.firebase.firestore.FieldValue.ServerTimestampFieldValue; import com.google.firebase.firestore.core.UserData; import com.google.firebase.firestore.core.UserData.ParseAccumulator; @@ -36,6 +39,8 @@ import com.google.firebase.firestore.model.mutation.ArrayTransformOperation; import com.google.firebase.firestore.model.mutation.FieldMask; import com.google.firebase.firestore.model.mutation.NumericIncrementTransformOperation; +import com.google.firebase.firestore.model.mutation.NumericMaximumTransformOperation; +import com.google.firebase.firestore.model.mutation.NumericMinimumTransformOperation; import com.google.firebase.firestore.model.mutation.ServerTimestampOperation; import com.google.firebase.firestore.pipeline.Expression; import com.google.firebase.firestore.util.Assert; @@ -369,16 +374,27 @@ private void parseSentinelFieldValue( ArrayTransformOperation arrayRemove = new ArrayTransformOperation.Remove(parsedElements); context.addToFieldTransforms(context.getPath(), arrayRemove); - } else if (value - instanceof com.google.firebase.firestore.FieldValue.NumericIncrementFieldValue) { - com.google.firebase.firestore.FieldValue.NumericIncrementFieldValue - numericIncrementFieldValue = - (com.google.firebase.firestore.FieldValue.NumericIncrementFieldValue) value; + } else if (value instanceof NumericIncrementFieldValue) { + NumericIncrementFieldValue numericIncrementFieldValue = (NumericIncrementFieldValue) value; Value operand = parseQueryValue(numericIncrementFieldValue.getOperand()); NumericIncrementTransformOperation incrementOperation = new NumericIncrementTransformOperation(operand); context.addToFieldTransforms(context.getPath(), incrementOperation); + } else if (value instanceof NumericMinimumFieldValue) { + NumericMinimumFieldValue numericMinimumFieldValue = (NumericMinimumFieldValue) value; + Value operand = parseQueryValue(numericMinimumFieldValue.getOperand()); + NumericMinimumTransformOperation minimumOperation = + new NumericMinimumTransformOperation(operand); + context.addToFieldTransforms(context.getPath(), minimumOperation); + + } else if (value instanceof NumericMaximumFieldValue) { + NumericMaximumFieldValue numericMaximumFieldValue = (NumericMaximumFieldValue) value; + Value operand = parseQueryValue(numericMaximumFieldValue.getOperand()); + NumericMaximumTransformOperation maximumOperation = + new NumericMaximumTransformOperation(operand); + context.addToFieldTransforms(context.getPath(), maximumOperation); + } else { throw Assert.fail("Unknown FieldValue type: %s", Util.typeName(value)); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericIncrementTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericIncrementTransformOperation.java index 0dae39ae03d..04b03923d5e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericIncrementTransformOperation.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericIncrementTransformOperation.java @@ -16,12 +16,10 @@ import static com.google.firebase.firestore.model.Values.isDouble; import static com.google.firebase.firestore.model.Values.isInteger; -import static com.google.firebase.firestore.util.Assert.fail; import static com.google.firebase.firestore.util.Assert.hardAssert; import androidx.annotation.Nullable; import com.google.firebase.Timestamp; -import com.google.firebase.firestore.model.Values; import com.google.firestore.v1.Value; /** @@ -29,14 +27,9 @@ * Converts all field values to longs or doubles and resolves overflows to * Long.MAX_VALUE/Long.MIN_VALUE. */ -public class NumericIncrementTransformOperation implements TransformOperation { - private Value operand; - +public class NumericIncrementTransformOperation extends NumericTransformOperation { public NumericIncrementTransformOperation(Value operand) { - hardAssert( - Values.isNumber(operand), - "NumericIncrementTransformOperation expects a NumberValue operand"); - this.operand = operand; + super(operand); } @Override @@ -60,26 +53,6 @@ public Value applyToLocalView(@Nullable Value previousValue, Timestamp localWrit } } - @Override - public Value applyToRemoteDocument(@Nullable Value previousValue, Value transformResult) { - return transformResult; - } - - public Value getOperand() { - return operand; - } - - /** - * Inspects the provided value, returning the provided value if it is already a NumberValue, - * otherwise returning a coerced IntegerValue of 0. - */ - @Override - public Value computeBaseValue(@Nullable Value previousValue) { - return Values.isNumber(previousValue) - ? previousValue - : Value.newBuilder().setIntegerValue(0).build(); - } - /** * Implementation of Java 8's `addExact()` that resolves positive and negative numeric overflows * to Long.MAX_VALUE or Long.MIN_VALUE respectively (instead of throwing an ArithmeticException). @@ -98,28 +71,4 @@ private long safeIncrement(long x, long y) { return Long.MAX_VALUE; } } - - private double operandAsDouble() { - if (isDouble(operand)) { - return operand.getDoubleValue(); - } else if (isInteger(operand)) { - return operand.getIntegerValue(); - } else { - throw fail( - "Expected 'operand' to be of Number type, but was " - + operand.getClass().getCanonicalName()); - } - } - - private long operandAsLong() { - if (isDouble(operand)) { - return (long) operand.getDoubleValue(); - } else if (isInteger(operand)) { - return operand.getIntegerValue(); - } else { - throw fail( - "Expected 'operand' to be of Number type, but was " - + operand.getClass().getCanonicalName()); - } - } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java new file mode 100644 index 00000000000..a6a7a41e9bb --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java @@ -0,0 +1,64 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.firestore.model.mutation; + +import static com.google.firebase.firestore.model.Values.isInteger; + +import androidx.annotation.Nullable; +import com.google.firebase.Timestamp; +import com.google.firebase.firestore.model.Values; +import com.google.firestore.v1.Value; + +/** + * Implements the backend semantics for locally computed NUMERIC_MAX (maximum) transforms. + */ +public class NumericMaximumTransformOperation extends NumericTransformOperation { + public NumericMaximumTransformOperation(Value operand) { + super(operand); + } + + @Override + public Value applyToLocalView(@Nullable Value previousValue, Timestamp localWriteTime) { + if (!Values.isNumber(previousValue)) { + return operand; + } + + // Return an integer value only if the previous value and the operand is an integer. + if (isInteger(previousValue) && isInteger(operand)) { + long max = Math.max(previousValue.getIntegerValue(), operandAsLong()); + return Value.newBuilder().setIntegerValue(max).build(); + } else { + double prevDouble = + isInteger(previousValue) + ? previousValue.getIntegerValue() + : previousValue.getDoubleValue(); + double operDouble = operandAsDouble(); + + if (Double.isNaN(prevDouble)) { + return previousValue; + } + if (Double.isNaN(operDouble)) { + return operand; + } + + if (prevDouble == operDouble) { + return previousValue; + } + + boolean choosePrevious = prevDouble > operDouble; + return choosePrevious ? previousValue : operand; + } + } +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java new file mode 100644 index 00000000000..939b2eb21de --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java @@ -0,0 +1,64 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.firestore.model.mutation; + +import static com.google.firebase.firestore.model.Values.isInteger; + +import androidx.annotation.Nullable; +import com.google.firebase.Timestamp; +import com.google.firebase.firestore.model.Values; +import com.google.firestore.v1.Value; + +/** + * Implements the backend semantics for locally computed NUMERIC_MIN (minimum) transforms. + */ +public class NumericMinimumTransformOperation extends NumericTransformOperation { + public NumericMinimumTransformOperation(Value operand) { + super(operand); + } + + @Override + public Value applyToLocalView(@Nullable Value previousValue, Timestamp localWriteTime) { + if (!Values.isNumber(previousValue)) { + return operand; + } + + // Return an integer value only if the previous value and the operand is an integer. + if (isInteger(previousValue) && isInteger(operand)) { + long min = Math.min(previousValue.getIntegerValue(), operandAsLong()); + return Value.newBuilder().setIntegerValue(min).build(); + } else { + double prevDouble = + isInteger(previousValue) + ? previousValue.getIntegerValue() + : previousValue.getDoubleValue(); + double operDouble = operandAsDouble(); + + if (Double.isNaN(prevDouble)) { + return previousValue; + } + if (Double.isNaN(operDouble)) { + return operand; + } + + if (prevDouble == operDouble) { + return previousValue; + } + + boolean choosePrevious = prevDouble < operDouble; + return choosePrevious ? previousValue : operand; + } + } +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java new file mode 100644 index 00000000000..8059b3b91f7 --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java @@ -0,0 +1,101 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.firestore.model.mutation; + +import static com.google.firebase.firestore.model.Values.isDouble; +import static com.google.firebase.firestore.model.Values.isInteger; +import static com.google.firebase.firestore.util.Assert.fail; +import static com.google.firebase.firestore.util.Assert.hardAssert; + +import androidx.annotation.Nullable; +import com.google.firebase.firestore.model.Values; +import com.google.firestore.v1.Value; + +/** + * // Implements the backend semantics for locally computed numeric transforms. + * // Base class for increment, minimum, and maximum transforms. + */ +public abstract class NumericTransformOperation implements TransformOperation { + protected final Value operand; + + public NumericTransformOperation(Value operand) { + hardAssert(Values.isNumber(operand), "NumericTransformOperation expects a NumberValue operand"); + this.operand = operand; + } + + @Override + public Value applyToRemoteDocument(@Nullable Value previousValue, Value transformResult) { + return transformResult; + } + + public Value getOperand() { + return operand; + } + + /** + * Inspects the provided value, returning the provided value if it is already a NumberValue, + * otherwise returning a coerced IntegerValue of 0. + */ + @Override + public Value computeBaseValue(@Nullable Value previousValue) { + return Values.isNumber(previousValue) + ? previousValue + : Value.newBuilder().setIntegerValue(0).build(); + } + + protected double operandAsDouble() { + if (isDouble(operand)) { + return operand.getDoubleValue(); + } else if (isInteger(operand)) { + return operand.getIntegerValue(); + } else { + throw fail( + "Expected 'operand' to be of Number type, but was " + + operand.getClass().getCanonicalName()); + } + } + + protected long operandAsLong() { + if (isDouble(operand)) { + return (long) operand.getDoubleValue(); + } else if (isInteger(operand)) { + return operand.getIntegerValue(); + } else { + throw fail( + "Expected 'operand' to be of Number type, but was " + + operand.getClass().getCanonicalName()); + } + } + + @Override + @SuppressWarnings("EqualsGetClass") + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NumericTransformOperation that = (NumericTransformOperation) o; + return operand.equals(that.operand); + } + + @Override + public int hashCode() { + int result = getClass().hashCode(); + result = 31 * result + operand.hashCode(); + return result; + } +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java index 1c82f0f9581..76c9001b41f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java @@ -48,6 +48,8 @@ import com.google.firebase.firestore.model.mutation.Mutation; import com.google.firebase.firestore.model.mutation.MutationResult; import com.google.firebase.firestore.model.mutation.NumericIncrementTransformOperation; +import com.google.firebase.firestore.model.mutation.NumericMaximumTransformOperation; +import com.google.firebase.firestore.model.mutation.NumericMinimumTransformOperation; import com.google.firebase.firestore.model.mutation.PatchMutation; import com.google.firebase.firestore.model.mutation.Precondition; import com.google.firebase.firestore.model.mutation.ServerTimestampOperation; @@ -419,6 +421,20 @@ private DocumentTransform.FieldTransform encodeFieldTransform(FieldTransform fie .setFieldPath(fieldTransform.getFieldPath().canonicalString()) .setIncrement(incrementOperation.getOperand()) .build(); + } else if (transform instanceof NumericMinimumTransformOperation) { + NumericMinimumTransformOperation minimumOperation = + (NumericMinimumTransformOperation) transform; + return DocumentTransform.FieldTransform.newBuilder() + .setFieldPath(fieldTransform.getFieldPath().canonicalString()) + .setMinimum(minimumOperation.getOperand()) + .build(); + } else if (transform instanceof NumericMaximumTransformOperation) { + NumericMaximumTransformOperation maximumOperation = + (NumericMaximumTransformOperation) transform; + return DocumentTransform.FieldTransform.newBuilder() + .setFieldPath(fieldTransform.getFieldPath().canonicalString()) + .setMaximum(maximumOperation.getOperand()) + .build(); } else { throw fail("Unknown transform: %s", transform); } @@ -449,6 +465,14 @@ private FieldTransform decodeFieldTransform(DocumentTransform.FieldTransform fie return new FieldTransform( FieldPath.fromServerFormat(fieldTransform.getFieldPath()), new NumericIncrementTransformOperation(fieldTransform.getIncrement())); + case MINIMUM: + return new FieldTransform( + FieldPath.fromServerFormat(fieldTransform.getFieldPath()), + new NumericMinimumTransformOperation(fieldTransform.getMinimum())); + case MAXIMUM: + return new FieldTransform( + FieldPath.fromServerFormat(fieldTransform.getFieldPath()), + new NumericMaximumTransformOperation(fieldTransform.getMaximum())); default: throw fail("Unknown FieldTransform proto: %s", fieldTransform); } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java index cc3671ff242..df328ee4057 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java @@ -233,6 +233,134 @@ public void testAppliesIncrementTransformToMissingField() { verifyTransform(baseDoc, transform, expected); } + @Test + public void testAppliesMinimumTransformToDocument() { + Map baseDoc = + map( + "longMinLong", + 5, + "longMinDouble", + 5, + "doubleMinLong", + 5.5, + "doubleMinDouble", + 5.5, + "longMinNan", + 5, + "doubleMinNan", + 5.5); + Map transform = + map( + "longMinLong", + FieldValue.minimum(2), + "longMinDouble", + FieldValue.minimum(2.2), + "doubleMinLong", + FieldValue.minimum(2), + "doubleMinDouble", + FieldValue.minimum(2.2), + "longMinNan", + FieldValue.minimum(Double.NaN), + "doubleMinNan", + FieldValue.minimum(Double.NaN)); + Map expected = + map( + "longMinLong", + 2L, + "longMinDouble", + 2.2D, + "doubleMinLong", + 2.0D, + "doubleMinDouble", + 2.2D, + "longMinNan", + Double.NaN, + "doubleMinNan", + Double.NaN); + + verifyTransform(baseDoc, transform, expected); + } + + @Test + public void testAppliesMaximumTransformToDocument() { + Map baseDoc = + map( + "longMaxLong", + 5, + "longMaxDouble", + 5, + "doubleMaxLong", + 5.5, + "doubleMaxDouble", + 5.5, + "longMaxNan", + 5, + "doubleMaxNan", + 5.5); + Map transform = + map( + "longMaxLong", + FieldValue.maximum(8), + "longMaxDouble", + FieldValue.maximum(8.8), + "doubleMaxLong", + FieldValue.maximum(8), + "doubleMaxDouble", + FieldValue.maximum(8.8), + "longMaxNan", + FieldValue.maximum(Double.NaN), + "doubleMaxNan", + FieldValue.maximum(Double.NaN)); + Map expected = + map( + "longMaxLong", + 8L, + "longMaxDouble", + 8.8D, + "doubleMaxLong", + 8.0D, + "doubleMaxDouble", + 8.8D, + "longMaxNan", + Double.NaN, + "doubleMaxNan", + Double.NaN); + + verifyTransform(baseDoc, transform, expected); + } + + @Test + public void testAppliesMinimumTransformToUnexpectedType() { + Map baseDoc = map("string", "value"); + Map transform = map("string", FieldValue.minimum(1)); + Map expected = map("string", 1); + verifyTransform(baseDoc, transform, expected); + } + + @Test + public void testAppliesMaximumTransformToUnexpectedType() { + Map baseDoc = map("string", "value"); + Map transform = map("string", FieldValue.maximum(1)); + Map expected = map("string", 1); + verifyTransform(baseDoc, transform, expected); + } + + @Test + public void testAppliesMinimumTransformToMissingField() { + Map baseDoc = map(); + Map transform = map("missing", FieldValue.minimum(1)); + Map expected = map("missing", 1); + verifyTransform(baseDoc, transform, expected); + } + + @Test + public void testAppliesMaximumTransformToMissingField() { + Map baseDoc = map(); + Map transform = map("missing", FieldValue.maximum(1)); + Map expected = map("missing", 1); + verifyTransform(baseDoc, transform, expected); + } + @Test public void testAppliesIncrementTransformsConsecutively() { Map baseDoc = map("number", 1); @@ -679,6 +807,90 @@ public void testNumericIncrementBaseValue() { assertEquals(expected, baseValue.get(FieldPath.EMPTY_PATH)); } + @Test + public void testNumericMinimumBaseValue() { + Map allValues = + map("ignore", "foo", "double", 42.0, "long", 42, "string", "foo", "map", map()); + allValues.put("nested", new HashMap<>(allValues)); + MutableDocument baseDoc = doc("collection/key", 1, allValues); + + Map allTransforms = + map( + "double", + FieldValue.minimum(1), + "long", + FieldValue.minimum(1), + "string", + FieldValue.minimum(1), + "map", + FieldValue.minimum(1), + "missing", + FieldValue.minimum(1)); + allTransforms.put("nested", new HashMap<>(allTransforms)); + + Mutation mutation = patchMutation("collection/key", allTransforms); + ObjectValue baseValue = mutation.extractTransformBaseValue(baseDoc); + + Value expected = + wrap( + map( + "double", + 42.0, + "long", + 42, + "string", + 0, + "map", + 0, + "missing", + 0, + "nested", + map("double", 42.0, "long", 42, "string", 0, "map", 0, "missing", 0))); + assertEquals(expected, baseValue.get(FieldPath.EMPTY_PATH)); + } + + @Test + public void testNumericMaximumBaseValue() { + Map allValues = + map("ignore", "foo", "double", 42.0, "long", 42, "string", "foo", "map", map()); + allValues.put("nested", new HashMap<>(allValues)); + MutableDocument baseDoc = doc("collection/key", 1, allValues); + + Map allTransforms = + map( + "double", + FieldValue.maximum(1), + "long", + FieldValue.maximum(1), + "string", + FieldValue.maximum(1), + "map", + FieldValue.maximum(1), + "missing", + FieldValue.maximum(1)); + allTransforms.put("nested", new HashMap<>(allTransforms)); + + Mutation mutation = patchMutation("collection/key", allTransforms); + ObjectValue baseValue = mutation.extractTransformBaseValue(baseDoc); + + Value expected = + wrap( + map( + "double", + 42.0, + "long", + 42, + "string", + 0, + "map", + 0, + "missing", + 0, + "nested", + map("double", 42.0, "long", 42, "string", 0, "map", 0, "missing", 0))); + assertEquals(expected, baseValue.get(FieldPath.EMPTY_PATH)); + } + @Test public void testIncrementTwice() { MutableDocument patchDoc = doc("collection/key", 1, map("sum", "0")); From 31c4b75aa6c82f748be6cb5df16c132af0314580 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 6 May 2026 13:59:59 -0600 Subject: [PATCH 2/8] Update firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../firestore/model/mutation/NumericTransformOperation.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java index 8059b3b91f7..09a63aa003e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java @@ -24,8 +24,8 @@ import com.google.firestore.v1.Value; /** - * // Implements the backend semantics for locally computed numeric transforms. - * // Base class for increment, minimum, and maximum transforms. + * Implements the backend semantics for locally computed numeric transforms. + * Base class for increment, minimum, and maximum transforms. */ public abstract class NumericTransformOperation implements TransformOperation { protected final Value operand; From 063550876752febbfb876e589548b9be59b54a46 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 6 May 2026 14:20:47 -0600 Subject: [PATCH 3/8] Make min and max transforms idempotent --- .../NumericIncrementTransformOperation.java | 8 ++++ .../mutation/NumericTransformOperation.java | 7 +--- .../model/mutation/MutationTest.java | 38 +------------------ 3 files changed, 12 insertions(+), 41 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericIncrementTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericIncrementTransformOperation.java index 04b03923d5e..51a9cd755a8 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericIncrementTransformOperation.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericIncrementTransformOperation.java @@ -20,6 +20,7 @@ import androidx.annotation.Nullable; import com.google.firebase.Timestamp; +import com.google.firebase.firestore.model.Values; import com.google.firestore.v1.Value; /** @@ -32,6 +33,13 @@ public NumericIncrementTransformOperation(Value operand) { super(operand); } + @Override + public Value computeBaseValue(@Nullable Value previousValue) { + return Values.isNumber(previousValue) + ? previousValue + : Value.newBuilder().setIntegerValue(0).build(); + } + @Override public Value applyToLocalView(@Nullable Value previousValue, Timestamp localWriteTime) { Value baseValue = computeBaseValue(previousValue); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java index 8059b3b91f7..4d5b9da3f30 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericTransformOperation.java @@ -45,14 +45,11 @@ public Value getOperand() { } /** - * Inspects the provided value, returning the provided value if it is already a NumberValue, - * otherwise returning a coerced IntegerValue of 0. + * Returns null since minimum and maximum operations do not require a base value. */ @Override public Value computeBaseValue(@Nullable Value previousValue) { - return Values.isNumber(previousValue) - ? previousValue - : Value.newBuilder().setIntegerValue(0).build(); + return null; } protected double operandAsDouble() { diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java index df328ee4057..45a23e5f7e3 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java @@ -829,24 +829,7 @@ public void testNumericMinimumBaseValue() { allTransforms.put("nested", new HashMap<>(allTransforms)); Mutation mutation = patchMutation("collection/key", allTransforms); - ObjectValue baseValue = mutation.extractTransformBaseValue(baseDoc); - - Value expected = - wrap( - map( - "double", - 42.0, - "long", - 42, - "string", - 0, - "map", - 0, - "missing", - 0, - "nested", - map("double", 42.0, "long", 42, "string", 0, "map", 0, "missing", 0))); - assertEquals(expected, baseValue.get(FieldPath.EMPTY_PATH)); + assertNull(mutation.extractTransformBaseValue(baseDoc)); } @Test @@ -871,24 +854,7 @@ public void testNumericMaximumBaseValue() { allTransforms.put("nested", new HashMap<>(allTransforms)); Mutation mutation = patchMutation("collection/key", allTransforms); - ObjectValue baseValue = mutation.extractTransformBaseValue(baseDoc); - - Value expected = - wrap( - map( - "double", - 42.0, - "long", - 42, - "string", - 0, - "map", - 0, - "missing", - 0, - "nested", - map("double", 42.0, "long", 42, "string", 0, "map", 0, "missing", 0))); - assertEquals(expected, baseValue.get(FieldPath.EMPTY_PATH)); + assertNull(mutation.extractTransformBaseValue(baseDoc)); } @Test From b453c748c3d7c09db00d3d687a7410faa32c3f22 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 6 May 2026 14:26:31 -0600 Subject: [PATCH 4/8] api.txt --- firebase-firestore/api.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 51e21d3d1a3..7232a19b414 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -154,6 +154,10 @@ package com.google.firebase.firestore { method public static com.google.firebase.firestore.FieldValue delete(); method public static com.google.firebase.firestore.FieldValue increment(double); method public static com.google.firebase.firestore.FieldValue increment(long); + method public static com.google.firebase.firestore.FieldValue maximum(double); + method public static com.google.firebase.firestore.FieldValue maximum(long); + method public static com.google.firebase.firestore.FieldValue minimum(double); + method public static com.google.firebase.firestore.FieldValue minimum(long); method public static com.google.firebase.firestore.FieldValue serverTimestamp(); method public static com.google.firebase.firestore.VectorValue vector(double[]); } From 4d242c8bcbd02a4d4c71f7b8df1b6eff11992011 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 6 May 2026 14:29:53 -0600 Subject: [PATCH 5/8] changelog --- firebase-firestore/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 9e1c59dbaa6..6252eb288a4 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- [feature] Added support for `minimum` and `maximum` FieldValue operations. - [feature] Added search stage support for `languageCode`, `offset`, `limit`, and `retrievalDepth`. - [feature] Added support for Pipeline expressions `arraySlice`, `arraySliceToEnd`, `arrayFilter`, `arrayTransform` and `arrayTransformWithIndex`. [#7989](https://github.com/firebase/firebase-android-sdk/pull/7989) From aca5a2a15cb7c3f68368e8335f15e4f1374963d0 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 6 May 2026 14:35:01 -0600 Subject: [PATCH 6/8] version bump --- firebase-firestore/gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-firestore/gradle.properties b/firebase-firestore/gradle.properties index 5d76df5886c..ed45c5b82da 100644 --- a/firebase-firestore/gradle.properties +++ b/firebase-firestore/gradle.properties @@ -1,2 +1,2 @@ -version=26.3.0 -latestReleasedVersion=26.2.0 +version=26.4.0 +latestReleasedVersion=26.3.0 From bac497a56f9943cce6856f9eb026cd32e6db3268 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 6 May 2026 16:03:19 -0600 Subject: [PATCH 7/8] backport promotion to double --- .../firestore/NumericTransformsTest.java | 26 +++++++++---------- .../NumericMaximumTransformOperation.java | 8 ++---- .../NumericMinimumTransformOperation.java | 8 ++---- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java index b05845716ee..f444aab239b 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java @@ -286,52 +286,50 @@ public void maximumWithExistingDouble() { @Test public void mixedTypesPreserveOperandTypeForMinimum() { - // field and input value of mixed types: field takes on type of smaller operand + // field and input value of mixed types: promoted to double if either is a double writeInitialData(map("sum", 10L)); waitFor(docRef.update("sum", FieldValue.minimum(5.5D))); - // 5.5D is smaller, so sum should become 5.5D (double) expectLocalAndRemoteValue(5.5D); writeInitialData(map("sum", 10.5D)); waitFor(docRef.update("sum", FieldValue.minimum(5L))); - // 5L is smaller, so sum should become 5L (long) - expectLocalAndRemoteValue(5L); + // Promotes to 5.0D (double) + expectLocalAndRemoteValue(5.0D); } @Test public void mixedTypesPreserveOperandTypeForMaximum() { - // field and input value of mixed types: field takes on type of larger operand + // field and input value of mixed types: promoted to double if either is a double writeInitialData(map("sum", 10L)); waitFor(docRef.update("sum", FieldValue.maximum(20.5D))); - // 20.5D is larger, so sum should become 20.5D (double) expectLocalAndRemoteValue(20.5D); writeInitialData(map("sum", 10.5D)); waitFor(docRef.update("sum", FieldValue.maximum(20L))); - // 20L is larger, so sum should become 20L (long) - expectLocalAndRemoteValue(20L); + // Promotes to 20.0D (double) + expectLocalAndRemoteValue(20.0D); } @Test public void equivalentValuesDoNotChangeTypeForMinimum() { - // equivalent (e.g. 3 and 3.0), field does not change type + // equivalent (e.g. 3 and 3.0), promoted to double if either is a double writeInitialData(map("sum", 3L)); waitFor(docRef.update("sum", FieldValue.minimum(3.0D))); - // 3L is equivalent to 3.0D, field type/value should remain 3L (integer) - expectLocalAndRemoteValue(3L); + // Promotes to 3.0D (double) + expectLocalAndRemoteValue(3.0D); writeInitialData(map("sum", 3.0D)); waitFor(docRef.update("sum", FieldValue.minimum(3L))); - // 3.0D is equivalent to 3L, field type/value should remain 3.0D (double) expectLocalAndRemoteValue(3.0D); } @Test public void equivalentValuesDoNotChangeTypeForMaximum() { - // equivalent (e.g. 3 and 3.0), field does not change type + // equivalent (e.g. 3 and 3.0), promoted to double if either is a double writeInitialData(map("sum", 3L)); waitFor(docRef.update("sum", FieldValue.maximum(3.0D))); - expectLocalAndRemoteValue(3L); + // Promotes to 3.0D (double) + expectLocalAndRemoteValue(3.0D); writeInitialData(map("sum", 3.0D)); waitFor(docRef.update("sum", FieldValue.maximum(3L))); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java index a6a7a41e9bb..c5aefb1d423 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java @@ -53,12 +53,8 @@ public Value applyToLocalView(@Nullable Value previousValue, Timestamp localWrit return operand; } - if (prevDouble == operDouble) { - return previousValue; - } - - boolean choosePrevious = prevDouble > operDouble; - return choosePrevious ? previousValue : operand; + double max = Math.max(prevDouble, operDouble); + return Value.newBuilder().setDoubleValue(max).build(); } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java index 939b2eb21de..c5df8f28706 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java @@ -53,12 +53,8 @@ public Value applyToLocalView(@Nullable Value previousValue, Timestamp localWrit return operand; } - if (prevDouble == operDouble) { - return previousValue; - } - - boolean choosePrevious = prevDouble < operDouble; - return choosePrevious ? previousValue : operand; + double min = Math.min(prevDouble, operDouble); + return Value.newBuilder().setDoubleValue(min).build(); } } } From 28fc613bc4c39b34f1e7033532fa69dbc08189c1 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 6 May 2026 17:09:24 -0600 Subject: [PATCH 8/8] fix tests --- .../firestore/NumericTransformsTest.java | 20 ++++++++----------- .../NumericMaximumTransformOperation.java | 8 ++++++-- .../NumericMinimumTransformOperation.java | 8 ++++++-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java index f444aab239b..9b3e089d04e 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/NumericTransformsTest.java @@ -286,37 +286,34 @@ public void maximumWithExistingDouble() { @Test public void mixedTypesPreserveOperandTypeForMinimum() { - // field and input value of mixed types: promoted to double if either is a double + // field and input value of mixed types: field takes on type of smaller operand writeInitialData(map("sum", 10L)); waitFor(docRef.update("sum", FieldValue.minimum(5.5D))); expectLocalAndRemoteValue(5.5D); writeInitialData(map("sum", 10.5D)); waitFor(docRef.update("sum", FieldValue.minimum(5L))); - // Promotes to 5.0D (double) - expectLocalAndRemoteValue(5.0D); + expectLocalAndRemoteValue(5L); } @Test public void mixedTypesPreserveOperandTypeForMaximum() { - // field and input value of mixed types: promoted to double if either is a double + // field and input value of mixed types: field takes on type of larger operand writeInitialData(map("sum", 10L)); waitFor(docRef.update("sum", FieldValue.maximum(20.5D))); expectLocalAndRemoteValue(20.5D); writeInitialData(map("sum", 10.5D)); waitFor(docRef.update("sum", FieldValue.maximum(20L))); - // Promotes to 20.0D (double) - expectLocalAndRemoteValue(20.0D); + expectLocalAndRemoteValue(20L); } @Test public void equivalentValuesDoNotChangeTypeForMinimum() { - // equivalent (e.g. 3 and 3.0), promoted to double if either is a double + // equivalent (e.g. 3 and 3.0), field does not change type writeInitialData(map("sum", 3L)); waitFor(docRef.update("sum", FieldValue.minimum(3.0D))); - // Promotes to 3.0D (double) - expectLocalAndRemoteValue(3.0D); + expectLocalAndRemoteValue(3L); writeInitialData(map("sum", 3.0D)); waitFor(docRef.update("sum", FieldValue.minimum(3L))); @@ -325,11 +322,10 @@ public void equivalentValuesDoNotChangeTypeForMinimum() { @Test public void equivalentValuesDoNotChangeTypeForMaximum() { - // equivalent (e.g. 3 and 3.0), promoted to double if either is a double + // equivalent (e.g. 3 and 3.0), field does not change type writeInitialData(map("sum", 3L)); waitFor(docRef.update("sum", FieldValue.maximum(3.0D))); - // Promotes to 3.0D (double) - expectLocalAndRemoteValue(3.0D); + expectLocalAndRemoteValue(3L); writeInitialData(map("sum", 3.0D)); waitFor(docRef.update("sum", FieldValue.maximum(3L))); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java index c5aefb1d423..a6a7a41e9bb 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMaximumTransformOperation.java @@ -53,8 +53,12 @@ public Value applyToLocalView(@Nullable Value previousValue, Timestamp localWrit return operand; } - double max = Math.max(prevDouble, operDouble); - return Value.newBuilder().setDoubleValue(max).build(); + if (prevDouble == operDouble) { + return previousValue; + } + + boolean choosePrevious = prevDouble > operDouble; + return choosePrevious ? previousValue : operand; } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java index c5df8f28706..939b2eb21de 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/NumericMinimumTransformOperation.java @@ -53,8 +53,12 @@ public Value applyToLocalView(@Nullable Value previousValue, Timestamp localWrit return operand; } - double min = Math.min(prevDouble, operDouble); - return Value.newBuilder().setDoubleValue(min).build(); + if (prevDouble == operDouble) { + return previousValue; + } + + boolean choosePrevious = prevDouble < operDouble; + return choosePrevious ? previousValue : operand; } } }