Skip to content

Commit df07a64

Browse files
authored
feat(firestore): Add ifNull and coalesce expressions (#2349)
1 parent 4e4e6ef commit df07a64

2 files changed

Lines changed: 207 additions & 0 deletions

File tree

  • java-firestore/google-cloud-firestore/src

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

Lines changed: 121 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
*
@@ -5118,6 +5212,33 @@ public Expression ifAbsent(Object elseValue) {
51185212
return Expression.ifAbsent(this, elseValue);
51195213
}
51205214

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+
51215242
/**
51225243
* Creates an expression that joins the elements of this array expression into a string.
51235244
*

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3185,6 +3185,92 @@ public void testIfAbsent() throws Exception {
31853185
assertThat(data(results)).containsExactly(map("res", "Frank Herbert"));
31863186
}
31873187

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+
31883274
@Test
31893275
public void testJoin() throws Exception {
31903276
// Test join with a constant delimiter

0 commit comments

Comments
 (0)