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 d270e42e1..73d0aa481 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 @@ -2707,6 +2707,196 @@ public static Expression arrayReverse(String arrayFieldName) { return arrayReverse(field(arrayFieldName)); } + /** + * Filters an array expression based on a predicate. + * + * @param array The expression representing the array to filter. + * @param alias The alias for the current element in the filter expression. + * @param filter The predicate boolean expression used to filter the elements. + * @return A new {@link Expression} representing the filtered array. + */ + @BetaApi + public static Expression arrayFilter(Expression array, String alias, BooleanExpression filter) { + return new FunctionExpression("array_filter", ImmutableList.of(array, constant(alias), filter)); + } + + /** + * Filters an array field based on a predicate. + * + * @param arrayFieldName The field name of the array to filter. + * @param alias The alias for the current element in the filter expression. + * @param filter The predicate boolean expression used to filter the elements. + * @return A new {@link Expression} representing the filtered array. + */ + @BetaApi + public static Expression arrayFilter( + String arrayFieldName, String alias, BooleanExpression filter) { + return arrayFilter(field(arrayFieldName), alias, filter); + } + + /** + * Creates an expression that applies a provided transformation to each element in an array. + * + * @param array The expression representing the array to transform. + * @param elementAlias The alias for the current element in the transform expression. + * @param transform The expression used to transform the elements. + * @return A new {@link Expression} representing the transformed array. + */ + @BetaApi + public static Expression arrayTransform( + Expression array, String elementAlias, Expression transform) { + return new FunctionExpression( + "array_transform", ImmutableList.of(array, constant(elementAlias), transform)); + } + + /** + * Creates an expression that applies a provided transformation to each element in an array. + * + * @param arrayFieldName The field name of the array to transform. + * @param elementAlias The alias for the current element in the transform expression. + * @param transform The expression used to transform the elements. + * @return A new {@link Expression} representing the transformed array. + */ + @BetaApi + public static Expression arrayTransform( + String arrayFieldName, String elementAlias, Expression transform) { + return arrayTransform(field(arrayFieldName), elementAlias, transform); + } + + /** + * Creates an expression that applies a provided transformation to each element in an array, + * providing the element's index to the transformation expression. + * + * @param array The expression representing the array to transform. + * @param elementAlias The alias for the current element in the transform expression. + * @param indexAlias The alias for the current index. + * @param transform The expression used to transform the elements. + * @return A new {@link Expression} representing the transformed array. + */ + @BetaApi + public static Expression arrayTransformWithIndex( + Expression array, String elementAlias, String indexAlias, Expression transform) { + return new FunctionExpression( + "array_transform", + ImmutableList.of(array, constant(elementAlias), constant(indexAlias), transform)); + } + + /** + * Creates an expression that applies a provided transformation to each element in an array, + * providing the element's index to the transformation expression. + * + * @param arrayFieldName The field name of the array to transform. + * @param elementAlias The alias for the current element in the transform expression. + * @param indexAlias The alias for the current index. + * @param transform The expression used to transform the elements. + * @return A new {@link Expression} representing the transformed array. + */ + @BetaApi + public static Expression arrayTransformWithIndex( + String arrayFieldName, String elementAlias, String indexAlias, Expression transform) { + return arrayTransformWithIndex(field(arrayFieldName), elementAlias, indexAlias, transform); + } + + /** + * Creates an expression that returns a slice of an array. + * + * @param array The expression representing the array to slice. + * @param offset The starting index. + * @param length The number of elements to return. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public static Expression arraySlice(Expression array, Expression offset, Expression length) { + return new FunctionExpression("array_slice", ImmutableList.of(array, offset, length)); + } + + /** + * Creates an expression that returns a slice of an array. + * + * @param array The expression representing the array to slice. + * @param offset The starting index. + * @param length The number of elements to return. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public static Expression arraySlice(Expression array, int offset, int length) { + return arraySlice(array, constant(offset), constant(length)); + } + + /** + * Creates an expression that returns a slice of an array. + * + * @param arrayFieldName The field name of the array to slice. + * @param offset The starting index. + * @param length The number of elements to return. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public static Expression arraySlice(String arrayFieldName, int offset, int length) { + return arraySlice(field(arrayFieldName), constant(offset), constant(length)); + } + + /** + * Creates an expression that returns a slice of an array. + * + * @param arrayFieldName The field name of the array to slice. + * @param offset The starting index. + * @param length The number of elements to return. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public static Expression arraySlice(String arrayFieldName, Expression offset, Expression length) { + return arraySlice(field(arrayFieldName), offset, length); + } + + /** + * Creates an expression that returns a slice of an array to its end. + * + * @param array The expression representing the array to slice. + * @param offset The expression representing the starting index. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public static Expression arraySliceToEnd(Expression array, Expression offset) { + return new FunctionExpression("array_slice", ImmutableList.of(array, offset)); + } + + /** + * Creates an expression that returns a slice of an array to its end. + * + * @param array The expression representing the array to slice. + * @param offset The starting index. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public static Expression arraySliceToEnd(Expression array, int offset) { + return arraySliceToEnd(array, constant(offset)); + } + + /** + * Creates an expression that returns a slice of an array to its end. + * + * @param arrayFieldName The field name of the array to slice. + * @param offset The starting index. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public static Expression arraySliceToEnd(String arrayFieldName, int offset) { + return arraySliceToEnd(field(arrayFieldName), constant(offset)); + } + + /** + * Creates an expression that returns a slice of an array to its end. + * + * @param arrayFieldName The field name of the array to slice. + * @param offset The expression representing the starting index. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public static Expression arraySliceToEnd(String arrayFieldName, Expression offset) { + return arraySliceToEnd(field(arrayFieldName), offset); + } + /** * Creates an expression that checks if an array contains a specified element. * @@ -6583,6 +6773,91 @@ public final Expression arrayReverse() { return arrayReverse(this); } + /** + * Filters this array based on a predicate. + * + * @param alias The alias for the current element in the filter expression. + * @param filter The predicate boolean expression used to filter the elements. + * @return A new {@link Expression} representing the filtered array. + */ + @BetaApi + public final Expression arrayFilter(String alias, BooleanExpression filter) { + return arrayFilter(this, alias, filter); + } + + /** + * Creates an expression that applies a provided transformation to each element in an array. + * + * @param elementAlias The alias for the current element in the transform expression. + * @param transform The expression used to transform the elements. + * @return A new {@link Expression} representing the transformed array. + */ + @BetaApi + public final Expression arrayTransform(String elementAlias, Expression transform) { + return arrayTransform(this, elementAlias, transform); + } + + /** + * Creates an expression that applies a provided transformation to each element in an array, + * providing the element's index to the transformation expression. + * + * @param elementAlias The alias for the current element in the transform expression. + * @param indexAlias The alias for the current index. + * @param transform The expression used to transform the elements. + * @return A new {@link Expression} representing the transformed array. + */ + @BetaApi + public final Expression arrayTransformWithIndex( + String elementAlias, String indexAlias, Expression transform) { + return arrayTransformWithIndex(this, elementAlias, indexAlias, transform); + } + + /** + * Returns a slice of this array. + * + * @param offset The starting index. + * @param length The number of elements to return. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public final Expression arraySlice(int offset, int length) { + return arraySlice(this, offset, length); + } + + /** + * Returns a slice of this array. + * + * @param offset The starting index expressed as an Expression. + * @param length The number of elements to return expressed as an Expression. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public final Expression arraySlice(Expression offset, Expression length) { + return arraySlice(this, offset, length); + } + + /** + * Returns a slice of this array to its end. + * + * @param offset The starting index. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public final Expression arraySliceToEnd(int offset) { + return arraySliceToEnd(this, offset); + } + + /** + * Returns a slice of this array to its end. + * + * @param offset The starting index expressed as an Expression. + * @return A new {@link Expression} representing the array slice. + */ + @BetaApi + public final Expression arraySliceToEnd(Expression offset) { + return arraySliceToEnd(this, offset); + } + /** * Creates an expression that checks if array contains a specific {@code element}. * 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 fd027e2b1..9c7fb4ccb 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 @@ -34,6 +34,7 @@ import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayContains; import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayContainsAll; import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayContainsAny; +import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayFilter; import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayFirst; import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayFirstN; import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayGet; @@ -47,6 +48,10 @@ import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayMinimum; import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayMinimumN; import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayReverse; +import static com.google.cloud.firestore.pipeline.expressions.Expression.arraySlice; +import static com.google.cloud.firestore.pipeline.expressions.Expression.arraySliceToEnd; +import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayTransform; +import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayTransformWithIndex; import static com.google.cloud.firestore.pipeline.expressions.Expression.ceil; import static com.google.cloud.firestore.pipeline.expressions.Expression.concat; import static com.google.cloud.firestore.pipeline.expressions.Expression.conditional; @@ -99,6 +104,7 @@ import static com.google.cloud.firestore.pipeline.expressions.Expression.unixMicrosToTimestamp; import static com.google.cloud.firestore.pipeline.expressions.Expression.unixMillisToTimestamp; import static com.google.cloud.firestore.pipeline.expressions.Expression.unixSecondsToTimestamp; +import static com.google.cloud.firestore.pipeline.expressions.Expression.variable; import static com.google.cloud.firestore.pipeline.expressions.Expression.vectorLength; import static com.google.cloud.firestore.pipeline.expressions.Expression.xor; import static com.google.common.truth.Truth.assertThat; @@ -1417,6 +1423,193 @@ public void testArrayMaximumN() throws Exception { assertThat(data(results)).isEqualTo(Lists.newArrayList(map("title", "The Lord of the Rings"))); } + @Test + public void testArrayFilter() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("title", "The Lord of the Rings")) + .select( + field("tags") + .arrayFilter("tag", notEqual(variable("tag"), "magic")) + .as("notMagicTags"), + arrayFilter("tags", "tag", notEqual(variable("tag"), "epic")).as("notEpicTags"), + arrayFilter("tags", "tag", equal(variable("tag"), "fantasy")).as("noMatchingTags")) + .execute() + .get() + .getResults(); + + Map result = data(results).get(0); + assertThat((List) result.get("notMagicTags")).containsExactly("adventure", "epic").inOrder(); + assertThat((List) result.get("notEpicTags")).containsExactly("adventure", "magic").inOrder(); + assertThat((List) result.get("noMatchingTags")).isEmpty(); + } + + @Test + public void testArrayFilterWithMixedTypesAndNulls() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .limit(1) + .replaceWith( + Expression.map( + ImmutableMap.of( + "arr", + ImmutableList.of( + 1, + "foo", + Expression.nullValue(), + 20.0, + "bar", + 30, + "40", + Expression.nullValue())))) + .select( + field("arr") + .arrayFilter("element", greaterThan(variable("element"), 10)) + .as("filtered")) + .execute() + .get() + .getResults(); + + Map result = data(results).get(0); + assertThat((List) result.get("filtered")).containsExactly(20.0, 30L).inOrder(); + } + + @Test + public void testSupportsArrayTransformAndArrayTransformWithIndex() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .limit(1) + .replaceWith(Expression.map(map("arr", Lists.newArrayList(10, 20, 30)))) + .select( + arrayTransform("arr", "element", multiply(variable("element"), 10)) + .as("staticTransform"), + field("arr") + .arrayTransform("element", multiply(variable("element"), 10)) + .as("instanceTransform"), + arrayTransformWithIndex( + "arr", "element", "i", add(variable("element"), variable("i"))) + .as("staticTransformWithIndex"), + field("arr") + .arrayTransformWithIndex( + "element", "i", add(variable("element"), variable("i"))) + .as("instanceTransformWithIndex")) + .execute() + .get() + .getResults(); + + Map result = data(results).get(0); + assertThat((List) result.get("staticTransform")).containsExactly(100L, 200L, 300L).inOrder(); + assertThat((List) result.get("instanceTransform")) + .containsExactly(100L, 200L, 300L) + .inOrder(); + assertThat((List) result.get("staticTransformWithIndex")) + .containsExactly(10L, 21L, 32L) + .inOrder(); + assertThat((List) result.get("instanceTransformWithIndex")) + .containsExactly(10L, 21L, 32L) + .inOrder(); + } + + @Test + public void testSupportsArrayTransformWithEmptyArrayAndNulls() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .limit(1) + .replaceWith( + Expression.map(map("arr", Lists.newArrayList(1, null, 3), "empty", emptyList()))) + .select( + field("arr") + .arrayTransform("element", add(variable("element"), 1)) + .as("transformedWithNulls"), + field("empty") + .arrayTransform("element", add(variable("element"), 1)) + .as("transformedEmpty"), + field("arr") + .arrayTransformWithIndex( + "element", "idx", add(variable("element"), variable("idx"))) + .as("transformedWithIndex"), + field("empty") + .arrayTransformWithIndex( + "element", "idx", add(variable("element"), variable("idx"))) + .as("transformedEmptyWithIndex")) + .execute() + .get() + .getResults(); + + Map result = data(results).get(0); + assertThat((List) result.get("transformedWithNulls")) + .containsExactly(2L, null, 4L) + .inOrder(); + assertThat((List) result.get("transformedEmpty")).isEmpty(); + assertThat((List) result.get("transformedWithIndex")) + .containsExactly(1L, null, 5L) + .inOrder(); + assertThat((List) result.get("transformedEmptyWithIndex")).isEmpty(); + } + + @Test + public void testArraySlice() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("title", "The Lord of the Rings")) + .select( + arraySlice("tags", 1, 1).as("staticMethodSlice"), + arraySliceToEnd("tags", 1).as("staticMethodSliceToEnd"), + field("tags").arraySlice(1, 1).as("instanceMethodSlice"), + field("tags").arraySliceToEnd(1).as("instanceMethodSliceToEnd"), + field("tags").arraySlice(1, 10).as("overflowLength"), + field("tags").arraySlice(-1, 1).as("negativeOffset"), + field("tags").arraySliceToEnd(-1).as("negativeOffsetSliceToEnd"), + field("tags").arraySliceToEnd(10).as("overflowOffset"), + field("tags").arraySliceToEnd(-10).as("negativeOverflowOffset")) + .execute() + .get() + .getResults(); + + Map result = data(results).get(0); + assertThat((List) result.get("staticMethodSlice")).containsExactly("magic").inOrder(); + assertThat((List) result.get("staticMethodSliceToEnd")) + .containsExactly("magic", "epic") + .inOrder(); + assertThat((List) result.get("instanceMethodSlice")).containsExactly("magic").inOrder(); + assertThat((List) result.get("instanceMethodSliceToEnd")) + .containsExactly("magic", "epic") + .inOrder(); + assertThat((List) result.get("overflowLength")).containsExactly("magic", "epic").inOrder(); + assertThat((List) result.get("overflowOffset")).isEmpty(); + assertThat((List) result.get("negativeOffset")).containsExactly("epic").inOrder(); + assertThat((List) result.get("negativeOffsetSliceToEnd")).containsExactly("epic").inOrder(); + assertThat((List) result.get("negativeOverflowOffset")) + .containsExactly("adventure", "magic", "epic") + .inOrder(); + } + + @Test + public void arraySliceThrowsErrorForNegativeLength() throws Exception { + ExecutionException exception = + assertThrows( + ExecutionException.class, + () -> + firestore + .pipeline() + .createFrom(collection) + .where(equal("title", "The Lord of the Rings")) + .select(arraySlice("tags", 1, -1).as("negativeLengthSlice")) + .execute() + .get()); + assertThat(exception).hasMessageThat().contains("length must be non-negative"); + } + @Test public void testArrayIndexOf() throws Exception { List results =