From 2e713799f6ec36127371e055036aea6983c4a944 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Tue, 5 May 2026 14:39:10 -0600 Subject: [PATCH 1/4] feature(firestore): added minimum and maximum FieldValue operations --- .../google/cloud/firestore/FieldValue.java | 154 ++++++++++++++++++ .../firestore/DocumentReferenceTest.java | 52 ++++++ .../cloud/firestore/LocalFirestoreHelper.java | 8 + .../cloud/firestore/it/ITSystemTest.java | 54 ++++++ 4 files changed, 268 insertions(+) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java index 2cfc41acf3..3854e2f4ba 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java @@ -127,6 +127,104 @@ public int hashCode() { } } + static class NumericMinimumFieldValue extends FieldValue { + final Number operand; + + NumericMinimumFieldValue(Number operand) { + this.operand = operand; + } + + @Override + boolean includeInDocumentMask() { + return false; + } + + @Override + boolean includeInDocumentTransform() { + return true; + } + + @Override + String getMethodName() { + return "FieldValue.minimum()"; + } + + @Override + FieldTransform toProto(FieldPath path) { + FieldTransform.Builder fieldTransform = FieldTransform.newBuilder(); + fieldTransform.setFieldPath(path.getEncodedPath()); + fieldTransform.setMinimum( + UserDataConverter.encodeValue(path, operand, UserDataConverter.ARGUMENT)); + return fieldTransform.build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NumericMinimumFieldValue that = (NumericMinimumFieldValue) o; + return Objects.equals(operand, that.operand); + } + + @Override + public int hashCode() { + return Objects.hash(operand); + } + } + + static class NumericMaximumFieldValue extends FieldValue { + final Number operand; + + NumericMaximumFieldValue(Number operand) { + this.operand = operand; + } + + @Override + boolean includeInDocumentMask() { + return false; + } + + @Override + boolean includeInDocumentTransform() { + return true; + } + + @Override + String getMethodName() { + return "FieldValue.maximum()"; + } + + @Override + FieldTransform toProto(FieldPath path) { + FieldTransform.Builder fieldTransform = FieldTransform.newBuilder(); + fieldTransform.setFieldPath(path.getEncodedPath()); + fieldTransform.setMaximum( + UserDataConverter.encodeValue(path, operand, UserDataConverter.ARGUMENT)); + return fieldTransform.build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NumericMaximumFieldValue that = (NumericMaximumFieldValue) o; + return Objects.equals(operand, that.operand); + } + + @Override + public int hashCode() { + return Objects.hash(operand); + } + } + static class ArrayUnionFieldValue extends FieldValue { final List elements; @@ -289,6 +387,62 @@ public static FieldValue increment(double d) { return new NumericIncrementFieldValue(d); } + /** + * Returns a special value that can be used with set(), create() or update() that tells the server + * to set the field to the numeric minimum of the field's current and the given value. + * + *

If the current field value is not of type 'number', or if the field does not yet exist, the + * transformation will set the field to the given value. + * + * @return The FieldValue sentinel for use in a call to set(), create() or update(). + */ + @Nonnull + public static FieldValue minimum(long l) { + return new NumericMinimumFieldValue(l); + } + + /** + * Returns a special value that can be used with set(), create() or update() that tells the server + * to set the field to the numeric minimum of the field's current and the given value. + * + *

If the current field value is not of type 'number', or if the field does not yet exist, the + * transformation will set the field to the given value. + * + * @return The FieldValue sentinel for use in a call to set(), create() or update(). + */ + @Nonnull + public static FieldValue minimum(double d) { + return new NumericMinimumFieldValue(d); + } + + /** + * Returns a special value that can be used with set(), create() or update() that tells the server + * to set the field to the numeric maximum of the field's current and the given value. + * + *

If the current field value is not of type 'number', or if the field does not yet exist, the + * transformation will set the field to the given value. + * + * @return The FieldValue sentinel for use in a call to set(), create() or update(). + */ + @Nonnull + public static FieldValue maximum(long l) { + return new NumericMaximumFieldValue(l); + } + + /** + * Returns a special value that can be used with set(), create() or update() that tells the server + * to set the field to the numeric maximum of the field's current and the given value. + * + *

If the current field value is not of type 'number', or if the field does not yet exist, the + * transformation will set the field to the given value. + * + * @return The FieldValue sentinel for use in a call to set(), create() or update(). + */ + @Nonnull + public static FieldValue maximum(double d) { + return new NumericMaximumFieldValue(d); + } + /** * Returns a special value that can be used with set(), create() or update() that tells the server * to union the given elements with any array value that already exists on the server. Each diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java index b425e58794..6c833f02a3 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java @@ -47,6 +47,8 @@ import static com.google.cloud.firestore.LocalFirestoreHelper.get; import static com.google.cloud.firestore.LocalFirestoreHelper.getAllResponse; import static com.google.cloud.firestore.LocalFirestoreHelper.increment; +import static com.google.cloud.firestore.LocalFirestoreHelper.minimum; +import static com.google.cloud.firestore.LocalFirestoreHelper.maximum; import static com.google.cloud.firestore.LocalFirestoreHelper.map; import static com.google.cloud.firestore.LocalFirestoreHelper.object; import static com.google.cloud.firestore.LocalFirestoreHelper.serverTimestamp; @@ -530,6 +532,56 @@ public void setWithIncrement() throws Exception { assertCommitEquals(set, commitRequest); } + @Test + public void setWithMinimum() throws Exception { + doReturn(FIELD_TRANSFORM_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), + ArgumentMatchers.>any()); + + documentReference + .set(map("integer", FieldValue.minimum(1), "double", FieldValue.minimum(1.1))) + .get(); + + CommitRequest set = + commit( + set(Collections.emptyMap()), + transform( + "integer", + minimum(Value.newBuilder().setIntegerValue(1).build()), + "double", + minimum(Value.newBuilder().setDoubleValue(1.1).build()))); + + CommitRequest commitRequest = commitCapture.getValue(); + assertCommitEquals(set, commitRequest); + } + + @Test + public void setWithMaximum() throws Exception { + doReturn(FIELD_TRANSFORM_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), + ArgumentMatchers.>any()); + + documentReference + .set(map("integer", FieldValue.maximum(1), "double", FieldValue.maximum(1.1))) + .get(); + + CommitRequest set = + commit( + set(Collections.emptyMap()), + transform( + "integer", + maximum(Value.newBuilder().setIntegerValue(1).build()), + "double", + maximum(Value.newBuilder().setDoubleValue(1.1).build()))); + + CommitRequest commitRequest = commitCapture.getValue(); + assertCommitEquals(set, commitRequest); + } + @Test public void setWithArrayUnion() throws Exception { doReturn(FIELD_TRANSFORM_COMMIT_RESPONSE) diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java index 500e35ff96..4396247cb4 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java @@ -501,6 +501,14 @@ public static FieldTransform increment(Value value) { return FieldTransform.newBuilder().setIncrement(value).build(); } + public static FieldTransform minimum(Value value) { + return FieldTransform.newBuilder().setMinimum(value).build(); + } + + public static FieldTransform maximum(Value value) { + return FieldTransform.newBuilder().setMaximum(value).build(); + } + public static FieldTransform arrayUnion(Value... values) { return FieldTransform.newBuilder() .setAppendMissingElements(ArrayValue.newBuilder().addAllValues(Arrays.asList(values))) diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java index 1712046be9..40c1ac5edc 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java @@ -1970,6 +1970,60 @@ public void floatIncrement() throws ExecutionException, InterruptedException { assertEquals(3.3, (Double) docSnap.get("sum"), DOUBLE_EPSILON); } + @Test + public void integerMinimum() throws ExecutionException, InterruptedException { + DocumentReference docRef = randomColl.document(); + docRef.set(Collections.singletonMap("min", 2L)).get(); + docRef.update("min", FieldValue.minimum(1)).get(); + DocumentSnapshot docSnap = docRef.get().get(); + assertEquals(1L, docSnap.get("min")); + } + + @Test + public void floatMinimum() throws ExecutionException, InterruptedException { + DocumentReference docRef = randomColl.document(); + docRef.set(Collections.singletonMap("min", 2.2)).get(); + docRef.update("min", FieldValue.minimum(1.1)).get(); + DocumentSnapshot docSnap = docRef.get().get(); + assertEquals(1.1, (Double) docSnap.get("min"), DOUBLE_EPSILON); + } + + @Test + public void minimumAgainstNonNumeric() throws ExecutionException, InterruptedException { + DocumentReference docRef = randomColl.document(); + docRef.set(Collections.singletonMap("min", "any string")).get(); + docRef.update("min", FieldValue.minimum(1)).get(); + DocumentSnapshot docSnap = docRef.get().get(); + assertEquals(1L, docSnap.get("min")); + } + + @Test + public void integerMaximum() throws ExecutionException, InterruptedException { + DocumentReference docRef = randomColl.document(); + docRef.set(Collections.singletonMap("max", 1L)).get(); + docRef.update("max", FieldValue.maximum(2)).get(); + DocumentSnapshot docSnap = docRef.get().get(); + assertEquals(2L, docSnap.get("max")); + } + + @Test + public void floatMaximum() throws ExecutionException, InterruptedException { + DocumentReference docRef = randomColl.document(); + docRef.set(Collections.singletonMap("max", 1.1)).get(); + docRef.update("max", FieldValue.maximum(2.2)).get(); + DocumentSnapshot docSnap = docRef.get().get(); + assertEquals(2.2, (Double) docSnap.get("max"), DOUBLE_EPSILON); + } + + @Test + public void maximumAgainstNonNumeric() throws ExecutionException, InterruptedException { + DocumentReference docRef = randomColl.document(); + docRef.set(Collections.singletonMap("max", "any string")).get(); + docRef.update("max", FieldValue.maximum(2)).get(); + DocumentSnapshot docSnap = docRef.get().get(); + assertEquals(2L, docSnap.get("max")); + } + @Test public void getAllWithObserver() throws Exception { DocumentReference ref1 = randomColl.document("doc1"); From 99af4cfdb9ed494f1d6c89ed11a241ef151670f2 Mon Sep 17 00:00:00 2001 From: cloud-java-bot Date: Tue, 5 May 2026 20:43:55 +0000 Subject: [PATCH 2/4] chore: generate libraries at Tue May 5 20:41:43 UTC 2026 --- .../com/google/cloud/firestore/DocumentReferenceTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java index 6c833f02a3..d043168b49 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java @@ -47,9 +47,9 @@ import static com.google.cloud.firestore.LocalFirestoreHelper.get; import static com.google.cloud.firestore.LocalFirestoreHelper.getAllResponse; import static com.google.cloud.firestore.LocalFirestoreHelper.increment; -import static com.google.cloud.firestore.LocalFirestoreHelper.minimum; -import static com.google.cloud.firestore.LocalFirestoreHelper.maximum; import static com.google.cloud.firestore.LocalFirestoreHelper.map; +import static com.google.cloud.firestore.LocalFirestoreHelper.maximum; +import static com.google.cloud.firestore.LocalFirestoreHelper.minimum; import static com.google.cloud.firestore.LocalFirestoreHelper.object; import static com.google.cloud.firestore.LocalFirestoreHelper.serverTimestamp; import static com.google.cloud.firestore.LocalFirestoreHelper.set; From 58bbee1fc04d4ee207ba06a2537d2ba3ea4bea1b Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 6 May 2026 17:10:59 -0600 Subject: [PATCH 3/4] fix: Correct github action paths for v1.86.0 update The recent renovate update to v1.86.0 in #2377 incorrectly pointed to a subfolder of google-cloud-java instead of the correct sdk-platform-java repository, breaking the hermetic library generation and unmanaged dependency check jobs. This restores the correct path. --- .github/workflows/hermetic_library_generation.yaml | 2 +- .github/workflows/unmanaged-dependency-check.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hermetic_library_generation.yaml b/.github/workflows/hermetic_library_generation.yaml index c41b1a72f2..574f357589 100644 --- a/.github/workflows/hermetic_library_generation.yaml +++ b/.github/workflows/hermetic_library_generation.yaml @@ -37,7 +37,7 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.CLOUD_JAVA_BOT_GITHUB_TOKEN }} - - uses: googleapis/google-cloud-java/sdk-platform-java/.github/scripts@v1.86.0 + - uses: googleapis/sdk-platform-java/.github/scripts@v1.86.0 if: env.SHOULD_RUN == 'true' with: base_ref: ${{ github.base_ref }} diff --git a/.github/workflows/unmanaged-dependency-check.yaml b/.github/workflows/unmanaged-dependency-check.yaml index 09dc4e7a2a..7ccaa99de8 100644 --- a/.github/workflows/unmanaged-dependency-check.yaml +++ b/.github/workflows/unmanaged-dependency-check.yaml @@ -14,6 +14,6 @@ jobs: shell: bash run: .kokoro/build.sh - name: Unmanaged dependency check - uses: googleapis/google-cloud-java/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@v1.86.0 + uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@v1.86.0 with: bom-path: google-cloud-firestore-bom/pom.xml From a6581907d6dfd13cef952e6479a7e564b5a27a8a Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Thu, 7 May 2026 09:41:30 -0600 Subject: [PATCH 4/4] Revert "fix: Correct github action paths for v1.86.0 update" This reverts commit 58bbee1fc04d4ee207ba06a2537d2ba3ea4bea1b. --- .github/workflows/hermetic_library_generation.yaml | 2 +- .github/workflows/unmanaged-dependency-check.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hermetic_library_generation.yaml b/.github/workflows/hermetic_library_generation.yaml index 574f357589..c41b1a72f2 100644 --- a/.github/workflows/hermetic_library_generation.yaml +++ b/.github/workflows/hermetic_library_generation.yaml @@ -37,7 +37,7 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.CLOUD_JAVA_BOT_GITHUB_TOKEN }} - - uses: googleapis/sdk-platform-java/.github/scripts@v1.86.0 + - uses: googleapis/google-cloud-java/sdk-platform-java/.github/scripts@v1.86.0 if: env.SHOULD_RUN == 'true' with: base_ref: ${{ github.base_ref }} diff --git a/.github/workflows/unmanaged-dependency-check.yaml b/.github/workflows/unmanaged-dependency-check.yaml index 7ccaa99de8..09dc4e7a2a 100644 --- a/.github/workflows/unmanaged-dependency-check.yaml +++ b/.github/workflows/unmanaged-dependency-check.yaml @@ -14,6 +14,6 @@ jobs: shell: bash run: .kokoro/build.sh - name: Unmanaged dependency check - uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@v1.86.0 + uses: googleapis/google-cloud-java/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@v1.86.0 with: bom-path: google-cloud-firestore-bom/pom.xml