From 0fc509fed48fb527b9e74c08191e3ec34cd86773 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:56:37 -0400 Subject: [PATCH 1/5] feat(firestore): Add ifNull and coalesce --- .../firebase/firestore/PipelineTest.java | 76 +++++++++ .../firestore/pipeline/expressions.kt | 157 ++++++++++++++++++ 2 files changed, 233 insertions(+) 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 f6f1f6d2d1e..3edfa05668f 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 @@ -3128,6 +3128,82 @@ public void testIfAbsent() { .containsExactly(ImmutableMap.of("res", "Frank Herbert")); } + @Test + public void testIfNull() { + Map values = new HashMap<>(); + values.put("title", "unique_foo_title"); + values.put("name", null); + waitFor(randomCol.document("bookWithNull").set(values)); + + Task execute = + firestore + .pipeline() + .collection(randomCol) + .where(equal("title", "unique_foo_title")) + .limit(1) + .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", "unique_foo_title"), + entry("instanceMethod", "unique_foo_title"), + entry("nameOrTitle", "unique_foo_title"), + entry("fieldIsNull", "default name"), + entry("fieldIsAbsent", "default name"))); + + waitFor(randomCol.document("bookWithNull").delete()); + } + + @Test + public void testCoalesce() { + Map values = new HashMap<>(); + values.put("numberValue", 1L); + values.put("stringValue", "unique_coalesce_hello"); + values.put("booleanValue", false); + values.put("nullValue", null); + values.put("nullValue2", null); + waitFor(randomCol.document("bookWithNull").set(values)); + + Task execute = + firestore + .pipeline() + .collection(randomCol) + .where(equal("stringValue", "unique_coalesce_hello")) + .limit(1) + .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(); + + Map expected = new HashMap<>(); + expected.put("staticMethod", 1L); + expected.put("instanceMethod", 1L); + expected.put("firstIsNull", "unique_coalesce_hello"); + expected.put("lastIsNotNull", false); + expected.put("allFieldsNull", null); + expected.put("allFieldsNullWithDefault", "default"); + expected.put("withAbsentField", 1L); + + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly(expected); + + waitFor(randomCol.document("bookWithNull").delete()); + } + @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 3ab61256422..d2858d4e8d3 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 @@ -5923,6 +5923,116 @@ 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] is null or absent, else + * return the result of the [ifExpr] argument evaluation. + * + * ```kotlin + * // Returns the value of the 'optional_field', or returns 'default_value' + * // if the field is null or absent. + * ifNull(field("optional_field"), constant("default_value")) + * ``` + * + * @param ifExpr The expression to check for null or absence. + * @param elseExpr The expression that will be evaluated and returned if [ifExpr] is null or absent. + * @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] is null or absent, else + * return the result of the [ifExpr] argument evaluation. + * + * ```kotlin + * // Returns the value of the 'optional_field', or returns 'default_value' + * // if the field is null or absent. + * ifNull(field("optional_field"), "default_value") + * ``` + * + * @param ifExpr The expression to check for null or absence. + * @param elseValue The value that will be returned if [ifExpr] evaluates to a null or absent value. + * @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] is null or absent, else + * return the value of the field. + * + * ```kotlin + * // Returns the value of the 'optional_field', or returns the value of + * // 'default_field' if 'optional_field' is null or absent. + * ifNull("optional_field", field("default_field")) + * ``` + * + * @param ifFieldName The field to check for null or absence. + * @param elseExpr The expression that will be evaluated and returned if [ifFieldName] is + * null or absent. + * @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] is null or absent, else + * return the value of the field. + * + * ```kotlin + * // Returns the value of the 'optional_field', or returns 'default_value' + * // if the field is null or absent. + * ifNull("optional_field", "default_value") + * ``` + * + * @param ifFieldName The field to check for null or absence. + * @param elseValue The value that will be returned if [ifFieldName] is null or absent. + * @return A new [Expression] representing the ifNull operation. + */ + @JvmStatic + fun ifNull(ifFieldName: String, elseValue: Any): Expression = + FunctionExpression("if_null", notImplemented, ifFieldName, 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. + * + * ```kotlin + * // Returns the first non-null/non-absent value among a, b, and "default" + * coalesce(field("a"), field("b"), constant("default")) + * ``` + * + * @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) + + /** + * 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 first non-null/non-absent value among a, b, and "default" + * coalesce("a", field("b"), "default") + * ``` + * + * @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. * @@ -8195,6 +8305,53 @@ 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 is null, else + * return the result of this expression. + * + * ```kotlin + * // Returns the value of the 'optional_field', or returns 'default_value' + * // if the field is null. + * field("optional_field").ifNull(constant("default_value")) + * ``` + * + * @param elseExpression The expression that will be evaluated and returned if this expression is + * null or absent. + * @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 is null or absent, else + * return the result of this expression. + * + * ```kotlin + * // Returns the value of the 'optional_field', or returns 'default_value' + * // if the field is null or absent. + * field("optional_field").ifNull("default_value") + * ``` + * + * @param elseValue The value that will be returned if this expression evaluates to a null or absent value. + * @return A new [Expression] representing the ifNull operation. + */ + fun ifNull(elseValue: Any): Expression = Companion.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. + * + * ```kotlin + * // Returns the first non-null/non-absent value among a, b, and "default" + * field("a").coalesce(field("b"), "default") + * ``` + * + * @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. * From 9e8d32ffa0121df318f616e1e6a909a84e0850ad Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:41:01 -0400 Subject: [PATCH 2/5] update java docs --- firebase-firestore/api.txt | 15 +++ .../firebase/firestore/PipelineTest.java | 70 ++++++----- .../firestore/pipeline/expressions.kt | 115 ++++++++++-------- 3 files changed, 111 insertions(+), 89 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index f5f8ed7e6c8..b206c86c0f4 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 3edfa05668f..214585f44d0 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 @@ -3130,17 +3130,12 @@ public void testIfAbsent() { @Test public void testIfNull() { - Map values = new HashMap<>(); - values.put("title", "unique_foo_title"); - values.put("name", null); - waitFor(randomCol.document("bookWithNull").set(values)); - Task execute = firestore .pipeline() .collection(randomCol) - .where(equal("title", "unique_foo_title")) .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"), @@ -3153,55 +3148,58 @@ public void testIfNull() { .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( mapOfEntries( - entry("staticMethod", "unique_foo_title"), - entry("instanceMethod", "unique_foo_title"), - entry("nameOrTitle", "unique_foo_title"), + entry("staticMethod", "foo"), + entry("instanceMethod", "foo"), + entry("nameOrTitle", "foo"), entry("fieldIsNull", "default name"), entry("fieldIsAbsent", "default name"))); - - waitFor(randomCol.document("bookWithNull").delete()); } @Test public void testCoalesce() { - Map values = new HashMap<>(); - values.put("numberValue", 1L); - values.put("stringValue", "unique_coalesce_hello"); - values.put("booleanValue", false); - values.put("nullValue", null); - values.put("nullValue2", null); - waitFor(randomCol.document("bookWithNull").set(values)); - Task execute = firestore .pipeline() .collection(randomCol) - .where(equal("stringValue", "unique_coalesce_hello")) .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"), + 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"), 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")) + Expression.coalesce(field("nullValue"), field("nullValue2"), constant("default")) + .alias("allFieldsNullWithDefault"), + Expression.coalesce(field("absentField"), field("numberValue"), constant("default")) + .alias("withAbsentField")) .execute(); - Map expected = new HashMap<>(); - expected.put("staticMethod", 1L); - expected.put("instanceMethod", 1L); - expected.put("firstIsNull", "unique_coalesce_hello"); - expected.put("lastIsNotNull", false); - expected.put("allFieldsNull", null); - expected.put("allFieldsNullWithDefault", "default"); - expected.put("withAbsentField", 1L); - assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) - .containsExactly(expected); - - waitFor(randomCol.document("bookWithNull").delete()); + .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 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 d2858d4e8d3..44291d3aa31 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 @@ -5924,17 +5924,19 @@ abstract class Expression internal constructor() { FunctionExpression("if_absent", notImplemented, ifFieldName, elseValue) /** - * Creates an expression that returns the [elseExpr] argument if [ifExpr] is null or absent, else - * return the result of the [ifExpr] argument evaluation. + * 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 value of the 'optional_field', or returns 'default_value' - * // if the field is null or absent. - * ifNull(field("optional_field"), constant("default_value")) + * // 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 or absence. - * @param elseExpr The expression that will be evaluated and returned if [ifExpr] is null or absent. + * @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 @@ -5942,17 +5944,19 @@ abstract class Expression internal constructor() { FunctionExpression("if_null", notImplemented, ifExpr, elseExpr) /** - * Creates an expression that returns the [elseValue] argument if [ifExpr] is null or absent, else - * return the result of the [ifExpr] argument evaluation. + * 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 value of the 'optional_field', or returns 'default_value' - * // if the field is null or absent. - * ifNull(field("optional_field"), "default_value") + * // 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 or absence. - * @param elseValue The value that will be returned if [ifExpr] evaluates to a null or absent value. + * @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 @@ -5960,18 +5964,16 @@ abstract class Expression internal constructor() { FunctionExpression("if_null", notImplemented, ifExpr, elseValue) /** - * Creates an expression that returns the [elseExpr] argument if [ifFieldName] is null or absent, else - * return the value of the field. + * Creates an expression that returns the [elseExpr] argument if [ifFieldName] field is null, + * else return the value of the field. * * ```kotlin - * // Returns the value of the 'optional_field', or returns the value of - * // 'default_field' if 'optional_field' is null or absent. - * ifNull("optional_field", field("default_field")) + * // 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 or absence. - * @param elseExpr The expression that will be evaluated and returned if [ifFieldName] is - * null or absent. + * @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 @@ -5979,17 +5981,16 @@ abstract class Expression internal constructor() { FunctionExpression("if_null", notImplemented, ifFieldName, elseExpr) /** - * Creates an expression that returns the [elseValue] argument if [ifFieldName] is null or absent, else - * return the value of the field. + * Creates an expression that returns the [elseValue] argument if [ifFieldName]field is null, + * else return the value of the field. * * ```kotlin - * // Returns the value of the 'optional_field', or returns 'default_value' - * // if the field is null or absent. - * ifNull("optional_field", "default_value") + * // 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 or absence. - * @param elseValue The value that will be returned if [ifFieldName] is null or absent. + * @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 @@ -5997,12 +5998,14 @@ abstract class Expression internal constructor() { FunctionExpression("if_null", notImplemented, ifFieldName, 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. + * 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 first non-null/non-absent value among a, b, and "default" - * coalesce(field("a"), field("b"), constant("default")) + * // 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. @@ -6015,12 +6018,14 @@ abstract class Expression internal constructor() { FunctionExpression("coalesce", notImplemented, expression, replacement, *others) /** - * 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. + * 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 first non-null/non-absent value among a, b, and "default" - * coalesce("a", field("b"), "default") + * // 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. @@ -6032,7 +6037,6 @@ abstract class Expression internal constructor() { 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. * @@ -8306,43 +8310,48 @@ 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 is null, else - * return the result of this expression. + * 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 value of the 'optional_field', or returns 'default_value' - * // if the field is null. - * field("optional_field").ifNull(constant("default_value")) + * // 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 or absent. + * 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 is null or absent, else - * return the result of this expression. + * 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 value of the 'optional_field', or returns 'default_value' - * // if the field is null or absent. - * field("optional_field").ifNull("default_value") + * // 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 a null or absent value. + * @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) /** - * Returns the first non-null, non-absent argument, without evaluating + * 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 first non-null/non-absent value among a, b, and "default" - * field("a").coalesce(field("b"), "default") + * // 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. From 8096f999d5a425af03f7db6775ce73cb29b20cc4 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:46:08 -0400 Subject: [PATCH 3/5] Update CHANGELOG.md --- firebase-firestore/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 52d77c8d351..12791456e75 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] Added support for Pipeline expressions `nor` and `switchOn`. [#7903](https://github.com/firebase/firebase-android-sdk/pull/7903) - [feature] Added support for `first`, `last`, `arrayAgg`, and `arrayAggDistinct` Pipeline expressions. From dbe1059030719d93e530db2440c0b7ebfdba865b Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:28:38 -0400 Subject: [PATCH 4/5] Update PipelineTest.java --- .../java/com/google/firebase/firestore/PipelineTest.java | 2 ++ 1 file changed, 2 insertions(+) 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 214585f44d0..8c1711bc867 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 @@ -3157,6 +3157,8 @@ public void testIfNull() { @Test public void testCoalesce() { + assumeFalse("Coalesce is not supported against the emulator.", isRunningAgainstEmulator()); + Task execute = firestore .pipeline() From 3a1ac5535bf3d5d033b71ded2420005b857b226b Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:38:26 -0400 Subject: [PATCH 5/5] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../java/com/google/firebase/firestore/pipeline/expressions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6bf49b06b59..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 @@ -5979,7 +5979,7 @@ abstract class Expression internal constructor() { FunctionExpression("if_null", notImplemented, ifFieldName, elseExpr) /** - * Creates an expression that returns the [elseValue] argument if [ifFieldName]field is null, + * Creates an expression that returns the [elseValue] argument if [ifFieldName] field is null, * else return the value of the field. * * ```kotlin