diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java index f75b0f95e..82a3b192b 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java @@ -277,6 +277,100 @@ public static Expression ifAbsent(String ifFieldName, Object elseValue) { return ifAbsent(field(ifFieldName), toExprOrConstant(elseValue)); } + /** + * Creates an expression that returns a default value if an expression evaluates to null. + * + *

Note: This function provides a fallback for both absent and explicit null values. In + * contrast, {@link ifAbsent} only triggers for missing fields. + * + * @param ifExpr The expression to check. + * @param elseExpression The default value. + * @return A new {@link Expression} representing the ifNull operation. + */ + @BetaApi + public static Expression ifNull(Expression ifExpr, Expression elseExpression) { + return new FunctionExpression("if_null", ImmutableList.of(ifExpr, elseExpression)); + } + + /** + * Creates an expression that returns a default value if an expression evaluates to null. + * + *

Note: This function provides a fallback for both absent and explicit null values. In + * contrast, {@link ifAbsent} only triggers for missing fields. + * + * @param ifExpr The expression to check. + * @param elseValue The default value. + * @return A new {@link Expression} representing the ifNull operation. + */ + @BetaApi + public static Expression ifNull(Expression ifExpr, Object elseValue) { + return ifNull(ifExpr, toExprOrConstant(elseValue)); + } + + /** + * Creates an expression that returns a default value if a field is null. + * + *

Note: This function provides a fallback for both absent and explicit null values. In + * contrast, {@link ifAbsent} only triggers for missing fields. + * + * @param ifFieldName The field to check. + * @param elseExpression The default value. + * @return A new {@link Expression} representing the ifNull operation. + */ + @BetaApi + public static Expression ifNull(String ifFieldName, Expression elseExpression) { + return ifNull(field(ifFieldName), elseExpression); + } + + /** + * Creates an expression that returns a default value if a field is null. + * + *

Note: This function provides a fallback for both absent and explicit null values. In + * contrast, {@link ifAbsent} only triggers for missing fields. + * + * @param ifFieldName The field to check. + * @param elseValue The default value. + * @return A new {@link Expression} representing the ifNull operation. + */ + @BetaApi + public static Expression ifNull(String ifFieldName, Object elseValue) { + return ifNull(field(ifFieldName), toExprOrConstant(elseValue)); + } + + /** + * Returns the first non-null, non-absent argument, without evaluating the rest of the arguments. + * When all arguments are null or absent, returns the last argument. + * + * @param expression The first expression to check for null. + * @param replacement The fallback expression or value if the first one is null. + * @param others Optional additional expressions to check if previous ones are null. + * @return A new {@link Expression} representing the coalesce operation. + */ + @BetaApi + public static Expression coalesce(Expression expression, Object replacement, Object... others) { + ImmutableList.Builder args = ImmutableList.builder(); + args.add(expression); + args.add(toExprOrConstant(replacement)); + for (Object other : others) { + args.add(toExprOrConstant(other)); + } + return new FunctionExpression("coalesce", args.build()); + } + + /** + * Returns the first non-null, non-absent argument, without evaluating the rest of the arguments. + * When all arguments are null or absent, returns the last argument. + * + * @param firstFieldName The name of the first field to check for null. + * @param replacement The fallback expression or value if the first one is null. + * @param others Optional additional expressions to check if previous ones are null. + * @return A new {@link Expression} representing the coalesce operation. + */ + @BetaApi + public static Expression coalesce(String firstFieldName, Object replacement, Object... others) { + return coalesce(field(firstFieldName), replacement, others); + } + /** * Creates an expression that joins the elements of an array into a string. * @@ -4206,6 +4300,33 @@ public Expression ifAbsent(Object elseValue) { return Expression.ifAbsent(this, elseValue); } + /** + * Creates an expression that returns a default value if this expression evaluates null. + * + *

Note: This function provides a fallback for both absent and explicit null values. In + * contrast, {@link ifAbsent} only triggers for missing fields. + * + * @param elseValue The default value. + * @return A new {@link Expression} representing the ifNull operation. + */ + @BetaApi + public Expression ifNull(Object elseValue) { + return Expression.ifNull(this, elseValue); + } + + /** + * Returns the first non-null, non-absent argument, without evaluating the rest of the arguments. + * When all arguments are null or absent, returns the last argument. + * + * @param second The next expression or literal to evaluate. + * @param others Additional expressions or literals to evaluate. + * @return A new {@link Expression} representing the coalesce operation. + */ + @BetaApi + public Expression coalesce(Object second, Object... others) { + return Expression.coalesce(this, second, others); + } + /** * Creates an expression that joins the elements of this array expression into a string. * diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java index c9b78f11a..ca82a302a 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java @@ -2662,6 +2662,92 @@ public void testIfAbsent() throws Exception { assertThat(data(results)).containsExactly(map("res", "Frank Herbert")); } + @Test + public void testIfNull() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .limit(1) + .replaceWith(Expression.map(map("title", "foo", "name", null))) + .select( + Expression.ifNull("title", "default title").as("staticMethod"), + field("title").ifNull("default title").as("instanceMethod"), + field("name").ifNull(field("title")).as("nameOrTitle"), + field("name").ifNull("default name").as("fieldIsNull"), + field("absent").ifNull("default name").as("fieldIsAbsent")) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .containsExactly( + map( + "staticMethod", "foo", + "instanceMethod", "foo", + "nameOrTitle", "foo", + "fieldIsNull", "default name", + "fieldIsAbsent", "default name")); + } + + @Test + public void testCoalesce() throws Exception { + assumeFalse( + "Coalesce is not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .limit(1) + .replaceWith( + Expression.map( + map( + "numberValue", + 1L, + "stringValue", + "hello", + "booleanValue", + false, + "nullValue", + null, + "nullValue2", + null))) + .select( + Expression.coalesce(field("numberValue"), field("stringValue")).as("staticMethod"), + field("numberValue").coalesce(field("stringValue")).as("instanceMethod"), + Expression.coalesce(field("nullValue"), field("stringValue")).as("firstIsNull"), + Expression.coalesce(field("nullValue"), field("nullValue2"), field("booleanValue")) + .as("lastIsNotNull"), + Expression.coalesce(field("nullValue"), field("nullValue2")).as("allFieldsNull"), + Expression.coalesce(field("nullValue"), field("nullValue2"), constant("default")) + .as("allFieldsNullWithDefault"), + Expression.coalesce(field("absentField"), field("numberValue"), constant("default")) + .as("withAbsentField")) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .containsExactly( + map( + "staticMethod", + 1L, + "instanceMethod", + 1L, + "firstIsNull", + "hello", + "lastIsNotNull", + false, + "allFieldsNull", + null, + "allFieldsNullWithDefault", + "default", + "withAbsentField", + 1L)); + } + @Test public void testJoin() throws Exception { // Test join with a constant delimiter