Skip to content
This repository was archived by the owner on May 8, 2026. It is now read-only.

Commit 196a1fa

Browse files
chore: merge main into generate-libraries-main
2 parents 75e6ecc + 51a1c43 commit 196a1fa

3 files changed

Lines changed: 296 additions & 9 deletions

File tree

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,19 @@ static BooleanExpression toPipelineBooleanExpr(FilterInternal f) {
174174
static AliasedAggregate toPipelineAggregatorTarget(AggregateField f) {
175175
String operator = f.getOperator();
176176
String fieldPath = f.getFieldPath();
177+
String alias = f.getAlias();
178+
if (alias.contains(".")) {
179+
alias = "`" + alias + "`";
180+
}
177181

178182
switch (operator) {
179183
case "sum":
180-
return Field.ofServerPath(fieldPath).sum().as(f.getAlias());
184+
return Field.ofServerPath(fieldPath).sum().as(alias);
181185

182186
case "count":
183-
return countAll().as(f.getAlias());
187+
return countAll().as(alias);
184188
case "average":
185-
return Field.ofServerPath(fieldPath).average().as(f.getAlias());
189+
return Field.ofServerPath(fieldPath).average().as(alias);
186190
default:
187191
// Handle the 'else' case appropriately in your Java code
188192
throw new IllegalArgumentException("Unsupported operator: " + operator);

google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,100 @@ public static Expression ifAbsent(String ifFieldName, Object elseValue) {
277277
return ifAbsent(field(ifFieldName), toExprOrConstant(elseValue));
278278
}
279279

280+
/**
281+
* Creates an expression that returns a default value if an expression evaluates to null.
282+
*
283+
* <p>Note: This function provides a fallback for both absent and explicit null values. In
284+
* contrast, {@link ifAbsent} only triggers for missing fields.
285+
*
286+
* @param ifExpr The expression to check.
287+
* @param elseExpression The default expression that will be evaluated and returned.
288+
* @return A new {@link Expression} representing the ifNull operation.
289+
*/
290+
@BetaApi
291+
public static Expression ifNull(Expression ifExpr, Expression elseExpression) {
292+
return new FunctionExpression("if_null", ImmutableList.of(ifExpr, elseExpression));
293+
}
294+
295+
/**
296+
* Creates an expression that returns a default value if an expression evaluates to null.
297+
*
298+
* <p>Note: This function provides a fallback for both absent and explicit null values. In
299+
* contrast, {@link ifAbsent} only triggers for missing fields.
300+
*
301+
* @param ifExpr The expression to check.
302+
* @param elseValue The default value that will be returned.
303+
* @return A new {@link Expression} representing the ifNull operation.
304+
*/
305+
@BetaApi
306+
public static Expression ifNull(Expression ifExpr, Object elseValue) {
307+
return ifNull(ifExpr, toExprOrConstant(elseValue));
308+
}
309+
310+
/**
311+
* Creates an expression that returns a default value if a field is null.
312+
*
313+
* <p>Note: This function provides a fallback for both absent and explicit null values. In
314+
* contrast, {@link ifAbsent} only triggers for missing fields.
315+
*
316+
* @param ifFieldName The field to check.
317+
* @param elseExpression The default expression that will be evaluated and returned.
318+
* @return A new {@link Expression} representing the ifNull operation.
319+
*/
320+
@BetaApi
321+
public static Expression ifNull(String ifFieldName, Expression elseExpression) {
322+
return ifNull(field(ifFieldName), elseExpression);
323+
}
324+
325+
/**
326+
* Creates an expression that returns a default value if a field is null.
327+
*
328+
* <p>Note: This function provides a fallback for both absent and explicit null values. In
329+
* contrast, {@link ifAbsent} only triggers for missing fields.
330+
*
331+
* @param ifFieldName The field to check.
332+
* @param elseValue The default value that will be returned.
333+
* @return A new {@link Expression} representing the ifNull operation.
334+
*/
335+
@BetaApi
336+
public static Expression ifNull(String ifFieldName, Object elseValue) {
337+
return ifNull(field(ifFieldName), toExprOrConstant(elseValue));
338+
}
339+
340+
/**
341+
* Returns the first non-null, non-absent argument, without evaluating the rest of the arguments.
342+
* When all arguments are null or absent, returns the last argument.
343+
*
344+
* @param expression The first expression to check for null.
345+
* @param replacement The fallback expression or value if the first one is null.
346+
* @param others Optional additional expressions to check if previous ones are null.
347+
* @return A new {@link Expression} representing the coalesce operation.
348+
*/
349+
@BetaApi
350+
public static Expression coalesce(Expression expression, Object replacement, Object... others) {
351+
ImmutableList.Builder<Expression> args = ImmutableList.builder();
352+
args.add(expression);
353+
args.add(toExprOrConstant(replacement));
354+
for (Object other : others) {
355+
args.add(toExprOrConstant(other));
356+
}
357+
return new FunctionExpression("coalesce", args.build());
358+
}
359+
360+
/**
361+
* Returns the first non-null, non-absent argument, without evaluating the rest of the arguments.
362+
* When all arguments are null or absent, returns the last argument.
363+
*
364+
* @param firstFieldName The name of the first field to check for null.
365+
* @param replacement The fallback expression or value if the first one is null.
366+
* @param others Optional additional expressions to check if previous ones are null.
367+
* @return A new {@link Expression} representing the coalesce operation.
368+
*/
369+
@BetaApi
370+
public static Expression coalesce(String firstFieldName, Object replacement, Object... others) {
371+
return coalesce(field(firstFieldName), replacement, others);
372+
}
373+
280374
/**
281375
* Creates an expression that joins the elements of an array into a string.
282376
*
@@ -4364,6 +4458,39 @@ public static Expression collectionId(String pathFieldName) {
43644458
return collectionId(field(pathFieldName));
43654459
}
43664460

4461+
/**
4462+
* Creates an expression that returns the parent document of a document reference.
4463+
*
4464+
* @param documentPath An expression that evaluates to a document path.
4465+
* @return A new {@link Expression} representing the parent operation.
4466+
*/
4467+
@BetaApi
4468+
public static Expression parent(Expression documentPath) {
4469+
return new FunctionExpression("parent", ImmutableList.of(documentPath));
4470+
}
4471+
4472+
/**
4473+
* Creates an expression that returns the parent document of a document reference.
4474+
*
4475+
* @param documentPath The string representation of the document path.
4476+
* @return A new {@link Expression} representing the parent operation.
4477+
*/
4478+
@BetaApi
4479+
public static Expression parent(String documentPath) {
4480+
return parent(constant(documentPath));
4481+
}
4482+
4483+
/**
4484+
* Creates an expression that returns the parent document of a document reference.
4485+
*
4486+
* @param docRef The {@link DocumentReference}.
4487+
* @return A new {@link Expression} representing the parent operation.
4488+
*/
4489+
@BetaApi
4490+
public static Expression parent(DocumentReference docRef) {
4491+
return parent(constant(docRef));
4492+
}
4493+
43674494
// Type Checking Functions
43684495
/**
43694496
* Creates an expression that checks if a field exists.
@@ -5085,6 +5212,33 @@ public Expression ifAbsent(Object elseValue) {
50855212
return Expression.ifAbsent(this, elseValue);
50865213
}
50875214

5215+
/**
5216+
* Creates an expression that returns a default value if this expression evaluates null.
5217+
*
5218+
* <p>Note: This function provides a fallback for both absent and explicit null values. In
5219+
* contrast, {@link ifAbsent} only triggers for missing fields.
5220+
*
5221+
* @param elseValue The default value that will be returned.
5222+
* @return A new {@link Expression} representing the ifNull operation.
5223+
*/
5224+
@BetaApi
5225+
public Expression ifNull(Object elseValue) {
5226+
return Expression.ifNull(this, elseValue);
5227+
}
5228+
5229+
/**
5230+
* Returns the first non-null, non-absent argument, without evaluating the rest of the arguments.
5231+
* When all arguments are null or absent, returns the last argument.
5232+
*
5233+
* @param second The next expression or literal to evaluate.
5234+
* @param others Additional expressions or literals to evaluate.
5235+
* @return A new {@link Expression} representing the coalesce operation.
5236+
*/
5237+
@BetaApi
5238+
public Expression coalesce(Object second, Object... others) {
5239+
return Expression.coalesce(this, second, others);
5240+
}
5241+
50885242
/**
50895243
* Creates an expression that joins the elements of this array expression into a string.
50905244
*
@@ -7016,6 +7170,16 @@ public final Expression collectionId() {
70167170
return collectionId(this);
70177171
}
70187172

7173+
/**
7174+
* Creates an expression that returns the parent document of a document reference.
7175+
*
7176+
* @return A new {@link Expression} representing the parent operation.
7177+
*/
7178+
@BetaApi
7179+
public final Expression parent() {
7180+
return parent(this);
7181+
}
7182+
70197183
/**
70207184
* Creates an expression that returns a string indicating the type of the value this expression
70217185
* evaluates to.

google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
import static com.google.cloud.firestore.pipeline.expressions.Expression.notEqual;
7373
import static com.google.cloud.firestore.pipeline.expressions.Expression.nullValue;
7474
import static com.google.cloud.firestore.pipeline.expressions.Expression.or;
75+
import static com.google.cloud.firestore.pipeline.expressions.Expression.parent;
7576
import static com.google.cloud.firestore.pipeline.expressions.Expression.pow;
7677
import static com.google.cloud.firestore.pipeline.expressions.Expression.rand;
7778
import static com.google.cloud.firestore.pipeline.expressions.Expression.regexMatch;
@@ -109,6 +110,7 @@
109110
import com.google.cloud.Timestamp;
110111
import com.google.cloud.firestore.Blob;
111112
import com.google.cloud.firestore.CollectionReference;
113+
import com.google.cloud.firestore.DocumentReference;
112114
import com.google.cloud.firestore.Firestore;
113115
import com.google.cloud.firestore.FirestoreOptions;
114116
import com.google.cloud.firestore.GeoPoint;
@@ -2295,7 +2297,7 @@ public void testChecks() throws Exception {
22952297
.select(
22962298
field("rating").equal(nullValue()).as("ratingIsNull"),
22972299
field("rating").equal(Double.NaN).as("ratingIsNaN"),
2298-
// arrayGet("title", 0) evaluates to UNSET so it is not an error
2300+
// arrayGet("title", 0) evaluates to ERROR
22992301
arrayGet("title", 0).isError().as("isError"),
23002302
arrayGet("title", 0).ifError(constant("was error")).as("ifError"),
23012303
field("foo").isAbsent().as("isAbsent"),
@@ -2316,7 +2318,9 @@ public void testChecks() throws Exception {
23162318
"ratingIsNaN",
23172319
false,
23182320
"isError",
2319-
false,
2321+
true,
2322+
"ifError",
2323+
"was error",
23202324
"isAbsent",
23212325
true,
23222326
"titleIsNotNull",
@@ -3181,6 +3185,92 @@ public void testIfAbsent() throws Exception {
31813185
assertThat(data(results)).containsExactly(map("res", "Frank Herbert"));
31823186
}
31833187

3188+
@Test
3189+
public void testIfNull() throws Exception {
3190+
List<PipelineResult> results =
3191+
firestore
3192+
.pipeline()
3193+
.collection(collection.getPath())
3194+
.limit(1)
3195+
.replaceWith(Expression.map(map("title", "foo", "name", null)))
3196+
.select(
3197+
Expression.ifNull("title", "default title").as("staticMethod"),
3198+
field("title").ifNull("default title").as("instanceMethod"),
3199+
field("name").ifNull(field("title")).as("nameOrTitle"),
3200+
field("name").ifNull("default name").as("fieldIsNull"),
3201+
field("absent").ifNull("default name").as("fieldIsAbsent"))
3202+
.execute()
3203+
.get()
3204+
.getResults();
3205+
3206+
assertThat(data(results))
3207+
.containsExactly(
3208+
map(
3209+
"staticMethod", "foo",
3210+
"instanceMethod", "foo",
3211+
"nameOrTitle", "foo",
3212+
"fieldIsNull", "default name",
3213+
"fieldIsAbsent", "default name"));
3214+
}
3215+
3216+
@Test
3217+
public void testCoalesce() throws Exception {
3218+
assumeFalse(
3219+
"Coalesce is not supported against the emulator.",
3220+
isRunningAgainstFirestoreEmulator(firestore));
3221+
3222+
List<PipelineResult> results =
3223+
firestore
3224+
.pipeline()
3225+
.collection(collection.getPath())
3226+
.limit(1)
3227+
.replaceWith(
3228+
Expression.map(
3229+
map(
3230+
"numberValue",
3231+
1L,
3232+
"stringValue",
3233+
"hello",
3234+
"booleanValue",
3235+
false,
3236+
"nullValue",
3237+
null,
3238+
"nullValue2",
3239+
null)))
3240+
.select(
3241+
Expression.coalesce(field("numberValue"), field("stringValue")).as("staticMethod"),
3242+
field("numberValue").coalesce(field("stringValue")).as("instanceMethod"),
3243+
Expression.coalesce(field("nullValue"), field("stringValue")).as("firstIsNull"),
3244+
Expression.coalesce(field("nullValue"), field("nullValue2"), field("booleanValue"))
3245+
.as("lastIsNotNull"),
3246+
Expression.coalesce(field("nullValue"), field("nullValue2")).as("allFieldsNull"),
3247+
Expression.coalesce(field("nullValue"), field("nullValue2"), constant("default"))
3248+
.as("allFieldsNullWithDefault"),
3249+
Expression.coalesce(field("absentField"), field("numberValue"), constant("default"))
3250+
.as("withAbsentField"))
3251+
.execute()
3252+
.get()
3253+
.getResults();
3254+
3255+
assertThat(data(results))
3256+
.containsExactly(
3257+
map(
3258+
"staticMethod",
3259+
1L,
3260+
"instanceMethod",
3261+
1L,
3262+
"firstIsNull",
3263+
"hello",
3264+
"lastIsNotNull",
3265+
false,
3266+
"allFieldsNull",
3267+
null,
3268+
"allFieldsNullWithDefault",
3269+
"default",
3270+
"withAbsentField",
3271+
1L));
3272+
}
3273+
31843274
@Test
31853275
public void testJoin() throws Exception {
31863276
// Test join with a constant delimiter
@@ -3535,8 +3625,8 @@ public void testNestedFields() throws Exception {
35353625
assertThat(data(results))
35363626
.isEqualTo(
35373627
Lists.newArrayList(
3538-
map("title", "The Hitchhiker's Guide to the Galaxy", "awards.hugo", true),
3539-
map("title", "Dune", "awards.hugo", true)));
3628+
map("title", "The Hitchhiker's Guide to the Galaxy", "awards", map("hugo", true)),
3629+
map("title", "Dune", "awards", map("hugo", true))));
35403630
}
35413631

35423632
@Test
@@ -3559,8 +3649,12 @@ public void testPipelineInTransactions() throws Exception {
35593649
assertThat(data(results))
35603650
.isEqualTo(
35613651
Lists.newArrayList(
3562-
map("title", "The Hitchhiker's Guide to the Galaxy", "awards.hugo", true),
3563-
map("title", "Dune", "awards.hugo", true)));
3652+
map(
3653+
"title",
3654+
"The Hitchhiker's Guide to the Galaxy",
3655+
"awards",
3656+
map("hugo", true)),
3657+
map("title", "Dune", "awards", map("hugo", true))));
35643658

35653659
transaction.update(collection.document("book1"), map("foo", "bar"));
35663660

@@ -4292,4 +4386,29 @@ public void disallowDuplicateAliasesAcrossStages() {
42924386
});
42934387
assertThat(exception).hasMessageThat().contains("Duplicate alias or field name");
42944388
}
4389+
4390+
@Test
4391+
public void testSupportsParent() throws Exception {
4392+
DocumentReference docRef =
4393+
collection.document("book4").collection("reviews").document("review1");
4394+
4395+
Pipeline pipeline =
4396+
firestore
4397+
.pipeline()
4398+
.collection(collection.getPath())
4399+
.limit(1)
4400+
.select(
4401+
parent(docRef).as("parentRefStatic"),
4402+
constant(docRef).parent().as("parentRefInstance"))
4403+
.select(
4404+
field("parentRefStatic").documentId().as("parentIdStatic"),
4405+
field("parentRefInstance").documentId().as("parentIdInstance"));
4406+
4407+
List<PipelineResult> results = pipeline.execute().get().getResults();
4408+
assertThat(results).hasSize(1);
4409+
Map<String, Object> data = results.get(0).getData();
4410+
4411+
assertThat(data.get("parentIdStatic")).isEqualTo("book4");
4412+
assertThat(data.get("parentIdInstance")).isEqualTo("book4");
4413+
}
42954414
}

0 commit comments

Comments
 (0)