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 363046c39..46ee0cb20 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 @@ -3130,6 +3130,34 @@ public static Expression type(String fieldName) { return type(field(fieldName)); } + /** + * Creates an expression that checks if the result of this expression is of the given type. + * + * @param expr The expression to check the type of. + * @param type The type to check for. + * @return A new {@link BooleanExpression} that evaluates to true if the expression's result is of + * the given type, false otherwise. + */ + @BetaApi + public static BooleanExpression isType(Expression expr, Type type) { + return new BooleanFunctionExpression( + "is_type", ImmutableList.of(expr, constant(type.name().toLowerCase()))); + } + + /** + * Creates an expression that checks if the result of this expression is of the given type. + * + * @param fieldName The name of the field to check the type of. + * @param type The type to check for. + * @return A new {@link BooleanExpression} that evaluates to true if the expression's result is of + * the given type, false otherwise. + */ + @BetaApi + public static BooleanExpression isType(String fieldName, Type type) { + return new BooleanFunctionExpression( + "is_type", ImmutableList.of(field(fieldName), constant(type.name().toLowerCase()))); + } + // Numeric Operations /** * Creates an expression that rounds {@code numericExpr} to nearest integer. @@ -4796,4 +4824,16 @@ public final Expression collectionId() { public final Expression type() { return type(this); } + + /** + * Creates an expression that checks if the result of this expression is of the given type. + * + * @param type The type to check for. + * @return A new {@link BooleanExpression} that evaluates to true if the expression's result is of + * the given type, false otherwise. + */ + @BetaApi + public final Expression isType(Type type) { + return isType(this, type); + } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Type.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Type.java new file mode 100644 index 000000000..0812b7475 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Type.java @@ -0,0 +1,44 @@ +/* + * Copyright 2026 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.pipeline.expressions; + +import com.google.api.core.BetaApi; + +/** An enumeration of the different types generated by the Firestore backend. */ +@BetaApi +public enum Type { + NULL, + ARRAY, + BOOLEAN, + BYTES, + TIMESTAMP, + GEO_POINT, + NUMBER, + INT32, + INT64, + FLOAT64, + DECIMAL128, + MAP, + REFERENCE, + STRING, + VECTOR, + MAX_KEY, + MIN_KEY, + OBJECT_ID, + REGEX, + REQUEST_TIMESTAMP, +} 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 5f810332f..34e624575 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 @@ -90,6 +90,7 @@ import com.google.cloud.firestore.pipeline.expressions.AggregateFunction; import com.google.cloud.firestore.pipeline.expressions.Expression; import com.google.cloud.firestore.pipeline.expressions.Field; +import com.google.cloud.firestore.pipeline.expressions.Type; import com.google.cloud.firestore.pipeline.stages.Aggregate; import com.google.cloud.firestore.pipeline.stages.AggregateHints; import com.google.cloud.firestore.pipeline.stages.AggregateOptions; @@ -2633,6 +2634,89 @@ public void testType() throws Exception { "vector")); } + @Test + public void testIsType() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .replaceWith( + Expression.map( + map( + "int", + 1, + "float", + 1.1, + "str", + "a string", + "bool", + true, + "null", + null, + "geoPoint", + new GeoPoint(0.1, 0.2), + "timestamp", + Timestamp.ofTimeSecondsAndNanos(123456, 0), + "bytes", + com.google.cloud.firestore.Blob.fromBytes(new byte[] {1, 2, 3}), + "docRef", + collection.document("bar"), + "vector", + vector(new double[] {1.0, 2.0, 3.0}), + "map", + Expression.map(map("numberK", 1, "stringK", "a string")), + "array", + array(1, 2, true)))) + .select( + Expression.isType("int", Type.INT64).as("isInt64"), + Expression.isType("int", Type.NUMBER).as("isInt64IsNumber"), + Expression.isType("int", Type.DECIMAL128).as("isInt64IsDecimal128"), + Expression.isType("float", Type.FLOAT64).as("isFloat64"), + Expression.isType("float", Type.NUMBER).as("isFloat64IsNumber"), + Expression.isType("float", Type.DECIMAL128).as("isFloat64IsDecimal128"), + Expression.isType("str", Type.STRING).as("isStr"), + Expression.isType("str", Type.INT64).as("isStrNum"), + Expression.isType("int", Type.STRING).as("isNumStr"), + Expression.isType("bool", Type.BOOLEAN).as("isBool"), + Expression.isType("null", Type.NULL).as("isNull"), + Expression.isType("geoPoint", Type.GEO_POINT).as("isGeoPoint"), + Expression.isType("timestamp", Type.TIMESTAMP).as("isTimestamp"), + Expression.isType("bytes", Type.BYTES).as("isBytes"), + Expression.isType("docRef", Type.REFERENCE).as("isDocRef"), + Expression.isType("vector", Type.VECTOR).as("isVector"), + Expression.isType("map", Type.MAP).as("isMap"), + Expression.isType("array", Type.ARRAY).as("isArray"), + Expression.isType(constant(1), Type.INT64).as("exprIsInt64"), + field("int").isType(Type.INT64).as("staticIsInt64")) + .limit(1) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map( + "isInt64", true, + "isInt64IsNumber", true, + "isInt64IsDecimal128", false, + "isFloat64", true, + "isFloat64IsNumber", true, + "isFloat64IsDecimal128", false, + "isStr", true, + "isStrNum", false, + "isNumStr", false, + "isBool", true, + "isNull", true, + "isGeoPoint", true, + "isTimestamp", true, + "isBytes", true, + "isDocRef", true, + "isVector", true, + "isMap", true, + "isArray", true, + "exprIsInt64", true, + "staticIsInt64", true)); + } + @Test public void testExplainWithError() { assumeFalse( diff --git a/samples/preview-snippets/src/main/java/com/example/firestore/PipelineSnippets.java b/samples/preview-snippets/src/main/java/com/example/firestore/PipelineSnippets.java index cf6efe72d..f61628339 100644 --- a/samples/preview-snippets/src/main/java/com/example/firestore/PipelineSnippets.java +++ b/samples/preview-snippets/src/main/java/com/example/firestore/PipelineSnippets.java @@ -31,6 +31,7 @@ import com.google.cloud.firestore.PlanSummary; import com.google.cloud.firestore.Query; import com.google.cloud.firestore.QuerySnapshot; +import com.google.cloud.firestore.pipeline.expressions.Type; import com.google.cloud.firestore.pipeline.stages.Aggregate; import com.google.cloud.firestore.pipeline.stages.FindNearest; import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; @@ -1551,6 +1552,19 @@ void vectorLengthFunction() throws ExecutionException, InterruptedException { System.out.println(result.getResults()); } + void isTypeFunction() throws ExecutionException, InterruptedException { + // [START is_type] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(field("rating").isType(Type.INT64).as("isRatingInt64")) + .execute() + .get(); + // [END is_type] + System.out.println(result.getResults()); + } + // https://cloud.google.com/firestore/docs/pipeline/getting-started/stages-expressions void stagesExpressionsExample() throws ExecutionException, InterruptedException { // [START stages_expressions_example]