From 96a001e4d91f3d48dca6355df5b99662155305da Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:49:42 -0400 Subject: [PATCH 1/4] feat(firestore): Add ifNull and coalesce --- .../pipeline/expressions/Expression.java | 115 ++++++++++++++++++ .../cloud/firestore/it/ITPipelineTest.java | 82 +++++++++++++ 2 files changed, 197 insertions(+) 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..5988708a1 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,95 @@ public static Expression ifAbsent(String ifFieldName, Object elseValue) { return ifAbsent(field(ifFieldName), toExprOrConstant(elseValue)); } + /** + * Creates an expression that returns the {@code elseExpression} argument if {@code ifExpr} is + * null or absent, else return the result of the {@code ifExpr} argument evaluation. + * + * @param ifExpr The expression to check for null or absence. + * @param elseExpression The expression that will be evaluated and returned if {@code ifExpr} is + * null or absent. + * @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 the {@code elseValue} argument if {@code ifExpr} is null or + * absent, else return the result of the {@code ifExpr} argument evaluation. + * + * @param ifExpr The expression to check for null or absence. + * @param elseValue The value that will be returned if {@code ifExpr} evaluates to a null or + * absent 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 the {@code elseExpression} argument if {@code ifFieldName} + * is null or absent, else return the value of the field. + * + * @param ifFieldName The field name to check for null or absence. + * @param elseExpression The expression that will be evaluated and returned if {@code ifFieldName} + * is null or absent. + * @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 the {@code elseValue} argument if {@code ifFieldName} is + * null or absent, else return the value of the field. + * + * @param ifFieldName The field name to check for null or absence. + * @param elseValue The value that will be returned if {@code ifFieldName} is null or absent. + * @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 first The first expression to check for null. + * @param second 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 first, Object second, Object... others) { + ImmutableList.Builder args = ImmutableList.builder(); + args.add(first); + args.add(toExprOrConstant(second)); + 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 second 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 second, Object... others) { + return coalesce(field(firstFieldName), second, others); + } + /** * Creates an expression that joins the elements of an array into a string. * @@ -4206,6 +4295,32 @@ public Expression ifAbsent(Object elseValue) { return Expression.ifAbsent(this, elseValue); } + /** + * Creates an expression that returns the {@code elseValue} argument if this expression is null or + * absent, else return the result of this expression. + * + * @param elseValue The value that will be returned if this expression evaluates to a null or + * absent 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..91068be14 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,88 @@ 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 { + 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 From 5b26c43ee60e5f1c5b4eb625a6a1b8f2a918d2b7 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:28:26 -0400 Subject: [PATCH 2/4] Update Expression.java --- .../pipeline/expressions/Expression.java | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) 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 5988708a1..2f85caa21 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 @@ -278,12 +278,13 @@ public static Expression ifAbsent(String ifFieldName, Object elseValue) { } /** - * Creates an expression that returns the {@code elseExpression} argument if {@code ifExpr} is - * null or absent, else return the result of the {@code ifExpr} argument evaluation. + * Creates an expression that returns a default value if an expression evaluates to a null value. * - * @param ifExpr The expression to check for null or absence. - * @param elseExpression The expression that will be evaluated and returned if {@code ifExpr} is - * null or absent. + *

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 for null. + * @param elseExpression The expression that will be evaluated and returned if ifExpr is null. * @return A new {@link Expression} representing the ifNull operation. */ @BetaApi @@ -292,12 +293,13 @@ public static Expression ifNull(Expression ifExpr, Expression elseExpression) { } /** - * Creates an expression that returns the {@code elseValue} argument if {@code ifExpr} is null or - * absent, else return the result of the {@code ifExpr} argument evaluation. + * Creates an expression that returns a default value if an expression evaluates to a null value. * - * @param ifExpr The expression to check for null or absence. - * @param elseValue The value that will be returned if {@code ifExpr} evaluates to a null or - * absent value. + *

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 for null. + * @param elseValue The value that will be returned if {@code ifExpr} evaluates to a null value. * @return A new {@link Expression} representing the ifNull operation. */ @BetaApi @@ -306,12 +308,13 @@ public static Expression ifNull(Expression ifExpr, Object elseValue) { } /** - * Creates an expression that returns the {@code elseExpression} argument if {@code ifFieldName} - * is null or absent, else return the value of the field. + * 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 name to check for null or absence. - * @param elseExpression The expression that will be evaluated and returned if {@code ifFieldName} - * is null or absent. + * @param ifFieldName The name of the field to check for null. + * @param elseExpression The expression that will be evaluated and returned if the field is null. * @return A new {@link Expression} representing the ifNull operation. */ @BetaApi @@ -320,11 +323,13 @@ public static Expression ifNull(String ifFieldName, Expression elseExpression) { } /** - * Creates an expression that returns the {@code elseValue} argument if {@code ifFieldName} is - * null or absent, else return the value of the field. + * Creates an expression that returns a default value if a field is null. * - * @param ifFieldName The field name to check for null or absence. - * @param elseValue The value that will be returned if {@code ifFieldName} is null or absent. + *

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 name of the field to check for null. + * @param elseValue The value that will be returned if the field is null. * @return A new {@link Expression} representing the ifNull operation. */ @BetaApi @@ -336,16 +341,16 @@ public static Expression ifNull(String ifFieldName, Object 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 first The first expression to check for null. - * @param second The fallback expression or value if the first one is null. + * @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 first, Object second, Object... others) { + public static Expression coalesce(Expression expression, Object replacement, Object... others) { ImmutableList.Builder args = ImmutableList.builder(); - args.add(first); - args.add(toExprOrConstant(second)); + args.add(expression); + args.add(toExprOrConstant(replacement)); for (Object other : others) { args.add(toExprOrConstant(other)); } @@ -357,13 +362,13 @@ public static Expression coalesce(Expression first, Object second, Object... oth * When all arguments are null or absent, returns the last argument. * * @param firstFieldName The name of the first field to check for null. - * @param second The fallback expression or value if the first one is 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 second, Object... others) { - return coalesce(field(firstFieldName), second, others); + public static Expression coalesce(String firstFieldName, Object replacement, Object... others) { + return coalesce(field(firstFieldName), replacement, others); } /** @@ -4296,8 +4301,11 @@ public Expression ifAbsent(Object elseValue) { } /** - * Creates an expression that returns the {@code 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 is null, else + * return the result of this expression. + * + *

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 value that will be returned if this expression evaluates to a null or * absent value. From 7883a5128d4972bb70d13101a266ecaccc47af42 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:42:34 -0400 Subject: [PATCH 3/4] format --- .../pipeline/expressions/Expression.java | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) 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 2f85caa21..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 @@ -278,13 +278,13 @@ public static Expression ifAbsent(String ifFieldName, Object elseValue) { } /** - * Creates an expression that returns a default value if an expression evaluates to a null value. + * 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 for null. - * @param elseExpression The expression that will be evaluated and returned if ifExpr is null. + * @param ifExpr The expression to check. + * @param elseExpression The default value. * @return A new {@link Expression} representing the ifNull operation. */ @BetaApi @@ -293,13 +293,13 @@ public static Expression ifNull(Expression ifExpr, Expression elseExpression) { } /** - * Creates an expression that returns a default value if an expression evaluates to a null value. + * 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 for null. - * @param elseValue The value that will be returned if {@code ifExpr} evaluates to a null value. + * @param ifExpr The expression to check. + * @param elseValue The default value. * @return A new {@link Expression} representing the ifNull operation. */ @BetaApi @@ -313,8 +313,8 @@ public static Expression ifNull(Expression ifExpr, Object elseValue) { *

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 name of the field to check for null. - * @param elseExpression The expression that will be evaluated and returned if the field is null. + * @param ifFieldName The field to check. + * @param elseExpression The default value. * @return A new {@link Expression} representing the ifNull operation. */ @BetaApi @@ -328,8 +328,8 @@ public static Expression ifNull(String ifFieldName, Expression elseExpression) { *

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 name of the field to check for null. - * @param elseValue The value that will be returned if the field is null. + * @param ifFieldName The field to check. + * @param elseValue The default value. * @return A new {@link Expression} representing the ifNull operation. */ @BetaApi @@ -4301,14 +4301,12 @@ public Expression ifAbsent(Object elseValue) { } /** - * Creates an expression that returns the elseValue argument if this expression is null, else - * return the result of this expression. + * 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 value that will be returned if this expression evaluates to a null or - * absent value. + * @param elseValue The default value. * @return A new {@link Expression} representing the ifNull operation. */ @BetaApi From 7956920e932805e844e40fd45358a1e9c948399d Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:26:06 -0400 Subject: [PATCH 4/4] Update ITPipelineTest.java --- .../java/com/google/cloud/firestore/it/ITPipelineTest.java | 4 ++++ 1 file changed, 4 insertions(+) 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 91068be14..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 @@ -2692,6 +2692,10 @@ public void testIfNull() throws Exception { @Test public void testCoalesce() throws Exception { + assumeFalse( + "Coalesce is not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + List results = firestore .pipeline()