Skip to content

Commit 7212327

Browse files
wu-huidlarocquecloud-java-bot
authored
feat: Add DML stages to pipelines (#2317)
* chore: add a simple verify skill * feat: DML prototype * make options and returns internal, add update stage (#2335) * make options and returns internal, add update stage * chore: generate libraries at Wed Mar 4 16:26:17 UTC 2026 --------- Co-authored-by: cloud-java-bot <cloud-java-bot@google.com> * Add pipeline DML tests and fix update verification * Delete upsert() from Pipeline.java and tests * Delete insert() --------- Co-authored-by: Daniel La Rocque <dlarocque@google.com> Co-authored-by: cloud-java-bot <cloud-java-bot@google.com>
1 parent df07a64 commit 7212327

7 files changed

Lines changed: 604 additions & 1 deletion

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
name: Verify Local Changes
3+
description: Verifies local Java SDK changes.
4+
---
5+
6+
# Verify Local Changes
7+
8+
This skill documents how to verify local code changes for the Java Firestore SDK. This should be run **every time** you complete a fix or feature and are prepared to push a pull request.
9+
10+
## Prerequisites
11+
12+
Ensure you have Maven installed and are in the `java-firestore` directory before running commands.
13+
14+
---
15+
16+
## Step 0: Format the Code
17+
18+
Run the formatter to ensure formatting checks pass:
19+
20+
```bash
21+
mvn com.spotify.fmt:fmt-maven-plugin:format
22+
```
23+
24+
---
25+
26+
## Step 1: Unit Testing (Isolated then Suite)
27+
28+
1. **Identify modified unit tests** in your changes.
29+
2. **Run specific units only** to test isolated logic regressions:
30+
```bash
31+
mvn test -Dtest=MyUnitTest#testMethod
32+
```
33+
3. **Run the entire unit test suite** that contains those modified tests if the isolated unit tests pass:
34+
```bash
35+
mvn test -Dtest=MyUnitTest
36+
```
37+
38+
---
39+
40+
## Step 2: Integration Testing (Isolated then Suite)
41+
42+
### 💡 Integration Test Nuances (from `ITBaseTest.java`)
43+
44+
When running integration tests, configure your execution using properties or environment variables:
45+
46+
- **`FIRESTORE_EDITION`**:
47+
- `standard` (Default)
48+
- `enterprise`
49+
- *Note*: **Pipelines can only be run against `enterprise` editions**, while standard Queries run on both.
50+
- **`FIRESTORE_NAMED_DATABASE`**:
51+
- Enterprise editions usually require a named database (often `enterprise`). Adjust this flag if pointing to specific instances.
52+
- **`FIRESTORE_TARGET_BACKEND`**:
53+
- `PROD` (Default)
54+
- `QA` (points to standard sandboxes)
55+
- `NIGHTLY` (points to `test-firestore.sandbox.googleapis.com:443`)
56+
- `EMULATOR` (points to `localhost:8080`)
57+
58+
1. **Identify modified integration tests** (usually Starting in `IT`).
59+
2. **Run specific integration tests only** (isolated checks run quicker):
60+
```bash
61+
mvn verify -Penable-integration-tests -DFIRESTORE_EDITION=enterprise -DFIRESTORE_NAMED_DATABASE=enterprise -Dtest=ITTest#testMethod -Dclirr.skip=true -Denforcer.skip=true -fae
62+
```
63+
3. **Run the entire integration test suite** for the modified class if isolation tests pass:
64+
```bash
65+
mvn verify -Penable-integration-tests -DFIRESTORE_EDITION=enterprise -DFIRESTORE_NAMED_DATABASE=enterprise -Dtest=ITTest -Dclirr.skip=true -Denforcer.skip=true -fae
66+
```
67+
68+
69+
70+
---
71+
72+
## Step 3: Full Suite Regressions
73+
74+
Run the full integration regression suite once you are confident subsets pass:
75+
76+
```bash
77+
mvn verify -Penable-integration-tests -DFIRESTORE_EDITION=enterprise -DFIRESTORE_NAMED_DATABASE=enterprise -Dclirr.skip=true -Denforcer.skip=true -fae
78+
```
79+
80+
---
81+
82+
> [!TIP]
83+
> Use `-Dclirr.skip=true -Denforcer.skip=true` to speed up iterations where appropriate without leaking compliance checks.
84+
85+
---
86+
87+
## 🛠️ Troubleshooting & Source of Truth
88+
89+
If you run into issues executing tests with the commands above, **consult the Kokoro configuration files** as the ultimate source of truth:
90+
91+
- **Presubmit configurations**: See `.kokoro/presubmit/integration.cfg` (or `integration-named-db.cfg`)
92+
- **Nightly configurations**: See `.kokoro/nightly/integration.cfg`
93+
- **Build shell scripts**: See `.kokoro/build.sh`
94+
95+
These files define the exact environment variables (e.g., specific endpoints or endpoints overrides) the CI server uses!

java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import com.google.cloud.firestore.pipeline.stages.AddFields;
4242
import com.google.cloud.firestore.pipeline.stages.Aggregate;
4343
import com.google.cloud.firestore.pipeline.stages.AggregateOptions;
44+
import com.google.cloud.firestore.pipeline.stages.Delete;
4445
import com.google.cloud.firestore.pipeline.stages.Distinct;
4546
import com.google.cloud.firestore.pipeline.stages.FindNearest;
4647
import com.google.cloud.firestore.pipeline.stages.FindNearestOptions;
@@ -58,6 +59,7 @@
5859
import com.google.cloud.firestore.pipeline.stages.Union;
5960
import com.google.cloud.firestore.pipeline.stages.Unnest;
6061
import com.google.cloud.firestore.pipeline.stages.UnnestOptions;
62+
import com.google.cloud.firestore.pipeline.stages.Update;
6163
import com.google.cloud.firestore.pipeline.stages.Where;
6264
import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext;
6365
import com.google.cloud.firestore.telemetry.TelemetryConstants;
@@ -996,7 +998,119 @@ public Pipeline unnest(Selectable field, UnnestOptions options) {
996998
}
997999

9981000
/**
999-
* Adds a generic stage to the pipeline.
1001+
* Performs a delete operation on documents from previous stages.
1002+
*
1003+
* <p>Example:
1004+
*
1005+
* <pre>{@code
1006+
* // Delete all documents in the "logs" collection where "status" is "archived"
1007+
* firestore.pipeline()
1008+
* .collection("logs")
1009+
* .where(field("status").equal("archived"))
1010+
* .delete()
1011+
* .execute()
1012+
* .get();
1013+
* }</pre>
1014+
*
1015+
* @return A new {@code Pipeline} object with this stage appended to the stage list.
1016+
*/
1017+
@BetaApi
1018+
public Pipeline delete() {
1019+
return append(new Delete());
1020+
}
1021+
1022+
/**
1023+
* Performs an update operation using documents from previous stages.
1024+
*
1025+
* <p>This method updates the documents in place based on the data flowing through the pipeline.
1026+
* To specify transformations, use {@link #update(Selectable...)}.
1027+
*
1028+
* <p>Example 1: Update a collection's schema by adding a new field and removing an old one.
1029+
*
1030+
* <pre>{@code
1031+
* firestore.pipeline()
1032+
* .collection("books")
1033+
* .addFields(constant("Fiction").as("genre"))
1034+
* .removeFields("old_genre")
1035+
* .update()
1036+
* .execute()
1037+
* .get();
1038+
* }</pre>
1039+
*
1040+
* <p>Example 2: Update documents in place with data from literals.
1041+
*
1042+
* <pre>{@code
1043+
* Map<String, Object> updateData = new HashMap<>();
1044+
* updateData.put("__name__", firestore.collection("books").document("book1"));
1045+
* updateData.put("status", "Updated");
1046+
*
1047+
* firestore.pipeline()
1048+
* .literals(updateData)
1049+
* .update()
1050+
* .execute()
1051+
* .get();
1052+
* }</pre>
1053+
*
1054+
* @return A new {@code Pipeline} object with this stage appended to the stage list.
1055+
*/
1056+
@BetaApi
1057+
public Pipeline update() {
1058+
return append(new Update());
1059+
}
1060+
1061+
/**
1062+
* Performs an update operation using documents from previous stages with specified
1063+
* transformations.
1064+
*
1065+
* <p>Example:
1066+
*
1067+
* <pre>{@code
1068+
* // Update the "status" field to "Discounted" for all books where price > 50
1069+
* firestore.pipeline()
1070+
* .collection("books")
1071+
* .where(field("price").greaterThan(50))
1072+
* .update(constant("Discounted").as("status"))
1073+
* .execute()
1074+
* .get();
1075+
* }</pre>
1076+
*
1077+
* @param transformedFields The transformations to apply.
1078+
* @return A new {@code Pipeline} object with this stage appended to the stage list.
1079+
*/
1080+
@BetaApi
1081+
public Pipeline update(Selectable... transformedFields) {
1082+
return append(new Update().withTransformedFields(transformedFields));
1083+
}
1084+
1085+
/**
1086+
* Performs an update operation using an {@link Update} stage.
1087+
*
1088+
* <p>This method allows you to use a pre-configured {@link Update} stage.
1089+
*
1090+
* <p>Example:
1091+
*
1092+
* <pre>{@code
1093+
* Update updateStage = new Update().withTransformedFields(constant("Updated").as("status"));
1094+
*
1095+
* firestore.pipeline()
1096+
* .collection("books")
1097+
* .where(field("title").equal("The Hitchhiker's Guide to the Galaxy"))
1098+
* .update(updateStage)
1099+
* .execute()
1100+
* .get();
1101+
* }</pre>
1102+
*
1103+
* @param update The {@code Update} stage to append.
1104+
* @return A new {@code Pipeline} object with this stage appended to the stage list.
1105+
*/
1106+
@BetaApi
1107+
public Pipeline update(Update update) {
1108+
return append(update);
1109+
}
1110+
1111+
/**
1112+
* Performs an insert operation using documents from previous stages. Adds a generic stage to the
1113+
* pipeline.
10001114
*
10011115
* <p>This method provides a flexible way to extend the pipeline's functionality by adding custom
10021116
* stages. Each generic stage is defined by a unique `name` and a set of `params` that control its

java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.cloud.firestore.pipeline.stages.CollectionOptions;
2525
import com.google.cloud.firestore.pipeline.stages.Database;
2626
import com.google.cloud.firestore.pipeline.stages.Documents;
27+
import com.google.cloud.firestore.pipeline.stages.Literals;
2728
import com.google.common.base.Preconditions;
2829
import java.util.Arrays;
2930
import javax.annotation.Nonnull;
@@ -157,6 +158,32 @@ public Pipeline documents(String... docs) {
157158
.toArray(DocumentReference[]::new)));
158159
}
159160

161+
/**
162+
* Creates a new {@link Pipeline} that operates on a static set of documents represented as Maps.
163+
*
164+
* <p>Example:
165+
*
166+
* <pre>{@code
167+
* Map<String, Object> doc1 = new HashMap<>();
168+
* doc1.put("title", "Book 1");
169+
* Map<String, Object> doc2 = new HashMap<>();
170+
* doc2.put("title", "Book 2");
171+
*
172+
* Snapshot snapshot = firestore.pipeline()
173+
* .literals(doc1, doc2)
174+
* .execute()
175+
* .get();
176+
* }</pre>
177+
*
178+
* @param data The Maps representing documents to include in the pipeline.
179+
* @return A new {@code Pipeline} instance with a literals source.
180+
*/
181+
@Nonnull
182+
@BetaApi
183+
public final Pipeline literals(java.util.Map<String, Object>... data) {
184+
return new Pipeline(this.rpcContext, new Literals(data));
185+
}
186+
160187
/**
161188
* Creates a new {@link Pipeline} from the given {@link Query}. Under the hood, this will
162189
* translate the query semantics (order by document ID, etc.) to an equivalent pipeline.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.firestore.pipeline.stages;
18+
19+
import com.google.api.core.BetaApi;
20+
import com.google.api.core.InternalApi;
21+
import com.google.firestore.v1.Value;
22+
import java.util.ArrayList;
23+
24+
@InternalApi
25+
public final class Delete extends Stage {
26+
private Delete(InternalOptions options) {
27+
super("delete", options);
28+
}
29+
30+
@BetaApi
31+
public Delete() {
32+
this(InternalOptions.EMPTY);
33+
}
34+
35+
@Override
36+
Iterable<Value> toStageArgs() {
37+
return new ArrayList<>();
38+
}
39+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.firestore.pipeline.stages;
18+
19+
import com.google.api.core.BetaApi;
20+
import com.google.api.core.InternalApi;
21+
import com.google.cloud.firestore.PipelineUtils;
22+
import com.google.firestore.v1.Value;
23+
import java.util.ArrayList;
24+
import java.util.Arrays;
25+
import java.util.List;
26+
import java.util.Map;
27+
28+
@InternalApi
29+
public final class Literals extends Stage {
30+
31+
private final List<Map<String, Object>> data;
32+
33+
@BetaApi
34+
public Literals(Map<String, Object>... data) {
35+
super("literals", InternalOptions.EMPTY);
36+
this.data = Arrays.asList(data);
37+
}
38+
39+
@Override
40+
Iterable<Value> toStageArgs() {
41+
List<Value> args = new ArrayList<>();
42+
for (Map<String, Object> map : data) {
43+
args.add(encodeLiteralMap(map));
44+
}
45+
return args;
46+
}
47+
48+
private Value encodeLiteralMap(Map<?, ?> map) {
49+
com.google.firestore.v1.MapValue.Builder mapValue =
50+
com.google.firestore.v1.MapValue.newBuilder();
51+
for (Map.Entry<?, ?> entry : map.entrySet()) {
52+
String key = String.valueOf(entry.getKey());
53+
Object v = entry.getValue();
54+
if (v instanceof com.google.cloud.firestore.pipeline.expressions.Expression) {
55+
mapValue.putFields(
56+
key,
57+
PipelineUtils.encodeValue(
58+
(com.google.cloud.firestore.pipeline.expressions.Expression) v));
59+
} else if (v instanceof Map) {
60+
mapValue.putFields(key, encodeLiteralMap((Map<?, ?>) v));
61+
} else {
62+
mapValue.putFields(key, PipelineUtils.encodeValue(v));
63+
}
64+
}
65+
return Value.newBuilder().setMapValue(mapValue.build()).build();
66+
}
67+
}

0 commit comments

Comments
 (0)