diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index b0723cf98a6..72790691c63 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- [feature] Added support for Pipeline expressions `ifNull` and `coalesce`. + [#7976](https://github.com/firebase/firebase-android-sdk/pull/7976) - [feature] Pipeline operations are GA now. - [feature] Added support for Pipeline expressions `nor` and `switchOn`. [#7903](https://github.com/firebase/firebase-android-sdk/pull/7903) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 8ac17922dc2..cb6c3370aeb 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -941,6 +941,9 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.Expression charLength(); method public static final com.google.firebase.firestore.pipeline.Expression charLength(com.google.firebase.firestore.pipeline.Expression expr); method public static final com.google.firebase.firestore.pipeline.Expression charLength(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Expression coalesce(com.google.firebase.firestore.pipeline.Expression expression, Object replacement, java.lang.Object... others); + method public final com.google.firebase.firestore.pipeline.Expression coalesce(Object replacement, java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expression coalesce(String fieldName, Object replacement, java.lang.Object... others); method public final com.google.firebase.firestore.pipeline.Expression collectionId(); method public static final com.google.firebase.firestore.pipeline.Expression collectionId(com.google.firebase.firestore.pipeline.Expression path); method public static final com.google.firebase.firestore.pipeline.Expression collectionId(String pathField); @@ -1056,6 +1059,12 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.Expression ifError(com.google.firebase.firestore.pipeline.Expression tryExpr, com.google.firebase.firestore.pipeline.Expression catchExpr); method public static final com.google.firebase.firestore.pipeline.Expression ifError(com.google.firebase.firestore.pipeline.Expression tryExpr, Object catchValue); method public final com.google.firebase.firestore.pipeline.Expression ifError(Object catchValue); + method public final com.google.firebase.firestore.pipeline.Expression ifNull(com.google.firebase.firestore.pipeline.Expression elseExpression); + method public static final com.google.firebase.firestore.pipeline.Expression ifNull(com.google.firebase.firestore.pipeline.Expression ifExpr, com.google.firebase.firestore.pipeline.Expression elseExpr); + method public static final com.google.firebase.firestore.pipeline.Expression ifNull(com.google.firebase.firestore.pipeline.Expression ifExpr, Object elseValue); + method public final com.google.firebase.firestore.pipeline.Expression ifNull(Object elseValue); + method public static final com.google.firebase.firestore.pipeline.Expression ifNull(String ifFieldName, com.google.firebase.firestore.pipeline.Expression elseExpr); + method public static final com.google.firebase.firestore.pipeline.Expression ifNull(String ifFieldName, Object elseValue); method public final com.google.firebase.firestore.pipeline.BooleanExpression isAbsent(); method public static final com.google.firebase.firestore.pipeline.BooleanExpression isAbsent(com.google.firebase.firestore.pipeline.Expression value); method public static final com.google.firebase.firestore.pipeline.BooleanExpression isAbsent(String fieldName); @@ -1407,6 +1416,8 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expression ceil(String numericField); method public com.google.firebase.firestore.pipeline.Expression charLength(com.google.firebase.firestore.pipeline.Expression expr); method public com.google.firebase.firestore.pipeline.Expression charLength(String fieldName); + method public com.google.firebase.firestore.pipeline.Expression coalesce(com.google.firebase.firestore.pipeline.Expression expression, Object replacement, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expression coalesce(String fieldName, Object replacement, java.lang.Object... others); method public com.google.firebase.firestore.pipeline.Expression collectionId(com.google.firebase.firestore.pipeline.Expression path); method public com.google.firebase.firestore.pipeline.Expression collectionId(String pathField); method public com.google.firebase.firestore.pipeline.Expression concat(com.google.firebase.firestore.pipeline.Expression first, com.google.firebase.firestore.pipeline.Expression second, java.lang.Object... others); @@ -1486,6 +1497,10 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpression ifError(com.google.firebase.firestore.pipeline.BooleanExpression tryExpr, com.google.firebase.firestore.pipeline.BooleanExpression catchExpr); method public com.google.firebase.firestore.pipeline.Expression ifError(com.google.firebase.firestore.pipeline.Expression tryExpr, com.google.firebase.firestore.pipeline.Expression catchExpr); method public com.google.firebase.firestore.pipeline.Expression ifError(com.google.firebase.firestore.pipeline.Expression tryExpr, Object catchValue); + method public com.google.firebase.firestore.pipeline.Expression ifNull(com.google.firebase.firestore.pipeline.Expression ifExpr, com.google.firebase.firestore.pipeline.Expression elseExpr); + method public com.google.firebase.firestore.pipeline.Expression ifNull(com.google.firebase.firestore.pipeline.Expression ifExpr, Object elseValue); + method public com.google.firebase.firestore.pipeline.Expression ifNull(String ifFieldName, com.google.firebase.firestore.pipeline.Expression elseExpr); + method public com.google.firebase.firestore.pipeline.Expression ifNull(String ifFieldName, Object elseValue); method public com.google.firebase.firestore.pipeline.BooleanExpression isAbsent(com.google.firebase.firestore.pipeline.Expression value); method public com.google.firebase.firestore.pipeline.BooleanExpression isAbsent(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpression isError(com.google.firebase.firestore.pipeline.Expression expr); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/PipelineTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/PipelineTest.java index 419c0f893c1..7e6c42ebbcc 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/PipelineTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/PipelineTest.java @@ -3169,6 +3169,82 @@ public void testIfAbsent() { .containsExactly(ImmutableMap.of("res", "Frank Herbert")); } + @Test + public void testIfNull() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .limit(1) + .replaceWith(map(ImmutableMap.of("title", "foo", "name", Expression.nullValue()))) + .select( + Expression.ifNull("title", "default title").alias("staticMethod"), + field("title").ifNull("default title").alias("instanceMethod"), + field("name").ifNull(field("title")).alias("nameOrTitle"), + field("name").ifNull("default name").alias("fieldIsNull"), + field("absent").ifNull("default name").alias("fieldIsAbsent")) + .execute(); + + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + mapOfEntries( + entry("staticMethod", "foo"), + entry("instanceMethod", "foo"), + entry("nameOrTitle", "foo"), + entry("fieldIsNull", "default name"), + entry("fieldIsAbsent", "default name"))); + } + + @Test + public void testCoalesce() { + assumeFalse("Coalesce is not supported against the emulator.", isRunningAgainstEmulator()); + + Task execute = + firestore + .pipeline() + .collection(randomCol) + .limit(1) + .replaceWith( + map( + ImmutableMap.of( + "numberValue", + 1L, + "stringValue", + "unique_coalesce_hello", + "booleanValue", + false, + "nullValue", + Expression.nullValue(), + "nullValue2", + Expression.nullValue()))) + .select( + Expression.coalesce(field("numberValue"), field("stringValue")) + .alias("staticMethod"), + field("numberValue").coalesce(field("stringValue")).alias("instanceMethod"), + Expression.coalesce(field("nullValue"), field("stringValue")).alias("firstIsNull"), + Expression.coalesce(field("nullValue"), field("nullValue2"), field("booleanValue")) + .alias("lastIsNotNull"), + Expression.coalesce(field("nullValue"), field("nullValue2")).alias("allFieldsNull"), + Expression.coalesce(field("nullValue"), field("nullValue2"), constant("default")) + .alias("allFieldsNullWithDefault"), + Expression.coalesce(field("absentField"), field("numberValue"), constant("default")) + .alias("withAbsentField")) + .execute(); + + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + mapOfEntries( + entry("staticMethod", 1L), + entry("instanceMethod", 1L), + entry("firstIsNull", "unique_coalesce_hello"), + entry("lastIsNotNull", false), + entry("allFieldsNull", null), + entry("allFieldsNullWithDefault", "default"), + entry("withAbsentField", 1L))); + } + @Test public void testCrossDatabaseRejection() { FirebaseFirestore firestore2 = IntegrationTestUtil.testAlternateFirestore(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt index add2a7fae40..295401e186e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt @@ -5921,6 +5921,120 @@ abstract class Expression internal constructor() { fun ifAbsent(ifFieldName: String, elseValue: Any): Expression = FunctionExpression("if_absent", notImplemented, ifFieldName, elseValue) + /** + * Creates an expression that returns the [elseExpr] argument if [ifExpr] evaluates to null, + * else return the result of the [ifExpr] argument evaluation. + * + * This function provides a fallback for both absent and explicit null values. In contrast, + * [ifAbsent] only triggers for missing fields. + * + * ```kotlin + * // Returns the user's preferred name, or if that is null, returns their full name. + * ifNull(field("preferredName"), field("fullName")) + * ``` + * + * @param ifExpr The expression to check for null. + * @param elseExpr The expression that will be evaluated and returned if [ifExpr] is null. + * @return A new [Expression] representing the ifNull operation. + */ + @JvmStatic + fun ifNull(ifExpr: Expression, elseExpr: Expression): Expression = + FunctionExpression("if_null", notImplemented, ifExpr, elseExpr) + + /** + * Creates an expression that returns the [elseValue] argument if [ifExpr] evaluates to null, + * else return the result of the [ifExpr] argument evaluation. + * + * This function provides a fallback for both absent and explicit null values. In contrast, + * [ifAbsent] only triggers for missing fields. + * + * ```kotlin + * // Returns the user's display name, or returns "Anonymous" if the field is null. + * ifNull(field("displayName"), "Anonymous") + * ``` + * + * @param ifExpr The expression to check for null. + * @param elseValue The value that will be returned if [ifExpr] evaluates to null. + * @return A new [Expression] representing the ifNull operation. + */ + @JvmStatic + fun ifNull(ifExpr: Expression, elseValue: Any): Expression = + FunctionExpression("if_null", notImplemented, ifExpr, elseValue) + + /** + * Creates an expression that returns the [elseExpr] argument if [ifFieldName] field is null, + * else return the value of the field. + * + * ```kotlin + * // Returns the user's preferred name, or if that is null, returns their full name. + * ifNull("preferredName", field("fullName")) + * ``` + * + * @param ifFieldName The field to check for null. + * @param elseExpr The expression that will be evaluated and returned if [ifFieldName] is null. + * @return A new [Expression] representing the ifNull operation. + */ + @JvmStatic + fun ifNull(ifFieldName: String, elseExpr: Expression): Expression = + FunctionExpression("if_null", notImplemented, ifFieldName, elseExpr) + + /** + * Creates an expression that returns the [elseValue] argument if [ifFieldName] field is null, + * else return the value of the field. + * + * ```kotlin + * // Returns the user's display name, or returns "Anonymous" if the field is null. + * ifNull("displayName", "Anonymous") + * ``` + * + * @param ifFieldName The field to check for null. + * @param elseValue The value that will be returned if [ifFieldName] is null. + * @return A new [Expression] representing the ifNull operation. + */ + @JvmStatic + fun ifNull(ifFieldName: String, elseValue: Any): Expression = + FunctionExpression("if_null", notImplemented, ifFieldName, elseValue) + + /** + * Creates an expression that 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. + * + * ```kotlin + * // Returns the value of the first non-null, non-absent field among 'preferredName', 'fullName', + * // or the last argument if all previous fields are null. + * coalesce(field("preferredName"), field("fullName"), constant("Anonymous")) + * ``` + * + * @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 [Expression] representing the coalesce operation. + */ + @JvmStatic + fun coalesce(expression: Expression, replacement: Any, vararg others: Any): Expression = + FunctionExpression("coalesce", notImplemented, expression, replacement, *others) + + /** + * Creates an expression that 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. + * + * ```kotlin + * // Returns the value of the first non-null, non-absent field among 'preferredName', 'fullName', + * // or the last argument if all previous fields are null. + * coalesce("preferredName", field("fullName"), constant("Anonymous")) + * ``` + * + * @param fieldName 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 [Expression] representing the coalesce operation. + */ + @JvmStatic + fun coalesce(fieldName: String, replacement: Any, vararg others: Any): Expression = + FunctionExpression("coalesce", notImplemented, fieldName, replacement, *others) + /** * Creates an expression that returns the collection ID from a path. * @@ -8193,6 +8307,58 @@ abstract class Expression internal constructor() { */ fun ifAbsent(elseValue: Any): Expression = Companion.ifAbsent(this, elseValue) + /** + * Creates an expression that returns the [elseExpression] argument if this expression evaluates + * to null, else return the result of this expression. + * + * This function provides a fallback for both absent and explicit null values. In contrast, + * [ifAbsent] only triggers for missing fields. + * + * ```kotlin + * // Returns the user's preferred name, or if that is null, returns their full name. + * field("preferredName").ifNull(field("fullName")) + * ``` + * + * @param elseExpression The expression that will be evaluated and returned if this expression is + * null. + * @return A new [Expression] representing the ifNull operation. + */ + fun ifNull(elseExpression: Expression): Expression = Companion.ifNull(this, elseExpression) + + /** + * Creates an expression that returns the [elseValue] argument if this expression evaluates to + * null, else return the result of this expression. + * + * This function provides a fallback for both absent and explicit null values. In contrast, + * [ifAbsent] only triggers for missing fields. + * + * ```kotlin + * // Returns the user's display name, or returns "Anonymous" if the field is null. + * field("displayName").ifNull("Anonymous") + * ``` + * + * @param elseValue The value that will be returned if this expression evaluates to null. + * @return A new [Expression] representing the ifNull operation. + */ + fun ifNull(elseValue: Any): Expression = Companion.ifNull(this, elseValue) + + /** + * Creates an expression that 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. + * + * ```kotlin + * // Returns the value of the first non-null, non-absent field among 'preferredName', 'fullName', + * // or the last argument if all previous fields are null. + * field("preferredName").coalesce(field("fullName"), "Anonymous") + * ``` + * + * @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 [Expression] representing the coalesce operation. + */ + fun coalesce(replacement: Any, vararg others: Any): Expression = + Companion.coalesce(this, replacement, *others) + /** * Creates an expression that checks if this expression produces an error. *