From fe05087659096748e4f6f0c72c7c185c0ef1ffc4 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 10 Oct 2024 16:32:14 -0400 Subject: [PATCH 001/152] Proto changes --- .../proto/google/firestore/v1/document.proto | 114 ++++++++++++++++++ .../proto/google/firestore/v1/firestore.proto | 84 +++++++++++++ .../proto/google/firestore/v1/pipeline.proto | 27 +++++ 3 files changed, 225 insertions(+) create mode 100644 firebase-firestore/src/proto/google/firestore/v1/pipeline.proto diff --git a/firebase-firestore/src/proto/google/firestore/v1/document.proto b/firebase-firestore/src/proto/google/firestore/v1/document.proto index 268947856a8..73b781df7a9 100644 --- a/firebase-firestore/src/proto/google/firestore/v1/document.proto +++ b/firebase-firestore/src/proto/google/firestore/v1/document.proto @@ -130,6 +130,50 @@ message Value { // A map value. MapValue map_value = 6; + + + // Value which references a field. + // + // This is considered relative (vs absolute) since it only refers to a field + // and not a field within a particular document. + // + // **Requires:** + // + // * Must follow [field reference][FieldReference.field_path] limitations. + // + // * Not allowed to be used when writing documents. + // + // (-- NOTE(batchik): long term, there is no reason this type should not be + // allowed to be used on the write path. --) + string field_reference_value = 19; + + // A value that represents an unevaluated expression. + // + // **Requires:** + // + // * Not allowed to be used when writing documents. + // + // (-- NOTE(batchik): similar to above, there is no reason to not allow + // storing expressions into the database, just no plan to support in + // the near term. + // + // This would actually be an interesting way to represent user-defined + // functions or more expressive rules-based systems. --) + Function function_value = 20; + + // A value that represents an unevaluated pipeline. + // + // **Requires:** + // + // * Not allowed to be used when writing documents. + // + // (-- NOTE(batchik): similar to above, there is no reason to not allow + // storing expressions into the database, just no plan to support in + // the near term. + // + // This would actually be an interesting way to represent user-defined + // functions or more expressive rules-based systems. --) + Pipeline pipeline_value = 21; } } @@ -149,3 +193,73 @@ message MapValue { // not exceed 1,500 bytes and cannot be empty. map fields = 1; } + +// Represents an unevaluated scalar expression. +// +// For example, the expression `like(user_name, "%alice%")` is represented as: +// +// ``` +// name: "like" +// args { field_reference: "user_name" } +// args { string_value: "%alice%" } +// ``` +// +// (-- api-linter: core::0123::resource-annotation=disabled +// aip.dev/not-precedent: this is not a One Platform API resource. --) +message Function { + // The name of the function to evaluate. + // + // **Requires:** + // + // * must be in snake case (lower case with underscore separator). + // + string name = 1; + + // Ordered list of arguments the given function expects. + repeated Value args = 2; + + // Optional named arguments that certain functions may support. + map options = 3; +} + +// A Firestore query represented as an ordered list of operations / stages. +message Pipeline { + // A single operation within a pipeline. + // + // A stage is made up of a unique name, and a list of arguments. The exact + // number of arguments & types is dependent on the stage type. + // + // To give an example, the stage `filter(state = "MD")` would be encoded as: + // + // ``` + // name: "filter" + // args { + // function_value { + // name: "eq" + // args { field_reference_value: "state" } + // args { string_value: "MD" } + // } + // } + // ``` + // + // See public documentation for the full list. + message Stage { + // The name of the stage to evaluate. + // + // **Requires:** + // + // * must be in snake case (lower case with underscore separator). + // + string name = 1; + + // Ordered list of arguments the given stage expects. + repeated Value args = 2; + + // Optional named arguments that certain functions may support. + map options = 3; + } + + // Ordered list of stages to evaluate. + repeated Stage stages = 1; +} + diff --git a/firebase-firestore/src/proto/google/firestore/v1/firestore.proto b/firebase-firestore/src/proto/google/firestore/v1/firestore.proto index 1bf75ea3c15..25e0b8dc76a 100644 --- a/firebase-firestore/src/proto/google/firestore/v1/firestore.proto +++ b/firebase-firestore/src/proto/google/firestore/v1/firestore.proto @@ -21,6 +21,7 @@ import "google/api/annotations.proto"; import "google/firestore/v1/aggregation_result.proto"; import "google/firestore/v1/common.proto"; import "google/firestore/v1/document.proto"; +import "google/firestore/v1/pipeline.proto"; import "google/firestore/v1/query.proto"; import "google/firestore/v1/write.proto"; import "google/protobuf/empty.proto"; @@ -138,6 +139,15 @@ service Firestore { }; } + // Executes a pipeline query. + rpc ExecutePipeline(ExecutePipelineRequest) + returns (stream ExecutePipelineResponse) { + option (google.api.http) = { + post: "/v1beta1/{database=projects/*/databases/*}:executePipeline" + body: "*" + }; + } + // Runs an aggregation query. // // Rather than producing [Document][google.firestore.v1.Document] results like [Firestore.RunQuery][google.firestore.v1.Firestore.RunQuery], @@ -510,6 +520,80 @@ message RunQueryResponse { int32 skipped_results = 4; } +// The request for [Firestore.ExecutePipeline][]. +message ExecutePipelineRequest { + // Database identifier, in the form `projects/{project}/databases/{database}`. + string database = 1; + + oneof pipeline_type { + // A pipelined operation. + StructuredPipeline structured_pipeline = 2; + } + + // Optional consistency arguments, defaults to strong consistency. + oneof consistency_selector { + // Run the query within an already active transaction. + // + // The value here is the opaque transaction ID to execute the query in. + bytes transaction = 5; + + // Execute the pipeline in a new transaction. + // + // The identifier of the newly created transaction will be returned in the + // first response on the stream. This defaults to a read-only transaction. + TransactionOptions new_transaction = 6; + + // Execute the pipeline in a snapshot transaction at the given time. + // + // This must be a microsecond precision timestamp within the past one hour, + // or if Point-in-Time Recovery is enabled, can additionally be a whole + // minute timestamp within the past 7 days. + google.protobuf.Timestamp read_time = 7; + } + + // Explain / analyze options for the pipeline. + // ExplainOptions explain_options = 8 [(google.api.field_behavior) = OPTIONAL]; +} + +// The response for [Firestore.Execute][]. +message ExecutePipelineResponse { + // Newly created transaction identifier. + // + // This field is only specified on the first response from the server when + // the request specified [ExecuteRequest.new_transaction][]. + bytes transaction = 1; + + // An ordered batch of results returned executing a pipeline. + // + // The batch size is variable, and can even be zero for when only a partial + // progress message is returned. + // + // The fields present in the returned documents are only those that were + // explicitly requested in the pipeline, this include those like + // [`__name__`][Document.name] & [`__update_time__`][Document.update_time]. + // This is explicitly a divergence from `Firestore.RunQuery` / + // `Firestore.GetDocument` RPCs which always return such fields even when they + // are not specified in the [`mask`][DocumentMask]. + repeated Document results = 2; + + // The time at which the document(s) were read. + // + // This may be monotonically increasing; in this case, the previous documents + // in the result stream are guaranteed not to have changed between their + // `execution_time` and this one. + // + // If the query returns no results, a response with `execution_time` and no + // `results` will be sent, and this represents the time at which the operation + // was run. + google.protobuf.Timestamp execution_time = 3; + + // Query explain metrics. + // + // Set on the last response when [ExecutePipelineRequest.explain_options][] + // was specified on the request. + // ExplainMetrics explain_metrics = 4; +} + // The request for [Firestore.RunAggregationQuery][google.firestore.v1.Firestore.RunAggregationQuery]. message RunAggregationQueryRequest { // Required. The parent resource name. In the format: diff --git a/firebase-firestore/src/proto/google/firestore/v1/pipeline.proto b/firebase-firestore/src/proto/google/firestore/v1/pipeline.proto new file mode 100644 index 00000000000..0a198cd6e29 --- /dev/null +++ b/firebase-firestore/src/proto/google/firestore/v1/pipeline.proto @@ -0,0 +1,27 @@ + +syntax = "proto3"; +package google.firestore.v1; +import "google/firestore/v1/document.proto"; +option csharp_namespace = "Google.Cloud.Firestore.V1"; +option php_namespace = "Google\\Cloud\\Firestore\\V1"; +option ruby_package = "Google::Cloud::Firestore::V1"; +option java_multiple_files = true; +option java_package = "com.google.firestore.v1"; +option java_outer_classname = "PipelineProto"; +option objc_class_prefix = "GCFS"; +// A Firestore query represented as an ordered list of operations / stages. +// +// This is considered the top-level function which plans & executes a query. +// It is logically equivalent to `query(stages, options)`, but prevents the +// client from having to build a function wrapper. +message StructuredPipeline { + // The pipeline query to execute. + Pipeline pipeline = 1; + // Optional query-level arguments. + // + // (-- Think query statement hints. --) + // + // (-- TODO(batchik): define the api contract of using an unsupported hint --) + map options = 2; +} + From 159cb37679facd57b64c3742ba94a5b8c11d251a Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 10 Oct 2024 16:35:11 -0400 Subject: [PATCH 002/152] Copyright --- .../src/proto/google/firestore/v1/pipeline.proto | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/firebase-firestore/src/proto/google/firestore/v1/pipeline.proto b/firebase-firestore/src/proto/google/firestore/v1/pipeline.proto index 0a198cd6e29..f425ec6911a 100644 --- a/firebase-firestore/src/proto/google/firestore/v1/pipeline.proto +++ b/firebase-firestore/src/proto/google/firestore/v1/pipeline.proto @@ -1,3 +1,16 @@ +// Copyright 2024 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. syntax = "proto3"; package google.firestore.v1; From 282409a1bd5827523c1b37307dc814d3cdcfccd5 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 24 Jan 2025 12:50:56 -0500 Subject: [PATCH 003/152] Expressions --- .../google/firebase/firestore/FieldPath.java | 14 +- .../firebase/firestore/model/FieldPath.java | 5 +- .../firebase/firestore/pipeline/Constant.kt | 84 ++++ .../firebase/firestore/pipeline/expression.kt | 444 ++++++++++++++++++ 4 files changed, 542 insertions(+), 5 deletions(-) create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java index 2b5302cff19..a4293fc530b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java @@ -18,6 +18,8 @@ import static com.google.firebase.firestore.util.Preconditions.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; + import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; @@ -33,15 +35,17 @@ public final class FieldPath { private final com.google.firebase.firestore.model.FieldPath internalPath; - private FieldPath(List segments) { + private FieldPath(@NonNull List segments) { this.internalPath = com.google.firebase.firestore.model.FieldPath.fromSegments(segments); } - private FieldPath(com.google.firebase.firestore.model.FieldPath internalPath) { + private FieldPath(@NonNull com.google.firebase.firestore.model.FieldPath internalPath) { this.internalPath = internalPath; } - com.google.firebase.firestore.model.FieldPath getInternalPath() { + @RestrictTo(RestrictTo.Scope.LIBRARY) + @NonNull + public com.google.firebase.firestore.model.FieldPath getInternalPath() { return internalPath; } @@ -78,7 +82,9 @@ public static FieldPath documentId() { } /** Parses a field path string into a {@code FieldPath}, treating dots as separators. */ - static FieldPath fromDotSeparatedPath(@NonNull String path) { + @RestrictTo(RestrictTo.Scope.LIBRARY) + @NonNull + public static FieldPath fromDotSeparatedPath(@NonNull String path) { checkNotNull(path, "Provided field path must not be null."); checkArgument( !RESERVED.matcher(path).find(), "Use FieldPath.of() for field names containing '~*/[]'."); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java index c1de25410fe..f28b07ff750 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java @@ -14,6 +14,8 @@ package com.google.firebase.firestore.model; +import androidx.annotation.NonNull; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -34,7 +36,8 @@ public static FieldPath fromSingleSegment(String fieldName) { } /** Creates a {@code FieldPath} from a list of parsed field path segments. */ - public static FieldPath fromSegments(List segments) { + @NonNull + public static FieldPath fromSegments(@NonNull List segments) { return segments.isEmpty() ? FieldPath.EMPTY_PATH : new FieldPath(segments); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt new file mode 100644 index 00000000000..69834c80e6f --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt @@ -0,0 +1,84 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firebase.Timestamp +import com.google.firebase.firestore.Blob +import com.google.firebase.firestore.DocumentReference +import com.google.firebase.firestore.FieldValue +import com.google.firebase.firestore.GeoPoint +import com.google.firebase.firestore.VectorValue +import com.google.firestore.v1.Value +import java.util.Date + +class Constant internal constructor(val value: Any?) : Expr() { + + companion object { + fun of(value: Any): Constant { + return when (value) { + is String -> of(value) + is Number -> of(value) + is Date -> of(value) + is Timestamp -> of(value) + is Boolean -> of(value) + is GeoPoint -> of(value) + is Blob -> of(value) + is DocumentReference -> of(value) + is Value -> of(value) + is Iterable<*> -> of(value) + is Map<*, *> -> of(value) + else -> throw IllegalArgumentException("Unknown type: $value") + } + } + + fun of(value: String): Constant { + return Constant(value) + } + + fun of(value: Number): Constant { + return Constant(value) + } + + fun of(value: Date): Constant { + return Constant(value) + } + + fun of(value: Timestamp): Constant { + return Constant(value) + } + + fun of(value: Boolean): Constant { + return Constant(value) + } + + fun of(value: GeoPoint): Constant { + return Constant(value) + } + + fun of(value: Blob): Constant { + return Constant(value) + } + + fun of(value: DocumentReference): Constant { + return Constant(value) + } + + fun of(value: Value): Constant { + return Constant(value) + } + + fun of(value: VectorValue) : Constant { + return Constant(value) + } + + fun nullValue(): Constant { + return Constant(null) + } + + fun vector(value: DoubleArray): Constant { + return of(FieldValue.vector(value)) + } + + fun vector(value: VectorValue): Constant { + return of(value) + } + } +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt new file mode 100644 index 00000000000..47f348a95bb --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt @@ -0,0 +1,444 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firebase.firestore.FieldPath +import com.google.firebase.firestore.VectorValue +import com.google.firebase.firestore.model.DocumentKey +import com.google.firebase.firestore.model.FieldPath as ModelFieldPath + +open class Expr protected constructor() { + companion object { + internal fun toExprOrConstant(other: Any): Expr { + return when (other) { + is Expr -> other + else -> Constant.of(other) + } + } + + internal fun toArrayOfExprOrConstant(others: Array): Array { + return others.map(::toExprOrConstant).toTypedArray() + } + } + + /** + * Creates an expression that this expression to another expression. + * + *

Example: + * + *

{@code
+     * // Add the value of the 'quantity' field and the 'reserve' field.
+     * Field.of("quantity").add(Field.of("reserve"));
+     * }
+ * + * @param other The expression to add to this expression. + * @return A new {@code Expr} representing the addition operation. + */ + fun add(other: Expr): Add { + return Add(this, other) + } + + /** + * Creates an expression that this expression to another expression. + * + *

Example: + * + *

{@code
+     * // Add the value of the 'quantity' field and the 'reserve' field.
+     * Field.of("quantity").add(Field.of("reserve"));
+     * }
+ * + * @param other The constant value to add to this expression. + * @return A new {@code Expr} representing the addition operation. + */ + fun add(other: Any): Add { + return Add(this, toExprOrConstant(other)) + } + + fun subtract(other: Expr): Subtract { + return Subtract(this, other) + } +} + +class Field private constructor(val fieldPath: ModelFieldPath) : Expr() { + companion object { + fun of(name: String): Field { + if (name == DocumentKey.KEY_FIELD_NAME) { + return Field(ModelFieldPath.KEY_PATH) + } + return Field(FieldPath.fromDotSeparatedPath(name).internalPath) + } + + fun of(fieldPath: FieldPath): Field { + if (fieldPath == FieldPath.documentId()) { + return Field(FieldPath.documentId().internalPath) + } + return Field(fieldPath.internalPath) + } + } +} + +class ListOfExpr(val expressions: Array) : Expr() { + constructor(vararg expressions: Expr) : this(arrayOf(*expressions)) + constructor(vararg expressions: Any) : this(expressions.map(::toExprOrConstant).toTypedArray()) +} + +open class Function protected constructor( + private val name: String, + private val params: Array +) : Expr() { + protected constructor(name: String, vararg params: Expr) : this(name, arrayOf(*params)) +} + +open class FilterCondition protected constructor( + name: String, + vararg params: Expr +) : Function(name, *params) + +open class Accumulator protected constructor( + name: String, + params: Array +) : Function(name, params) { + protected constructor(name: String, vararg params: Expr) : this(name, arrayOf(*params)) + protected constructor(name: String, param: Expr?) : this(name, if (param == null) emptyArray() else arrayOf(param)) +} + +class Add(left: Expr, right: Expr) : Function("add", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class Subtract(left: Expr, right: Expr) : Function("subtract", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class Multiply(left: Expr, right: Expr) : Function("multiply", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class Divide(left: Expr, right: Expr) : Function("divide", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class Mod(left: Expr, right: Expr) : Function("mod", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +// class BitAnd(left: Expr, right: Expr) : Function("bit_and", left, right) { +// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) +// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) +// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +//} + +// class BitOr(left: Expr, right: Expr) : Function("bit_or", left, right) { +// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) +// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) +// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +//} + +// class BitXor(left: Expr, right: Expr) : Function("bit_xor", left, right) { +// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) +// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) +// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +//} + +// class BitNot(left: Expr, right: Expr) : Function("bit_not", left, right) { +// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) +// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) +// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +//} + +// class BitLeftShift(left: Expr, right: Expr) : Function("bit_left_shift", left, right) { +// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) +// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) +// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +//} + +// class BitRightShift(left: Expr, right: Expr) : Function("bit_right_shift", left, right) { +// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) +// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) +// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +//} + +class Eq(left: Expr, right: Expr) : FilterCondition("eq", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class Neq(left: Expr, right: Expr) : FilterCondition("neq", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class Lt(left: Expr, right: Expr) : FilterCondition("lt", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class Lte(left: Expr, right: Expr) : FilterCondition("lte", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class Gt(left: Expr, right: Expr) : FilterCondition("gt", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class Gte(left: Expr, right: Expr) : FilterCondition("gte", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class ArrayConcat(array: Expr, vararg arrays: Expr) : Function("array_concat", array, *arrays) { + constructor(array: Expr, arrays: List) : this(array, toExprOrConstant(arrays)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class ArrayReverse(array: Expr) : Function("array_reverse", array) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class ArrayContains(array: Expr, value: Expr) : FilterCondition("array_contains", array, value) { + constructor(array: Expr, right: Any) : this(array, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class ArrayContainsAll(array: Expr, values: List) : FilterCondition("array_contains_all", array, ListOfExpr(values)) { + constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) +} + +class ArrayContainsAny(array: Expr, values: List) : FilterCondition("array_contains_any", array, ListOfExpr(values)) { + constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) +} + +class ArrayLength(array: Expr) : Function("array_length", array) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class In(array: Expr, values: List) : FilterCondition("in", array, ListOfExpr(values)) { + constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) +} + +class IsNan(expr: Expr) : FilterCondition("is_nan", expr) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Exists(expr: Expr) : FilterCondition("exists", expr) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Not(expr: Expr) : FilterCondition("not", expr) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class And(condition: Expr, vararg conditions: Expr) : FilterCondition("and", condition, *conditions) { + constructor(condition: Expr, vararg conditions: Any) : this(condition, *toArrayOfExprOrConstant(conditions)) + constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) + constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) +} + +class Or(condition: Expr, vararg conditions: Expr) : FilterCondition("or", condition, *conditions) { + constructor(condition: Expr, vararg conditions: Any) : this(condition, *toArrayOfExprOrConstant(conditions)) + constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) + constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) +} + +class Xor(condition: Expr, vararg conditions: Expr) : FilterCondition("xor", condition, *conditions) { + constructor(condition: Expr, vararg conditions: Any) : this(condition, *toArrayOfExprOrConstant(conditions)) + constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) + constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) +} + +class If(condition: FilterCondition, thenExpr: Expr, elseExpr: Expr) : Function("if", condition, thenExpr, elseExpr) + +class LogicalMax(left: Expr, right: Expr) : Function("logical_max", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class LogicalMin(left: Expr, right: Expr) : Function("logical_min", left, right) { + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) +} + +class Reverse(expr: Expr) : Function("reverse", expr) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class ReplaceFirst(value: Expr, find: Expr, replace: Expr) : Function("replace_first", value, find, replace) { + constructor(value: Expr, find: String, replace: String) : this(value, Constant.of(find), Constant.of(replace)) + constructor(fieldName: String, find: String, replace: String) : this(Field.of(fieldName), find, replace) +} + +class ReplaceAll(value: Expr, find: Expr, replace: Expr) : Function("replace_all", value, find, replace) { + constructor(value: Expr, find: String, replace: String) : this(value, Constant.of(find), Constant.of(replace)) + constructor(fieldName: String, find: String, replace: String) : this(Field.of(fieldName), find, replace) +} + +class CharLength(value: Expr) : Function("char_length", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class ByteLength(value: Expr) : Function("byte_length", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Like(expr: Expr, pattern: Expr) : FilterCondition("like", expr, pattern) { + constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) + constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) + constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) +} + +class RegexContains(expr: Expr, pattern: Expr) : FilterCondition("regex_contains", expr, pattern) { + constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) + constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) + constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) +} + +class RegexMatch(expr: Expr, pattern: Expr) : FilterCondition("regex_match", expr, pattern) { + constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) + constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) + constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) +} + +class StrContains(expr: Expr, substring: Expr) : FilterCondition("str_contains", expr, substring) { + constructor(expr: Expr, substring: String) : this(expr, Constant.of(substring)) + constructor(fieldName: String, substring: Expr) : this(Field.of(fieldName), substring) + constructor(fieldName: String, substring: String) : this(Field.of(fieldName), substring) +} + +class StartsWith(expr: Expr, prefix: Expr) : FilterCondition("starts_with", expr, prefix) { + constructor(expr: Expr, prefix: String) : this(expr, Constant.of(prefix)) + constructor(fieldName: String, prefix: Expr) : this(Field.of(fieldName), prefix) + constructor(fieldName: String, prefix: String) : this(Field.of(fieldName), prefix) +} + +class EndsWith(expr: Expr, suffix: Expr) : FilterCondition("ends_with", expr, suffix) { + constructor(expr: Expr, suffix: String) : this(expr, Constant.of(suffix)) + constructor(fieldName: String, suffix: Expr) : this(Field.of(fieldName), suffix) + constructor(fieldName: String, suffix: String) : this(Field.of(fieldName), suffix) +} + +class ToLower(expr: Expr) : Function("to_lower", expr) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class ToUpper(expr: Expr) : Function("to_upper", expr) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Trim(expr: Expr) : Function("trim", expr) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class StrConcat internal constructor(first: Expr, vararg rest: Expr) : Function("str_concat", first, *rest) { + constructor(first: Expr, vararg rest: String) : this(first, *rest.map(Constant::of).toTypedArray()) + constructor(fieldName: String, vararg rest: Expr) : this(Field.of(fieldName), *rest) + constructor(fieldName: String, vararg rest: String) : this(Field.of(fieldName), *rest) +} + +class MapGet(map: Expr, name: String) : Function("map_get", map, Constant.of(name)) { + constructor(fieldName: String, name: String) : this(Field.of(fieldName), name) +} + +class Count(value: Expr?) : Accumulator("count", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Sum(value: Expr) : Accumulator("sum", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Avg(value: Expr) : Accumulator("avg", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Min(value: Expr) : Accumulator("min", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Max(value: Expr) : Accumulator("max", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class CosineDistance(vector1: Expr, vector2: Expr) : Function("cosine_distance", vector1, vector2) { + constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) + constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) + constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) +} + +class DotProduct(vector1: Expr, vector2: Expr) : Function("dot_product", vector1, vector2) { + constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) + constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) + constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) +} + +class EuclideanDistance(vector1: Expr, vector2: Expr) : Function("euclidean_distance", vector1, vector2) { + constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) + constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) + constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) +} + +class VectorLength(vector: Expr) : Function("vector_length", vector) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class UnixMicrosToTimestamp(input: Expr) : Function("unix_micros_to_timestamp", input) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class TimestampToUnixMicros(input: Expr) : Function("timestamp_to_unix_micros", input) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class UnixMillisToTimestamp(input: Expr) : Function("unix_millis_to_timestamp", input) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class TimestampToUnixMillis(input: Expr) : Function("timestamp_to_unix_millis", input) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class UnixSecondsToTimestamp(input: Expr) : Function("unix_seconds_to_timestamp", input) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class TimestampToUnixSeconds(input: Expr) : Function("timestamp_to_unix_seconds", input) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class TimestampAdd(timestamp: Expr, unit: Expr, amount: Expr) : Function("timestamp_add", timestamp, unit, amount) { + constructor(timestamp: Expr, unit: String, amount: Double) : this(timestamp, Constant.of(unit), Constant.of(amount)) + constructor(fieldName: String, unit: String, amount: Double) : this(Field.of(fieldName), unit, amount) + constructor(fieldName: String, unit: Expr, amount: Expr) : this(Field.of(fieldName), unit, amount) +} + +class TimestampSub(timestamp: Expr, unit: Expr, amount: Expr) : Function("timestamp_sub", timestamp, unit, amount) { + constructor(timestamp: Expr, unit: String, amount: Double) : this(timestamp, Constant.of(unit), Constant.of(amount)) + constructor(fieldName: String, unit: String, amount: Double) : this(Field.of(fieldName), unit, amount) + constructor(fieldName: String, unit: Expr, amount: Expr) : this(Field.of(fieldName), unit, amount) +} From 7f95bb5b907d41a1b35dd7ba39c391c5e0ded344 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 3 Feb 2025 11:34:59 -0500 Subject: [PATCH 004/152] Test --- .../firebase/firestore/pipeline/ExprTest.java | 16 ++++++++++++++++ .../firebase/firestore/pipeline/StageTest.java | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java new file mode 100644 index 00000000000..2d7e3b47243 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java @@ -0,0 +1,16 @@ +package com.google.firebase.firestore.pipeline; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ExprTest { + + @Test + public void test() { + + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java new file mode 100644 index 00000000000..938960c5bab --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java @@ -0,0 +1,16 @@ +package com.google.firebase.firestore.pipeline; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class StageTest { + + @Test + public void test() { + + } +} From 2ec848d690f9ceef325094aa9580e62c4a97d505 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 3 Feb 2025 11:36:02 -0500 Subject: [PATCH 005/152] Stage --- .../firebase/firestore/pipeline/stage.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt new file mode 100644 index 00000000000..516db44bc4e --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -0,0 +1,44 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firebase.firestore.DocumentReference +import com.google.firestore.v1.Pipeline +import com.google.firestore.v1.Value + +abstract class Stage(protected val name: String) { + internal fun toProtoStage(): Pipeline.Stage { + val builder = Pipeline.Stage.newBuilder() + builder.setName(name) + args().forEach { arg -> builder.addArgs(arg) } + return builder.build(); + } + protected abstract fun args(): Sequence +} + +class DatabaseSource : Stage("database") { + override fun args(): Sequence { + return emptySequence() + } +} + +class CollectionSource internal constructor(path: String) : Stage("collection") { + private val path: String = if (path.startsWith("/")) path else "/" + path + override fun args(): Sequence { + return sequenceOf(Value.newBuilder().setReferenceValue(path).build()) + } +} + +class CollectionGroupSource internal constructor(val collectionId: String): Stage("collection_group") { + override fun args(): Sequence { + return sequenceOf( + Value.newBuilder().setReferenceValue("").build(), + Value.newBuilder().setStringValue(collectionId).build() + ) + } +} + +class DocumentsSource private constructor(private val documents: List) : Stage("documents") { + internal constructor(documents: Array) : this(documents.map { docRef -> "/" + docRef.path }) + override fun args(): Sequence { + return documents.asSequence().map { doc -> Value.newBuilder().setStringValue(doc).build() } + } +} From 16bb38ca4ed881686489a18228ec8adfb382f465 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 3 Feb 2025 11:42:45 -0500 Subject: [PATCH 006/152] Pipeline --- .../firebase/firestore/FirebaseFirestore.java | 5 + .../com/google/firebase/firestore/Pipeline.kt | 122 ++++++++++++++++++ .../firestore/core/FirestoreClient.java | 6 + .../firebase/firestore/remote/Datastore.java | 44 +++++++ .../firestore/remote/RemoteStore.java | 12 ++ 5 files changed, 189 insertions(+) create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index c1218829b8a..079619647ab 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -881,4 +881,9 @@ void validateReference(DocumentReference docRef) { static void setClientLanguage(@NonNull String languageToken) { FirestoreChannel.setClientLanguage(languageToken); } + + @NonNull + public PipelineSource pipeline() { + return new PipelineSource(this); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt new file mode 100644 index 00000000000..44e0c61cae3 --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -0,0 +1,122 @@ +package com.google.firebase.firestore + +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.TaskCompletionSource +import com.google.common.collect.FluentIterable +import com.google.common.collect.ImmutableList +import com.google.firebase.firestore.model.DocumentKey +import com.google.firebase.firestore.model.SnapshotVersion +import com.google.firebase.firestore.pipeline.CollectionGroupSource +import com.google.firebase.firestore.pipeline.CollectionSource +import com.google.firebase.firestore.pipeline.DatabaseSource +import com.google.firebase.firestore.pipeline.DocumentsSource +import com.google.firebase.firestore.pipeline.Stage +import com.google.firestore.v1.ExecutePipelineRequest +import com.google.firestore.v1.StructuredPipeline +import com.google.firestore.v1.Value + +class Pipeline internal constructor( + internal val firestore: FirebaseFirestore, + internal val stages: FluentIterable +) { + internal constructor(firestore: FirebaseFirestore, stage: Stage) : this(firestore, FluentIterable.of(stage)) + + private fun append(stage: Stage): Pipeline { + return Pipeline(firestore, stages.append(stage)) + } + + fun execute(): Task { + val observerTask = ObserverSnapshotTask() + execute(observerTask); + return observerTask.task + } + + private fun execute(observer: PipelineResultObserver) { + firestore.callClient { call -> call!!.executePipeline(toProto(), observer) } + } + + internal fun documentReference(key: DocumentKey): DocumentReference { + return DocumentReference(key, firestore) + } + + fun toProto(): ExecutePipelineRequest { + val database = firestore.databaseId + val builder = ExecutePipelineRequest.newBuilder() + builder.database = "projects/${database.projectId}/databases/${database.databaseId}" + builder.structuredPipeline = toStructuredPipelineProto() + return builder.build() + } + + private fun toStructuredPipelineProto(): StructuredPipeline { + val builder = StructuredPipeline.newBuilder() + builder.pipeline = toPipelineProto() + return builder.build() + } + + private fun toPipelineProto(): com.google.firestore.v1.Pipeline = + com.google.firestore.v1.Pipeline.newBuilder() + .addAllStages(stages.map(Stage::toProtoStage)) + .build() + + private inner class ObserverSnapshotTask : PipelineResultObserver { + private val taskCompletionSource = TaskCompletionSource() + private val results: ImmutableList.Builder = ImmutableList.builder() + override fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) { + results.add( + PipelineResult( + if (key == null) null else DocumentReference(key, firestore), + data, + version + ) + ) + } + + override fun onComplete(executionTime: SnapshotVersion) { + taskCompletionSource.setResult(PipelineSnapshot(executionTime, results.build())) + } + + override fun onError(exception: FirebaseFirestoreException) { + taskCompletionSource.setException(exception) + } + + val task: Task + get() = taskCompletionSource.task + } +} + +class PipelineSource( + private val firestore: FirebaseFirestore +) { + fun collection(path: String): Pipeline { + return Pipeline(firestore, CollectionSource(path)) + } + + fun collectionGroup(collectionId: String): Pipeline { + return Pipeline(firestore, CollectionGroupSource(collectionId)) + } + + fun database(): Pipeline { + return Pipeline(firestore, DatabaseSource()) + } + + fun documents(vararg documents: DocumentReference): Pipeline { + return Pipeline(firestore, DocumentsSource(documents)) + } +} + +class PipelineSnapshot internal constructor( + val executionTime: SnapshotVersion, + val results: List +) + +class PipelineResult internal constructor( + private val key: DocumentReference?, + val fields: Map, + val version: SnapshotVersion, +) + +interface PipelineResultObserver { + fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) + fun onComplete(executionTime: SnapshotVersion) + fun onError(exception: FirebaseFirestoreException) +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java index 6e2d9b87b84..d54e3458d52 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java @@ -27,6 +27,7 @@ import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.FirebaseFirestoreException.Code; import com.google.firebase.firestore.LoadBundleTask; +import com.google.firebase.firestore.PipelineResultObserver; import com.google.firebase.firestore.TransactionOptions; import com.google.firebase.firestore.auth.CredentialsProvider; import com.google.firebase.firestore.auth.User; @@ -49,6 +50,7 @@ import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.Function; import com.google.firebase.firestore.util.Logger; +import com.google.firestore.v1.ExecutePipelineRequest; import com.google.firestore.v1.Value; import java.io.InputStream; import java.util.List; @@ -249,6 +251,10 @@ public Task> runAggregateQuery( return result.getTask(); } + public void executePipeline(ExecutePipelineRequest request, PipelineResultObserver observer) { + asyncQueue.enqueueAndForget(() -> remoteStore.executePipeline(request, observer)); + } + /** * Returns a task resolves when all the pending writes at the time when this method is called * received server acknowledgement. An acknowledgement can be either acceptance or rejections. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java index 87365361be4..db9e06ad219 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java @@ -23,6 +23,7 @@ import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.firestore.AggregateField; import com.google.firebase.firestore.FirebaseFirestoreException; +import com.google.firebase.firestore.PipelineResultObserver; import com.google.firebase.firestore.core.Query; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.MutableDocument; @@ -34,6 +35,8 @@ import com.google.firestore.v1.BatchGetDocumentsResponse; import com.google.firestore.v1.CommitRequest; import com.google.firestore.v1.CommitResponse; +import com.google.firestore.v1.ExecutePipelineRequest; +import com.google.firestore.v1.ExecutePipelineResponse; import com.google.firestore.v1.FirestoreGrpc; import com.google.firestore.v1.RunAggregationQueryRequest; import com.google.firestore.v1.RunAggregationQueryResponse; @@ -237,6 +240,47 @@ public Task> runAggregateQuery( }); } + public void executePipeline( + ExecutePipelineRequest request, + PipelineResultObserver observer + ) { + channel + .runStreamingResponseRpc(FirestoreGrpc.getExecutePipelineMethod(), request, new FirestoreChannel.StreamingListener() { + + private SnapshotVersion executionTime = SnapshotVersion.NONE; + + @Override + public void onMessage(ExecutePipelineResponse message) { + setExecutionTime(serializer.decodeVersion(message.getExecutionTime())); + message.getResultsList().forEach(document -> observer.onDocument( + document.getName() == null ? null : serializer.decodeKey(document.getName()), + document.getFieldsMap(), + serializer.decodeVersion(document.getUpdateTime()) + )); + } + + @Override + public void onClose(Status status) { + if (status.isOk()) { + observer.onComplete(executionTime); + } else { + FirebaseFirestoreException exception = exceptionFromStatus(status); + if (exception.getCode() == FirebaseFirestoreException.Code.UNAUTHENTICATED) { + channel.invalidateToken(); + } + observer.onError(exception); + } + } + + private void setExecutionTime(SnapshotVersion executionTime) { + if (executionTime.equals(SnapshotVersion.NONE)) { + return; + } + this.executionTime = executionTime; + } + }); + } + /** * Determines whether the given status has an error code that represents a permanent error when * received in response to a non-write operation. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index d2a139d4b6f..05f2bfa9837 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -23,6 +23,7 @@ import com.google.firebase.database.collection.ImmutableSortedSet; import com.google.firebase.firestore.AggregateField; import com.google.firebase.firestore.FirebaseFirestoreException; +import com.google.firebase.firestore.PipelineResultObserver; import com.google.firebase.firestore.core.OnlineState; import com.google.firebase.firestore.core.Query; import com.google.firebase.firestore.core.Transaction; @@ -43,6 +44,7 @@ import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.Logger; import com.google.firebase.firestore.util.Util; +import com.google.firestore.v1.ExecutePipelineRequest; import com.google.firestore.v1.Value; import com.google.protobuf.ByteString; import io.grpc.Status; @@ -777,4 +779,14 @@ public Task> runAggregateQuery( "Failed to get result from server.", FirebaseFirestoreException.Code.UNAVAILABLE)); } } + + public void executePipeline(ExecutePipelineRequest request, PipelineResultObserver observer) { + if (canUseNetwork()) { + datastore.executePipeline(request, observer); + } else { + observer.onError( + new FirebaseFirestoreException( + "Failed to get result from server.", FirebaseFirestoreException.Code.UNAVAILABLE)); + } + } } From a8361ca490d86b8c8ff884c537de2cd7844add8f Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 3 Feb 2025 13:06:54 -0500 Subject: [PATCH 007/152] Fix --- .../firebase/firestore/pipeline/expression.kt | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt index 47f348a95bb..4f169e522a0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt @@ -14,7 +14,11 @@ open class Expr protected constructor() { } } - internal fun toArrayOfExprOrConstant(others: Array): Array { + internal fun toArrayOfExprOrConstant(others: Iterable): Array { + return others.map(::toExprOrConstant).toTypedArray() + } + + internal fun toArrayOfExprOrConstant(others: Array): Array { return others.map(::toExprOrConstant).toTypedArray() } } @@ -76,28 +80,26 @@ class Field private constructor(val fieldPath: ModelFieldPath) : Expr() { } } -class ListOfExpr(val expressions: Array) : Expr() { - constructor(vararg expressions: Expr) : this(arrayOf(*expressions)) - constructor(vararg expressions: Any) : this(expressions.map(::toExprOrConstant).toTypedArray()) -} +class ListOfExpr(val expressions: Array) : Expr() open class Function protected constructor( private val name: String, - private val params: Array + private val params: Array ) : Expr() { - protected constructor(name: String, vararg params: Expr) : this(name, arrayOf(*params)) + protected constructor(name: String, param1: Expr) : this(name, arrayOf(param1)) + protected constructor(name: String, param1: Expr, param2: Expr) : this(name, arrayOf(param1, param2)) + protected constructor(name: String, param1: Expr, param2: Expr, param3: Expr) : this(name, arrayOf(param1, param2, param3)) } open class FilterCondition protected constructor( name: String, vararg params: Expr -) : Function(name, *params) +) : Function(name, params) open class Accumulator protected constructor( name: String, - params: Array + params: Array ) : Function(name, params) { - protected constructor(name: String, vararg params: Expr) : this(name, arrayOf(*params)) protected constructor(name: String, param: Expr?) : this(name, if (param == null) emptyArray() else arrayOf(param)) } @@ -203,10 +205,10 @@ class Gte(left: Expr, right: Expr) : FilterCondition("gte", left, right) { constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } -class ArrayConcat(array: Expr, vararg arrays: Expr) : Function("array_concat", array, *arrays) { +class ArrayConcat(array: Expr, vararg arrays: Expr) : Function("array_concat", arrayOf(array, *arrays)) { constructor(array: Expr, arrays: List) : this(array, toExprOrConstant(arrays)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(fieldName: String, vararg arrays: Expr) : this(Field.of(fieldName), *arrays) + constructor(fieldName: String, right: List) : this(Field.of(fieldName), right) } class ArrayReverse(array: Expr) : Function("array_reverse", array) { @@ -219,11 +221,11 @@ class ArrayContains(array: Expr, value: Expr) : FilterCondition("array_contains" constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } -class ArrayContainsAll(array: Expr, values: List) : FilterCondition("array_contains_all", array, ListOfExpr(values)) { +class ArrayContainsAll(array: Expr, values: List) : FilterCondition("array_contains_all", array, ListOfExpr(toArrayOfExprOrConstant(values))) { constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) } -class ArrayContainsAny(array: Expr, values: List) : FilterCondition("array_contains_any", array, ListOfExpr(values)) { +class ArrayContainsAny(array: Expr, values: List) : FilterCondition("array_contains_any", array, ListOfExpr(toArrayOfExprOrConstant(values))) { constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) } @@ -231,7 +233,7 @@ class ArrayLength(array: Expr) : Function("array_length", array) { constructor(fieldName: String) : this(Field.of(fieldName)) } -class In(array: Expr, values: List) : FilterCondition("in", array, ListOfExpr(values)) { +class In(array: Expr, values: List) : FilterCondition("in", array, ListOfExpr(toArrayOfExprOrConstant(values))) { constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) } @@ -349,7 +351,7 @@ class Trim(expr: Expr) : Function("trim", expr) { constructor(fieldName: String) : this(Field.of(fieldName)) } -class StrConcat internal constructor(first: Expr, vararg rest: Expr) : Function("str_concat", first, *rest) { +class StrConcat internal constructor(first: Expr, vararg rest: Expr) : Function("str_concat", arrayOf(first, *rest)) { constructor(first: Expr, vararg rest: String) : this(first, *rest.map(Constant::of).toTypedArray()) constructor(fieldName: String, vararg rest: Expr) : this(Field.of(fieldName), *rest) constructor(fieldName: String, vararg rest: String) : this(Field.of(fieldName), *rest) From a0da1508300fe6aefbbf74601300f2b11cd530d7 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 3 Feb 2025 18:22:50 -0500 Subject: [PATCH 008/152] Spotless --- .../google/firebase/firestore/FieldPath.java | 2 +- .../com/google/firebase/firestore/Pipeline.kt | 172 +++--- .../firebase/firestore/model/FieldPath.java | 1 - .../firebase/firestore/pipeline/Constant.kt | 138 ++--- .../firebase/firestore/pipeline/expression.kt | 525 ++++++++++-------- .../firebase/firestore/pipeline/stage.kt | 56 +- .../firebase/firestore/remote/Datastore.java | 26 +- .../firebase/firestore/pipeline/ExprTest.java | 4 +- .../firestore/pipeline/StageTest.java | 4 +- 9 files changed, 495 insertions(+), 433 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java index a4293fc530b..fe353b3391b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java @@ -19,7 +19,6 @@ import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; - import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; @@ -43,6 +42,7 @@ private FieldPath(@NonNull com.google.firebase.firestore.model.FieldPath interna this.internalPath = internalPath; } + /** @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY) @NonNull public com.google.firebase.firestore.model.FieldPath getInternalPath() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 44e0c61cae3..c75e7106c4e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -15,108 +15,108 @@ import com.google.firestore.v1.ExecutePipelineRequest import com.google.firestore.v1.StructuredPipeline import com.google.firestore.v1.Value -class Pipeline internal constructor( - internal val firestore: FirebaseFirestore, - internal val stages: FluentIterable +class Pipeline +internal constructor( + internal val firestore: FirebaseFirestore, + internal val stages: FluentIterable ) { - internal constructor(firestore: FirebaseFirestore, stage: Stage) : this(firestore, FluentIterable.of(stage)) - - private fun append(stage: Stage): Pipeline { - return Pipeline(firestore, stages.append(stage)) - } - - fun execute(): Task { - val observerTask = ObserverSnapshotTask() - execute(observerTask); - return observerTask.task - } - - private fun execute(observer: PipelineResultObserver) { - firestore.callClient { call -> call!!.executePipeline(toProto(), observer) } - } - - internal fun documentReference(key: DocumentKey): DocumentReference { - return DocumentReference(key, firestore) + internal constructor( + firestore: FirebaseFirestore, + stage: Stage + ) : this(firestore, FluentIterable.of(stage)) + + private fun append(stage: Stage): Pipeline { + return Pipeline(firestore, stages.append(stage)) + } + + fun execute(): Task { + val observerTask = ObserverSnapshotTask() + execute(observerTask) + return observerTask.task + } + + private fun execute(observer: PipelineResultObserver) { + firestore.callClient { call -> call!!.executePipeline(toProto(), observer) } + } + + internal fun documentReference(key: DocumentKey): DocumentReference { + return DocumentReference(key, firestore) + } + + fun toProto(): ExecutePipelineRequest { + val database = firestore.databaseId + val builder = ExecutePipelineRequest.newBuilder() + builder.database = "projects/${database.projectId}/databases/${database.databaseId}" + builder.structuredPipeline = toStructuredPipelineProto() + return builder.build() + } + + private fun toStructuredPipelineProto(): StructuredPipeline { + val builder = StructuredPipeline.newBuilder() + builder.pipeline = toPipelineProto() + return builder.build() + } + + private fun toPipelineProto(): com.google.firestore.v1.Pipeline = + com.google.firestore.v1.Pipeline.newBuilder() + .addAllStages(stages.map(Stage::toProtoStage)) + .build() + + private inner class ObserverSnapshotTask : PipelineResultObserver { + private val taskCompletionSource = TaskCompletionSource() + private val results: ImmutableList.Builder = ImmutableList.builder() + override fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) { + results.add( + PipelineResult(if (key == null) null else DocumentReference(key, firestore), data, version) + ) } - fun toProto(): ExecutePipelineRequest { - val database = firestore.databaseId - val builder = ExecutePipelineRequest.newBuilder() - builder.database = "projects/${database.projectId}/databases/${database.databaseId}" - builder.structuredPipeline = toStructuredPipelineProto() - return builder.build() + override fun onComplete(executionTime: SnapshotVersion) { + taskCompletionSource.setResult(PipelineSnapshot(executionTime, results.build())) } - private fun toStructuredPipelineProto(): StructuredPipeline { - val builder = StructuredPipeline.newBuilder() - builder.pipeline = toPipelineProto() - return builder.build() + override fun onError(exception: FirebaseFirestoreException) { + taskCompletionSource.setException(exception) } - private fun toPipelineProto(): com.google.firestore.v1.Pipeline = - com.google.firestore.v1.Pipeline.newBuilder() - .addAllStages(stages.map(Stage::toProtoStage)) - .build() - - private inner class ObserverSnapshotTask : PipelineResultObserver { - private val taskCompletionSource = TaskCompletionSource() - private val results: ImmutableList.Builder = ImmutableList.builder() - override fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) { - results.add( - PipelineResult( - if (key == null) null else DocumentReference(key, firestore), - data, - version - ) - ) - } - - override fun onComplete(executionTime: SnapshotVersion) { - taskCompletionSource.setResult(PipelineSnapshot(executionTime, results.build())) - } - - override fun onError(exception: FirebaseFirestoreException) { - taskCompletionSource.setException(exception) - } - - val task: Task - get() = taskCompletionSource.task - } + val task: Task + get() = taskCompletionSource.task + } } -class PipelineSource( - private val firestore: FirebaseFirestore -) { - fun collection(path: String): Pipeline { - return Pipeline(firestore, CollectionSource(path)) - } +class PipelineSource(private val firestore: FirebaseFirestore) { + fun collection(path: String): Pipeline { + return Pipeline(firestore, CollectionSource(path)) + } - fun collectionGroup(collectionId: String): Pipeline { - return Pipeline(firestore, CollectionGroupSource(collectionId)) - } + fun collectionGroup(collectionId: String): Pipeline { + return Pipeline(firestore, CollectionGroupSource(collectionId)) + } - fun database(): Pipeline { - return Pipeline(firestore, DatabaseSource()) - } + fun database(): Pipeline { + return Pipeline(firestore, DatabaseSource()) + } - fun documents(vararg documents: DocumentReference): Pipeline { - return Pipeline(firestore, DocumentsSource(documents)) - } + fun documents(vararg documents: DocumentReference): Pipeline { + return Pipeline(firestore, DocumentsSource(documents)) + } } -class PipelineSnapshot internal constructor( - val executionTime: SnapshotVersion, - val results: List +class PipelineSnapshot +internal constructor( + private val executionTime: SnapshotVersion, + private val results: List ) -class PipelineResult internal constructor( - private val key: DocumentReference?, - val fields: Map, - val version: SnapshotVersion, +class PipelineResult +internal constructor( + private val key: DocumentReference?, + private val fields: Map, + private val version: SnapshotVersion, ) -interface PipelineResultObserver { - fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) - fun onComplete(executionTime: SnapshotVersion) - fun onError(exception: FirebaseFirestoreException) +internal interface PipelineResultObserver { + fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) + fun onComplete(executionTime: SnapshotVersion) + fun onError(exception: FirebaseFirestoreException) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java index f28b07ff750..051dfce922b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java @@ -15,7 +15,6 @@ package com.google.firebase.firestore.model; import androidx.annotation.NonNull; - import java.util.ArrayList; import java.util.Collections; import java.util.List; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt index 69834c80e6f..4ff2a25ee86 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt @@ -11,74 +11,74 @@ import java.util.Date class Constant internal constructor(val value: Any?) : Expr() { - companion object { - fun of(value: Any): Constant { - return when (value) { - is String -> of(value) - is Number -> of(value) - is Date -> of(value) - is Timestamp -> of(value) - is Boolean -> of(value) - is GeoPoint -> of(value) - is Blob -> of(value) - is DocumentReference -> of(value) - is Value -> of(value) - is Iterable<*> -> of(value) - is Map<*, *> -> of(value) - else -> throw IllegalArgumentException("Unknown type: $value") - } - } - - fun of(value: String): Constant { - return Constant(value) - } - - fun of(value: Number): Constant { - return Constant(value) - } - - fun of(value: Date): Constant { - return Constant(value) - } - - fun of(value: Timestamp): Constant { - return Constant(value) - } - - fun of(value: Boolean): Constant { - return Constant(value) - } - - fun of(value: GeoPoint): Constant { - return Constant(value) - } - - fun of(value: Blob): Constant { - return Constant(value) - } - - fun of(value: DocumentReference): Constant { - return Constant(value) - } - - fun of(value: Value): Constant { - return Constant(value) - } - - fun of(value: VectorValue) : Constant { - return Constant(value) - } - - fun nullValue(): Constant { - return Constant(null) - } - - fun vector(value: DoubleArray): Constant { - return of(FieldValue.vector(value)) - } - - fun vector(value: VectorValue): Constant { - return of(value) - } + companion object { + fun of(value: Any): Constant { + return when (value) { + is String -> of(value) + is Number -> of(value) + is Date -> of(value) + is Timestamp -> of(value) + is Boolean -> of(value) + is GeoPoint -> of(value) + is Blob -> of(value) + is DocumentReference -> of(value) + is Value -> of(value) + is Iterable<*> -> of(value) + is Map<*, *> -> of(value) + else -> throw IllegalArgumentException("Unknown type: $value") + } } + + fun of(value: String): Constant { + return Constant(value) + } + + fun of(value: Number): Constant { + return Constant(value) + } + + fun of(value: Date): Constant { + return Constant(value) + } + + fun of(value: Timestamp): Constant { + return Constant(value) + } + + fun of(value: Boolean): Constant { + return Constant(value) + } + + fun of(value: GeoPoint): Constant { + return Constant(value) + } + + fun of(value: Blob): Constant { + return Constant(value) + } + + fun of(value: DocumentReference): Constant { + return Constant(value) + } + + fun of(value: Value): Constant { + return Constant(value) + } + + fun of(value: VectorValue): Constant { + return Constant(value) + } + + fun nullValue(): Constant { + return Constant(null) + } + + fun vector(value: DoubleArray): Constant { + return of(FieldValue.vector(value)) + } + + fun vector(value: VectorValue): Constant { + return of(value) + } + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt index 4f169e522a0..b5449fc570b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt @@ -6,441 +6,500 @@ import com.google.firebase.firestore.model.DocumentKey import com.google.firebase.firestore.model.FieldPath as ModelFieldPath open class Expr protected constructor() { - companion object { - internal fun toExprOrConstant(other: Any): Expr { - return when (other) { - is Expr -> other - else -> Constant.of(other) - } - } - - internal fun toArrayOfExprOrConstant(others: Iterable): Array { - return others.map(::toExprOrConstant).toTypedArray() - } - - internal fun toArrayOfExprOrConstant(others: Array): Array { - return others.map(::toExprOrConstant).toTypedArray() - } + companion object { + internal fun toExprOrConstant(other: Any): Expr { + return when (other) { + is Expr -> other + else -> Constant.of(other) + } } - /** - * Creates an expression that this expression to another expression. - * - *

Example: - * - *

{@code
-     * // Add the value of the 'quantity' field and the 'reserve' field.
-     * Field.of("quantity").add(Field.of("reserve"));
-     * }
- * - * @param other The expression to add to this expression. - * @return A new {@code Expr} representing the addition operation. - */ - fun add(other: Expr): Add { - return Add(this, other) + internal fun toArrayOfExprOrConstant(others: Iterable): Array { + return others.map(::toExprOrConstant).toTypedArray() } - /** - * Creates an expression that this expression to another expression. - * - *

Example: - * - *

{@code
-     * // Add the value of the 'quantity' field and the 'reserve' field.
-     * Field.of("quantity").add(Field.of("reserve"));
-     * }
- * - * @param other The constant value to add to this expression. - * @return A new {@code Expr} representing the addition operation. - */ - fun add(other: Any): Add { - return Add(this, toExprOrConstant(other)) + internal fun toArrayOfExprOrConstant(others: Array): Array { + return others.map(::toExprOrConstant).toTypedArray() } - - fun subtract(other: Expr): Subtract { - return Subtract(this, other) + } + + /** + * Creates an expression that this expression to another expression. + * + *

Example: + * + *

{@code // Add the value of the 'quantity' field and the 'reserve' field.
+   * Field.of("quantity").add(Field.of("reserve")); }
+ * + * @param other The expression to add to this expression. + * @return A new {@code Expr} representing the addition operation. + */ + fun add(other: Expr): Add { + return Add(this, other) + } + + /** + * Creates an expression that this expression to another expression. + * + *

Example: + * + *

{@code // Add the value of the 'quantity' field and the 'reserve' field.
+   * Field.of("quantity").add(Field.of("reserve")); }
+ * + * @param other The constant value to add to this expression. + * @return A new {@code Expr} representing the addition operation. + */ + fun add(other: Any): Add { + return Add(this, toExprOrConstant(other)) + } + + fun subtract(other: Expr): Subtract { + return Subtract(this, other) + } +} + +class Field private constructor(private val fieldPath: ModelFieldPath) : Expr() { + companion object { + fun of(name: String): Field { + if (name == DocumentKey.KEY_FIELD_NAME) { + return Field(ModelFieldPath.KEY_PATH) + } + return Field(FieldPath.fromDotSeparatedPath(name).internalPath) } -} -class Field private constructor(val fieldPath: ModelFieldPath) : Expr() { - companion object { - fun of(name: String): Field { - if (name == DocumentKey.KEY_FIELD_NAME) { - return Field(ModelFieldPath.KEY_PATH) - } - return Field(FieldPath.fromDotSeparatedPath(name).internalPath) - } - - fun of(fieldPath: FieldPath): Field { - if (fieldPath == FieldPath.documentId()) { - return Field(FieldPath.documentId().internalPath) - } - return Field(fieldPath.internalPath) - } + fun of(fieldPath: FieldPath): Field { + if (fieldPath == FieldPath.documentId()) { + return Field(FieldPath.documentId().internalPath) + } + return Field(fieldPath.internalPath) } + } } class ListOfExpr(val expressions: Array) : Expr() -open class Function protected constructor( - private val name: String, - private val params: Array -) : Expr() { - protected constructor(name: String, param1: Expr) : this(name, arrayOf(param1)) - protected constructor(name: String, param1: Expr, param2: Expr) : this(name, arrayOf(param1, param2)) - protected constructor(name: String, param1: Expr, param2: Expr, param3: Expr) : this(name, arrayOf(param1, param2, param3)) +open class Function +protected constructor(private val name: String, private val params: Array) : Expr() { + protected constructor(name: String, param1: Expr) : this(name, arrayOf(param1)) + protected constructor( + name: String, + param1: Expr, + param2: Expr + ) : this(name, arrayOf(param1, param2)) + protected constructor( + name: String, + param1: Expr, + param2: Expr, + param3: Expr + ) : this(name, arrayOf(param1, param2, param3)) } -open class FilterCondition protected constructor( - name: String, - vararg params: Expr -) : Function(name, params) +open class FilterCondition protected constructor(name: String, vararg params: Expr) : + Function(name, params) -open class Accumulator protected constructor( +open class Accumulator protected constructor(name: String, params: Array) : + Function(name, params) { + protected constructor( name: String, - params: Array -) : Function(name, params) { - protected constructor(name: String, param: Expr?) : this(name, if (param == null) emptyArray() else arrayOf(param)) + param: Expr? + ) : this(name, if (param == null) emptyArray() else arrayOf(param)) } class Add(left: Expr, right: Expr) : Function("add", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class Subtract(left: Expr, right: Expr) : Function("subtract", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class Multiply(left: Expr, right: Expr) : Function("multiply", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class Divide(left: Expr, right: Expr) : Function("divide", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class Mod(left: Expr, right: Expr) : Function("mod", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } // class BitAnd(left: Expr, right: Expr) : Function("bit_and", left, right) { // constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) // constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) // constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -//} +// } // class BitOr(left: Expr, right: Expr) : Function("bit_or", left, right) { // constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) // constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) // constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -//} +// } // class BitXor(left: Expr, right: Expr) : Function("bit_xor", left, right) { // constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) // constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) // constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -//} +// } // class BitNot(left: Expr, right: Expr) : Function("bit_not", left, right) { // constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) // constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) // constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -//} +// } // class BitLeftShift(left: Expr, right: Expr) : Function("bit_left_shift", left, right) { // constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) // constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) // constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -//} +// } // class BitRightShift(left: Expr, right: Expr) : Function("bit_right_shift", left, right) { // constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) // constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) // constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -//} +// } class Eq(left: Expr, right: Expr) : FilterCondition("eq", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class Neq(left: Expr, right: Expr) : FilterCondition("neq", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class Lt(left: Expr, right: Expr) : FilterCondition("lt", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class Lte(left: Expr, right: Expr) : FilterCondition("lte", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class Gt(left: Expr, right: Expr) : FilterCondition("gt", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class Gte(left: Expr, right: Expr) : FilterCondition("gte", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } -class ArrayConcat(array: Expr, vararg arrays: Expr) : Function("array_concat", arrayOf(array, *arrays)) { - constructor(array: Expr, arrays: List) : this(array, toExprOrConstant(arrays)) - constructor(fieldName: String, vararg arrays: Expr) : this(Field.of(fieldName), *arrays) - constructor(fieldName: String, right: List) : this(Field.of(fieldName), right) +class ArrayConcat(array: Expr, vararg arrays: Expr) : + Function("array_concat", arrayOf(array, *arrays)) { + constructor(array: Expr, arrays: List) : this(array, toExprOrConstant(arrays)) + constructor(fieldName: String, vararg arrays: Expr) : this(Field.of(fieldName), *arrays) + constructor(fieldName: String, right: List) : this(Field.of(fieldName), right) } class ArrayReverse(array: Expr) : Function("array_reverse", array) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class ArrayContains(array: Expr, value: Expr) : FilterCondition("array_contains", array, value) { - constructor(array: Expr, right: Any) : this(array, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(array: Expr, right: Any) : this(array, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } -class ArrayContainsAll(array: Expr, values: List) : FilterCondition("array_contains_all", array, ListOfExpr(toArrayOfExprOrConstant(values))) { - constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) +class ArrayContainsAll(array: Expr, values: List) : + FilterCondition("array_contains_all", array, ListOfExpr(toArrayOfExprOrConstant(values))) { + constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) } -class ArrayContainsAny(array: Expr, values: List) : FilterCondition("array_contains_any", array, ListOfExpr(toArrayOfExprOrConstant(values))) { - constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) +class ArrayContainsAny(array: Expr, values: List) : + FilterCondition("array_contains_any", array, ListOfExpr(toArrayOfExprOrConstant(values))) { + constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) } class ArrayLength(array: Expr) : Function("array_length", array) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } -class In(array: Expr, values: List) : FilterCondition("in", array, ListOfExpr(toArrayOfExprOrConstant(values))) { - constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) +class In(array: Expr, values: List) : + FilterCondition("in", array, ListOfExpr(toArrayOfExprOrConstant(values))) { + constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) } class IsNan(expr: Expr) : FilterCondition("is_nan", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class Exists(expr: Expr) : FilterCondition("exists", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class Not(expr: Expr) : FilterCondition("not", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } -class And(condition: Expr, vararg conditions: Expr) : FilterCondition("and", condition, *conditions) { - constructor(condition: Expr, vararg conditions: Any) : this(condition, *toArrayOfExprOrConstant(conditions)) - constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) - constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) +class And(condition: Expr, vararg conditions: Expr) : + FilterCondition("and", condition, *conditions) { + constructor( + condition: Expr, + vararg conditions: Any + ) : this(condition, *toArrayOfExprOrConstant(conditions)) + constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) + constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) } class Or(condition: Expr, vararg conditions: Expr) : FilterCondition("or", condition, *conditions) { - constructor(condition: Expr, vararg conditions: Any) : this(condition, *toArrayOfExprOrConstant(conditions)) - constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) - constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) + constructor( + condition: Expr, + vararg conditions: Any + ) : this(condition, *toArrayOfExprOrConstant(conditions)) + constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) + constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) } -class Xor(condition: Expr, vararg conditions: Expr) : FilterCondition("xor", condition, *conditions) { - constructor(condition: Expr, vararg conditions: Any) : this(condition, *toArrayOfExprOrConstant(conditions)) - constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) - constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) +class Xor(condition: Expr, vararg conditions: Expr) : + FilterCondition("xor", condition, *conditions) { + constructor( + condition: Expr, + vararg conditions: Any + ) : this(condition, *toArrayOfExprOrConstant(conditions)) + constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) + constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) } -class If(condition: FilterCondition, thenExpr: Expr, elseExpr: Expr) : Function("if", condition, thenExpr, elseExpr) +class If(condition: FilterCondition, thenExpr: Expr, elseExpr: Expr) : + Function("if", condition, thenExpr, elseExpr) class LogicalMax(left: Expr, right: Expr) : Function("logical_max", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class LogicalMin(left: Expr, right: Expr) : Function("logical_min", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class Reverse(expr: Expr) : Function("reverse", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class ReplaceFirst(value: Expr, find: Expr, replace: Expr) : Function("replace_first", value, find, replace) { - constructor(value: Expr, find: String, replace: String) : this(value, Constant.of(find), Constant.of(replace)) - constructor(fieldName: String, find: String, replace: String) : this(Field.of(fieldName), find, replace) -} - -class ReplaceAll(value: Expr, find: Expr, replace: Expr) : Function("replace_all", value, find, replace) { - constructor(value: Expr, find: String, replace: String) : this(value, Constant.of(find), Constant.of(replace)) - constructor(fieldName: String, find: String, replace: String) : this(Field.of(fieldName), find, replace) + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class ReplaceFirst(value: Expr, find: Expr, replace: Expr) : + Function("replace_first", value, find, replace) { + constructor( + value: Expr, + find: String, + replace: String + ) : this(value, Constant.of(find), Constant.of(replace)) + constructor( + fieldName: String, + find: String, + replace: String + ) : this(Field.of(fieldName), find, replace) +} + +class ReplaceAll(value: Expr, find: Expr, replace: Expr) : + Function("replace_all", value, find, replace) { + constructor( + value: Expr, + find: String, + replace: String + ) : this(value, Constant.of(find), Constant.of(replace)) + constructor( + fieldName: String, + find: String, + replace: String + ) : this(Field.of(fieldName), find, replace) } class CharLength(value: Expr) : Function("char_length", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class ByteLength(value: Expr) : Function("byte_length", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class Like(expr: Expr, pattern: Expr) : FilterCondition("like", expr, pattern) { - constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) - constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) - constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) + constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) + constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) + constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) } class RegexContains(expr: Expr, pattern: Expr) : FilterCondition("regex_contains", expr, pattern) { - constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) - constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) - constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) + constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) + constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) + constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) } class RegexMatch(expr: Expr, pattern: Expr) : FilterCondition("regex_match", expr, pattern) { - constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) - constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) - constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) + constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) + constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) + constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) } class StrContains(expr: Expr, substring: Expr) : FilterCondition("str_contains", expr, substring) { - constructor(expr: Expr, substring: String) : this(expr, Constant.of(substring)) - constructor(fieldName: String, substring: Expr) : this(Field.of(fieldName), substring) - constructor(fieldName: String, substring: String) : this(Field.of(fieldName), substring) + constructor(expr: Expr, substring: String) : this(expr, Constant.of(substring)) + constructor(fieldName: String, substring: Expr) : this(Field.of(fieldName), substring) + constructor(fieldName: String, substring: String) : this(Field.of(fieldName), substring) } class StartsWith(expr: Expr, prefix: Expr) : FilterCondition("starts_with", expr, prefix) { - constructor(expr: Expr, prefix: String) : this(expr, Constant.of(prefix)) - constructor(fieldName: String, prefix: Expr) : this(Field.of(fieldName), prefix) - constructor(fieldName: String, prefix: String) : this(Field.of(fieldName), prefix) + constructor(expr: Expr, prefix: String) : this(expr, Constant.of(prefix)) + constructor(fieldName: String, prefix: Expr) : this(Field.of(fieldName), prefix) + constructor(fieldName: String, prefix: String) : this(Field.of(fieldName), prefix) } class EndsWith(expr: Expr, suffix: Expr) : FilterCondition("ends_with", expr, suffix) { - constructor(expr: Expr, suffix: String) : this(expr, Constant.of(suffix)) - constructor(fieldName: String, suffix: Expr) : this(Field.of(fieldName), suffix) - constructor(fieldName: String, suffix: String) : this(Field.of(fieldName), suffix) + constructor(expr: Expr, suffix: String) : this(expr, Constant.of(suffix)) + constructor(fieldName: String, suffix: Expr) : this(Field.of(fieldName), suffix) + constructor(fieldName: String, suffix: String) : this(Field.of(fieldName), suffix) } class ToLower(expr: Expr) : Function("to_lower", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class ToUpper(expr: Expr) : Function("to_upper", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class Trim(expr: Expr) : Function("trim", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } -class StrConcat internal constructor(first: Expr, vararg rest: Expr) : Function("str_concat", arrayOf(first, *rest)) { - constructor(first: Expr, vararg rest: String) : this(first, *rest.map(Constant::of).toTypedArray()) - constructor(fieldName: String, vararg rest: Expr) : this(Field.of(fieldName), *rest) - constructor(fieldName: String, vararg rest: String) : this(Field.of(fieldName), *rest) +class StrConcat internal constructor(first: Expr, vararg rest: Expr) : + Function("str_concat", arrayOf(first, *rest)) { + constructor( + first: Expr, + vararg rest: String + ) : this(first, *rest.map(Constant::of).toTypedArray()) + constructor(fieldName: String, vararg rest: Expr) : this(Field.of(fieldName), *rest) + constructor(fieldName: String, vararg rest: String) : this(Field.of(fieldName), *rest) } class MapGet(map: Expr, name: String) : Function("map_get", map, Constant.of(name)) { - constructor(fieldName: String, name: String) : this(Field.of(fieldName), name) + constructor(fieldName: String, name: String) : this(Field.of(fieldName), name) } class Count(value: Expr?) : Accumulator("count", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class Sum(value: Expr) : Accumulator("sum", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class Avg(value: Expr) : Accumulator("avg", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class Min(value: Expr) : Accumulator("min", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class Max(value: Expr) : Accumulator("max", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class CosineDistance(vector1: Expr, vector2: Expr) : Function("cosine_distance", vector1, vector2) { - constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) - constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) - constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) + constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) + constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) + constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) } class DotProduct(vector1: Expr, vector2: Expr) : Function("dot_product", vector1, vector2) { - constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) - constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) - constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) + constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) + constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) + constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) } -class EuclideanDistance(vector1: Expr, vector2: Expr) : Function("euclidean_distance", vector1, vector2) { - constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) - constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) - constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) +class EuclideanDistance(vector1: Expr, vector2: Expr) : + Function("euclidean_distance", vector1, vector2) { + constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) + constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) + constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) + constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) } class VectorLength(vector: Expr) : Function("vector_length", vector) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class UnixMicrosToTimestamp(input: Expr) : Function("unix_micros_to_timestamp", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class TimestampToUnixMicros(input: Expr) : Function("timestamp_to_unix_micros", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class UnixMillisToTimestamp(input: Expr) : Function("unix_millis_to_timestamp", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class TimestampToUnixMillis(input: Expr) : Function("timestamp_to_unix_millis", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class UnixSecondsToTimestamp(input: Expr) : Function("unix_seconds_to_timestamp", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String) : this(Field.of(fieldName)) } class TimestampToUnixSeconds(input: Expr) : Function("timestamp_to_unix_seconds", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class TimestampAdd(timestamp: Expr, unit: Expr, amount: Expr) : Function("timestamp_add", timestamp, unit, amount) { - constructor(timestamp: Expr, unit: String, amount: Double) : this(timestamp, Constant.of(unit), Constant.of(amount)) - constructor(fieldName: String, unit: String, amount: Double) : this(Field.of(fieldName), unit, amount) - constructor(fieldName: String, unit: Expr, amount: Expr) : this(Field.of(fieldName), unit, amount) -} - -class TimestampSub(timestamp: Expr, unit: Expr, amount: Expr) : Function("timestamp_sub", timestamp, unit, amount) { - constructor(timestamp: Expr, unit: String, amount: Double) : this(timestamp, Constant.of(unit), Constant.of(amount)) - constructor(fieldName: String, unit: String, amount: Double) : this(Field.of(fieldName), unit, amount) - constructor(fieldName: String, unit: Expr, amount: Expr) : this(Field.of(fieldName), unit, amount) + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class TimestampAdd(timestamp: Expr, unit: Expr, amount: Expr) : + Function("timestamp_add", timestamp, unit, amount) { + constructor( + timestamp: Expr, + unit: String, + amount: Double + ) : this(timestamp, Constant.of(unit), Constant.of(amount)) + constructor( + fieldName: String, + unit: String, + amount: Double + ) : this(Field.of(fieldName), unit, amount) + constructor(fieldName: String, unit: Expr, amount: Expr) : this(Field.of(fieldName), unit, amount) +} + +class TimestampSub(timestamp: Expr, unit: Expr, amount: Expr) : + Function("timestamp_sub", timestamp, unit, amount) { + constructor( + timestamp: Expr, + unit: String, + amount: Double + ) : this(timestamp, Constant.of(unit), Constant.of(amount)) + constructor( + fieldName: String, + unit: String, + amount: Double + ) : this(Field.of(fieldName), unit, amount) + constructor(fieldName: String, unit: Expr, amount: Expr) : this(Field.of(fieldName), unit, amount) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index 516db44bc4e..ce63d74389d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -5,40 +5,44 @@ import com.google.firestore.v1.Pipeline import com.google.firestore.v1.Value abstract class Stage(protected val name: String) { - internal fun toProtoStage(): Pipeline.Stage { - val builder = Pipeline.Stage.newBuilder() - builder.setName(name) - args().forEach { arg -> builder.addArgs(arg) } - return builder.build(); - } - protected abstract fun args(): Sequence + internal fun toProtoStage(): Pipeline.Stage { + val builder = Pipeline.Stage.newBuilder() + builder.setName(name) + args().forEach { arg -> builder.addArgs(arg) } + return builder.build() + } + protected abstract fun args(): Sequence } class DatabaseSource : Stage("database") { - override fun args(): Sequence { - return emptySequence() - } + override fun args(): Sequence { + return emptySequence() + } } class CollectionSource internal constructor(path: String) : Stage("collection") { - private val path: String = if (path.startsWith("/")) path else "/" + path - override fun args(): Sequence { - return sequenceOf(Value.newBuilder().setReferenceValue(path).build()) - } + private val path: String = if (path.startsWith("/")) path else "/" + path + override fun args(): Sequence { + return sequenceOf(Value.newBuilder().setReferenceValue(path).build()) + } } -class CollectionGroupSource internal constructor(val collectionId: String): Stage("collection_group") { - override fun args(): Sequence { - return sequenceOf( - Value.newBuilder().setReferenceValue("").build(), - Value.newBuilder().setStringValue(collectionId).build() - ) - } +class CollectionGroupSource internal constructor(val collectionId: String) : + Stage("collection_group") { + override fun args(): Sequence { + return sequenceOf( + Value.newBuilder().setReferenceValue("").build(), + Value.newBuilder().setStringValue(collectionId).build() + ) + } } -class DocumentsSource private constructor(private val documents: List) : Stage("documents") { - internal constructor(documents: Array) : this(documents.map { docRef -> "/" + docRef.path }) - override fun args(): Sequence { - return documents.asSequence().map { doc -> Value.newBuilder().setStringValue(doc).build() } - } +class DocumentsSource private constructor(private val documents: List) : + Stage("documents") { + internal constructor( + documents: Array + ) : this(documents.map { docRef -> "/" + docRef.path }) + override fun args(): Sequence { + return documents.asSequence().map { doc -> Value.newBuilder().setStringValue(doc).build() } + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java index db9e06ad219..9ea1d7513cb 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java @@ -240,23 +240,27 @@ public Task> runAggregateQuery( }); } - public void executePipeline( - ExecutePipelineRequest request, - PipelineResultObserver observer - ) { - channel - .runStreamingResponseRpc(FirestoreGrpc.getExecutePipelineMethod(), request, new FirestoreChannel.StreamingListener() { + public void executePipeline(ExecutePipelineRequest request, PipelineResultObserver observer) { + channel.runStreamingResponseRpc( + FirestoreGrpc.getExecutePipelineMethod(), + request, + new FirestoreChannel.StreamingListener() { private SnapshotVersion executionTime = SnapshotVersion.NONE; @Override public void onMessage(ExecutePipelineResponse message) { setExecutionTime(serializer.decodeVersion(message.getExecutionTime())); - message.getResultsList().forEach(document -> observer.onDocument( - document.getName() == null ? null : serializer.decodeKey(document.getName()), - document.getFieldsMap(), - serializer.decodeVersion(document.getUpdateTime()) - )); + message + .getResultsList() + .forEach( + document -> + observer.onDocument( + document.getName() == null + ? null + : serializer.decodeKey(document.getName()), + document.getFieldsMap(), + serializer.decodeVersion(document.getUpdateTime()))); } @Override diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java index 2d7e3b47243..4468151e344 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java @@ -10,7 +10,5 @@ public class ExprTest { @Test - public void test() { - - } + public void test() {} } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java index 938960c5bab..eb1b03119f4 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java @@ -10,7 +10,5 @@ public class StageTest { @Test - public void test() { - - } + public void test() {} } From 0ce7d4f8410e13e381a14ff6c69a600101360f33 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 3 Feb 2025 18:39:55 -0500 Subject: [PATCH 009/152] Spotless --- .../firebase/firestore/remote/Datastore.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java index 9ea1d7513cb..8e7aab83565 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java @@ -35,6 +35,7 @@ import com.google.firestore.v1.BatchGetDocumentsResponse; import com.google.firestore.v1.CommitRequest; import com.google.firestore.v1.CommitResponse; +import com.google.firestore.v1.Document; import com.google.firestore.v1.ExecutePipelineRequest; import com.google.firestore.v1.ExecutePipelineResponse; import com.google.firestore.v1.FirestoreGrpc; @@ -251,16 +252,13 @@ public void executePipeline(ExecutePipelineRequest request, PipelineResultObserv @Override public void onMessage(ExecutePipelineResponse message) { setExecutionTime(serializer.decodeVersion(message.getExecutionTime())); - message - .getResultsList() - .forEach( - document -> - observer.onDocument( - document.getName() == null - ? null - : serializer.decodeKey(document.getName()), - document.getFieldsMap(), - serializer.decodeVersion(document.getUpdateTime()))); + for (Document document : message.getResultsList()) { + String documentName = document.getName(); + observer.onDocument( + documentName == null ? null : serializer.decodeKey(documentName), + document.getFieldsMap(), + serializer.decodeVersion(document.getUpdateTime())); + } } @Override From ab5f2173e8e92df3b30a228daa1ab15ba4ec286d Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 14 Feb 2025 20:23:58 -0500 Subject: [PATCH 010/152] Rename .java to .kt --- .../google/firebase/firestore/model/{Values.java => Values.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename firebase-firestore/src/main/java/com/google/firebase/firestore/model/{Values.java => Values.kt} (100%) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt similarity index 100% rename from firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.java rename to firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt From dec61c0b808c90751d701ef93efb465e2b91e284 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 14 Feb 2025 20:23:58 -0500 Subject: [PATCH 011/152] Work --- .../firebase/firestore/PipelineTest.java | 437 ++++++++ .../firestore/CollectionReference.java | 6 + .../firebase/firestore/DocumentReference.java | 12 +- .../firebase/firestore/DocumentSnapshot.java | 1 + .../firebase/firestore/FirebaseFirestore.java | 6 +- .../com/google/firebase/firestore/Pipeline.kt | 144 ++- .../firebase/firestore/UserDataReader.java | 91 +- .../firebase/firestore/VectorValue.java | 17 - .../firestore/model/ResourcePath.java | 12 +- .../google/firebase/firestore/model/Values.kt | 962 ++++++++++-------- .../firebase/firestore/pipeline/Constant.kt | 51 +- .../firestore/pipeline/accumulators.kt | 104 ++ .../firebase/firestore/pipeline/expression.kt | 488 +++++++-- .../firebase/firestore/pipeline/stage.kt | 172 +++- .../firebase/firestore/remote/Datastore.java | 3 +- .../firebase/firestore/TestAccessHelper.java | 6 +- 16 files changed, 1793 insertions(+), 719 deletions(-) create mode 100644 firebase-firestore/src/androidTest/java/com/google/firebase/firestore/PipelineTest.java create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt 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 new file mode 100644 index 00000000000..a88959dd930 --- /dev/null +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/PipelineTest.java @@ -0,0 +1,437 @@ +package com.google.firebase.firestore; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.firestore.pipeline.Function.and; +import static com.google.firebase.firestore.pipeline.Function.arrayContains; +import static com.google.firebase.firestore.pipeline.Function.arrayContainsAny; +import static com.google.firebase.firestore.pipeline.Function.eq; +import static com.google.firebase.firestore.pipeline.Function.gt; +import static com.google.firebase.firestore.pipeline.Function.or; +import static com.google.firebase.firestore.pipeline.Ordering.ascending; +import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor; +import static java.util.Map.entry; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.gms.tasks.Task; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.truth.Correspondence; +import com.google.firebase.firestore.pipeline.Accumulator; +import com.google.firebase.firestore.pipeline.AggregateStage; +import com.google.firebase.firestore.pipeline.Field; +import com.google.firebase.firestore.pipeline.Function; +import com.google.firebase.firestore.testutil.IntegrationTestUtil; +import java.util.Map; +import java.util.Objects; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class PipelineTest { + + private static final Correspondence> DATA_CORRESPONDENCE = Correspondence.from((result, expected) -> { + assertThat(result.getData()) + .comparingValuesUsing(Correspondence.from( + (x, y) -> { + if (x instanceof Long && y instanceof Integer) { + return (long) x == (long) (int) y; + } + return Objects.equals(x, y); + }, + "MapValueCompare" + )) + .containsExactlyEntriesIn(expected); + return true; + }, "GetData"); + + private static final Correspondence ID_CORRESPONDENCE = Correspondence.transforming(x -> x.getRef().getId(), "GetRefId"); + + private CollectionReference randomCol; + private FirebaseFirestore firestore; + + @After + public void tearDown() { + IntegrationTestUtil.tearDown(); + } + + private Map> bookDocs = + ImmutableMap.ofEntries( + entry( + "book1", + ImmutableMap.ofEntries( + entry("title", "The Hitchhiker's Guide to the Galaxy"), + entry("author", "Douglas Adams"), + entry("genre", "Science Fiction"), + entry("published", 1979), + entry("rating", 4.2), + entry("tags", ImmutableList.of("comedy", "space", "adventure")), + entry("awards", ImmutableMap.of("hugo", true, "nebula", false)))), + entry( + "book2", + ImmutableMap.ofEntries( + entry("title", "Pride and Prejudice"), + entry("author", "Jane Austen"), + entry("genre", "Romance"), + entry("published", 1813), + entry("rating", 4.5), + entry("tags", ImmutableList.of("classic", "social commentary", "love")), + entry("awards", ImmutableMap.of("none", true)))), + entry( + "book3", + ImmutableMap.ofEntries( + entry("title", "One Hundred Years of Solitude"), + entry("author", "Gabriel García Márquez"), + entry("genre", "Magical Realism"), + entry("published", 1967), + entry("rating", 4.3), + entry("tags", ImmutableList.of("family", "history", "fantasy")), + entry("awards", ImmutableMap.of("nobel", true, "nebula", false)))), + entry( + "book4", + ImmutableMap.ofEntries( + entry("title", "The Lord of the Rings"), + entry("author", "J.R.R. Tolkien"), + entry("genre", "Fantasy"), + entry("published", 1954), + entry("rating", 4.7), + entry("tags", ImmutableList.of("adventure", "magic", "epic")), + entry("awards", ImmutableMap.of("hugo", false, "nebula", false)))), + entry( + "book5", + ImmutableMap.ofEntries( + entry("title", "The Handmaid's Tale"), + entry("author", "Margaret Atwood"), + entry("genre", "Dystopian"), + entry("published", 1985), + entry("rating", 4.1), + entry("tags", ImmutableList.of("feminism", "totalitarianism", "resistance")), + entry( + "awards", ImmutableMap.of("arthur c. clarke", true, "booker prize", false)))), + entry( + "book6", + ImmutableMap.ofEntries( + entry("title", "Crime and Punishment"), + entry("author", "Fyodor Dostoevsky"), + entry("genre", "Psychological Thriller"), + entry("published", 1866), + entry("rating", 4.3), + entry("tags", ImmutableList.of("philosophy", "crime", "redemption")), + entry("awards", ImmutableMap.of("none", true)))), + entry( + "book7", + ImmutableMap.ofEntries( + entry("title", "To Kill a Mockingbird"), + entry("author", "Harper Lee"), + entry("genre", "Southern Gothic"), + entry("published", 1960), + entry("rating", 4.2), + entry("tags", ImmutableList.of("racism", "injustice", "coming-of-age")), + entry("awards", ImmutableMap.of("pulitzer", true)))), + entry( + "book8", + ImmutableMap.ofEntries( + entry("title", "1984"), + entry("author", "George Orwell"), + entry("genre", "Dystopian"), + entry("published", 1949), + entry("rating", 4.2), + entry("tags", ImmutableList.of("surveillance", "totalitarianism", "propaganda")), + entry("awards", ImmutableMap.of("prometheus", true)))), + entry( + "book9", + ImmutableMap.ofEntries( + entry("title", "The Great Gatsby"), + entry("author", "F. Scott Fitzgerald"), + entry("genre", "Modernist"), + entry("published", 1925), + entry("rating", 4.0), + entry("tags", ImmutableList.of("wealth", "american dream", "love")), + entry("awards", ImmutableMap.of("none", true)))), + entry( + "book10", + ImmutableMap.ofEntries( + entry("title", "Dune"), + entry("author", "Frank Herbert"), + entry("genre", "Science Fiction"), + entry("published", 1965), + entry("rating", 4.6), + entry("tags", ImmutableList.of("politics", "desert", "ecology")), + entry("awards", ImmutableMap.of("hugo", true, "nebula", true))))); + + @Before + public void setup() { + randomCol = IntegrationTestUtil.testCollectionWithDocs(bookDocs); + firestore = randomCol.firestore; + } + + @Test + public void emptyResults() { + Task execute = + firestore.pipeline().collection(randomCol.getPath()).limit(0).execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()).isEmpty(); + } + + @Test + public void fullResults() { + Task execute = firestore.pipeline().collection(randomCol.getPath()).execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()).hasSize(10); + } + + @Test + public void aggregateResultsCountAll() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .aggregate(Accumulator.countAll().as("count")) + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly(ImmutableMap.of("count", 10)); + } + + @Test + @Ignore("Not supported yet") + public void aggregateResultsMany() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .where(Function.eq("genre", "Science Fiction")) + .aggregate( + Accumulator.countAll().as("count"), + Accumulator.avg("rating").as("avgRating"), + Field.of("rating").max().as("maxRating")) + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.ofEntries( + entry("count", 10), entry("avgRating", 4.4), entry("maxRating", 4.6))); + } + + @Test + public void groupAndAccumulateResults() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .where(Function.lt(Field.of("published"), 1984)) + .aggregate( + AggregateStage.withAccumulators(Accumulator.avg("rating").as("avgRating")) + .withGroups("genre")) + .where(Function.gt("avgRating", 4.3)) + .sort(Field.of("avgRating").descending()) + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.ofEntries(entry("avgRating", 4.7), entry("genre", "Fantasy")), + ImmutableMap.ofEntries(entry("avgRating", 4.5), entry("genre", "Romance")), + ImmutableMap.ofEntries(entry("avgRating", 4.4), entry("genre", "Science Fiction"))); + } + + @Test + @Ignore("Not supported yet") + public void minAndMaxAccumulations() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .aggregate( + Accumulator.countAll().as("count"), + Field.of("rating").max().as("maxRating"), + Field.of("published").min().as("minPublished")) + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.ofEntries( + entry("count", 10), entry("maxRating", 4.7), entry("minPublished", 1813))); + } + + @Test + public void canSelectFields() { + Task execute = + firestore.pipeline().collection(randomCol).select("title", "author").sort(Field.of("author").ascending()).execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.ofEntries( + entry("title", "The Hitchhiker's Guide to the Galaxy"), + entry("author", "Douglas Adams")), + ImmutableMap.ofEntries( + entry("title", "The Great Gatsby"), entry("author", "F. Scott Fitzgerald")), + ImmutableMap.ofEntries(entry("title", "Dune"), entry("author", "Frank Herbert")), + ImmutableMap.ofEntries( + entry("title", "Crime and Punishment"), entry("author", "Fyodor Dostoevsky")), + ImmutableMap.ofEntries( + entry("title", "One Hundred Years of Solitude"), + entry("author", "Gabriel García Márquez")), + ImmutableMap.ofEntries(entry("title", "1984"), entry("author", "George Orwell")), + ImmutableMap.ofEntries( + entry("title", "To Kill a Mockingbird"), entry("author", "Harper Lee")), + ImmutableMap.ofEntries( + entry("title", "The Lord of the Rings"), entry("author", "J.R.R. Tolkien")), + ImmutableMap.ofEntries( + entry("title", "Pride and Prejudice"), entry("author", "Jane Austen")), + ImmutableMap.ofEntries( + entry("title", "The Handmaid's Tale"), entry("author", "Margaret Atwood"))) + .inOrder(); + } + + @Test + public void whereWithAnd() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .where(and(gt("rating", 4.5), eq("genre", "Science Fiction"))) + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(ID_CORRESPONDENCE) + .containsExactly("book10"); + } + + @Test + public void whereWithOr() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .where(or(eq("genre", "Romance"), eq("genre", "Dystopian"))) + .select("title") + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of("title", "Pride and Prejudice"), + ImmutableMap.of("title", "The Handmaid's Tale"), + ImmutableMap.of("title", "1984")); + } + + @Test + public void offsetAndLimits() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .sort(ascending("author")) + .offset(5) + .limit(3) + .select("title", "author") + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.ofEntries(entry("title", "1984"), entry("author", "George Orwell")), + ImmutableMap.ofEntries( + entry("title", "To Kill a Mockingbird"), entry("author", "Harper Lee")), + ImmutableMap.ofEntries( + entry("title", "The Lord of the Rings"), entry("author", "J.R.R. Tolkien"))); + } + + @Test + public void arrayContainsWorks() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .where(arrayContains("tags", "comedy")) + .select("title") + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly(ImmutableMap.of("title", "The Hitchhiker's Guide to the Galaxy")); + } + + @Test + public void arrayContainsAnyWorks() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .where(arrayContainsAny("tags", ImmutableList.of("comedy", "classic"))) + .select("title") + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of("title", "The Hitchhiker's Guide to the Galaxy"), + ImmutableMap.of("title", "Pride and Prejudice")); + } + + @Test + public void arrayContainsAllWorks() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .where(Field.of("tags").arrayContainsAll(ImmutableList.of("adventure", "magic"))) + .select("title") + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly(ImmutableMap.of("title", "The Lord of the Rings")); + } + + @Test + public void arrayLengthWorks() { + Task execute = + randomCol + .pipeline() + .select(Field.of("tags").arrayLength().as("tagsCount")) + .where(eq("tagsCount", 3)) + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()).hasSize(10); + } + + @Test + @Ignore("Not supported yet") + public void arrayConcatWorks() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .where(eq("title", "The Hitchhiker's Guide to the Galaxy")) + .select(Field.of("tags").arrayConcat(ImmutableList.of("newTag1", "newTag2")).as("modifiedTags")) + .limit(1) + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly(ImmutableMap.of("modifiedTags", ImmutableList.of("comedy", "space", "adventure", "newTag1", "newTag2"))); + } + + @Test + public void testStrConcat() { + Task execute = + randomCol + .pipeline() + .select(Field.of("author").strConcat(" - ", Field.of("title")).as("bookInfo")) + .limit(1) + .execute(); + PipelineSnapshot snapshot = waitFor(execute); + assertThat(snapshot.getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly(ImmutableMap.of("bookInfo", "Douglas Adams - The Hitchhiker's Guide to the Galaxy")); + } +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java index d0a358e2233..297d018c9d6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java @@ -21,6 +21,7 @@ import com.google.android.gms.tasks.Task; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.ResourcePath; +import com.google.firebase.firestore.pipeline.CollectionSource; import com.google.firebase.firestore.util.Executors; import com.google.firebase.firestore.util.Util; @@ -127,4 +128,9 @@ public Task add(@NonNull Object data) { return ref; }); } + + @NonNull + public Pipeline pipeline() { + return new Pipeline(firestore, new CollectionSource(getPath())); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java index e3097d32b00..0161432843f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java @@ -57,7 +57,7 @@ * in test mocks. Subclassing is not supported in production code and new SDK releases may break * code that does so. */ -public class DocumentReference { +public final class DocumentReference { private final DocumentKey key; @@ -65,9 +65,7 @@ public class DocumentReference { DocumentReference(DocumentKey key, FirebaseFirestore firestore) { this.key = checkNotNull(key); - // TODO: We should checkNotNull(firestore), but tests are currently cheating - // and setting it to null. - this.firestore = firestore; + this.firestore = checkNotNull(firestore); } /** @hide */ @@ -564,6 +562,12 @@ public int hashCode() { return result; } + @NonNull + @Override + public String toString() { + return "DocumentReference{" + "key=" + key + ", firestore=" + firestore + '}'; + } + private com.google.firebase.firestore.core.Query asQuery() { return com.google.firebase.firestore.core.Query.atPath(key.getPath()); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentSnapshot.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentSnapshot.java index 4540608fc48..5c978b8cce9 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentSnapshot.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentSnapshot.java @@ -555,6 +555,7 @@ public int hashCode() { return hash; } + @NonNull @Override public String toString() { return "DocumentSnapshot{" + "key=" + key + ", metadata=" + metadata + ", doc=" + doc + '}'; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 079619647ab..557471c9b3d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -23,6 +23,7 @@ import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; @@ -850,7 +851,9 @@ T callClient(Function call) { return clientProvider.call(call); } - DatabaseId getDatabaseId() { + @RestrictTo(RestrictTo.Scope.LIBRARY) + @NonNull + public DatabaseId getDatabaseId() { return databaseId; } @@ -884,6 +887,7 @@ static void setClientLanguage(@NonNull String languageToken) { @NonNull public PipelineSource pipeline() { + clientProvider.ensureConfigured(); return new PipelineSource(this); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index c75e7106c4e..4eb6ba5d1d6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -6,11 +6,26 @@ import com.google.common.collect.FluentIterable import com.google.common.collect.ImmutableList import com.google.firebase.firestore.model.DocumentKey import com.google.firebase.firestore.model.SnapshotVersion +import com.google.firebase.firestore.pipeline.AccumulatorWithAlias +import com.google.firebase.firestore.pipeline.AddFieldsStage +import com.google.firebase.firestore.pipeline.AggregateStage +import com.google.firebase.firestore.pipeline.BooleanExpr import com.google.firebase.firestore.pipeline.CollectionGroupSource import com.google.firebase.firestore.pipeline.CollectionSource import com.google.firebase.firestore.pipeline.DatabaseSource +import com.google.firebase.firestore.pipeline.DistinctStage import com.google.firebase.firestore.pipeline.DocumentsSource +import com.google.firebase.firestore.pipeline.Field +import com.google.firebase.firestore.pipeline.LimitStage +import com.google.firebase.firestore.pipeline.OffsetStage +import com.google.firebase.firestore.pipeline.Ordering +import com.google.firebase.firestore.pipeline.RemoveFieldsStage +import com.google.firebase.firestore.pipeline.SelectStage +import com.google.firebase.firestore.pipeline.Selectable +import com.google.firebase.firestore.pipeline.SortStage import com.google.firebase.firestore.pipeline.Stage +import com.google.firebase.firestore.pipeline.WhereStage +import com.google.firebase.firestore.util.Preconditions import com.google.firestore.v1.ExecutePipelineRequest import com.google.firestore.v1.StructuredPipeline import com.google.firestore.v1.Value @@ -31,19 +46,15 @@ internal constructor( fun execute(): Task { val observerTask = ObserverSnapshotTask() - execute(observerTask) + firestore.callClient { call -> call!!.executePipeline(toProto(), observerTask) } return observerTask.task } - private fun execute(observer: PipelineResultObserver) { - firestore.callClient { call -> call!!.executePipeline(toProto(), observer) } - } - internal fun documentReference(key: DocumentKey): DocumentReference { return DocumentReference(key, firestore) } - fun toProto(): ExecutePipelineRequest { + internal fun toProto(): ExecutePipelineRequest { val database = firestore.databaseId val builder = ExecutePipelineRequest.newBuilder() builder.database = "projects/${database.projectId}/databases/${database.databaseId}" @@ -57,17 +68,74 @@ internal constructor( return builder.build() } - private fun toPipelineProto(): com.google.firestore.v1.Pipeline = + internal fun toPipelineProto(): com.google.firestore.v1.Pipeline = com.google.firestore.v1.Pipeline.newBuilder() .addAllStages(stages.map(Stage::toProtoStage)) .build() - private inner class ObserverSnapshotTask : PipelineResultObserver { + fun addFields(vararg fields: Selectable): Pipeline { + return append(AddFieldsStage(fields)) + } + + fun removeFields(vararg fields: Field): Pipeline { + return append(RemoveFieldsStage(fields)) + } + + fun removeFields(vararg fields: String): Pipeline { + return append(RemoveFieldsStage(fields.map(Field::of).toTypedArray())) + } + + fun select(vararg fields: Selectable): Pipeline { + return append(SelectStage(fields)) + } + + fun select(vararg fields: String): Pipeline { + return append(SelectStage(fields.map(Field::of).toTypedArray())) + } + + fun sort(vararg orders: Ordering): Pipeline { + return append(SortStage(orders)) + } + + fun where(condition: BooleanExpr): Pipeline { + return append(WhereStage(condition)) + } + + fun offset(offset: Long): Pipeline { + return append(OffsetStage(offset)) + } + + fun limit(limit: Long): Pipeline { + return append(LimitStage(limit)) + } + + fun distinct(vararg groups: Selectable): Pipeline { + return append(DistinctStage(groups)) + } + + fun distinct(vararg groups: String): Pipeline { + return append(DistinctStage(groups.map(Field::of).toTypedArray())) + } + + fun aggregate(vararg accumulators: AccumulatorWithAlias): Pipeline { + return append(AggregateStage.withAccumulators(*accumulators)) + } + + fun aggregate(aggregateStage: AggregateStage): Pipeline { + return append(aggregateStage) + } + + private inner class ObserverSnapshotTask() : PipelineResultObserver { private val taskCompletionSource = TaskCompletionSource() private val results: ImmutableList.Builder = ImmutableList.builder() override fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) { results.add( - PipelineResult(if (key == null) null else DocumentReference(key, firestore), data, version) + PipelineResult( + firestore, + if (key == null) null else DocumentReference(key, firestore), + data, + version + ) ) } @@ -84,12 +152,26 @@ internal constructor( } } -class PipelineSource(private val firestore: FirebaseFirestore) { +class PipelineSource internal constructor(private val firestore: FirebaseFirestore) { fun collection(path: String): Pipeline { - return Pipeline(firestore, CollectionSource(path)) + // Validate path by converting to CollectionReference + return collection(firestore.collection(path)) + } + + fun collection(ref: CollectionReference): Pipeline { + if (ref.firestore.databaseId != firestore.databaseId) { + throw IllegalArgumentException( + "Provided collection reference is from a different Firestore instance." + ) + } + return Pipeline(firestore, CollectionSource(ref.path)) } fun collectionGroup(collectionId: String): Pipeline { + Preconditions.checkNotNull(collectionId, "Provided collection ID must not be null.") + require(!collectionId.contains("/")) { + "Invalid collectionId '$collectionId'. Collection IDs must not contain '/'." + } return Pipeline(firestore, CollectionGroupSource(collectionId)) } @@ -97,23 +179,49 @@ class PipelineSource(private val firestore: FirebaseFirestore) { return Pipeline(firestore, DatabaseSource()) } + fun documents(vararg documents: String): Pipeline { + // Validate document path by converting to DocumentReference + return documents(*documents.map(firestore::document).toTypedArray()) + } + fun documents(vararg documents: DocumentReference): Pipeline { - return Pipeline(firestore, DocumentsSource(documents)) + val databaseId = firestore.databaseId + for (document in documents) { + if (document.firestore.databaseId != databaseId) { + throw IllegalArgumentException( + "Provided document reference is from a different Firestore instance." + ) + } + } + return Pipeline( + firestore, + DocumentsSource(documents.map { docRef -> "/" + docRef.path }.toTypedArray()) + ) } } class PipelineSnapshot -internal constructor( - private val executionTime: SnapshotVersion, - private val results: List -) +internal constructor(private val executionTime: SnapshotVersion, val results: List) class PipelineResult internal constructor( - private val key: DocumentReference?, + private val firestore: FirebaseFirestore, + val ref: DocumentReference?, private val fields: Map, private val version: SnapshotVersion, -) +) { + + fun getData(): Map { + return userDataWriter().convertObject(fields) + } + + private fun userDataWriter(): UserDataWriter = + UserDataWriter(firestore, DocumentSnapshot.ServerTimestampBehavior.DEFAULT) + + override fun toString(): String { + return "PipelineResult{ref=$ref, version=$version}, data=${getData()}" + } +} internal interface PipelineResultObserver { fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java index 297479d0262..8079a30cf57 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java @@ -19,7 +19,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; -import com.google.firebase.Timestamp; import com.google.firebase.firestore.FieldValue.ArrayRemoveFieldValue; import com.google.firebase.firestore.FieldValue.ArrayUnionFieldValue; import com.google.firebase.firestore.FieldValue.DeleteFieldValue; @@ -44,9 +43,7 @@ import com.google.firestore.v1.MapValue; import com.google.firestore.v1.Value; import com.google.protobuf.NullValue; -import com.google.type.LatLng; import java.util.ArrayList; -import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -387,92 +384,18 @@ private void parseSentinelFieldValue( * @return The parsed value, or {@code null} if the value was a FieldValue sentinel that should * not be included in the resulting parsed data. */ - private Value parseScalarValue(Object input, ParseContext context) { - if (input == null) { - return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); - } else if (input instanceof Integer) { - return Value.newBuilder().setIntegerValue((Integer) input).build(); - } else if (input instanceof Long) { - return Value.newBuilder().setIntegerValue((Long) input).build(); - } else if (input instanceof Float) { - return Value.newBuilder().setDoubleValue(((Float) input).doubleValue()).build(); - } else if (input instanceof Double) { - return Value.newBuilder().setDoubleValue((Double) input).build(); - } else if (input instanceof Boolean) { - return Value.newBuilder().setBooleanValue((Boolean) input).build(); - } else if (input instanceof String) { - return Value.newBuilder().setStringValue((String) input).build(); - } else if (input instanceof Date) { - Timestamp timestamp = new Timestamp((Date) input); - return parseTimestamp(timestamp); - } else if (input instanceof Timestamp) { - Timestamp timestamp = (Timestamp) input; - return parseTimestamp(timestamp); - } else if (input instanceof GeoPoint) { - GeoPoint geoPoint = (GeoPoint) input; - return Value.newBuilder() - .setGeoPointValue( - LatLng.newBuilder() - .setLatitude(geoPoint.getLatitude()) - .setLongitude(geoPoint.getLongitude())) - .build(); - } else if (input instanceof Blob) { - return Value.newBuilder().setBytesValue(((Blob) input).toByteString()).build(); - } else if (input instanceof DocumentReference) { - DocumentReference ref = (DocumentReference) input; - // TODO: Rework once pre-converter is ported to Android. - if (ref.getFirestore() != null) { - DatabaseId otherDb = ref.getFirestore().getDatabaseId(); - if (!otherDb.equals(databaseId)) { - throw context.createError( - String.format( - "Document reference is for database %s/%s but should be for database %s/%s", - otherDb.getProjectId(), - otherDb.getDatabaseId(), - databaseId.getProjectId(), - databaseId.getDatabaseId())); - } - } - return Value.newBuilder() - .setReferenceValue( - String.format( - "projects/%s/databases/%s/documents/%s", - databaseId.getProjectId(), - databaseId.getDatabaseId(), - ((DocumentReference) input).getPath())) - .build(); - } else if (input instanceof VectorValue) { - return parseVectorValue(((VectorValue) input), context); - } else if (input.getClass().isArray()) { + public Value parseScalarValue(Object input, ParseContext context) { + if (input.getClass().isArray()) { throw context.createError("Arrays are not supported; use a List instead"); } else { - throw context.createError("Unsupported type: " + Util.typeName(input)); + try { + return Values.encodeAnyValue(input); + } catch (IllegalArgumentException e) { + throw context.createError("Unsupported type: " + Util.typeName(input)); + } } } - private Value parseVectorValue(VectorValue vector, ParseContext context) { - MapValue.Builder mapBuilder = MapValue.newBuilder(); - - mapBuilder.putFields(Values.TYPE_KEY, Values.VECTOR_VALUE_TYPE); - mapBuilder.putFields(Values.VECTOR_MAP_VECTORS_KEY, parseData(vector.toList(), context)); - - return Value.newBuilder().setMapValue(mapBuilder).build(); - } - - private Value parseTimestamp(Timestamp timestamp) { - // Firestore backend truncates precision down to microseconds. To ensure offline mode works - // the same with regards to truncation, perform the truncation immediately without waiting for - // the backend to do that. - int truncatedNanoseconds = timestamp.getNanoseconds() / 1000 * 1000; - - return Value.newBuilder() - .setTimestampValue( - com.google.protobuf.Timestamp.newBuilder() - .setSeconds(timestamp.getSeconds()) - .setNanos(truncatedNanoseconds)) - .build(); - } - private List parseArrayTransformElements(List elements) { ParseAccumulator accumulator = new ParseAccumulator(UserData.Source.Argument); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/VectorValue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/VectorValue.java index 2f355648376..efcefd45bf4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/VectorValue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/VectorValue.java @@ -16,9 +16,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; /** * Represent a vector type in Firestore documents. @@ -41,21 +39,6 @@ public double[] toArray() { return this.values.clone(); } - /** - * Package private. - * Returns a representation of the vector as a List. - * - * @return A representation of the vector as an List - */ - @NonNull - List toList() { - ArrayList result = new ArrayList(this.values.length); - for (int i = 0; i < this.values.length; i++) { - result.add(i, this.values[i]); - } - return result; - } - /** * Returns true if this VectorValue is equal to the provided object. * diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ResourcePath.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ResourcePath.java index c96fcbdc3ee..776953d55d2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ResourcePath.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ResourcePath.java @@ -14,6 +14,7 @@ package com.google.firebase.firestore.model; +import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -36,7 +37,7 @@ public static ResourcePath fromSegments(List segments) { return segments.isEmpty() ? ResourcePath.EMPTY : new ResourcePath(segments); } - public static ResourcePath fromString(String path) { + public static ResourcePath fromString(@NonNull String path) { // NOTE: The client is ignorant of any path segments containing escape // sequences (for example, __id123__) and just passes them through raw (they exist // for legacy reasons and should not be used frequently). @@ -65,13 +66,6 @@ public String canonicalString() { // NOTE: The client is ignorant of any path segments containing escape // sequences (for example, __id123__) and just passes them through raw (they exist // for legacy reasons and should not be used frequently). - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < segments.size(); i++) { - if (i > 0) { - builder.append("/"); - } - builder.append(segments.get(i)); - } - return builder.toString(); + return String.join("/", segments); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 834fb2454a3..6504208f082 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -11,605 +11,683 @@ // 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.firebase.firestore.model; - -import static com.google.firebase.firestore.model.ServerTimestamps.getLocalWriteTime; -import static com.google.firebase.firestore.model.ServerTimestamps.isServerTimestamp; -import static com.google.firebase.firestore.util.Assert.fail; -import static com.google.firebase.firestore.util.Assert.hardAssert; - -import androidx.annotation.Nullable; -import com.google.firebase.firestore.util.Util; -import com.google.firestore.v1.ArrayValue; -import com.google.firestore.v1.ArrayValueOrBuilder; -import com.google.firestore.v1.MapValue; -import com.google.firestore.v1.Value; -import com.google.protobuf.ByteString; -import com.google.protobuf.NullValue; -import com.google.protobuf.Timestamp; -import com.google.type.LatLng; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -public class Values { - public static final String TYPE_KEY = "__type__"; - public static final Value NAN_VALUE = Value.newBuilder().setDoubleValue(Double.NaN).build(); - public static final Value NULL_VALUE = - Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); - public static final Value MIN_VALUE = NULL_VALUE; - public static final Value MAX_VALUE_TYPE = Value.newBuilder().setStringValue("__max__").build(); - public static final Value MAX_VALUE = - Value.newBuilder() - .setMapValue(MapValue.newBuilder().putFields(TYPE_KEY, MAX_VALUE_TYPE)) - .build(); - - public static final Value VECTOR_VALUE_TYPE = - Value.newBuilder().setStringValue("__vector__").build(); - public static final String VECTOR_MAP_VECTORS_KEY = "value"; - private static final Value MIN_VECTOR_VALUE = - Value.newBuilder() - .setMapValue( - MapValue.newBuilder() - .putFields(TYPE_KEY, VECTOR_VALUE_TYPE) - .putFields( - VECTOR_MAP_VECTORS_KEY, - Value.newBuilder().setArrayValue(ArrayValue.newBuilder()).build())) - .build(); +package com.google.firebase.firestore.model + +import com.google.firebase.firestore.Blob +import com.google.firebase.firestore.DocumentReference +import com.google.firebase.firestore.GeoPoint +import com.google.firebase.firestore.VectorValue +import com.google.firebase.firestore.util.Assert +import com.google.firebase.firestore.util.Util +import com.google.firestore.v1.ArrayValue +import com.google.firestore.v1.ArrayValueOrBuilder +import com.google.firestore.v1.MapValue +import com.google.firestore.v1.Value +import com.google.firestore.v1.Value.ValueTypeCase +import com.google.protobuf.ByteString +import com.google.protobuf.NullValue +import com.google.protobuf.Timestamp +import com.google.type.LatLng +import java.lang.Double.doubleToLongBits +import java.util.Date +import java.util.TreeMap +import kotlin.math.min + +internal object Values { + const val TYPE_KEY: String = "__type__" + @JvmField val NAN_VALUE: Value = Value.newBuilder().setDoubleValue(Double.NaN).build() + @JvmField val NULL_VALUE: Value = Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build() + @JvmField val MIN_VALUE: Value = NULL_VALUE + @JvmField val MAX_VALUE_TYPE: Value = Value.newBuilder().setStringValue("__max__").build() + @JvmField + val MAX_VALUE: Value = + Value.newBuilder() + .setMapValue(MapValue.newBuilder().putFields(TYPE_KEY, MAX_VALUE_TYPE)) + .build() + + @JvmField val VECTOR_VALUE_TYPE: Value = Value.newBuilder().setStringValue("__vector__").build() + const val VECTOR_MAP_VECTORS_KEY: String = "value" + private val MIN_VECTOR_VALUE: Value = + Value.newBuilder() + .setMapValue( + MapValue.newBuilder() + .putFields(TYPE_KEY, VECTOR_VALUE_TYPE) + .putFields( + VECTOR_MAP_VECTORS_KEY, + Value.newBuilder().setArrayValue(ArrayValue.newBuilder()).build() + ) + ) + .build() /** * The order of types in Firestore. This order is based on the backend's ordering, but modified to - * support server timestamps and {@link #MAX_VALUE}. + * support server timestamps and [.MAX_VALUE]. */ - public static final int TYPE_ORDER_NULL = 0; - - public static final int TYPE_ORDER_BOOLEAN = 1; - public static final int TYPE_ORDER_NUMBER = 2; - public static final int TYPE_ORDER_TIMESTAMP = 3; - public static final int TYPE_ORDER_SERVER_TIMESTAMP = 4; - public static final int TYPE_ORDER_STRING = 5; - public static final int TYPE_ORDER_BLOB = 6; - public static final int TYPE_ORDER_REFERENCE = 7; - public static final int TYPE_ORDER_GEOPOINT = 8; - public static final int TYPE_ORDER_ARRAY = 9; - public static final int TYPE_ORDER_VECTOR = 10; - public static final int TYPE_ORDER_MAP = 11; - - public static final int TYPE_ORDER_MAX_VALUE = Integer.MAX_VALUE; + const val TYPE_ORDER_NULL: Int = 0 + + const val TYPE_ORDER_BOOLEAN: Int = 1 + const val TYPE_ORDER_NUMBER: Int = 2 + const val TYPE_ORDER_TIMESTAMP: Int = 3 + const val TYPE_ORDER_SERVER_TIMESTAMP: Int = 4 + const val TYPE_ORDER_STRING: Int = 5 + const val TYPE_ORDER_BLOB: Int = 6 + const val TYPE_ORDER_REFERENCE: Int = 7 + const val TYPE_ORDER_GEOPOINT: Int = 8 + const val TYPE_ORDER_ARRAY: Int = 9 + const val TYPE_ORDER_VECTOR: Int = 10 + const val TYPE_ORDER_MAP: Int = 11 + + const val TYPE_ORDER_MAX_VALUE: Int = Int.MAX_VALUE /** Returns the backend's type order of the given Value type. */ - public static int typeOrder(Value value) { - switch (value.getValueTypeCase()) { - case NULL_VALUE: - return TYPE_ORDER_NULL; - case BOOLEAN_VALUE: - return TYPE_ORDER_BOOLEAN; - case INTEGER_VALUE: - return TYPE_ORDER_NUMBER; - case DOUBLE_VALUE: - return TYPE_ORDER_NUMBER; - case TIMESTAMP_VALUE: - return TYPE_ORDER_TIMESTAMP; - case STRING_VALUE: - return TYPE_ORDER_STRING; - case BYTES_VALUE: - return TYPE_ORDER_BLOB; - case REFERENCE_VALUE: - return TYPE_ORDER_REFERENCE; - case GEO_POINT_VALUE: - return TYPE_ORDER_GEOPOINT; - case ARRAY_VALUE: - return TYPE_ORDER_ARRAY; - case MAP_VALUE: - if (isServerTimestamp(value)) { - return TYPE_ORDER_SERVER_TIMESTAMP; + @JvmStatic + fun typeOrder(value: Value): Int { + return when (value.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> TYPE_ORDER_NULL + ValueTypeCase.BOOLEAN_VALUE -> TYPE_ORDER_BOOLEAN + ValueTypeCase.INTEGER_VALUE -> TYPE_ORDER_NUMBER + ValueTypeCase.DOUBLE_VALUE -> TYPE_ORDER_NUMBER + ValueTypeCase.TIMESTAMP_VALUE -> TYPE_ORDER_TIMESTAMP + ValueTypeCase.STRING_VALUE -> TYPE_ORDER_STRING + ValueTypeCase.BYTES_VALUE -> TYPE_ORDER_BLOB + ValueTypeCase.REFERENCE_VALUE -> TYPE_ORDER_REFERENCE + ValueTypeCase.GEO_POINT_VALUE -> TYPE_ORDER_GEOPOINT + ValueTypeCase.ARRAY_VALUE -> TYPE_ORDER_ARRAY + ValueTypeCase.MAP_VALUE -> + if (ServerTimestamps.isServerTimestamp(value)) { + TYPE_ORDER_SERVER_TIMESTAMP } else if (isMaxValue(value)) { - return TYPE_ORDER_MAX_VALUE; + TYPE_ORDER_MAX_VALUE } else if (isVectorValue(value)) { - return TYPE_ORDER_VECTOR; + TYPE_ORDER_VECTOR } else { - return TYPE_ORDER_MAP; + TYPE_ORDER_MAP } - default: - throw fail("Invalid value type: " + value.getValueTypeCase()); + else -> throw Assert.fail("Invalid value type: " + value.valueTypeCase) } } - public static boolean equals(Value left, Value right) { - if (left == right) { - return true; + @JvmStatic + fun equals(left: Value?, right: Value?): Boolean { + if (left === right) { + return true } if (left == null || right == null) { - return false; + return false } - int leftType = typeOrder(left); - int rightType = typeOrder(right); + val leftType = typeOrder(left) + val rightType = typeOrder(right) if (leftType != rightType) { - return false; + return false } - switch (leftType) { - case TYPE_ORDER_NUMBER: - return numberEquals(left, right); - case TYPE_ORDER_ARRAY: - return arrayEquals(left, right); - case TYPE_ORDER_VECTOR: - case TYPE_ORDER_MAP: - return objectEquals(left, right); - case TYPE_ORDER_SERVER_TIMESTAMP: - return getLocalWriteTime(left).equals(getLocalWriteTime(right)); - case TYPE_ORDER_MAX_VALUE: - return true; - default: - return left.equals(right); + return when (leftType) { + TYPE_ORDER_NUMBER -> numberEquals(left, right) + TYPE_ORDER_ARRAY -> arrayEquals(left, right) + TYPE_ORDER_VECTOR, + TYPE_ORDER_MAP -> objectEquals(left, right) + TYPE_ORDER_SERVER_TIMESTAMP -> + ServerTimestamps.getLocalWriteTime(left) == ServerTimestamps.getLocalWriteTime(right) + TYPE_ORDER_MAX_VALUE -> true + else -> left == right } } - private static boolean numberEquals(Value left, Value right) { - if (left.getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE - && right.getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE) { - return left.getIntegerValue() == right.getIntegerValue(); - } else if (left.getValueTypeCase() == Value.ValueTypeCase.DOUBLE_VALUE - && right.getValueTypeCase() == Value.ValueTypeCase.DOUBLE_VALUE) { - return Double.doubleToLongBits(left.getDoubleValue()) - == Double.doubleToLongBits(right.getDoubleValue()); + private fun numberEquals(left: Value, right: Value): Boolean { + if (left.valueTypeCase != right.valueTypeCase) { + return false + } + return when (left.valueTypeCase) { + ValueTypeCase.INTEGER_VALUE -> left.integerValue == right.integerValue + ValueTypeCase.DOUBLE_VALUE -> + doubleToLongBits(left.doubleValue) == doubleToLongBits(right.doubleValue) + else -> false } - - return false; } - private static boolean arrayEquals(Value left, Value right) { - ArrayValue leftArray = left.getArrayValue(); - ArrayValue rightArray = right.getArrayValue(); + private fun arrayEquals(left: Value, right: Value): Boolean { + val leftArray = left.arrayValue + val rightArray = right.arrayValue - if (leftArray.getValuesCount() != rightArray.getValuesCount()) { - return false; + if (leftArray.valuesCount != rightArray.valuesCount) { + return false } - for (int i = 0; i < leftArray.getValuesCount(); ++i) { + for (i in 0 until leftArray.valuesCount) { if (!equals(leftArray.getValues(i), rightArray.getValues(i))) { - return false; + return false } } - return true; + return true } - private static boolean objectEquals(Value left, Value right) { - MapValue leftMap = left.getMapValue(); - MapValue rightMap = right.getMapValue(); + private fun objectEquals(left: Value, right: Value): Boolean { + val leftMap = left.mapValue + val rightMap = right.mapValue - if (leftMap.getFieldsCount() != rightMap.getFieldsCount()) { - return false; + if (leftMap.fieldsCount != rightMap.fieldsCount) { + return false } - for (Map.Entry entry : leftMap.getFieldsMap().entrySet()) { - Value otherEntry = rightMap.getFieldsMap().get(entry.getKey()); - if (!equals(entry.getValue(), otherEntry)) { - return false; + for ((key, value) in leftMap.fieldsMap) { + val otherEntry = rightMap.fieldsMap[key] + if (!equals(value, otherEntry)) { + return false } } - return true; + return true } /** Returns true if the Value list contains the specified element. */ - public static boolean contains(ArrayValueOrBuilder haystack, Value needle) { - for (Value haystackElement : haystack.getValuesList()) { + @JvmStatic + fun contains(haystack: ArrayValueOrBuilder, needle: Value?): Boolean { + for (haystackElement in haystack.valuesList) { if (equals(haystackElement, needle)) { - return true; + return true } } - return false; + return false } - public static int compare(Value left, Value right) { - int leftType = typeOrder(left); - int rightType = typeOrder(right); + @JvmStatic + fun compare(left: Value, right: Value): Int { + val leftType = typeOrder(left) + val rightType = typeOrder(right) if (leftType != rightType) { - return Util.compareIntegers(leftType, rightType); - } - - switch (leftType) { - case TYPE_ORDER_NULL: - case TYPE_ORDER_MAX_VALUE: - return 0; - case TYPE_ORDER_BOOLEAN: - return Util.compareBooleans(left.getBooleanValue(), right.getBooleanValue()); - case TYPE_ORDER_NUMBER: - return compareNumbers(left, right); - case TYPE_ORDER_TIMESTAMP: - return compareTimestamps(left.getTimestampValue(), right.getTimestampValue()); - case TYPE_ORDER_SERVER_TIMESTAMP: - return compareTimestamps(getLocalWriteTime(left), getLocalWriteTime(right)); - case TYPE_ORDER_STRING: - return Util.compareUtf8Strings(left.getStringValue(), right.getStringValue()); - case TYPE_ORDER_BLOB: - return Util.compareByteStrings(left.getBytesValue(), right.getBytesValue()); - case TYPE_ORDER_REFERENCE: - return compareReferences(left.getReferenceValue(), right.getReferenceValue()); - case TYPE_ORDER_GEOPOINT: - return compareGeoPoints(left.getGeoPointValue(), right.getGeoPointValue()); - case TYPE_ORDER_ARRAY: - return compareArrays(left.getArrayValue(), right.getArrayValue()); - case TYPE_ORDER_MAP: - return compareMaps(left.getMapValue(), right.getMapValue()); - case TYPE_ORDER_VECTOR: - return compareVectors(left.getMapValue(), right.getMapValue()); - default: - throw fail("Invalid value type: " + leftType); - } - } - - public static int lowerBoundCompare( - Value left, boolean leftInclusive, Value right, boolean rightInclusive) { - int cmp = compare(left, right); + return Util.compareIntegers(leftType, rightType) + } + + return when (leftType) { + TYPE_ORDER_NULL, + TYPE_ORDER_MAX_VALUE -> 0 + TYPE_ORDER_BOOLEAN -> Util.compareBooleans(left.booleanValue, right.booleanValue) + TYPE_ORDER_NUMBER -> compareNumbers(left, right) + TYPE_ORDER_TIMESTAMP -> compareTimestamps(left.timestampValue, right.timestampValue) + TYPE_ORDER_SERVER_TIMESTAMP -> + compareTimestamps( + ServerTimestamps.getLocalWriteTime(left), + ServerTimestamps.getLocalWriteTime(right) + ) + TYPE_ORDER_STRING -> Util.compareUtf8Strings(left.stringValue, right.stringValue) + TYPE_ORDER_BLOB -> Util.compareByteStrings(left.bytesValue, right.bytesValue) + TYPE_ORDER_REFERENCE -> compareReferences(left.referenceValue, right.referenceValue) + TYPE_ORDER_GEOPOINT -> compareGeoPoints(left.geoPointValue, right.geoPointValue) + TYPE_ORDER_ARRAY -> compareArrays(left.arrayValue, right.arrayValue) + TYPE_ORDER_MAP -> compareMaps(left.mapValue, right.mapValue) + TYPE_ORDER_VECTOR -> compareVectors(left.mapValue, right.mapValue) + else -> throw Assert.fail("Invalid value type: $leftType") + } + } + + @JvmStatic + fun lowerBoundCompare( + left: Value, + leftInclusive: Boolean, + right: Value, + rightInclusive: Boolean + ): Int { + val cmp = compare(left, right) if (cmp != 0) { - return cmp; + return cmp } if (leftInclusive && !rightInclusive) { - return -1; + return -1 } else if (!leftInclusive && rightInclusive) { - return 1; + return 1 } - return 0; + return 0 } - public static int upperBoundCompare( - Value left, boolean leftInclusive, Value right, boolean rightInclusive) { - int cmp = compare(left, right); + @JvmStatic + fun upperBoundCompare( + left: Value, + leftInclusive: Boolean, + right: Value, + rightInclusive: Boolean + ): Int { + val cmp = compare(left, right) if (cmp != 0) { - return cmp; + return cmp } if (leftInclusive && !rightInclusive) { - return 1; + return 1 } else if (!leftInclusive && rightInclusive) { - return -1; + return -1 } - return 0; + return 0 } - private static int compareNumbers(Value left, Value right) { - if (left.getValueTypeCase() == Value.ValueTypeCase.DOUBLE_VALUE) { - double leftDouble = left.getDoubleValue(); - if (right.getValueTypeCase() == Value.ValueTypeCase.DOUBLE_VALUE) { - return Util.compareDoubles(leftDouble, right.getDoubleValue()); - } else if (right.getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE) { - return Util.compareMixed(leftDouble, right.getIntegerValue()); + private fun compareNumbers(left: Value, right: Value): Int { + if (left.valueTypeCase == ValueTypeCase.DOUBLE_VALUE) { + if (right.valueTypeCase == ValueTypeCase.DOUBLE_VALUE) { + return Util.compareDoubles(left.doubleValue, right.doubleValue) + } else if (right.valueTypeCase == ValueTypeCase.INTEGER_VALUE) { + return Util.compareMixed(left.doubleValue, right.integerValue) } - } else if (left.getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE) { - long leftLong = left.getIntegerValue(); - if (right.getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE) { - return Util.compareLongs(leftLong, right.getIntegerValue()); - } else if (right.getValueTypeCase() == Value.ValueTypeCase.DOUBLE_VALUE) { - return -1 * Util.compareMixed(right.getDoubleValue(), leftLong); + } else if (left.valueTypeCase == ValueTypeCase.INTEGER_VALUE) { + if (right.valueTypeCase == ValueTypeCase.INTEGER_VALUE) { + return Util.compareLongs(left.integerValue, right.integerValue) + } else if (right.valueTypeCase == ValueTypeCase.DOUBLE_VALUE) { + return -1 * Util.compareMixed(right.doubleValue, left.integerValue) } } - throw fail("Unexpected values: %s vs %s", left, right); + throw Assert.fail("Unexpected values: %s vs %s", left, right) } - private static int compareTimestamps(Timestamp left, Timestamp right) { - int cmp = Util.compareLongs(left.getSeconds(), right.getSeconds()); + private fun compareTimestamps(left: Timestamp, right: Timestamp): Int { + val cmp = Util.compareLongs(left.seconds, right.seconds) if (cmp != 0) { - return cmp; + return cmp } - return Util.compareIntegers(left.getNanos(), right.getNanos()); + return Util.compareIntegers(left.nanos, right.nanos) } - private static int compareReferences(String leftPath, String rightPath) { - String[] leftSegments = leftPath.split("/", -1); - String[] rightSegments = rightPath.split("/", -1); + private fun compareReferences(leftPath: String, rightPath: String): Int { + val leftSegments = leftPath.split("/".toRegex()).toTypedArray() + val rightSegments = rightPath.split("/".toRegex()).toTypedArray() - int minLength = Math.min(leftSegments.length, rightSegments.length); - for (int i = 0; i < minLength; i++) { - int cmp = leftSegments[i].compareTo(rightSegments[i]); + val minLength = min(leftSegments.size.toDouble(), rightSegments.size.toDouble()).toInt() + for (i in 0 until minLength) { + val cmp = leftSegments[i].compareTo(rightSegments[i]) if (cmp != 0) { - return cmp; + return cmp } } - return Util.compareIntegers(leftSegments.length, rightSegments.length); + return Util.compareIntegers(leftSegments.size, rightSegments.size) } - private static int compareGeoPoints(LatLng left, LatLng right) { - int comparison = Util.compareDoubles(left.getLatitude(), right.getLatitude()); + private fun compareGeoPoints(left: LatLng, right: LatLng): Int { + val comparison = Util.compareDoubles(left.latitude, right.latitude) if (comparison == 0) { - return Util.compareDoubles(left.getLongitude(), right.getLongitude()); + return Util.compareDoubles(left.longitude, right.longitude) } - return comparison; + return comparison } - private static int compareArrays(ArrayValue left, ArrayValue right) { - int minLength = Math.min(left.getValuesCount(), right.getValuesCount()); - for (int i = 0; i < minLength; i++) { - int cmp = compare(left.getValues(i), right.getValues(i)); + private fun compareArrays(left: ArrayValue, right: ArrayValue): Int { + val minLength = min(left.valuesCount.toDouble(), right.valuesCount.toDouble()).toInt() + for (i in 0 until minLength) { + val cmp = compare(left.getValues(i), right.getValues(i)) if (cmp != 0) { - return cmp; + return cmp } } - return Util.compareIntegers(left.getValuesCount(), right.getValuesCount()); + return Util.compareIntegers(left.valuesCount, right.valuesCount) } - private static int compareMaps(MapValue left, MapValue right) { - Iterator> iterator1 = - new TreeMap<>(left.getFieldsMap()).entrySet().iterator(); - Iterator> iterator2 = - new TreeMap<>(right.getFieldsMap()).entrySet().iterator(); + private fun compareMaps(left: MapValue, right: MapValue): Int { + val iterator1: Iterator> = TreeMap(left.fieldsMap).entries.iterator() + val iterator2: Iterator> = TreeMap(right.fieldsMap).entries.iterator() while (iterator1.hasNext() && iterator2.hasNext()) { - Map.Entry entry1 = iterator1.next(); - Map.Entry entry2 = iterator2.next(); - int keyCompare = Util.compareUtf8Strings(entry1.getKey(), entry2.getKey()); + val entry1 = iterator1.next() + val entry2 = iterator2.next() + val keyCompare = Util.compareUtf8Strings(entry1.key, entry2.key) if (keyCompare != 0) { - return keyCompare; + return keyCompare } - int valueCompare = compare(entry1.getValue(), entry2.getValue()); + val valueCompare = compare(entry1.value, entry2.value) if (valueCompare != 0) { - return valueCompare; + return valueCompare } } // Only equal if both iterators are exhausted. - return Util.compareBooleans(iterator1.hasNext(), iterator2.hasNext()); + return Util.compareBooleans(iterator1.hasNext(), iterator2.hasNext()) } - private static int compareVectors(MapValue left, MapValue right) { - Map leftMap = left.getFieldsMap(); - Map rightMap = right.getFieldsMap(); + private fun compareVectors(left: MapValue, right: MapValue): Int { + val leftMap = left.fieldsMap + val rightMap = right.fieldsMap // The vector is a map, but only vector value is compared. - ArrayValue leftArrayValue = leftMap.get(Values.VECTOR_MAP_VECTORS_KEY).getArrayValue(); - ArrayValue rightArrayValue = rightMap.get(Values.VECTOR_MAP_VECTORS_KEY).getArrayValue(); + val leftArrayValue = leftMap[VECTOR_MAP_VECTORS_KEY]!!.arrayValue + val rightArrayValue = rightMap[VECTOR_MAP_VECTORS_KEY]!!.arrayValue - int lengthCompare = - Util.compareIntegers(leftArrayValue.getValuesCount(), rightArrayValue.getValuesCount()); + val lengthCompare = + Util.compareIntegers(leftArrayValue.valuesCount, rightArrayValue.valuesCount) if (lengthCompare != 0) { - return lengthCompare; + return lengthCompare } - return compareArrays(leftArrayValue, rightArrayValue); + return compareArrays(leftArrayValue, rightArrayValue) } /** Generate the canonical ID for the provided field value (as used in Target serialization). */ - public static String canonicalId(Value value) { - StringBuilder builder = new StringBuilder(); - canonifyValue(builder, value); - return builder.toString(); - } - - private static void canonifyValue(StringBuilder builder, Value value) { - switch (value.getValueTypeCase()) { - case NULL_VALUE: - builder.append("null"); - break; - case BOOLEAN_VALUE: - builder.append(value.getBooleanValue()); - break; - case INTEGER_VALUE: - builder.append(value.getIntegerValue()); - break; - case DOUBLE_VALUE: - builder.append(value.getDoubleValue()); - break; - case TIMESTAMP_VALUE: - canonifyTimestamp(builder, value.getTimestampValue()); - break; - case STRING_VALUE: - builder.append(value.getStringValue()); - break; - case BYTES_VALUE: - builder.append(Util.toDebugString(value.getBytesValue())); - break; - case REFERENCE_VALUE: - canonifyReference(builder, value); - break; - case GEO_POINT_VALUE: - canonifyGeoPoint(builder, value.getGeoPointValue()); - break; - case ARRAY_VALUE: - canonifyArray(builder, value.getArrayValue()); - break; - case MAP_VALUE: - canonifyObject(builder, value.getMapValue()); - break; - default: - throw fail("Invalid value type: " + value.getValueTypeCase()); - } - } - - private static void canonifyTimestamp(StringBuilder builder, Timestamp timestamp) { - builder.append(String.format("time(%s,%s)", timestamp.getSeconds(), timestamp.getNanos())); - } - - private static void canonifyGeoPoint(StringBuilder builder, LatLng latLng) { - builder.append(String.format("geo(%s,%s)", latLng.getLatitude(), latLng.getLongitude())); - } - - private static void canonifyReference(StringBuilder builder, Value value) { - hardAssert(isReferenceValue(value), "Value should be a ReferenceValue"); - builder.append(DocumentKey.fromName(value.getReferenceValue())); - } - - private static void canonifyObject(StringBuilder builder, MapValue mapValue) { + @JvmStatic + fun canonicalId(value: Value): String { + val builder = StringBuilder() + canonifyValue(builder, value) + return builder.toString() + } + + private fun canonifyValue(builder: StringBuilder, value: Value) { + when (value.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> builder.append("null") + ValueTypeCase.BOOLEAN_VALUE -> builder.append(value.booleanValue) + ValueTypeCase.INTEGER_VALUE -> builder.append(value.integerValue) + ValueTypeCase.DOUBLE_VALUE -> builder.append(value.doubleValue) + ValueTypeCase.TIMESTAMP_VALUE -> canonifyTimestamp(builder, value.timestampValue) + ValueTypeCase.STRING_VALUE -> builder.append(value.stringValue) + ValueTypeCase.BYTES_VALUE -> builder.append(Util.toDebugString(value.bytesValue)) + ValueTypeCase.REFERENCE_VALUE -> canonifyReference(builder, value) + ValueTypeCase.GEO_POINT_VALUE -> canonifyGeoPoint(builder, value.geoPointValue) + ValueTypeCase.ARRAY_VALUE -> canonifyArray(builder, value.arrayValue) + ValueTypeCase.MAP_VALUE -> canonifyObject(builder, value.mapValue) + else -> throw Assert.fail("Invalid value type: " + value.valueTypeCase) + } + } + + private fun canonifyTimestamp(builder: StringBuilder, timestamp: Timestamp) { + builder.append(String.format("time(%s,%s)", timestamp.seconds, timestamp.nanos)) + } + + private fun canonifyGeoPoint(builder: StringBuilder, latLng: LatLng) { + builder.append(String.format("geo(%s,%s)", latLng.latitude, latLng.longitude)) + } + + private fun canonifyReference(builder: StringBuilder, value: Value) { + Assert.hardAssert(isReferenceValue(value), "Value should be a ReferenceValue") + builder.append(DocumentKey.fromName(value.referenceValue)) + } + + private fun canonifyObject(builder: StringBuilder, mapValue: MapValue) { // Even though MapValue are likely sorted correctly based on their insertion order (for example, // when received from the backend), local modifications can bring elements out of order. We need // to re-sort the elements to ensure that canonical IDs are independent of insertion order. - List keys = new ArrayList<>(mapValue.getFieldsMap().keySet()); - Collections.sort(keys); - - builder.append("{"); - boolean first = true; - for (String key : keys) { - if (!first) { - builder.append(","); - } else { - first = false; + val keys = ArrayList(mapValue.fieldsMap.keys) + keys.sort() + + builder.append("{") + val iterator = keys.iterator() + while (iterator.hasNext()) { + val key = iterator.next() + builder.append(key).append(":") + canonifyValue(builder, mapValue.getFieldsOrThrow(key)) + if (iterator.hasNext()) { + builder.append(",") } - builder.append(key).append(":"); - canonifyValue(builder, mapValue.getFieldsOrThrow(key)); } - builder.append("}"); + builder.append("}") } - private static void canonifyArray(StringBuilder builder, ArrayValue arrayValue) { - builder.append("["); - for (int i = 0; i < arrayValue.getValuesCount(); ++i) { - canonifyValue(builder, arrayValue.getValues(i)); - if (i != arrayValue.getValuesCount() - 1) { - builder.append(","); + private fun canonifyArray(builder: StringBuilder, arrayValue: ArrayValue) { + builder.append("[") + if (arrayValue.valuesCount > 0) { + canonifyValue(builder, arrayValue.getValues(0)) + for (i in 1 until arrayValue.valuesCount) { + builder.append(",") + canonifyValue(builder, arrayValue.getValues(i)) } } - builder.append("]"); + builder.append("]") } /** Returns true if `value` is a INTEGER_VALUE. */ - public static boolean isInteger(@Nullable Value value) { - return value != null && value.getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE; + @JvmStatic + fun isInteger(value: Value?): Boolean { + return value != null && value.valueTypeCase == ValueTypeCase.INTEGER_VALUE } /** Returns true if `value` is a DOUBLE_VALUE. */ - public static boolean isDouble(@Nullable Value value) { - return value != null && value.getValueTypeCase() == Value.ValueTypeCase.DOUBLE_VALUE; + @JvmStatic + fun isDouble(value: Value?): Boolean { + return value != null && value.valueTypeCase == ValueTypeCase.DOUBLE_VALUE } /** Returns true if `value` is either a INTEGER_VALUE or a DOUBLE_VALUE. */ - public static boolean isNumber(@Nullable Value value) { - return isInteger(value) || isDouble(value); + @JvmStatic + fun isNumber(value: Value?): Boolean { + return isInteger(value) || isDouble(value) } /** Returns true if `value` is an ARRAY_VALUE. */ - public static boolean isArray(@Nullable Value value) { - return value != null && value.getValueTypeCase() == Value.ValueTypeCase.ARRAY_VALUE; - } - - public static boolean isReferenceValue(@Nullable Value value) { - return value != null && value.getValueTypeCase() == Value.ValueTypeCase.REFERENCE_VALUE; + @JvmStatic + fun isArray(value: Value?): Boolean { + return value != null && value.valueTypeCase == ValueTypeCase.ARRAY_VALUE } - public static boolean isNullValue(@Nullable Value value) { - return value != null && value.getValueTypeCase() == Value.ValueTypeCase.NULL_VALUE; + @JvmStatic + fun isReferenceValue(value: Value?): Boolean { + return value != null && value.valueTypeCase == ValueTypeCase.REFERENCE_VALUE } - public static boolean isNanValue(@Nullable Value value) { - return value != null && Double.isNaN(value.getDoubleValue()); + @JvmStatic + fun isNullValue(value: Value?): Boolean { + return value != null && value.valueTypeCase == ValueTypeCase.NULL_VALUE } - public static boolean isMapValue(@Nullable Value value) { - return value != null && value.getValueTypeCase() == Value.ValueTypeCase.MAP_VALUE; + @JvmStatic + fun isNanValue(value: Value?): Boolean { + return value != null && java.lang.Double.isNaN(value.doubleValue) } - public static Value refValue(DatabaseId databaseId, DocumentKey key) { - Value value = - Value.newBuilder() - .setReferenceValue( - String.format( - "projects/%s/databases/%s/documents/%s", - databaseId.getProjectId(), databaseId.getDatabaseId(), key.toString())) - .build(); - return value; + @JvmStatic + fun isMapValue(value: Value?): Boolean { + return value != null && value.valueTypeCase == ValueTypeCase.MAP_VALUE } - public static Value MIN_BOOLEAN = Value.newBuilder().setBooleanValue(false).build(); - public static Value MIN_NUMBER = Value.newBuilder().setDoubleValue(Double.NaN).build(); - public static Value MIN_TIMESTAMP = + @JvmStatic + fun refValue(databaseId: DatabaseId, key: DocumentKey): Value { + val value = Value.newBuilder() - .setTimestampValue(Timestamp.newBuilder().setSeconds(Long.MIN_VALUE)) - .build(); - public static Value MIN_STRING = Value.newBuilder().setStringValue("").build(); - public static Value MIN_BYTES = Value.newBuilder().setBytesValue(ByteString.EMPTY).build(); - public static Value MIN_REFERENCE = refValue(DatabaseId.EMPTY, DocumentKey.empty()); - public static Value MIN_GEO_POINT = - Value.newBuilder() - .setGeoPointValue(LatLng.newBuilder().setLatitude(-90.0).setLongitude(-180.0)) - .build(); - public static Value MIN_ARRAY = - Value.newBuilder().setArrayValue(ArrayValue.getDefaultInstance()).build(); - public static Value MIN_MAP = - Value.newBuilder().setMapValue(MapValue.getDefaultInstance()).build(); + .setReferenceValue( + String.format( + "projects/%s/databases/%s/documents/%s", + databaseId.projectId, + databaseId.databaseId, + key.toString() + ) + ) + .build() + return value + } + + private val MIN_BOOLEAN: Value = Value.newBuilder().setBooleanValue(false).build() + private val MIN_NUMBER: Value = Value.newBuilder().setDoubleValue(Double.NaN).build() + private val MIN_TIMESTAMP: Value = + Value.newBuilder().setTimestampValue(Timestamp.newBuilder().setSeconds(Long.MIN_VALUE)).build() + private val MIN_STRING: Value = Value.newBuilder().setStringValue("").build() + private val MIN_BYTES: Value = Value.newBuilder().setBytesValue(ByteString.EMPTY).build() + private val MIN_REFERENCE: Value = refValue(DatabaseId.EMPTY, DocumentKey.empty()) + private val MIN_GEO_POINT: Value = + Value.newBuilder() + .setGeoPointValue(LatLng.newBuilder().setLatitude(-90.0).setLongitude(-180.0)) + .build() + private val MIN_ARRAY: Value = + Value.newBuilder().setArrayValue(ArrayValue.getDefaultInstance()).build() + private val MIN_MAP: Value = Value.newBuilder().setMapValue(MapValue.getDefaultInstance()).build() /** Returns the lowest value for the given value type (inclusive). */ - public static Value getLowerBound(Value value) { - switch (value.getValueTypeCase()) { - case NULL_VALUE: - return Values.NULL_VALUE; - case BOOLEAN_VALUE: - return MIN_BOOLEAN; - case INTEGER_VALUE: - case DOUBLE_VALUE: - return MIN_NUMBER; - case TIMESTAMP_VALUE: - return MIN_TIMESTAMP; - case STRING_VALUE: - return MIN_STRING; - case BYTES_VALUE: - return MIN_BYTES; - case REFERENCE_VALUE: - return MIN_REFERENCE; - case GEO_POINT_VALUE: - return MIN_GEO_POINT; - case ARRAY_VALUE: - return MIN_ARRAY; - case MAP_VALUE: - // VectorValue sorts after ArrayValue and before an empty MapValue - if (isVectorValue(value)) { - return MIN_VECTOR_VALUE; - } - return MIN_MAP; - default: - throw new IllegalArgumentException("Unknown value type: " + value.getValueTypeCase()); + @JvmStatic + fun getLowerBound(value: Value): Value { + return when (value.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> NULL_VALUE + ValueTypeCase.BOOLEAN_VALUE -> MIN_BOOLEAN + ValueTypeCase.INTEGER_VALUE, + ValueTypeCase.DOUBLE_VALUE -> MIN_NUMBER + ValueTypeCase.TIMESTAMP_VALUE -> MIN_TIMESTAMP + ValueTypeCase.STRING_VALUE -> MIN_STRING + ValueTypeCase.BYTES_VALUE -> MIN_BYTES + ValueTypeCase.REFERENCE_VALUE -> MIN_REFERENCE + ValueTypeCase.GEO_POINT_VALUE -> MIN_GEO_POINT + ValueTypeCase.ARRAY_VALUE -> MIN_ARRAY + // VectorValue sorts after ArrayValue and before an empty MapValue + ValueTypeCase.MAP_VALUE -> if (isVectorValue(value)) MIN_VECTOR_VALUE else MIN_MAP + else -> throw IllegalArgumentException("Unknown value type: " + value.valueTypeCase) } } /** Returns the largest value for the given value type (exclusive). */ - public static Value getUpperBound(Value value) { - switch (value.getValueTypeCase()) { - case NULL_VALUE: - return MIN_BOOLEAN; - case BOOLEAN_VALUE: - return MIN_NUMBER; - case INTEGER_VALUE: - case DOUBLE_VALUE: - return MIN_TIMESTAMP; - case TIMESTAMP_VALUE: - return MIN_STRING; - case STRING_VALUE: - return MIN_BYTES; - case BYTES_VALUE: - return MIN_REFERENCE; - case REFERENCE_VALUE: - return MIN_GEO_POINT; - case GEO_POINT_VALUE: - return MIN_ARRAY; - case ARRAY_VALUE: - return MIN_VECTOR_VALUE; - case MAP_VALUE: - // VectorValue sorts after ArrayValue and before an empty MapValue - if (isVectorValue(value)) { - return MIN_MAP; - } - return MAX_VALUE; - default: - throw new IllegalArgumentException("Unknown value type: " + value.getValueTypeCase()); + @JvmStatic + fun getUpperBound(value: Value): Value { + return when (value.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> MIN_BOOLEAN + ValueTypeCase.BOOLEAN_VALUE -> MIN_NUMBER + ValueTypeCase.INTEGER_VALUE, + ValueTypeCase.DOUBLE_VALUE -> MIN_TIMESTAMP + ValueTypeCase.TIMESTAMP_VALUE -> MIN_STRING + ValueTypeCase.STRING_VALUE -> MIN_BYTES + ValueTypeCase.BYTES_VALUE -> MIN_REFERENCE + ValueTypeCase.REFERENCE_VALUE -> MIN_GEO_POINT + ValueTypeCase.GEO_POINT_VALUE -> MIN_ARRAY + ValueTypeCase.ARRAY_VALUE -> MIN_VECTOR_VALUE + // VectorValue sorts after ArrayValue and before an empty MapValue + ValueTypeCase.MAP_VALUE -> if (isVectorValue(value)) MIN_MAP else MAX_VALUE + else -> throw IllegalArgumentException("Unknown value type: " + value.valueTypeCase) } } - /** Returns true if the Value represents the canonical {@link #MAX_VALUE} . */ - public static boolean isMaxValue(Value value) { - return MAX_VALUE_TYPE.equals(value.getMapValue().getFieldsMap().get(TYPE_KEY)); + /** Returns true if the Value represents the canonical [.MAX_VALUE] . */ + @JvmStatic + fun isMaxValue(value: Value): Boolean { + return MAX_VALUE_TYPE == value.mapValue.fieldsMap[TYPE_KEY] } /** Returns true if the Value represents a VectorValue . */ - public static boolean isVectorValue(Value value) { - return VECTOR_VALUE_TYPE.equals(value.getMapValue().getFieldsMap().get(TYPE_KEY)); + @JvmStatic + fun isVectorValue(value: Value): Boolean { + return VECTOR_VALUE_TYPE == value.mapValue.fieldsMap[TYPE_KEY] + } + + @JvmStatic + fun encodeValue(value: Long): Value { + return Value.newBuilder().setIntegerValue(value).build() + } + + @JvmStatic + fun encodeValue(value: Int): Value { + return Value.newBuilder().setIntegerValue(value.toLong()).build() + } + + @JvmStatic + fun encodeValue(value: Double): Value { + return Value.newBuilder().setDoubleValue(value).build() + } + + @JvmStatic + fun encodeValue(value: Float): Value { + return Value.newBuilder().setDoubleValue(value.toDouble()).build() + } + + @JvmStatic + fun encodeValue(value: Number): Value { + return when (value) { + is Long -> encodeValue(value) + is Int -> encodeValue(value) + is Double -> encodeValue(value) + is Float -> encodeValue(value) + else -> throw IllegalArgumentException("Unexpected number type: $value") + } + } + + @JvmStatic + fun encodeValue(value: String): Value { + return Value.newBuilder().setStringValue(value).build() + } + + @JvmStatic + fun encodeValue(date: Date): Value { + return encodeValue(com.google.firebase.Timestamp((date))) + } + + @JvmStatic + fun encodeValue(timestamp: com.google.firebase.Timestamp): Value { + // Firestore backend truncates precision down to microseconds. To ensure offline mode works + // the same with regards to truncation, perform the truncation immediately without waiting for + // the backend to do that. + val truncatedNanoseconds: Int = timestamp.nanoseconds / 1000 * 1000 + + return Value.newBuilder() + .setTimestampValue( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(timestamp.seconds) + .setNanos(truncatedNanoseconds) + ) + .build() + } + + @JvmStatic + fun encodeValue(value: Boolean): Value { + return Value.newBuilder().setBooleanValue(value).build() + } + + @JvmStatic + fun encodeValue(geoPoint: GeoPoint): Value { + return Value.newBuilder() + .setGeoPointValue( + LatLng.newBuilder().setLatitude(geoPoint.latitude).setLongitude(geoPoint.longitude) + ) + .build() + } + + @JvmStatic + fun encodeValue(value: Blob): Value { + return Value.newBuilder().setBytesValue(value.toByteString()).build() + } + + @JvmStatic + fun encodeValue(docRef: DocumentReference): Value { + val databaseId = docRef.firestore.databaseId + return Value.newBuilder() + .setReferenceValue( + String.format( + "projects/%s/databases/%s/documents/%s", + databaseId.projectId, + databaseId.databaseId, + docRef.path + ) + ) + .build() + } + + @JvmStatic + fun encodeValue(vector: VectorValue): Value { + return encodeVectorValue(vector.toArray()) + } + + @JvmStatic + fun encodeVectorValue(vector: DoubleArray): Value { + val listBuilder = ArrayValue.newBuilder() + for (value in vector) { + listBuilder.addValues(encodeValue(value)) + } + return Value.newBuilder() + .setMapValue( + MapValue.newBuilder() + .putFields(TYPE_KEY, VECTOR_VALUE_TYPE) + .putFields(VECTOR_MAP_VECTORS_KEY, Value.newBuilder().setArrayValue(listBuilder).build()) + ) + .build() + } + + @JvmStatic + fun encodeValue(map: Map): Value { + return Value.newBuilder().setMapValue(MapValue.newBuilder().putAllFields(map)).build() + } + + @JvmStatic + fun encodeAnyValue(value: Any?): Value { + return when (value) { + null -> NULL_VALUE + is String -> encodeValue(value) + is Number -> encodeValue(value) + is Date -> encodeValue(value) + is com.google.firebase.Timestamp -> encodeValue(value) + is Boolean -> encodeValue(value) + is GeoPoint -> encodeValue(value) + is Blob -> encodeValue(value) + is DocumentReference -> encodeValue(value) + else -> throw IllegalArgumentException("Unexpected type: $value") + } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt index 4ff2a25ee86..b584392fbc2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt @@ -3,13 +3,14 @@ package com.google.firebase.firestore.pipeline import com.google.firebase.Timestamp import com.google.firebase.firestore.Blob import com.google.firebase.firestore.DocumentReference -import com.google.firebase.firestore.FieldValue import com.google.firebase.firestore.GeoPoint import com.google.firebase.firestore.VectorValue +import com.google.firebase.firestore.model.Values +import com.google.firebase.firestore.model.Values.encodeValue import com.google.firestore.v1.Value import java.util.Date -class Constant internal constructor(val value: Any?) : Expr() { +class Constant internal constructor(val value: Value) : Expr() { companion object { fun of(value: Any): Constant { @@ -23,62 +24,72 @@ class Constant internal constructor(val value: Any?) : Expr() { is Blob -> of(value) is DocumentReference -> of(value) is Value -> of(value) - is Iterable<*> -> of(value) - is Map<*, *> -> of(value) else -> throw IllegalArgumentException("Unknown type: $value") } } + @JvmStatic fun of(value: String): Constant { - return Constant(value) + return Constant(encodeValue(value)) } + @JvmStatic fun of(value: Number): Constant { - return Constant(value) + return Constant(encodeValue(value)) } + @JvmStatic fun of(value: Date): Constant { - return Constant(value) + return Constant(encodeValue(value)) } + @JvmStatic fun of(value: Timestamp): Constant { - return Constant(value) + return Constant(encodeValue(value)) } + @JvmStatic fun of(value: Boolean): Constant { - return Constant(value) + return Constant(encodeValue(value)) } + @JvmStatic fun of(value: GeoPoint): Constant { - return Constant(value) + return Constant(encodeValue(value)) } + @JvmStatic fun of(value: Blob): Constant { - return Constant(value) + return Constant(encodeValue(value)) } + @JvmStatic fun of(value: DocumentReference): Constant { - return Constant(value) - } - - fun of(value: Value): Constant { - return Constant(value) + return Constant(encodeValue(value)) } + @JvmStatic fun of(value: VectorValue): Constant { - return Constant(value) + return Constant(encodeValue(value)) } + @JvmStatic fun nullValue(): Constant { - return Constant(null) + return Constant(Values.NULL_VALUE) } + @JvmStatic fun vector(value: DoubleArray): Constant { - return of(FieldValue.vector(value)) + return Constant(Values.encodeVectorValue(value)) } + @JvmStatic fun vector(value: VectorValue): Constant { - return of(value) + return Constant(encodeValue(value)) } } + + override fun toProto(): Value { + return value + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt new file mode 100644 index 00000000000..34bd8a9988c --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt @@ -0,0 +1,104 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firestore.v1.Value + +class AccumulatorWithAlias +internal constructor(internal val alias: String, internal val accumulator: Accumulator) + +open class Accumulator +protected constructor(private val name: String, private val params: Array) { + protected constructor( + name: String, + param: Expr? + ) : this(name, if (param == null) emptyArray() else arrayOf(param)) + + companion object { + @JvmStatic + fun countAll(): Count { + return Count(null) + } + + @JvmStatic + fun count(fieldName: String): Count { + return Count(fieldName) + } + + @JvmStatic + fun count(expr: Expr): Count { + return Count(expr) + } + + @JvmStatic + fun sum(fieldName: String): Accumulator { + return Sum(fieldName) + } + + @JvmStatic + fun sum(expr: Expr): Accumulator { + return Sum(expr) + } + + @JvmStatic + fun avg(fieldName: String): Accumulator { + return Avg(fieldName) + } + + @JvmStatic + fun avg(expr: Expr): Accumulator { + return Avg(expr) + } + + @JvmStatic + fun min(fieldName: String): Accumulator { + return min(fieldName) + } + + @JvmStatic + fun min(expr: Expr): Accumulator { + return min(expr) + } + + @JvmStatic + fun max(fieldName: String): Accumulator { + return Max(fieldName) + } + + @JvmStatic + fun max(expr: Expr): Accumulator { + return Max(expr) + } + } + + fun `as`(alias: String): AccumulatorWithAlias { + return AccumulatorWithAlias(alias, this) + } + + fun toProto(): Value { + val builder = com.google.firestore.v1.Function.newBuilder() + builder.setName(name) + for (param in params) { + builder.addArgs(param.toProto()) + } + return Value.newBuilder().setFunctionValue(builder).build() + } +} + +class Count(value: Expr?) : Accumulator("count", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Sum(value: Expr) : Accumulator("sum", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Avg(value: Expr) : Accumulator("avg", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Min(value: Expr) : Accumulator("min", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} + +class Max(value: Expr) : Accumulator("max", value) { + constructor(fieldName: String) : this(Field.of(fieldName)) +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt index b5449fc570b..3488d011a24 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt @@ -4,9 +4,14 @@ import com.google.firebase.firestore.FieldPath import com.google.firebase.firestore.VectorValue import com.google.firebase.firestore.model.DocumentKey import com.google.firebase.firestore.model.FieldPath as ModelFieldPath - -open class Expr protected constructor() { - companion object { +import com.google.firebase.firestore.model.Values.encodeValue +import com.google.firestore.v1.ArrayValue +import com.google.firestore.v1.MapValue +import com.google.firestore.v1.Value +import com.google.protobuf.Timestamp + +abstract class Expr protected constructor() { + internal companion object { internal fun toExprOrConstant(other: Any): Expr { return when (other) { is Expr -> other @@ -14,15 +19,33 @@ open class Expr protected constructor() { } } - internal fun toArrayOfExprOrConstant(others: Iterable): Array { - return others.map(::toExprOrConstant).toTypedArray() - } + internal fun toArrayOfExprOrConstant(others: Iterable): Array = + others.map(::toExprOrConstant).toTypedArray() - internal fun toArrayOfExprOrConstant(others: Array): Array { - return others.map(::toExprOrConstant).toTypedArray() - } + internal fun toArrayOfExprOrConstant(others: Array): Array = + others.map(::toExprOrConstant).toTypedArray() } + /** + * Assigns an alias to this expression. + * + *

Aliases are useful for renaming fields in the output of a stage or for giving meaningful + * names to calculated values. + * + *

Example: + * + *

{@code // Calculate the total price and assign it the alias "totalPrice" and add it to the
+   * output. firestore.pipeline().collection("items")
+   * .addFields(Field.of("price").multiply(Field.of("quantity")).as("totalPrice")); }
+ * + * @param alias The alias to assign to this expression. + * @return A new {@code Selectable} (typically an {@link ExprWithAlias}) that wraps this + * ``` + * expression and associates it with the provided alias. + * ``` + */ + open fun `as`(alias: String) = ExprWithAlias(alias, this) + /** * Creates an expression that this expression to another expression. * @@ -34,9 +57,7 @@ open class Expr protected constructor() { * @param other The expression to add to this expression. * @return A new {@code Expr} representing the addition operation. */ - fun add(other: Expr): Add { - return Add(this, other) - } + fun add(other: Expr) = Add(this, other) /** * Creates an expression that this expression to another expression. @@ -49,17 +70,171 @@ open class Expr protected constructor() { * @param other The constant value to add to this expression. * @return A new {@code Expr} representing the addition operation. */ - fun add(other: Any): Add { - return Add(this, toExprOrConstant(other)) - } + fun add(other: Any) = Add(this, other) - fun subtract(other: Expr): Subtract { - return Subtract(this, other) - } + fun subtract(other: Expr) = Subtract(this, other) + + fun subtract(other: Any) = Subtract(this, other) + + fun multiply(other: Expr) = Multiply(this, other) + + fun multiply(other: Any) = Multiply(this, other) + + fun divide(other: Expr) = Divide(this, other) + + fun divide(other: Any) = Divide(this, other) + + fun mod(other: Expr) = Mod(this, other) + + fun mod(other: Any) = Mod(this, other) + + fun `in`(values: List) = In(this, values) + + fun isNan() = IsNan(this) + + fun replaceFirst(find: Expr, replace: Expr) = ReplaceFirst(this, find, replace) + + fun replaceFirst(find: String, replace: String) = ReplaceFirst(this, find, replace) + + fun replaceAll(find: Expr, replace: Expr) = ReplaceAll(this, find, replace) + + fun replaceAll(find: String, replace: String) = ReplaceAll(this, find, replace) + + fun charLength() = CharLength(this) + + fun byteLength() = ByteLength(this) + + fun like(pattern: Expr) = Like(this, pattern) + + fun like(pattern: String) = Like(this, pattern) + + fun regexContains(pattern: Expr) = RegexContains(this, pattern) + + fun regexContains(pattern: String) = RegexContains(this, pattern) + + fun regexMatch(pattern: Expr) = RegexMatch(this, pattern) + + fun regexMatch(pattern: String) = RegexMatch(this, pattern) + + fun logicalMax(other: Expr) = LogicalMax(this, other) + + fun logicalMax(other: Any) = LogicalMax(this, other) + + fun logicalMin(other: Expr) = LogicalMin(this, other) + + fun logicalMin(other: Any) = LogicalMin(this, other) + + fun reverse() = Reverse(this) + + fun strContains(substring: Expr) = StrContains(this, substring) + + fun strContains(substring: String) = StrContains(this, substring) + + fun startsWith(prefix: Expr) = StartsWith(this, prefix) + + fun startsWith(prefix: String) = StartsWith(this, prefix) + + fun endsWith(suffix: Expr) = EndsWith(this, suffix) + + fun endsWith(suffix: String) = EndsWith(this, suffix) + + fun toLower() = ToLower(this) + + fun toUpper() = ToUpper(this) + + fun trim() = Trim(this) + + fun strConcat(vararg expr: Expr) = StrConcat(this, *expr) + + fun strConcat(string: String, vararg expr: Expr) = StrConcat(this, string, *expr) + + fun mapGet(key: Expr) = MapGet(this, key) + + fun mapGet(key: String) = MapGet(this, key) + + fun cosineDistance(vector: Expr) = CosineDistance(this, vector) + + fun cosineDistance(vector: DoubleArray) = CosineDistance(this, vector) + + fun cosineDistance(vector: VectorValue) = CosineDistance(this, vector) + + fun dotProduct(vector: Expr) = DotProduct(this, vector) + + fun dotProduct(vector: DoubleArray) = DotProduct(this, vector) + + fun dotProduct(vector: VectorValue) = DotProduct(this, vector) + + fun euclideanDistance(vector: Expr) = EuclideanDistance(this, vector) + + fun euclideanDistance(vector: DoubleArray) = EuclideanDistance(this, vector) + + fun euclideanDistance(vector: VectorValue) = EuclideanDistance(this, vector) + + fun vectorLength() = VectorLength(this) + + fun unixMicrosToTimestamp() = UnixMicrosToTimestamp(this) + + fun timestampToUnixMicros() = TimestampToUnixMicros(this) + + fun unixMillisToTimestamp() = UnixMillisToTimestamp(this) + + fun timestampToUnixMillis() = TimestampToUnixMillis(this) + + fun unixSecondsToTimestamp() = UnixSecondsToTimestamp(this) + + fun timestampToUnixSeconds() = TimestampToUnixSeconds(this) + + fun timestampAdd(unit: Expr, amount: Expr) = TimestampAdd(this, unit, amount) + + fun timestampAdd(unit: String, amount: Double) = TimestampAdd(this, unit, amount) + + fun timestampSub(unit: Expr, amount: Expr) = TimestampSub(this, unit, amount) + + fun timestampSub(unit: String, amount: Double) = TimestampSub(this, unit, amount) + + fun arrayConcat(vararg arrays: Expr) = ArrayConcat(this, *arrays) + + fun arrayConcat(arrays: List) = ArrayConcat(this, arrays) + + fun arrayReverse() = ArrayReverse(this) + + fun arrayContains(value: Expr) = ArrayContains(this, value) + + fun arrayContains(value: Any) = ArrayContains(this, value) + + fun arrayContainsAll(values: List) = ArrayContainsAll(this, values) + + fun arrayContainsAny(values: List) = ArrayContainsAny(this, values) + + fun arrayLength() = ArrayLength(this) + + fun sum() = Sum(this) + + fun avg() = Avg(this) + + fun min() = Min(this) + + fun max() = Max(this) + + fun ascending() = Ordering.ascending(this) + + fun descending() = Ordering.descending(this) + + internal abstract fun toProto(): Value +} + +abstract class Selectable(internal val alias: String) : Expr() + +open class ExprWithAlias internal constructor(alias: String, internal val expr: Expr) : + Selectable(alias) { + override fun toProto(): Value = expr.toProto() } -class Field private constructor(private val fieldPath: ModelFieldPath) : Expr() { +class Field private constructor(val fieldPath: ModelFieldPath) : + Selectable(fieldPath.canonicalString()) { companion object { + + @JvmStatic fun of(name: String): Field { if (name == DocumentKey.KEY_FIELD_NAME) { return Field(ModelFieldPath.KEY_PATH) @@ -67,6 +242,7 @@ class Field private constructor(private val fieldPath: ModelFieldPath) : Expr() return Field(FieldPath.fromDotSeparatedPath(name).internalPath) } + @JvmStatic fun of(fieldPath: FieldPath): Field { if (fieldPath == FieldPath.documentId()) { return Field(FieldPath.documentId().internalPath) @@ -74,12 +250,120 @@ class Field private constructor(private val fieldPath: ModelFieldPath) : Expr() return Field(fieldPath.internalPath) } } + override fun toProto() = + Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() } -class ListOfExpr(val expressions: Array) : Expr() +class ListOfExprs(val expressions: Array) : Expr() { + override fun toProto(): Value { + val builder = ArrayValue.newBuilder() + for (expr in expressions) { + builder.addValues(expr.toProto()) + } + return Value.newBuilder().setArrayValue(builder).build() + } +} open class Function protected constructor(private val name: String, private val params: Array) : Expr() { + companion object { + @JvmStatic + fun and(condition: BooleanExpr, vararg conditions: BooleanExpr) = And(condition, *conditions) + + @JvmStatic + fun or(condition: BooleanExpr, vararg conditions: BooleanExpr) = Or(condition, *conditions) + + @JvmStatic + fun xor(condition: BooleanExpr, vararg conditions: BooleanExpr) = Xor(condition, *conditions) + + @JvmStatic fun not(cond: BooleanExpr) = Not(cond) + + @JvmStatic fun eq(left: Expr, right: Expr) = Eq(left, right) + + @JvmStatic fun eq(left: Expr, right: Any) = Eq(left, right) + + @JvmStatic fun eq(fieldName: String, right: Expr) = Eq(fieldName, right) + + @JvmStatic fun eq(fieldName: String, right: Any) = Eq(fieldName, right) + + @JvmStatic fun neq(left: Expr, right: Expr) = Neq(left, right) + + @JvmStatic fun neq(left: Expr, right: Any) = Neq(left, right) + + @JvmStatic fun neq(fieldName: String, right: Expr) = Neq(fieldName, right) + + @JvmStatic fun neq(fieldName: String, right: Any) = Neq(fieldName, right) + + @JvmStatic fun gt(left: Expr, right: Expr) = Gt(left, right) + + @JvmStatic fun gt(left: Expr, right: Any) = Gt(left, right) + + @JvmStatic fun gt(fieldName: String, right: Expr) = Gt(fieldName, right) + + @JvmStatic fun gt(fieldName: String, right: Any) = Gt(fieldName, right) + + @JvmStatic fun gte(left: Expr, right: Expr) = Gte(left, right) + + @JvmStatic fun gte(left: Expr, right: Any) = Gte(left, right) + + @JvmStatic fun gte(fieldName: String, right: Expr) = Gte(fieldName, right) + + @JvmStatic fun gte(fieldName: String, right: Any) = Gte(fieldName, right) + + @JvmStatic fun lt(left: Expr, right: Expr) = Lt(left, right) + + @JvmStatic fun lt(left: Expr, right: Any) = Lt(left, right) + + @JvmStatic fun lt(fieldName: String, right: Expr) = Lt(fieldName, right) + + @JvmStatic fun lt(fieldName: String, right: Any) = Lt(fieldName, right) + + @JvmStatic fun lte(left: Expr, right: Expr) = Lte(left, right) + + @JvmStatic fun lte(left: Expr, right: Any) = Lte(left, right) + + @JvmStatic fun lte(fieldName: String, right: Expr) = Lte(fieldName, right) + + @JvmStatic fun lte(fieldName: String, right: Any) = Lte(fieldName, right) + + @JvmStatic fun arrayConcat(array: Expr, vararg arrays: Expr) = ArrayConcat(array, *arrays) + + @JvmStatic + fun arrayConcat(fieldName: String, vararg arrays: Expr) = ArrayConcat(fieldName, *arrays) + + @JvmStatic fun arrayConcat(array: Expr, arrays: List) = ArrayConcat(array, arrays) + + @JvmStatic + fun arrayConcat(fieldName: String, arrays: List) = ArrayConcat(fieldName, arrays) + + @JvmStatic fun arrayReverse(array: Expr) = ArrayReverse(array) + + @JvmStatic fun arrayReverse(fieldName: String) = ArrayReverse(fieldName) + + @JvmStatic fun arrayContains(array: Expr, value: Expr) = ArrayContains(array, value) + + @JvmStatic fun arrayContains(fieldName: String, value: Expr) = ArrayContains(fieldName, value) + + @JvmStatic fun arrayContains(array: Expr, value: Any) = ArrayContains(array, value) + + @JvmStatic fun arrayContains(fieldName: String, value: Any) = ArrayContains(fieldName, value) + + @JvmStatic + fun arrayContainsAll(array: Expr, values: List) = ArrayContainsAll(array, values) + + @JvmStatic + fun arrayContainsAll(fieldName: String, values: List) = ArrayContainsAll(fieldName, values) + + @JvmStatic + fun arrayContainsAny(array: Expr, values: List) = ArrayContainsAny(array, values) + + @JvmStatic + fun arrayContainsAny(fieldName: String, values: List) = ArrayContainsAny(fieldName, values) + + @JvmStatic fun arrayLength(array: Expr) = ArrayLength(array) + + @JvmStatic fun arrayLength(fieldName: String) = ArrayLength(fieldName) + } protected constructor(name: String, param1: Expr) : this(name, arrayOf(param1)) protected constructor( name: String, @@ -92,17 +376,56 @@ protected constructor(private val name: String, private val params: Array) : +open class BooleanExpr protected constructor(name: String, vararg params: Expr) : Function(name, params) { - protected constructor( - name: String, - param: Expr? - ) : this(name, if (param == null) emptyArray() else arrayOf(param)) + + fun and(vararg conditions: BooleanExpr): And = And(this, *conditions) + + fun or(vararg conditions: BooleanExpr): Or = Or(this, *conditions) + + fun xor(vararg conditions: BooleanExpr): Xor = Xor(this, *conditions) + + fun not(): Not = Not(this) +} + +class Ordering private constructor(private val expr: Expr, private val dir: Direction) { + companion object { + @JvmStatic fun ascending(expr: Expr): Ordering = Ordering(expr, Direction.ASCENDING) + + @JvmStatic + fun ascending(fieldName: String): Ordering = Ordering(Field.of(fieldName), Direction.ASCENDING) + + @JvmStatic fun descending(expr: Expr): Ordering = Ordering(expr, Direction.DESCENDING) + + @JvmStatic + fun descending(fieldName: String): Ordering = + Ordering(Field.of(fieldName), Direction.DESCENDING) + } + private class Direction private constructor(internal val proto: Value) { + private constructor(protoString: String) : this(encodeValue(protoString)) + companion object { + val ASCENDING = Direction("ascending") + val DESCENDING = Direction("descending") + } + } + internal fun toProto(): Value = + Value.newBuilder() + .setMapValue( + MapValue.newBuilder() + .putFields("direction", dir.proto) + .putFields("expression", expr.toProto()) + ) + .build() } class Add(left: Expr, right: Expr) : Function("add", left, right) { @@ -171,37 +494,37 @@ class Mod(left: Expr, right: Expr) : Function("mod", left, right) { // constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) // } -class Eq(left: Expr, right: Expr) : FilterCondition("eq", left, right) { +class Eq(left: Expr, right: Expr) : BooleanExpr("eq", left, right) { constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } -class Neq(left: Expr, right: Expr) : FilterCondition("neq", left, right) { +class Neq(left: Expr, right: Expr) : BooleanExpr("neq", left, right) { constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } -class Lt(left: Expr, right: Expr) : FilterCondition("lt", left, right) { +class Lt(left: Expr, right: Expr) : BooleanExpr("lt", left, right) { constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } -class Lte(left: Expr, right: Expr) : FilterCondition("lte", left, right) { +class Lte(left: Expr, right: Expr) : BooleanExpr("lte", left, right) { constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } -class Gt(left: Expr, right: Expr) : FilterCondition("gt", left, right) { +class Gt(left: Expr, right: Expr) : BooleanExpr("gt", left, right) { constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } -class Gte(left: Expr, right: Expr) : FilterCondition("gte", left, right) { +class Gte(left: Expr, right: Expr) : BooleanExpr("gte", left, right) { constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) @@ -209,7 +532,7 @@ class Gte(left: Expr, right: Expr) : FilterCondition("gte", left, right) { class ArrayConcat(array: Expr, vararg arrays: Expr) : Function("array_concat", arrayOf(array, *arrays)) { - constructor(array: Expr, arrays: List) : this(array, toExprOrConstant(arrays)) + constructor(array: Expr, arrays: List) : this(array, ListOfExprs(toArrayOfExprOrConstant(arrays))) constructor(fieldName: String, vararg arrays: Expr) : this(Field.of(fieldName), *arrays) constructor(fieldName: String, right: List) : this(Field.of(fieldName), right) } @@ -218,19 +541,19 @@ class ArrayReverse(array: Expr) : Function("array_reverse", array) { constructor(fieldName: String) : this(Field.of(fieldName)) } -class ArrayContains(array: Expr, value: Expr) : FilterCondition("array_contains", array, value) { +class ArrayContains(array: Expr, value: Expr) : BooleanExpr("array_contains", array, value) { constructor(array: Expr, right: Any) : this(array, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) } class ArrayContainsAll(array: Expr, values: List) : - FilterCondition("array_contains_all", array, ListOfExpr(toArrayOfExprOrConstant(values))) { + BooleanExpr("array_contains_all", array, ListOfExprs(toArrayOfExprOrConstant(values))) { constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) } class ArrayContainsAny(array: Expr, values: List) : - FilterCondition("array_contains_any", array, ListOfExpr(toArrayOfExprOrConstant(values))) { + BooleanExpr("array_contains_any", array, ListOfExprs(toArrayOfExprOrConstant(values))) { constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) } @@ -239,52 +562,30 @@ class ArrayLength(array: Expr) : Function("array_length", array) { } class In(array: Expr, values: List) : - FilterCondition("in", array, ListOfExpr(toArrayOfExprOrConstant(values))) { + BooleanExpr("in", array, ListOfExprs(toArrayOfExprOrConstant(values))) { constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) } -class IsNan(expr: Expr) : FilterCondition("is_nan", expr) { +class IsNan(expr: Expr) : BooleanExpr("is_nan", expr) { constructor(fieldName: String) : this(Field.of(fieldName)) } -class Exists(expr: Expr) : FilterCondition("exists", expr) { +class Exists(expr: Expr) : BooleanExpr("exists", expr) { constructor(fieldName: String) : this(Field.of(fieldName)) } -class Not(expr: Expr) : FilterCondition("not", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} +class Not(cond: BooleanExpr) : BooleanExpr("not", cond) -class And(condition: Expr, vararg conditions: Expr) : - FilterCondition("and", condition, *conditions) { - constructor( - condition: Expr, - vararg conditions: Any - ) : this(condition, *toArrayOfExprOrConstant(conditions)) - constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) - constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) -} +class And(condition: BooleanExpr, vararg conditions: BooleanExpr) : + BooleanExpr("and", condition, *conditions) -class Or(condition: Expr, vararg conditions: Expr) : FilterCondition("or", condition, *conditions) { - constructor( - condition: Expr, - vararg conditions: Any - ) : this(condition, *toArrayOfExprOrConstant(conditions)) - constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) - constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) -} +class Or(condition: BooleanExpr, vararg conditions: BooleanExpr) : + BooleanExpr("or", condition, *conditions) -class Xor(condition: Expr, vararg conditions: Expr) : - FilterCondition("xor", condition, *conditions) { - constructor( - condition: Expr, - vararg conditions: Any - ) : this(condition, *toArrayOfExprOrConstant(conditions)) - constructor(fieldName: String, vararg conditions: Expr) : this(Field.of(fieldName), *conditions) - constructor(fieldName: String, vararg conditions: Any) : this(Field.of(fieldName), *conditions) -} +class Xor(condition: BooleanExpr, vararg conditions: Expr) : + BooleanExpr("xor", condition, *conditions) -class If(condition: FilterCondition, thenExpr: Expr, elseExpr: Expr) : +class If(condition: BooleanExpr, thenExpr: Expr, elseExpr: Expr) : Function("if", condition, thenExpr, elseExpr) class LogicalMax(left: Expr, right: Expr) : Function("logical_max", left, right) { @@ -339,37 +640,37 @@ class ByteLength(value: Expr) : Function("byte_length", value) { constructor(fieldName: String) : this(Field.of(fieldName)) } -class Like(expr: Expr, pattern: Expr) : FilterCondition("like", expr, pattern) { +class Like(expr: Expr, pattern: Expr) : BooleanExpr("like", expr, pattern) { constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) } -class RegexContains(expr: Expr, pattern: Expr) : FilterCondition("regex_contains", expr, pattern) { +class RegexContains(expr: Expr, pattern: Expr) : BooleanExpr("regex_contains", expr, pattern) { constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) } -class RegexMatch(expr: Expr, pattern: Expr) : FilterCondition("regex_match", expr, pattern) { +class RegexMatch(expr: Expr, pattern: Expr) : BooleanExpr("regex_match", expr, pattern) { constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) } -class StrContains(expr: Expr, substring: Expr) : FilterCondition("str_contains", expr, substring) { +class StrContains(expr: Expr, substring: Expr) : BooleanExpr("str_contains", expr, substring) { constructor(expr: Expr, substring: String) : this(expr, Constant.of(substring)) constructor(fieldName: String, substring: Expr) : this(Field.of(fieldName), substring) constructor(fieldName: String, substring: String) : this(Field.of(fieldName), substring) } -class StartsWith(expr: Expr, prefix: Expr) : FilterCondition("starts_with", expr, prefix) { +class StartsWith(expr: Expr, prefix: Expr) : BooleanExpr("starts_with", expr, prefix) { constructor(expr: Expr, prefix: String) : this(expr, Constant.of(prefix)) constructor(fieldName: String, prefix: Expr) : this(Field.of(fieldName), prefix) constructor(fieldName: String, prefix: String) : this(Field.of(fieldName), prefix) } -class EndsWith(expr: Expr, suffix: Expr) : FilterCondition("ends_with", expr, suffix) { +class EndsWith(expr: Expr, suffix: Expr) : BooleanExpr("ends_with", expr, suffix) { constructor(expr: Expr, suffix: String) : this(expr, Constant.of(suffix)) constructor(fieldName: String, suffix: Expr) : this(Field.of(fieldName), suffix) constructor(fieldName: String, suffix: String) : this(Field.of(fieldName), suffix) @@ -391,34 +692,17 @@ class StrConcat internal constructor(first: Expr, vararg rest: Expr) : Function("str_concat", arrayOf(first, *rest)) { constructor( first: Expr, - vararg rest: String - ) : this(first, *rest.map(Constant::of).toTypedArray()) + second: String, + vararg rest: Expr + ) : this(first, Constant.of(second), *rest) constructor(fieldName: String, vararg rest: Expr) : this(Field.of(fieldName), *rest) - constructor(fieldName: String, vararg rest: String) : this(Field.of(fieldName), *rest) -} - -class MapGet(map: Expr, name: String) : Function("map_get", map, Constant.of(name)) { - constructor(fieldName: String, name: String) : this(Field.of(fieldName), name) -} - -class Count(value: Expr?) : Accumulator("count", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class Sum(value: Expr) : Accumulator("sum", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) + constructor(fieldName: String, second: String, vararg rest: Expr) : this(Field.of(fieldName), second, *rest) } -class Avg(value: Expr) : Accumulator("avg", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class Min(value: Expr) : Accumulator("min", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class Max(value: Expr) : Accumulator("max", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) +class MapGet(map: Expr, key: Expr) : Function("map_get", map, key) { + constructor(map: Expr, key: String) : this(map, Constant.of(key)) + constructor(fieldName: String, key: Expr) : this(Field.of(fieldName), key) + constructor(fieldName: String, key: String) : this(Field.of(fieldName), key) } class CosineDistance(vector1: Expr, vector2: Expr) : Function("cosine_distance", vector1, vector2) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index ce63d74389d..d1ed15d2ef7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -1,48 +1,182 @@ package com.google.firebase.firestore.pipeline -import com.google.firebase.firestore.DocumentReference +import com.google.common.collect.ImmutableMap +import com.google.firebase.firestore.model.Values.encodeValue +import com.google.firebase.firestore.model.Values.encodeVectorValue import com.google.firestore.v1.Pipeline import com.google.firestore.v1.Value -abstract class Stage(protected val name: String) { +abstract class Stage +internal constructor(private val name: String, private val options: Map) { + internal constructor(name: String) : this(name, emptyMap()) internal fun toProtoStage(): Pipeline.Stage { val builder = Pipeline.Stage.newBuilder() builder.setName(name) args().forEach { arg -> builder.addArgs(arg) } + builder.putAllOptions(options) return builder.build() } protected abstract fun args(): Sequence } class DatabaseSource : Stage("database") { - override fun args(): Sequence { - return emptySequence() - } + override fun args(): Sequence = emptySequence() } class CollectionSource internal constructor(path: String) : Stage("collection") { private val path: String = if (path.startsWith("/")) path else "/" + path - override fun args(): Sequence { - return sequenceOf(Value.newBuilder().setReferenceValue(path).build()) - } + override fun args(): Sequence = + sequenceOf(Value.newBuilder().setReferenceValue(path).build()) } class CollectionGroupSource internal constructor(val collectionId: String) : Stage("collection_group") { - override fun args(): Sequence { - return sequenceOf( - Value.newBuilder().setReferenceValue("").build(), - Value.newBuilder().setStringValue(collectionId).build() + override fun args(): Sequence = + sequenceOf(Value.newBuilder().setReferenceValue("").build(), encodeValue(collectionId)) +} + +class DocumentsSource internal constructor(private val documents: Array) : + Stage("documents") { + override fun args(): Sequence = documents.asSequence().map(::encodeValue) +} + +class AddFieldsStage internal constructor(private val fields: Array) : + Stage("add_fields") { + override fun args(): Sequence = + sequenceOf(encodeValue(fields.associate { it.alias to it.toProto() })) +} + +class AggregateStage +internal constructor( + private val accumulators: Map, + private val groups: Map +) : Stage("aggregate") { + internal constructor(accumulators: Map) : this(accumulators, emptyMap()) + companion object { + @JvmStatic + fun withAccumulators(vararg accumulators: AccumulatorWithAlias): AggregateStage { + if (accumulators.isEmpty()) { + throw IllegalArgumentException( + "Must specify at least one accumulator for aggregate() stage. There is a distinct() stage if only distinct group values are needed." + ) + } + return AggregateStage(accumulators.associate { it.alias to it.accumulator }) + } + } + + fun withGroups(vararg selectable: Selectable) = + AggregateStage(accumulators, selectable.associateBy(Selectable::alias)) + + fun withGroups(vararg fields: String) = + AggregateStage(accumulators, fields.associateWith(Field::of)) + + override fun args(): Sequence = + sequenceOf( + encodeValue(accumulators.mapValues { entry -> entry.value.toProto() }), + encodeValue(groups.mapValues { entry -> entry.value.toProto() }) ) +} + +class WhereStage internal constructor(private val condition: BooleanExpr) : Stage("where") { + override fun args(): Sequence = sequenceOf(condition.toProto()) +} + +class FindNearestStage +internal constructor( + private val property: Expr, + private val vector: DoubleArray, + private val distanceMeasure: DistanceMeasure, + private val options: FindNearestOptions +) : Stage("find_nearest", options.toProto()) { + + class DistanceMeasure private constructor(internal val proto: Value) { + private constructor(protoString: String) : this(encodeValue(protoString)) + companion object { + val EUCLIDEAN = DistanceMeasure("euclidean") + val COSINE = DistanceMeasure("cosine") + val DOT_PRODUCT = DistanceMeasure("dot_product") + } } + + override fun args(): Sequence = + sequenceOf(property.toProto(), encodeVectorValue(vector), distanceMeasure.proto) } -class DocumentsSource private constructor(private val documents: List) : - Stage("documents") { - internal constructor( - documents: Array - ) : this(documents.map { docRef -> "/" + docRef.path }) - override fun args(): Sequence { - return documents.asSequence().map { doc -> Value.newBuilder().setStringValue(doc).build() } +class FindNearestOptions +internal constructor(private val limit: Long?, private val distanceField: Field?) { + fun toProto(): Map { + val builder = ImmutableMap.builder() + if (limit != null) { + builder.put("limit", encodeValue(limit)) + } + if (distanceField != null) { + builder.put("distance_field", distanceField.toProto()) + } + return builder.build() } } + +class LimitStage internal constructor(private val limit: Long) : Stage("limit") { + override fun args(): Sequence = sequenceOf(encodeValue(limit)) +} + +class OffsetStage internal constructor(private val offset: Long) : Stage("offset") { + override fun args(): Sequence = sequenceOf(encodeValue(offset)) +} + +class SelectStage internal constructor(private val fields: Array) : + Stage("select") { + override fun args(): Sequence = + sequenceOf(encodeValue(fields.associate { it.alias to it.toProto() })) +} + +class SortStage internal constructor(private val orders: Array) : Stage("sort") { + override fun args(): Sequence = orders.asSequence().map(Ordering::toProto) +} + +class DistinctStage internal constructor(private val groups: Array) : + Stage("distinct") { + override fun args(): Sequence = + sequenceOf(encodeValue(groups.associate { it.alias to it.toProto() })) +} + +class RemoveFieldsStage internal constructor(private val fields: Array) : + Stage("remove_fields") { + override fun args(): Sequence = fields.asSequence().map(Field::toProto) +} + +class ReplaceStage internal constructor(private val field: Selectable, private val mode: Mode) : + Stage("replace") { + class Mode private constructor(internal val proto: Value) { + private constructor(protoString: String) : this(encodeValue(protoString)) + companion object { + val FULL_REPLACE = Mode("full_replace") + val MERGE_PREFER_NEXT = Mode("merge_prefer_nest") + val MERGE_PREFER_PARENT = Mode("merge_prefer_parent") + } + } + override fun args(): Sequence = sequenceOf(field.toProto(), mode.proto) +} + +class SampleStage internal constructor(private val size: Number, private val mode: Mode) : + Stage("sample") { + class Mode private constructor(internal val proto: Value) { + private constructor(protoString: String) : this(encodeValue(protoString)) + companion object { + val DOCUMENTS = Mode("documents") + val PERCENT = Mode("percent") + } + } + override fun args(): Sequence = sequenceOf(encodeValue(size), mode.proto) +} + +class UnionStage internal constructor(private val other: com.google.firebase.firestore.Pipeline) : + Stage("union") { + override fun args(): Sequence = + sequenceOf(Value.newBuilder().setPipelineValue(other.toPipelineProto()).build()) +} + +class UnnestStage internal constructor(private val selectable: Selectable) : Stage("unnest") { + override fun args(): Sequence = + sequenceOf(encodeValue(selectable.alias), selectable.toProto()) +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java index 8e7aab83565..91086531db1 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java @@ -21,6 +21,7 @@ import androidx.annotation.VisibleForTesting; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.common.base.Strings; import com.google.firebase.firestore.AggregateField; import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.PipelineResultObserver; @@ -255,7 +256,7 @@ public void onMessage(ExecutePipelineResponse message) { for (Document document : message.getResultsList()) { String documentName = document.getName(); observer.onDocument( - documentName == null ? null : serializer.decodeKey(documentName), + Strings.isNullOrEmpty(documentName) ? null : serializer.decodeKey(documentName), document.getFieldsMap(), serializer.decodeVersion(document.getUpdateTime())); } diff --git a/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/TestAccessHelper.java b/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/TestAccessHelper.java index bab88979493..2ace80dacef 100644 --- a/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/TestAccessHelper.java +++ b/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/TestAccessHelper.java @@ -14,14 +14,16 @@ package com.google.firebase.firestore; +import static org.mockito.Mockito.mock; + import com.google.firebase.firestore.model.DocumentKey; public final class TestAccessHelper { /** Makes the DocumentReference constructor accessible. */ public static DocumentReference createDocumentReference(DocumentKey documentKey) { - // We can use null here because the tests only use this as a wrapper for documentKeys. - return new DocumentReference(documentKey, null); + // We can use mock here because the tests only use this as a wrapper for documentKeys. + return new DocumentReference(documentKey, mock(FirebaseFirestore.class)); } /** Makes the getKey() method accessible. */ From e5734cd7f04034ed88a6273e9702c7361cf1aed3 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 14 Feb 2025 20:31:08 -0500 Subject: [PATCH 012/152] Copyright --- .../google/firebase/firestore/PipelineTest.java | 14 ++++++++++++++ .../java/com/google/firebase/firestore/Pipeline.kt | 14 ++++++++++++++ .../google/firebase/firestore/pipeline/Constant.kt | 14 ++++++++++++++ .../firebase/firestore/pipeline/accumulators.kt | 14 ++++++++++++++ .../firebase/firestore/pipeline/expression.kt | 14 ++++++++++++++ .../google/firebase/firestore/pipeline/stage.kt | 14 ++++++++++++++ .../firebase/firestore/pipeline/ExprTest.java | 14 -------------- .../firebase/firestore/pipeline/StageTest.java | 14 -------------- 8 files changed, 84 insertions(+), 28 deletions(-) delete mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java delete mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java 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 a88959dd930..ce701cd1167 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 @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore; import static com.google.common.truth.Truth.assertThat; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 4eb6ba5d1d6..941690eb372 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore import com.google.android.gms.tasks.Task diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt index b584392fbc2..fdcac5c59c3 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.firebase.Timestamp diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt index 34bd8a9988c..8098183c1c2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.firestore.v1.Value diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt index 3488d011a24..875a952e9a7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.firebase.firestore.FieldPath diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index d1ed15d2ef7..dd9c48ab5bf 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.common.collect.ImmutableMap diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java deleted file mode 100644 index 4468151e344..00000000000 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ExprTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.google.firebase.firestore.pipeline; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class ExprTest { - - @Test - public void test() {} -} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java deleted file mode 100644 index eb1b03119f4..00000000000 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StageTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.google.firebase.firestore.pipeline; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class StageTest { - - @Test - public void test() {} -} From 70bdf626e6de82d09c388dbb84719007dfa19b06 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 14 Feb 2025 20:45:13 -0500 Subject: [PATCH 013/152] spotless --- .../firebase/firestore/PipelineTest.java | 55 ++++++++++++------- .../firebase/firestore/pipeline/expression.kt | 12 +++- 2 files changed, 44 insertions(+), 23 deletions(-) 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 ce701cd1167..128ae39da02 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 @@ -37,7 +37,6 @@ import com.google.firebase.firestore.testutil.IntegrationTestUtil; import java.util.Map; import java.util.Objects; - import org.junit.After; import org.junit.Before; import org.junit.Ignore; @@ -47,22 +46,26 @@ @RunWith(AndroidJUnit4.class) public class PipelineTest { - private static final Correspondence> DATA_CORRESPONDENCE = Correspondence.from((result, expected) -> { - assertThat(result.getData()) - .comparingValuesUsing(Correspondence.from( - (x, y) -> { - if (x instanceof Long && y instanceof Integer) { - return (long) x == (long) (int) y; - } - return Objects.equals(x, y); - }, - "MapValueCompare" - )) - .containsExactlyEntriesIn(expected); - return true; - }, "GetData"); + private static final Correspondence> DATA_CORRESPONDENCE = + Correspondence.from( + (result, expected) -> { + assertThat(result.getData()) + .comparingValuesUsing( + Correspondence.from( + (x, y) -> { + if (x instanceof Long && y instanceof Integer) { + return (long) x == (long) (int) y; + } + return Objects.equals(x, y); + }, + "MapValueCompare")) + .containsExactlyEntriesIn(expected); + return true; + }, + "GetData"); - private static final Correspondence ID_CORRESPONDENCE = Correspondence.transforming(x -> x.getRef().getId(), "GetRefId"); + private static final Correspondence ID_CORRESPONDENCE = + Correspondence.transforming(x -> x.getRef().getId(), "GetRefId"); private CollectionReference randomCol; private FirebaseFirestore firestore; @@ -277,7 +280,12 @@ public void minAndMaxAccumulations() { @Test public void canSelectFields() { Task execute = - firestore.pipeline().collection(randomCol).select("title", "author").sort(Field.of("author").ascending()).execute(); + firestore + .pipeline() + .collection(randomCol) + .select("title", "author") + .sort(Field.of("author").ascending()) + .execute(); PipelineSnapshot snapshot = waitFor(execute); assertThat(snapshot.getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -426,13 +434,19 @@ public void arrayConcatWorks() { .pipeline() .collection(randomCol) .where(eq("title", "The Hitchhiker's Guide to the Galaxy")) - .select(Field.of("tags").arrayConcat(ImmutableList.of("newTag1", "newTag2")).as("modifiedTags")) + .select( + Field.of("tags") + .arrayConcat(ImmutableList.of("newTag1", "newTag2")) + .as("modifiedTags")) .limit(1) .execute(); PipelineSnapshot snapshot = waitFor(execute); assertThat(snapshot.getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) - .containsExactly(ImmutableMap.of("modifiedTags", ImmutableList.of("comedy", "space", "adventure", "newTag1", "newTag2"))); + .containsExactly( + ImmutableMap.of( + "modifiedTags", + ImmutableList.of("comedy", "space", "adventure", "newTag1", "newTag2"))); } @Test @@ -446,6 +460,7 @@ public void testStrConcat() { PipelineSnapshot snapshot = waitFor(execute); assertThat(snapshot.getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) - .containsExactly(ImmutableMap.of("bookInfo", "Douglas Adams - The Hitchhiker's Guide to the Galaxy")); + .containsExactly( + ImmutableMap.of("bookInfo", "Douglas Adams - The Hitchhiker's Guide to the Galaxy")); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt index 875a952e9a7..f0e473dabfc 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt @@ -22,7 +22,6 @@ import com.google.firebase.firestore.model.Values.encodeValue import com.google.firestore.v1.ArrayValue import com.google.firestore.v1.MapValue import com.google.firestore.v1.Value -import com.google.protobuf.Timestamp abstract class Expr protected constructor() { internal companion object { @@ -546,7 +545,10 @@ class Gte(left: Expr, right: Expr) : BooleanExpr("gte", left, right) { class ArrayConcat(array: Expr, vararg arrays: Expr) : Function("array_concat", arrayOf(array, *arrays)) { - constructor(array: Expr, arrays: List) : this(array, ListOfExprs(toArrayOfExprOrConstant(arrays))) + constructor( + array: Expr, + arrays: List + ) : this(array, ListOfExprs(toArrayOfExprOrConstant(arrays))) constructor(fieldName: String, vararg arrays: Expr) : this(Field.of(fieldName), *arrays) constructor(fieldName: String, right: List) : this(Field.of(fieldName), right) } @@ -710,7 +712,11 @@ class StrConcat internal constructor(first: Expr, vararg rest: Expr) : vararg rest: Expr ) : this(first, Constant.of(second), *rest) constructor(fieldName: String, vararg rest: Expr) : this(Field.of(fieldName), *rest) - constructor(fieldName: String, second: String, vararg rest: Expr) : this(Field.of(fieldName), second, *rest) + constructor( + fieldName: String, + second: String, + vararg rest: Expr + ) : this(Field.of(fieldName), second, *rest) } class MapGet(map: Expr, key: Expr) : Function("map_get", map, key) { From fc8b2d10d0ca2152abca91e859be5a4c157ce06e Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 14 Feb 2025 21:09:58 -0500 Subject: [PATCH 014/152] docStubs fix --- .../google/firebase/firestore/DocumentReference.java | 11 +++++++++++ .../google/firebase/firestore/FirebaseFirestore.java | 4 +--- .../com/google/firebase/firestore/model/Values.kt | 12 +----------- .../google/firebase/firestore/pipeline/expression.kt | 4 ++-- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java index 0161432843f..f31e3103060 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java @@ -22,6 +22,7 @@ import android.app.Activity; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; @@ -33,6 +34,7 @@ import com.google.firebase.firestore.core.UserData.ParsedSetData; import com.google.firebase.firestore.core.UserData.ParsedUpdateData; import com.google.firebase.firestore.core.ViewSnapshot; +import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.ResourcePath; @@ -118,6 +120,15 @@ public String getPath() { return key.getPath().canonicalString(); } + @RestrictTo(RestrictTo.Scope.LIBRARY) + @NonNull + public String getFullPath() { + DatabaseId databaseId = firestore.getDatabaseId(); + return String.format( + "projects/%s/databases/%s/documents/%s", + databaseId.getProjectId(), databaseId.getDatabaseId(), getPath()); + } + /** * Gets a {@code CollectionReference} instance that refers to the subcollection at the specified * path relative to this document. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 557471c9b3d..114fc18da95 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -23,7 +23,6 @@ import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; @@ -851,9 +850,8 @@ T callClient(Function call) { return clientProvider.call(call); } - @RestrictTo(RestrictTo.Scope.LIBRARY) @NonNull - public DatabaseId getDatabaseId() { + DatabaseId getDatabaseId() { return databaseId; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 6504208f082..603c8004422 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -637,17 +637,7 @@ internal object Values { @JvmStatic fun encodeValue(docRef: DocumentReference): Value { - val databaseId = docRef.firestore.databaseId - return Value.newBuilder() - .setReferenceValue( - String.format( - "projects/%s/databases/%s/documents/%s", - databaseId.projectId, - databaseId.databaseId, - docRef.path - ) - ) - .build() + return Value.newBuilder().setReferenceValue(docRef.fullPath).build() } @JvmStatic diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt index f0e473dabfc..e727576dbf2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt @@ -238,12 +238,12 @@ abstract class Expr protected constructor() { abstract class Selectable(internal val alias: String) : Expr() -open class ExprWithAlias internal constructor(alias: String, internal val expr: Expr) : +open class ExprWithAlias internal constructor(alias: String, private val expr: Expr) : Selectable(alias) { override fun toProto(): Value = expr.toProto() } -class Field private constructor(val fieldPath: ModelFieldPath) : +class Field private constructor(private val fieldPath: ModelFieldPath) : Selectable(fieldPath.canonicalString()) { companion object { From 1e7e72e5644022358ffe973dd2813741c2f1d462 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 17 Feb 2025 15:29:53 -0500 Subject: [PATCH 015/152] fix --- .../java/com/google/firebase/firestore/UserDataReader.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java index 8079a30cf57..b1462ed9f74 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java @@ -385,7 +385,9 @@ private void parseSentinelFieldValue( * not be included in the resulting parsed data. */ public Value parseScalarValue(Object input, ParseContext context) { - if (input.getClass().isArray()) { + if (input == null) { + return Values.NULL_VALUE; + } else if (input.getClass().isArray()) { throw context.createError("Arrays are not supported; use a List instead"); } else { try { From 35bcff7dd2c8a6063ad79769fbba0579b55adc4b Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 17 Feb 2025 16:16:37 -0500 Subject: [PATCH 016/152] fix --- .../main/java/com/google/firebase/firestore/model/Values.kt | 1 + .../com/google/firebase/firestore/TestAccessHelper.java | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 603c8004422..3b9701bc4ec 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -677,6 +677,7 @@ internal object Values { is GeoPoint -> encodeValue(value) is Blob -> encodeValue(value) is DocumentReference -> encodeValue(value) + is VectorValue -> encodeValue(value) else -> throw IllegalArgumentException("Unexpected type: $value") } } diff --git a/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/TestAccessHelper.java b/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/TestAccessHelper.java index 2ace80dacef..b62ec1b9933 100644 --- a/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/TestAccessHelper.java +++ b/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/TestAccessHelper.java @@ -15,7 +15,9 @@ package com.google.firebase.firestore; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.model.DocumentKey; public final class TestAccessHelper { @@ -23,7 +25,9 @@ public final class TestAccessHelper { /** Makes the DocumentReference constructor accessible. */ public static DocumentReference createDocumentReference(DocumentKey documentKey) { // We can use mock here because the tests only use this as a wrapper for documentKeys. - return new DocumentReference(documentKey, mock(FirebaseFirestore.class)); + FirebaseFirestore mock = mock(FirebaseFirestore.class); + when(mock.getDatabaseId()).thenReturn(DatabaseId.forProject("project")); + return new DocumentReference(documentKey, mock); } /** Makes the getKey() method accessible. */ From 3cb1886ab8fc56cee11cbc737aaf72dc72e45b74 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 18 Feb 2025 11:54:49 -0500 Subject: [PATCH 017/152] More tests --- .../firebase/firestore/PipelineTest.java | 286 ++++++++++-- .../firebase/firestore/pipeline/expression.kt | 407 ++++++++++++++++-- 2 files changed, 612 insertions(+), 81 deletions(-) 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 128ae39da02..04e71d64068 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 @@ -15,12 +15,21 @@ package com.google.firebase.firestore; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.firestore.pipeline.Function.add; import static com.google.firebase.firestore.pipeline.Function.and; import static com.google.firebase.firestore.pipeline.Function.arrayContains; import static com.google.firebase.firestore.pipeline.Function.arrayContainsAny; +import static com.google.firebase.firestore.pipeline.Function.endsWith; import static com.google.firebase.firestore.pipeline.Function.eq; import static com.google.firebase.firestore.pipeline.Function.gt; +import static com.google.firebase.firestore.pipeline.Function.lt; +import static com.google.firebase.firestore.pipeline.Function.lte; +import static com.google.firebase.firestore.pipeline.Function.neq; +import static com.google.firebase.firestore.pipeline.Function.not; import static com.google.firebase.firestore.pipeline.Function.or; +import static com.google.firebase.firestore.pipeline.Function.startsWith; +import static com.google.firebase.firestore.pipeline.Function.strConcat; +import static com.google.firebase.firestore.pipeline.Function.subtract; import static com.google.firebase.firestore.pipeline.Ordering.ascending; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor; import static java.util.Map.entry; @@ -56,6 +65,9 @@ public class PipelineTest { if (x instanceof Long && y instanceof Integer) { return (long) x == (long) (int) y; } + if (x instanceof Double && y instanceof Integer) { + return (double) x == (double) (int) y; + } return Objects.equals(x, y); }, "MapValueCompare")) @@ -189,15 +201,13 @@ public void setup() { public void emptyResults() { Task execute = firestore.pipeline().collection(randomCol.getPath()).limit(0).execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()).isEmpty(); + assertThat(waitFor(execute).getResults()).isEmpty(); } @Test public void fullResults() { Task execute = firestore.pipeline().collection(randomCol.getPath()).execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()).hasSize(10); + assertThat(waitFor(execute).getResults()).hasSize(10); } @Test @@ -208,8 +218,7 @@ public void aggregateResultsCountAll() { .collection(randomCol) .aggregate(Accumulator.countAll().as("count")) .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly(ImmutableMap.of("count", 10)); } @@ -227,8 +236,7 @@ public void aggregateResultsMany() { Accumulator.avg("rating").as("avgRating"), Field.of("rating").max().as("maxRating")) .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( ImmutableMap.ofEntries( @@ -241,15 +249,14 @@ public void groupAndAccumulateResults() { firestore .pipeline() .collection(randomCol) - .where(Function.lt(Field.of("published"), 1984)) + .where(lt(Field.of("published"), 1984)) .aggregate( AggregateStage.withAccumulators(Accumulator.avg("rating").as("avgRating")) .withGroups("genre")) - .where(Function.gt("avgRating", 4.3)) + .where(gt("avgRating", 4.3)) .sort(Field.of("avgRating").descending()) .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( ImmutableMap.ofEntries(entry("avgRating", 4.7), entry("genre", "Fantasy")), @@ -269,8 +276,7 @@ public void minAndMaxAccumulations() { Field.of("rating").max().as("maxRating"), Field.of("published").min().as("minPublished")) .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( ImmutableMap.ofEntries( @@ -286,8 +292,7 @@ public void canSelectFields() { .select("title", "author") .sort(Field.of("author").ascending()) .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( ImmutableMap.ofEntries( @@ -321,8 +326,7 @@ public void whereWithAnd() { .collection(randomCol) .where(and(gt("rating", 4.5), eq("genre", "Science Fiction"))) .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(ID_CORRESPONDENCE) .containsExactly("book10"); } @@ -336,8 +340,7 @@ public void whereWithOr() { .where(or(eq("genre", "Romance"), eq("genre", "Dystopian"))) .select("title") .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( ImmutableMap.of("title", "Pride and Prejudice"), @@ -356,8 +359,7 @@ public void offsetAndLimits() { .limit(3) .select("title", "author") .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( ImmutableMap.ofEntries(entry("title", "1984"), entry("author", "George Orwell")), @@ -376,8 +378,7 @@ public void arrayContainsWorks() { .where(arrayContains("tags", "comedy")) .select("title") .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly(ImmutableMap.of("title", "The Hitchhiker's Guide to the Galaxy")); } @@ -391,8 +392,7 @@ public void arrayContainsAnyWorks() { .where(arrayContainsAny("tags", ImmutableList.of("comedy", "classic"))) .select("title") .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( ImmutableMap.of("title", "The Hitchhiker's Guide to the Galaxy"), @@ -408,8 +408,7 @@ public void arrayContainsAllWorks() { .where(Field.of("tags").arrayContainsAll(ImmutableList.of("adventure", "magic"))) .select("title") .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly(ImmutableMap.of("title", "The Lord of the Rings")); } @@ -422,8 +421,7 @@ public void arrayLengthWorks() { .select(Field.of("tags").arrayLength().as("tagsCount")) .where(eq("tagsCount", 3)) .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()).hasSize(10); + assertThat(waitFor(execute).getResults()).hasSize(10); } @Test @@ -440,8 +438,7 @@ public void arrayConcatWorks() { .as("modifiedTags")) .limit(1) .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( ImmutableMap.of( @@ -457,10 +454,231 @@ public void testStrConcat() { .select(Field.of("author").strConcat(" - ", Field.of("title")).as("bookInfo")) .limit(1) .execute(); - PipelineSnapshot snapshot = waitFor(execute); - assertThat(snapshot.getResults()) + assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( ImmutableMap.of("bookInfo", "Douglas Adams - The Hitchhiker's Guide to the Galaxy")); } + + @Test + public void testStartsWith() { + Task execute = + randomCol + .pipeline() + .where(startsWith("title", "The")) + .select("title") + .sort(Field.of("title").ascending()) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of("title", "The Great Gatsby"), + ImmutableMap.of("title", "The Handmaid's Tale"), + ImmutableMap.of("title", "The Hitchhiker's Guide to the Galaxy"), + ImmutableMap.of("title", "The Lord of the Rings")); + } + + @Test + public void testEndsWith() { + Task execute = + randomCol + .pipeline() + .where(endsWith("title", "y")) + .select("title") + .sort(Field.of("title").descending()) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of("title", "The Hitchhiker's Guide to the Galaxy"), + ImmutableMap.of("title", "The Great Gatsby")); + } + + @Test + public void testLength() { + Task execute = + randomCol + .pipeline() + .select(Field.of("title").charLength().as("titleLength"), Field.of("title")) + .where(gt("titleLength", 20)) + .sort(Field.of("title").ascending()) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of("titleLength", 29, "title", "One Hundred Years of Solitude"), + ImmutableMap.of("titleLength", 36, "title", "The Hitchhiker's Guide to the Galaxy"), + ImmutableMap.of("titleLength", 21, "title", "The Lord of the Rings"), + ImmutableMap.of("titleLength", 21, "title", "To Kill a Mockingbird")); + } + + @Test + @Ignore("Not supported yet") + public void testToLowercase() { + Task execute = + randomCol + .pipeline() + .select(Field.of("title").toLower().as("lowercaseTitle")) + .limit(1) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly(ImmutableMap.of("lowercaseTitle", "the hitchhiker's guide to the galaxy")); + } + + @Test + @Ignore("Not supported yet") + public void testToUppercase() { + Task execute = + randomCol + .pipeline() + .select(Field.of("author").toLower().as("uppercaseAuthor")) + .limit(1) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly(ImmutableMap.of("uppercaseAuthor", "DOUGLAS ADAMS")); + } + + @Test + @Ignore("Not supported yet") + public void testTrim() { + Task execute = + randomCol + .pipeline() + .addFields(strConcat(" ", Field.of("title"), " ").as("spacedTitle")) + .select(Field.of("spacedTitle").trim().as("trimmedTitle")) + .limit(1) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of( + "spacedTitle", + " The Hitchhiker's Guide to the Galaxy ", + "trimmedTitle", + "The Hitchhiker's Guide to the Galaxy")); + } + + @Test + public void testLike() { + Task execute = + randomCol.pipeline().where(Function.like("title", "%Guide%")).select("title").execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly(ImmutableMap.of("title", "The Hitchhiker's Guide to the Galaxy")); + } + + @Test + public void testRegexContains() { + Task execute = + randomCol.pipeline().where(Function.regexContains("title", "(?i)(the|of)")).execute(); + assertThat(waitFor(execute).getResults()).hasSize(5); + } + + @Test + public void testRegexMatches() { + Task execute = + randomCol.pipeline().where(Function.regexContains("title", ".*(?i)(the|of).*")).execute(); + assertThat(waitFor(execute).getResults()).hasSize(5); + } + + @Test + public void testArithmeticOperations() { + Task execute = + randomCol + .pipeline() + .select( + add(Field.of("rating"), 1).as("ratingPlusOne"), + subtract(Field.of("published"), 1900).as("yearsSince1900"), + Field.of("rating").multiply(10).as("ratingTimesTen"), + Field.of("rating").divide(2).as("ratingDividedByTwo")) + .limit(1) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.ofEntries( + entry("ratingPlusOne", 5.2), + entry("yearsSince1900", 79), + entry("ratingTimesTen", 42), + entry("ratingDividedByTwo", 2.1))); + } + + @Test + public void testComparisonOperators() { + Task execute = + randomCol + .pipeline() + .where( + and( + gt("rating", 4.2), + lte(Field.of("rating"), 4.5), + neq("genre", "Science Function"))) + .select("rating", "title") + .sort(Field.of("title").ascending()) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of("rating", 4.3, "title", "Crime and Punishment"), + ImmutableMap.of("rating", 4.3, "title", "One Hundred Years of Solitude"), + ImmutableMap.of("rating", 4.5, "title", "Pride and Prejudice")); + } + + @Test + public void testLogicalOperators() { + Task execute = + randomCol + .pipeline() + .where( + or( + and(gt("rating", 4.5), eq("genre", "Science Fiction")), + lt(Field.of("published"), 1900))) + .select("title") + .sort(Field.of("title").ascending()) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of("title", "Crime and Punishment"), + ImmutableMap.of("title", "Dune"), + ImmutableMap.of("title", "Pride and Prejudice")); + } + + @Test + public void testChecks() { + Task execute = + randomCol + .pipeline() + .where(not(Field.of("rating").isNan())) + .select(Field.of("rating").eq(null).as("ratingIsNull")) + .select("title") + .sort(Field.of("title").ascending()) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of("title", "Crime and Punishment"), + ImmutableMap.of("title", "Dune"), + ImmutableMap.of("title", "Pride and Prejudice")); + } + + @Test + public void testMapGet() {} + + @Test + public void testParent() {} + + @Test + public void testCollectionId() {} + + @Test + public void testDistanceFunctions() {} + + @Test + public void testNestedFields() {} + + @Test + public void testMapGetWithFieldNameIncludingNotation() {} } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt index e727576dbf2..fa6bdda4b97 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt @@ -25,7 +25,10 @@ import com.google.firestore.v1.Value abstract class Expr protected constructor() { internal companion object { - internal fun toExprOrConstant(other: Any): Expr { + internal fun toExprOrConstant(other: Any?): Expr { + if (other == null) { + return Constant.nullValue() + } return when (other) { is Expr -> other else -> Constant.of(other) @@ -131,11 +134,11 @@ abstract class Expr protected constructor() { fun logicalMax(other: Expr) = LogicalMax(this, other) - fun logicalMax(other: Any) = LogicalMax(this, other) + fun logicalMax(other: Any?) = LogicalMax(this, other) fun logicalMin(other: Expr) = LogicalMin(this, other) - fun logicalMin(other: Any) = LogicalMin(this, other) + fun logicalMin(other: Any?) = LogicalMin(this, other) fun reverse() = Reverse(this) @@ -159,7 +162,9 @@ abstract class Expr protected constructor() { fun strConcat(vararg expr: Expr) = StrConcat(this, *expr) - fun strConcat(string: String, vararg expr: Expr) = StrConcat(this, string, *expr) + fun strConcat(vararg string: String) = StrConcat(this, *string) + + fun strConcat(vararg string: Any) = StrConcat(this, *string) fun mapGet(key: Expr) = MapGet(this, key) @@ -213,7 +218,7 @@ abstract class Expr protected constructor() { fun arrayContains(value: Expr) = ArrayContains(this, value) - fun arrayContains(value: Any) = ArrayContains(this, value) + fun arrayContains(value: Any?) = ArrayContains(this, value) fun arrayContainsAll(values: List) = ArrayContainsAll(this, values) @@ -233,6 +238,30 @@ abstract class Expr protected constructor() { fun descending() = Ordering.descending(this) + fun eq(other: Expr) = Eq(this, other) + + fun eq(other: Any?) = Eq(this, other) + + fun neq(other: Expr) = Neq(this, other) + + fun neq(other: Any?) = Neq(this, other) + + fun gt(other: Expr) = Gt(this, other) + + fun gt(other: Any?) = Gt(this, other) + + fun gte(other: Expr) = Gte(this, other) + + fun gte(other: Any?) = Gte(this, other) + + fun lt(other: Expr) = Lt(this, other) + + fun lt(other: Any?) = Lt(this, other) + + fun lte(other: Expr) = Lte(this, other) + + fun lte(other: Any?) = Lte(this, other) + internal abstract fun toProto(): Value } @@ -291,53 +320,343 @@ protected constructor(private val name: String, private val params: Array) = In(array, values) + + @JvmStatic fun `in`(fieldName: String, values: List) = In(fieldName, values) + + @JvmStatic fun isNan(expr: Expr) = IsNan(expr) + + @JvmStatic fun isNan(fieldName: String) = IsNan(fieldName) + + @JvmStatic + fun replaceFirst(value: Expr, find: Expr, replace: Expr) = ReplaceFirst(value, find, replace) + + @JvmStatic + fun replaceFirst(value: Expr, find: String, replace: String) = + ReplaceFirst(value, find, replace) + + @JvmStatic + fun replaceFirst(fieldName: String, find: String, replace: String) = + ReplaceFirst(fieldName, find, replace) + + @JvmStatic + fun replaceAll(value: Expr, find: Expr, replace: Expr) = ReplaceAll(value, find, replace) + + @JvmStatic + fun replaceAll(value: Expr, find: String, replace: String) = ReplaceAll(value, find, replace) + + @JvmStatic + fun replaceAll(fieldName: String, find: String, replace: String) = + ReplaceAll(fieldName, find, replace) + + @JvmStatic fun charLength(value: Expr) = CharLength(value) + + @JvmStatic fun charLength(fieldName: String) = CharLength(fieldName) + + @JvmStatic fun byteLength(value: Expr) = ByteLength(value) + + @JvmStatic fun byteLength(fieldName: String) = ByteLength(fieldName) + + @JvmStatic fun like(expr: Expr, pattern: Expr) = Like(expr, pattern) + + @JvmStatic fun like(fieldName: String, pattern: String) = Like(fieldName, pattern) + + @JvmStatic fun regexContains(expr: Expr, pattern: Expr) = RegexContains(expr, pattern) + + @JvmStatic fun regexContains(expr: Expr, pattern: String) = RegexContains(expr, pattern) + + @JvmStatic + fun regexContains(fieldName: String, pattern: Expr) = RegexContains(fieldName, pattern) + + @JvmStatic + fun regexContains(fieldName: String, pattern: String) = RegexContains(fieldName, pattern) + + @JvmStatic fun regexMatch(expr: Expr, pattern: Expr) = RegexMatch(expr, pattern) + + @JvmStatic fun regexMatch(expr: Expr, pattern: String) = RegexMatch(expr, pattern) + + @JvmStatic fun regexMatch(fieldName: String, pattern: Expr) = RegexMatch(fieldName, pattern) + + @JvmStatic fun regexMatch(fieldName: String, pattern: String) = RegexMatch(fieldName, pattern) + + @JvmStatic fun logicalMax(left: Expr, right: Expr) = LogicalMax(left, right) + + @JvmStatic fun logicalMax(left: Expr, right: Any?) = LogicalMax(left, right) + + @JvmStatic fun logicalMax(fieldName: String, other: Expr) = LogicalMax(fieldName, other) + + @JvmStatic fun logicalMax(fieldName: String, other: Any?) = LogicalMax(fieldName, other) + + @JvmStatic fun logicalMin(left: Expr, right: Expr) = LogicalMin(left, right) + + @JvmStatic fun logicalMin(left: Expr, right: Any?) = LogicalMin(left, right) + + @JvmStatic fun logicalMin(fieldName: String, other: Expr) = LogicalMin(fieldName, other) + + @JvmStatic fun logicalMin(fieldName: String, other: Any?) = LogicalMin(fieldName, other) + + @JvmStatic fun reverse(expr: Expr) = Reverse(expr) + + @JvmStatic fun reverse(fieldName: String) = Reverse(fieldName) + + @JvmStatic fun strContains(expr: Expr, substring: Expr) = StrContains(expr, substring) + + @JvmStatic fun strContains(expr: Expr, substring: String) = StrContains(expr, substring) + + @JvmStatic + fun strContains(fieldName: String, substring: Expr) = StrContains(fieldName, substring) + + @JvmStatic + fun strContains(fieldName: String, substring: String) = StrContains(fieldName, substring) + + @JvmStatic fun startsWith(expr: Expr, prefix: Expr) = StartsWith(expr, prefix) + + @JvmStatic fun startsWith(expr: Expr, prefix: String) = StartsWith(expr, prefix) + + @JvmStatic fun startsWith(fieldName: String, prefix: Expr) = StartsWith(fieldName, prefix) + + @JvmStatic fun startsWith(fieldName: String, prefix: String) = StartsWith(fieldName, prefix) + + @JvmStatic fun endsWith(expr: Expr, suffix: Expr) = EndsWith(expr, suffix) + + @JvmStatic fun endsWith(expr: Expr, suffix: String) = EndsWith(expr, suffix) + + @JvmStatic fun endsWith(fieldName: String, suffix: Expr) = EndsWith(fieldName, suffix) + + @JvmStatic fun endsWith(fieldName: String, suffix: String) = EndsWith(fieldName, suffix) + + @JvmStatic fun toLower(expr: Expr) = ToLower(expr) + + @JvmStatic + fun toLower( + fieldName: String, + ) = ToLower(fieldName) + + @JvmStatic fun toUpper(expr: Expr) = ToUpper(expr) + + @JvmStatic + fun toUpper( + fieldName: String, + ) = ToUpper(fieldName) + + @JvmStatic fun trim(expr: Expr) = Trim(expr) + + @JvmStatic fun trim(fieldName: String) = Trim(fieldName) + + @JvmStatic fun strConcat(first: Expr, vararg rest: Expr) = StrConcat(first, *rest) + + @JvmStatic fun strConcat(first: Expr, vararg rest: Any) = StrConcat(first, *rest) + + @JvmStatic fun strConcat(fieldName: String, vararg rest: Expr) = StrConcat(fieldName, *rest) + + @JvmStatic fun strConcat(fieldName: String, vararg rest: Any) = StrConcat(fieldName, *rest) + + @JvmStatic fun mapGet(map: Expr, key: Expr) = MapGet(map, key) + + @JvmStatic fun mapGet(map: Expr, key: String) = MapGet(map, key) + + @JvmStatic fun mapGet(fieldName: String, key: Expr) = MapGet(fieldName, key) + + @JvmStatic fun mapGet(fieldName: String, key: String) = MapGet(fieldName, key) + + @JvmStatic fun cosineDistance(vector1: Expr, vector2: Expr) = CosineDistance(vector1, vector2) + + @JvmStatic + fun cosineDistance(vector1: Expr, vector2: DoubleArray) = CosineDistance(vector1, vector2) + + @JvmStatic + fun cosineDistance(vector1: Expr, vector2: VectorValue) = CosineDistance(vector1, vector2) + + @JvmStatic + fun cosineDistance(fieldName: String, vector: Expr) = CosineDistance(fieldName, vector) + + @JvmStatic + fun cosineDistance(fieldName: String, vector: DoubleArray) = CosineDistance(fieldName, vector) + + @JvmStatic + fun cosineDistance(fieldName: String, vector: VectorValue) = CosineDistance(fieldName, vector) + + @JvmStatic fun dotProduct(vector1: Expr, vector2: Expr) = DotProduct(vector1, vector2) + + @JvmStatic fun dotProduct(vector1: Expr, vector2: DoubleArray) = DotProduct(vector1, vector2) + + @JvmStatic fun dotProduct(vector1: Expr, vector2: VectorValue) = DotProduct(vector1, vector2) + + @JvmStatic fun dotProduct(fieldName: String, vector: Expr) = DotProduct(fieldName, vector) + + @JvmStatic + fun dotProduct(fieldName: String, vector: DoubleArray) = DotProduct(fieldName, vector) + + @JvmStatic + fun dotProduct(fieldName: String, vector: VectorValue) = DotProduct(fieldName, vector) + + @JvmStatic + fun euclideanDistance(vector1: Expr, vector2: Expr) = EuclideanDistance(vector1, vector2) + + @JvmStatic + fun euclideanDistance(vector1: Expr, vector2: DoubleArray) = EuclideanDistance(vector1, vector2) + + @JvmStatic + fun euclideanDistance(vector1: Expr, vector2: VectorValue) = EuclideanDistance(vector1, vector2) + + @JvmStatic + fun euclideanDistance(fieldName: String, vector: Expr) = EuclideanDistance(fieldName, vector) + + @JvmStatic + fun euclideanDistance(fieldName: String, vector: DoubleArray) = + EuclideanDistance(fieldName, vector) + + @JvmStatic + fun euclideanDistance(fieldName: String, vector: VectorValue) = + EuclideanDistance(fieldName, vector) + + @JvmStatic fun vectorLength(vector: Expr) = VectorLength(vector) + + @JvmStatic fun vectorLength(fieldName: String) = VectorLength(fieldName) + + @JvmStatic fun unixMicrosToTimestamp(input: Expr) = UnixMicrosToTimestamp(input) + + @JvmStatic fun unixMicrosToTimestamp(fieldName: String) = UnixMicrosToTimestamp(fieldName) + + @JvmStatic fun timestampToUnixMicros(input: Expr) = TimestampToUnixMicros(input) + + @JvmStatic fun timestampToUnixMicros(fieldName: String) = TimestampToUnixMicros(fieldName) + + @JvmStatic fun unixMillisToTimestamp(input: Expr) = UnixMillisToTimestamp(input) + + @JvmStatic fun unixMillisToTimestamp(fieldName: String) = UnixMillisToTimestamp(fieldName) + + @JvmStatic fun timestampToUnixMillis(input: Expr) = TimestampToUnixMillis(input) + + @JvmStatic fun timestampToUnixMillis(fieldName: String) = TimestampToUnixMillis(fieldName) + + @JvmStatic fun unixSecondsToTimestamp(input: Expr) = UnixSecondsToTimestamp(input) + + @JvmStatic fun unixSecondsToTimestamp(fieldName: String) = UnixSecondsToTimestamp(fieldName) + + @JvmStatic fun timestampToUnixSeconds(input: Expr) = TimestampToUnixSeconds(input) + + @JvmStatic fun timestampToUnixSeconds(fieldName: String) = TimestampToUnixSeconds(fieldName) + + @JvmStatic + fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr) = + TimestampAdd(timestamp, unit, amount) + + @JvmStatic + fun timestampAdd(timestamp: Expr, unit: String, amount: Double) = + TimestampAdd(timestamp, unit, amount) + + @JvmStatic + fun timestampAdd(fieldName: String, unit: Expr, amount: Expr) = + TimestampAdd(fieldName, unit, amount) + + @JvmStatic + fun timestampAdd(fieldName: String, unit: String, amount: Double) = + TimestampAdd(fieldName, unit, amount) + + @JvmStatic + fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr) = + TimestampSub(timestamp, unit, amount) + + @JvmStatic + fun timestampSub(timestamp: Expr, unit: String, amount: Double) = + TimestampSub(timestamp, unit, amount) + + @JvmStatic + fun timestampSub(fieldName: String, unit: Expr, amount: Expr) = + TimestampSub(fieldName, unit, amount) + + @JvmStatic + fun timestampSub(fieldName: String, unit: String, amount: Double) = + TimestampSub(fieldName, unit, amount) + @JvmStatic fun eq(left: Expr, right: Expr) = Eq(left, right) - @JvmStatic fun eq(left: Expr, right: Any) = Eq(left, right) + @JvmStatic fun eq(left: Expr, right: Any?) = Eq(left, right) @JvmStatic fun eq(fieldName: String, right: Expr) = Eq(fieldName, right) - @JvmStatic fun eq(fieldName: String, right: Any) = Eq(fieldName, right) + @JvmStatic fun eq(fieldName: String, right: Any?) = Eq(fieldName, right) @JvmStatic fun neq(left: Expr, right: Expr) = Neq(left, right) - @JvmStatic fun neq(left: Expr, right: Any) = Neq(left, right) + @JvmStatic fun neq(left: Expr, right: Any?) = Neq(left, right) @JvmStatic fun neq(fieldName: String, right: Expr) = Neq(fieldName, right) - @JvmStatic fun neq(fieldName: String, right: Any) = Neq(fieldName, right) + @JvmStatic fun neq(fieldName: String, right: Any?) = Neq(fieldName, right) @JvmStatic fun gt(left: Expr, right: Expr) = Gt(left, right) - @JvmStatic fun gt(left: Expr, right: Any) = Gt(left, right) + @JvmStatic fun gt(left: Expr, right: Any?) = Gt(left, right) @JvmStatic fun gt(fieldName: String, right: Expr) = Gt(fieldName, right) - @JvmStatic fun gt(fieldName: String, right: Any) = Gt(fieldName, right) + @JvmStatic fun gt(fieldName: String, right: Any?) = Gt(fieldName, right) @JvmStatic fun gte(left: Expr, right: Expr) = Gte(left, right) - @JvmStatic fun gte(left: Expr, right: Any) = Gte(left, right) + @JvmStatic fun gte(left: Expr, right: Any?) = Gte(left, right) @JvmStatic fun gte(fieldName: String, right: Expr) = Gte(fieldName, right) - @JvmStatic fun gte(fieldName: String, right: Any) = Gte(fieldName, right) + @JvmStatic fun gte(fieldName: String, right: Any?) = Gte(fieldName, right) @JvmStatic fun lt(left: Expr, right: Expr) = Lt(left, right) - @JvmStatic fun lt(left: Expr, right: Any) = Lt(left, right) + @JvmStatic fun lt(left: Expr, right: Any?) = Lt(left, right) @JvmStatic fun lt(fieldName: String, right: Expr) = Lt(fieldName, right) - @JvmStatic fun lt(fieldName: String, right: Any) = Lt(fieldName, right) + @JvmStatic fun lt(fieldName: String, right: Any?) = Lt(fieldName, right) @JvmStatic fun lte(left: Expr, right: Expr) = Lte(left, right) - @JvmStatic fun lte(left: Expr, right: Any) = Lte(left, right) + @JvmStatic fun lte(left: Expr, right: Any?) = Lte(left, right) @JvmStatic fun lte(fieldName: String, right: Expr) = Lte(fieldName, right) - @JvmStatic fun lte(fieldName: String, right: Any) = Lte(fieldName, right) + @JvmStatic fun lte(fieldName: String, right: Any?) = Lte(fieldName, right) @JvmStatic fun arrayConcat(array: Expr, vararg arrays: Expr) = ArrayConcat(array, *arrays) @@ -357,9 +676,9 @@ protected constructor(private val name: String, private val params: Array) = ArrayContainsAll(array, values) @@ -508,39 +827,39 @@ class Mod(left: Expr, right: Expr) : Function("mod", left, right) { // } class Eq(left: Expr, right: Expr) : BooleanExpr("eq", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) } class Neq(left: Expr, right: Expr) : BooleanExpr("neq", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) } class Lt(left: Expr, right: Expr) : BooleanExpr("lt", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) } class Lte(left: Expr, right: Expr) : BooleanExpr("lte", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) } class Gt(left: Expr, right: Expr) : BooleanExpr("gt", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) } class Gte(left: Expr, right: Expr) : BooleanExpr("gte", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) } class ArrayConcat(array: Expr, vararg arrays: Expr) : @@ -558,9 +877,9 @@ class ArrayReverse(array: Expr) : Function("array_reverse", array) { } class ArrayContains(array: Expr, value: Expr) : BooleanExpr("array_contains", array, value) { - constructor(array: Expr, right: Any) : this(array, toExprOrConstant(right)) + constructor(array: Expr, right: Any?) : this(array, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) } class ArrayContainsAll(array: Expr, values: List) : @@ -605,15 +924,15 @@ class If(condition: BooleanExpr, thenExpr: Expr, elseExpr: Expr) : Function("if", condition, thenExpr, elseExpr) class LogicalMax(left: Expr, right: Expr) : Function("logical_max", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) } class LogicalMin(left: Expr, right: Expr) : Function("logical_min", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) + constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) + constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) } class Reverse(expr: Expr) : Function("reverse", expr) { @@ -706,17 +1025,11 @@ class Trim(expr: Expr) : Function("trim", expr) { class StrConcat internal constructor(first: Expr, vararg rest: Expr) : Function("str_concat", arrayOf(first, *rest)) { - constructor( - first: Expr, - second: String, - vararg rest: Expr - ) : this(first, Constant.of(second), *rest) + constructor(first: Expr, vararg rest: String) : this(first, *toArrayOfExprOrConstant(rest)) + constructor(first: Expr, vararg rest: Any) : this(first, *toArrayOfExprOrConstant(rest)) constructor(fieldName: String, vararg rest: Expr) : this(Field.of(fieldName), *rest) - constructor( - fieldName: String, - second: String, - vararg rest: Expr - ) : this(Field.of(fieldName), second, *rest) + constructor(fieldName: String, vararg rest: Any) : this(Field.of(fieldName), *rest) + constructor(fieldName: String, vararg rest: String) : this(Field.of(fieldName), *rest) } class MapGet(map: Expr, key: Expr) : Function("map_get", map, key) { From 5abf13ff9781ee657433f2953570c3adfca6fce1 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 19 Feb 2025 13:29:57 -0500 Subject: [PATCH 018/152] Refactor and more tests. --- .../firebase/firestore/PipelineTest.java | 207 +++- .../com/google/firebase/firestore/Pipeline.kt | 2 +- .../firebase/firestore/pipeline/Constant.kt | 5 +- .../firestore/pipeline/accumulators.kt | 64 +- .../firebase/firestore/pipeline/expression.kt | 895 ++++++------------ 5 files changed, 486 insertions(+), 687 deletions(-) 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 04e71d64068..94842ba009d 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 @@ -19,9 +19,13 @@ import static com.google.firebase.firestore.pipeline.Function.and; import static com.google.firebase.firestore.pipeline.Function.arrayContains; import static com.google.firebase.firestore.pipeline.Function.arrayContainsAny; +import static com.google.firebase.firestore.pipeline.Function.cosineDistance; import static com.google.firebase.firestore.pipeline.Function.endsWith; import static com.google.firebase.firestore.pipeline.Function.eq; +import static com.google.firebase.firestore.pipeline.Function.euclideanDistance; import static com.google.firebase.firestore.pipeline.Function.gt; +import static com.google.firebase.firestore.pipeline.Function.logicalMax; +import static com.google.firebase.firestore.pipeline.Function.logicalMin; import static com.google.firebase.firestore.pipeline.Function.lt; import static com.google.firebase.firestore.pipeline.Function.lte; import static com.google.firebase.firestore.pipeline.Function.neq; @@ -41,9 +45,12 @@ import com.google.common.truth.Correspondence; import com.google.firebase.firestore.pipeline.Accumulator; import com.google.firebase.firestore.pipeline.AggregateStage; +import com.google.firebase.firestore.pipeline.Constant; import com.google.firebase.firestore.pipeline.Field; import com.google.firebase.firestore.pipeline.Function; import com.google.firebase.firestore.testutil.IntegrationTestUtil; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import org.junit.After; @@ -87,11 +94,11 @@ public void tearDown() { IntegrationTestUtil.tearDown(); } - private Map> bookDocs = - ImmutableMap.ofEntries( + private final Map> bookDocs = + mapOfEntries( entry( "book1", - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "The Hitchhiker's Guide to the Galaxy"), entry("author", "Douglas Adams"), entry("genre", "Science Fiction"), @@ -101,7 +108,7 @@ public void tearDown() { entry("awards", ImmutableMap.of("hugo", true, "nebula", false)))), entry( "book2", - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "Pride and Prejudice"), entry("author", "Jane Austen"), entry("genre", "Romance"), @@ -111,7 +118,7 @@ public void tearDown() { entry("awards", ImmutableMap.of("none", true)))), entry( "book3", - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "One Hundred Years of Solitude"), entry("author", "Gabriel García Márquez"), entry("genre", "Magical Realism"), @@ -121,7 +128,7 @@ public void tearDown() { entry("awards", ImmutableMap.of("nobel", true, "nebula", false)))), entry( "book4", - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "The Lord of the Rings"), entry("author", "J.R.R. Tolkien"), entry("genre", "Fantasy"), @@ -131,7 +138,7 @@ public void tearDown() { entry("awards", ImmutableMap.of("hugo", false, "nebula", false)))), entry( "book5", - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "The Handmaid's Tale"), entry("author", "Margaret Atwood"), entry("genre", "Dystopian"), @@ -142,7 +149,7 @@ public void tearDown() { "awards", ImmutableMap.of("arthur c. clarke", true, "booker prize", false)))), entry( "book6", - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "Crime and Punishment"), entry("author", "Fyodor Dostoevsky"), entry("genre", "Psychological Thriller"), @@ -152,7 +159,7 @@ public void tearDown() { entry("awards", ImmutableMap.of("none", true)))), entry( "book7", - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "To Kill a Mockingbird"), entry("author", "Harper Lee"), entry("genre", "Southern Gothic"), @@ -162,7 +169,7 @@ public void tearDown() { entry("awards", ImmutableMap.of("pulitzer", true)))), entry( "book8", - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "1984"), entry("author", "George Orwell"), entry("genre", "Dystopian"), @@ -172,7 +179,7 @@ public void tearDown() { entry("awards", ImmutableMap.of("prometheus", true)))), entry( "book9", - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "The Great Gatsby"), entry("author", "F. Scott Fitzgerald"), entry("genre", "Modernist"), @@ -182,7 +189,7 @@ public void tearDown() { entry("awards", ImmutableMap.of("none", true)))), entry( "book10", - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "Dune"), entry("author", "Frank Herbert"), entry("genre", "Science Fiction"), @@ -239,8 +246,7 @@ public void aggregateResultsMany() { assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( - ImmutableMap.ofEntries( - entry("count", 10), entry("avgRating", 4.4), entry("maxRating", 4.6))); + mapOfEntries(entry("count", 10), entry("avgRating", 4.4), entry("maxRating", 4.6))); } @Test @@ -259,9 +265,9 @@ public void groupAndAccumulateResults() { assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( - ImmutableMap.ofEntries(entry("avgRating", 4.7), entry("genre", "Fantasy")), - ImmutableMap.ofEntries(entry("avgRating", 4.5), entry("genre", "Romance")), - ImmutableMap.ofEntries(entry("avgRating", 4.4), entry("genre", "Science Fiction"))); + mapOfEntries(entry("avgRating", 4.7), entry("genre", "Fantasy")), + mapOfEntries(entry("avgRating", 4.5), entry("genre", "Romance")), + mapOfEntries(entry("avgRating", 4.4), entry("genre", "Science Fiction"))); } @Test @@ -279,8 +285,7 @@ public void minAndMaxAccumulations() { assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( - ImmutableMap.ofEntries( - entry("count", 10), entry("maxRating", 4.7), entry("minPublished", 1813))); + mapOfEntries(entry("count", 10), entry("maxRating", 4.7), entry("minPublished", 1813))); } @Test @@ -295,26 +300,23 @@ public void canSelectFields() { assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "The Hitchhiker's Guide to the Galaxy"), entry("author", "Douglas Adams")), - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "The Great Gatsby"), entry("author", "F. Scott Fitzgerald")), - ImmutableMap.ofEntries(entry("title", "Dune"), entry("author", "Frank Herbert")), - ImmutableMap.ofEntries( + mapOfEntries(entry("title", "Dune"), entry("author", "Frank Herbert")), + mapOfEntries( entry("title", "Crime and Punishment"), entry("author", "Fyodor Dostoevsky")), - ImmutableMap.ofEntries( + mapOfEntries( entry("title", "One Hundred Years of Solitude"), entry("author", "Gabriel García Márquez")), - ImmutableMap.ofEntries(entry("title", "1984"), entry("author", "George Orwell")), - ImmutableMap.ofEntries( - entry("title", "To Kill a Mockingbird"), entry("author", "Harper Lee")), - ImmutableMap.ofEntries( + mapOfEntries(entry("title", "1984"), entry("author", "George Orwell")), + mapOfEntries(entry("title", "To Kill a Mockingbird"), entry("author", "Harper Lee")), + mapOfEntries( entry("title", "The Lord of the Rings"), entry("author", "J.R.R. Tolkien")), - ImmutableMap.ofEntries( - entry("title", "Pride and Prejudice"), entry("author", "Jane Austen")), - ImmutableMap.ofEntries( - entry("title", "The Handmaid's Tale"), entry("author", "Margaret Atwood"))) + mapOfEntries(entry("title", "Pride and Prejudice"), entry("author", "Jane Austen")), + mapOfEntries(entry("title", "The Handmaid's Tale"), entry("author", "Margaret Atwood"))) .inOrder(); } @@ -362,10 +364,9 @@ public void offsetAndLimits() { assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( - ImmutableMap.ofEntries(entry("title", "1984"), entry("author", "George Orwell")), - ImmutableMap.ofEntries( - entry("title", "To Kill a Mockingbird"), entry("author", "Harper Lee")), - ImmutableMap.ofEntries( + mapOfEntries(entry("title", "1984"), entry("author", "George Orwell")), + mapOfEntries(entry("title", "To Kill a Mockingbird"), entry("author", "Harper Lee")), + mapOfEntries( entry("title", "The Lord of the Rings"), entry("author", "J.R.R. Tolkien"))); } @@ -598,7 +599,7 @@ public void testArithmeticOperations() { assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( - ImmutableMap.ofEntries( + mapOfEntries( entry("ratingPlusOne", 5.2), entry("yearsSince1900", 79), entry("ratingTimesTen", 42), @@ -652,20 +653,66 @@ public void testChecks() { randomCol .pipeline() .where(not(Field.of("rating").isNan())) - .select(Field.of("rating").eq(null).as("ratingIsNull")) - .select("title") - .sort(Field.of("title").ascending()) + .select( + Field.of("rating").isNull().as("ratingIsNull"), + Field.of("rating").eq(Constant.nullValue()).as("ratingEqNull"), + not(Field.of("rating").isNan()).as("ratingIsNotNan")) + .limit(1) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly( - ImmutableMap.of("title", "Crime and Punishment"), - ImmutableMap.of("title", "Dune"), - ImmutableMap.of("title", "Pride and Prejudice")); + mapOfEntries( + entry("ratingIsNull", false), + entry("ratingEqNull", null), + entry("ratingIsNotNan", true))); + } + + @Test + @Ignore("Not supported yet") + public void testLogicalMax() { + Task execute = + randomCol + .pipeline() + .where(Field.of("author").eq("Douglas Adams")) + .select( + Field.of("rating").logicalMax(4.5).as("max_rating"), + logicalMax(Field.of("published"), 1900).as("max_published")) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly(ImmutableMap.of("max_rating", 4.5, "max_published", 1979)); + } + + @Test + @Ignore("Not supported yet") + public void testLogicalMin() { + Task execute = + randomCol + .pipeline() + .select( + Field.of("rating").logicalMin(4.5).as("min_rating"), + logicalMin(Field.of("published"), 1900).as("min_published")) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly(ImmutableMap.of("min_rating", 4.2, "min_published", 1900)); } @Test - public void testMapGet() {} + public void testMapGet() { + Task execute = + randomCol + .pipeline() + .select(Field.of("awards").mapGet("hugo").as("hugoAward"), Field.of("title")) + .where(eq("hugoAward", true)) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of("hugoAward", true, "title", "The Hitchhiker's Guide to the Galaxy"), + ImmutableMap.of("hugoAward", true, "title", "Dune")); + } @Test public void testParent() {} @@ -674,11 +721,81 @@ public void testParent() {} public void testCollectionId() {} @Test - public void testDistanceFunctions() {} + public void testDistanceFunctions() { + double[] sourceVector = {0.1, 0.1}; + double[] targetVector = {0.5, 0.8}; + Task execute = + randomCol + .pipeline() + .select( + cosineDistance(Constant.vector(sourceVector), targetVector).as("cosineDistance"), + Function.dotProduct(Constant.vector(sourceVector), targetVector) + .as("dotProductDistance"), + euclideanDistance(Constant.vector(sourceVector), targetVector) + .as("euclideanDistance")) + .limit(1) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of( + "cosineDistance", 0.02560880430538015, + "dotProductDistance", 0.13, + "euclideanDistance", 0.806225774829855)); + } @Test public void testNestedFields() {} @Test public void testMapGetWithFieldNameIncludingNotation() {} + + static Map.Entry entry(String key, T value) { + return new Map.Entry() { + private String k = key; + private T v = value; + + @Override + public String getKey() { + return k; + } + + @Override + public T getValue() { + return v; + } + + @Override + public T setValue(T value) { + T old = v; + v = value; + return old; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + + Map.Entry that = (Map.Entry) o; + return com.google.common.base.Objects.equal(k, that.getKey()) + && com.google.common.base.Objects.equal(v, that.getValue()); + } + + @Override + public int hashCode() { + return com.google.common.base.Objects.hashCode(k, v); + } + }; + } + + @SafeVarargs + static Map mapOfEntries(Map.Entry... entries) { + Map res = new LinkedHashMap<>(); + for (Map.Entry entry : entries) { + res.put(entry.getKey(), entry.getValue()); + } + return Collections.unmodifiableMap(res); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 941690eb372..75825ad2cec 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -225,7 +225,7 @@ internal constructor( private val version: SnapshotVersion, ) { - fun getData(): Map { + fun getData(): Map { return userDataWriter().convertObject(fields) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt index fdcac5c59c3..bc9a39a50d4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt @@ -27,6 +27,8 @@ import java.util.Date class Constant internal constructor(val value: Value) : Expr() { companion object { + internal val NULL = Constant(Values.NULL_VALUE) + fun of(value: Any): Constant { return when (value) { is String -> of(value) @@ -38,6 +40,7 @@ class Constant internal constructor(val value: Value) : Expr() { is Blob -> of(value) is DocumentReference -> of(value) is Value -> of(value) + is VectorValue -> of(value) else -> throw IllegalArgumentException("Unknown type: $value") } } @@ -89,7 +92,7 @@ class Constant internal constructor(val value: Value) : Expr() { @JvmStatic fun nullValue(): Constant { - return Constant(Values.NULL_VALUE) + return NULL } @JvmStatic diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt index 8098183c1c2..b89c318636a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt @@ -19,67 +19,71 @@ import com.google.firestore.v1.Value class AccumulatorWithAlias internal constructor(internal val alias: String, internal val accumulator: Accumulator) -open class Accumulator -protected constructor(private val name: String, private val params: Array) { - protected constructor( - name: String, - param: Expr? - ) : this(name, if (param == null) emptyArray() else arrayOf(param)) +class Accumulator +private constructor(private val name: String, private val params: Array) { + private constructor(name: String) : this(name, emptyArray()) + private constructor(name: String, expr: Expr) : this(name, arrayOf(expr)) + private constructor(name: String, fieldName: String) : this(name, Field.of(fieldName)) companion object { @JvmStatic - fun countAll(): Count { - return Count(null) + fun countAll(): Accumulator { + return Accumulator("count") } @JvmStatic - fun count(fieldName: String): Count { - return Count(fieldName) + fun count(fieldName: String): Accumulator { + return Accumulator("count", fieldName) } @JvmStatic - fun count(expr: Expr): Count { - return Count(expr) + fun count(expr: Expr): Accumulator { + return Accumulator("count", expr) + } + + @JvmStatic + fun countIf(condition: BooleanExpr): Accumulator { + return Accumulator("countIf", condition) } @JvmStatic fun sum(fieldName: String): Accumulator { - return Sum(fieldName) + return Accumulator("sum", fieldName) } @JvmStatic fun sum(expr: Expr): Accumulator { - return Sum(expr) + return Accumulator("sum", expr) } @JvmStatic fun avg(fieldName: String): Accumulator { - return Avg(fieldName) + return Accumulator("avg", fieldName) } @JvmStatic fun avg(expr: Expr): Accumulator { - return Avg(expr) + return Accumulator("avg", expr) } @JvmStatic fun min(fieldName: String): Accumulator { - return min(fieldName) + return Accumulator("min", fieldName) } @JvmStatic fun min(expr: Expr): Accumulator { - return min(expr) + return Accumulator("min", expr) } @JvmStatic fun max(fieldName: String): Accumulator { - return Max(fieldName) + return Accumulator("max", fieldName) } @JvmStatic fun max(expr: Expr): Accumulator { - return Max(expr) + return Accumulator("max", expr) } } @@ -96,23 +100,3 @@ protected constructor(private val name: String, private val params: Array other - else -> Constant.of(other) + internal fun toExprOrConstant(value: Any): Expr { + return when (value) { + is Expr -> value + else -> Constant.of(value) } } @@ -73,7 +70,7 @@ abstract class Expr protected constructor() { * @param other The expression to add to this expression. * @return A new {@code Expr} representing the addition operation. */ - fun add(other: Expr) = Add(this, other) + fun add(other: Expr) = Function.add(this, other) /** * Creates an expression that this expression to another expression. @@ -86,181 +83,185 @@ abstract class Expr protected constructor() { * @param other The constant value to add to this expression. * @return A new {@code Expr} representing the addition operation. */ - fun add(other: Any) = Add(this, other) + fun add(other: Any) = Function.add(this, other) + + fun subtract(other: Expr) = Function.subtract(this, other) - fun subtract(other: Expr) = Subtract(this, other) + fun subtract(other: Any) = Function.subtract(this, other) - fun subtract(other: Any) = Subtract(this, other) + fun multiply(other: Expr) = Function.multiply(this, other) - fun multiply(other: Expr) = Multiply(this, other) + fun multiply(other: Any) = Function.multiply(this, other) - fun multiply(other: Any) = Multiply(this, other) + fun divide(other: Expr) = Function.divide(this, other) - fun divide(other: Expr) = Divide(this, other) + fun divide(other: Any) = Function.divide(this, other) - fun divide(other: Any) = Divide(this, other) + fun mod(other: Expr) = Function.mod(this, other) - fun mod(other: Expr) = Mod(this, other) + fun mod(other: Any) = Function.mod(this, other) - fun mod(other: Any) = Mod(this, other) + fun inAny(values: List) = Function.inAny(this, values) - fun `in`(values: List) = In(this, values) + fun notInAny(values: List) = Function.notInAny(this, values) - fun isNan() = IsNan(this) + fun isNan() = Function.isNan(this) - fun replaceFirst(find: Expr, replace: Expr) = ReplaceFirst(this, find, replace) + fun isNull() = Function.isNull(this) - fun replaceFirst(find: String, replace: String) = ReplaceFirst(this, find, replace) + fun replaceFirst(find: Expr, replace: Expr) = Function.replaceFirst(this, find, replace) - fun replaceAll(find: Expr, replace: Expr) = ReplaceAll(this, find, replace) + fun replaceFirst(find: String, replace: String) = Function.replaceFirst(this, find, replace) - fun replaceAll(find: String, replace: String) = ReplaceAll(this, find, replace) + fun replaceAll(find: Expr, replace: Expr) = Function.replaceAll(this, find, replace) - fun charLength() = CharLength(this) + fun replaceAll(find: String, replace: String) = Function.replaceAll(this, find, replace) - fun byteLength() = ByteLength(this) + fun charLength() = Function.charLength(this) - fun like(pattern: Expr) = Like(this, pattern) + fun byteLength() = Function.byteLength(this) - fun like(pattern: String) = Like(this, pattern) + fun like(pattern: Expr) = Function.like(this, pattern) - fun regexContains(pattern: Expr) = RegexContains(this, pattern) + fun like(pattern: String) = Function.like(this, pattern) - fun regexContains(pattern: String) = RegexContains(this, pattern) + fun regexContains(pattern: Expr) = Function.regexContains(this, pattern) - fun regexMatch(pattern: Expr) = RegexMatch(this, pattern) + fun regexContains(pattern: String) = Function.regexContains(this, pattern) - fun regexMatch(pattern: String) = RegexMatch(this, pattern) + fun regexMatch(pattern: Expr) = Function.regexMatch(this, pattern) - fun logicalMax(other: Expr) = LogicalMax(this, other) + fun regexMatch(pattern: String) = Function.regexMatch(this, pattern) - fun logicalMax(other: Any?) = LogicalMax(this, other) + fun logicalMax(other: Expr) = Function.logicalMax(this, other) - fun logicalMin(other: Expr) = LogicalMin(this, other) + fun logicalMax(other: Any) = Function.logicalMax(this, other) - fun logicalMin(other: Any?) = LogicalMin(this, other) + fun logicalMin(other: Expr) = Function.logicalMin(this, other) - fun reverse() = Reverse(this) + fun logicalMin(other: Any) = Function.logicalMin(this, other) - fun strContains(substring: Expr) = StrContains(this, substring) + fun reverse() = Function.reverse(this) - fun strContains(substring: String) = StrContains(this, substring) + fun strContains(substring: Expr) = Function.strContains(this, substring) - fun startsWith(prefix: Expr) = StartsWith(this, prefix) + fun strContains(substring: String) = Function.strContains(this, substring) - fun startsWith(prefix: String) = StartsWith(this, prefix) + fun startsWith(prefix: Expr) = Function.startsWith(this, prefix) - fun endsWith(suffix: Expr) = EndsWith(this, suffix) + fun startsWith(prefix: String) = Function.startsWith(this, prefix) - fun endsWith(suffix: String) = EndsWith(this, suffix) + fun endsWith(suffix: Expr) = Function.endsWith(this, suffix) - fun toLower() = ToLower(this) + fun endsWith(suffix: String) = Function.endsWith(this, suffix) - fun toUpper() = ToUpper(this) + fun toLower() = Function.toLower(this) - fun trim() = Trim(this) + fun toUpper() = Function.toUpper(this) - fun strConcat(vararg expr: Expr) = StrConcat(this, *expr) + fun trim() = Function.trim(this) - fun strConcat(vararg string: String) = StrConcat(this, *string) + fun strConcat(vararg expr: Expr) = Function.strConcat(this, *expr) - fun strConcat(vararg string: Any) = StrConcat(this, *string) + fun strConcat(vararg string: String) = Function.strConcat(this, *string) - fun mapGet(key: Expr) = MapGet(this, key) + fun strConcat(vararg string: Any) = Function.strConcat(this, *string) - fun mapGet(key: String) = MapGet(this, key) + fun mapGet(key: Expr) = Function.mapGet(this, key) - fun cosineDistance(vector: Expr) = CosineDistance(this, vector) + fun mapGet(key: String) = Function.mapGet(this, key) - fun cosineDistance(vector: DoubleArray) = CosineDistance(this, vector) + fun cosineDistance(vector: Expr) = Function.cosineDistance(this, vector) - fun cosineDistance(vector: VectorValue) = CosineDistance(this, vector) + fun cosineDistance(vector: DoubleArray) = Function.cosineDistance(this, vector) - fun dotProduct(vector: Expr) = DotProduct(this, vector) + fun cosineDistance(vector: VectorValue) = Function.cosineDistance(this, vector) - fun dotProduct(vector: DoubleArray) = DotProduct(this, vector) + fun dotProduct(vector: Expr) = Function.dotProduct(this, vector) - fun dotProduct(vector: VectorValue) = DotProduct(this, vector) + fun dotProduct(vector: DoubleArray) = Function.dotProduct(this, vector) - fun euclideanDistance(vector: Expr) = EuclideanDistance(this, vector) + fun dotProduct(vector: VectorValue) = Function.dotProduct(this, vector) - fun euclideanDistance(vector: DoubleArray) = EuclideanDistance(this, vector) + fun euclideanDistance(vector: Expr) = Function.euclideanDistance(this, vector) - fun euclideanDistance(vector: VectorValue) = EuclideanDistance(this, vector) + fun euclideanDistance(vector: DoubleArray) = Function.euclideanDistance(this, vector) - fun vectorLength() = VectorLength(this) + fun euclideanDistance(vector: VectorValue) = Function.euclideanDistance(this, vector) - fun unixMicrosToTimestamp() = UnixMicrosToTimestamp(this) + fun vectorLength() = Function.vectorLength(this) - fun timestampToUnixMicros() = TimestampToUnixMicros(this) + fun unixMicrosToTimestamp() = Function.unixMicrosToTimestamp(this) - fun unixMillisToTimestamp() = UnixMillisToTimestamp(this) + fun timestampToUnixMicros() = Function.timestampToUnixMicros(this) - fun timestampToUnixMillis() = TimestampToUnixMillis(this) + fun unixMillisToTimestamp() = Function.unixMillisToTimestamp(this) - fun unixSecondsToTimestamp() = UnixSecondsToTimestamp(this) + fun timestampToUnixMillis() = Function.timestampToUnixMillis(this) - fun timestampToUnixSeconds() = TimestampToUnixSeconds(this) + fun unixSecondsToTimestamp() = Function.unixSecondsToTimestamp(this) - fun timestampAdd(unit: Expr, amount: Expr) = TimestampAdd(this, unit, amount) + fun timestampToUnixSeconds() = Function.timestampToUnixSeconds(this) - fun timestampAdd(unit: String, amount: Double) = TimestampAdd(this, unit, amount) + fun timestampAdd(unit: Expr, amount: Expr) = Function.timestampAdd(this, unit, amount) - fun timestampSub(unit: Expr, amount: Expr) = TimestampSub(this, unit, amount) + fun timestampAdd(unit: String, amount: Double) = Function.timestampAdd(this, unit, amount) - fun timestampSub(unit: String, amount: Double) = TimestampSub(this, unit, amount) + fun timestampSub(unit: Expr, amount: Expr) = Function.timestampSub(this, unit, amount) - fun arrayConcat(vararg arrays: Expr) = ArrayConcat(this, *arrays) + fun timestampSub(unit: String, amount: Double) = Function.timestampSub(this, unit, amount) - fun arrayConcat(arrays: List) = ArrayConcat(this, arrays) + fun arrayConcat(vararg arrays: Expr) = Function.arrayConcat(this, *arrays) - fun arrayReverse() = ArrayReverse(this) + fun arrayConcat(arrays: List) = Function.arrayConcat(this, arrays) - fun arrayContains(value: Expr) = ArrayContains(this, value) + fun arrayReverse() = Function.arrayReverse(this) - fun arrayContains(value: Any?) = ArrayContains(this, value) + fun arrayContains(value: Expr) = Function.arrayContains(this, value) - fun arrayContainsAll(values: List) = ArrayContainsAll(this, values) + fun arrayContains(value: Any) = Function.arrayContains(this, value) - fun arrayContainsAny(values: List) = ArrayContainsAny(this, values) + fun arrayContainsAll(values: List) = Function.arrayContainsAll(this, values) - fun arrayLength() = ArrayLength(this) + fun arrayContainsAny(values: List) = Function.arrayContainsAny(this, values) - fun sum() = Sum(this) + fun arrayLength() = Function.arrayLength(this) - fun avg() = Avg(this) + fun sum() = Accumulator.sum(this) - fun min() = Min(this) + fun avg() = Accumulator.avg(this) - fun max() = Max(this) + fun min() = Accumulator.min(this) + + fun max() = Accumulator.max(this) fun ascending() = Ordering.ascending(this) fun descending() = Ordering.descending(this) - fun eq(other: Expr) = Eq(this, other) + fun eq(other: Expr) = Function.eq(this, other) - fun eq(other: Any?) = Eq(this, other) + fun eq(other: Any) = Function.eq(this, other) - fun neq(other: Expr) = Neq(this, other) + fun neq(other: Expr) = Function.neq(this, other) - fun neq(other: Any?) = Neq(this, other) + fun neq(other: Any) = Function.neq(this, other) - fun gt(other: Expr) = Gt(this, other) + fun gt(other: Expr) = Function.gt(this, other) - fun gt(other: Any?) = Gt(this, other) + fun gt(other: Any) = Function.gt(this, other) - fun gte(other: Expr) = Gte(this, other) + fun gte(other: Expr) = Function.gte(this, other) - fun gte(other: Any?) = Gte(this, other) + fun gte(other: Any) = Function.gte(this, other) - fun lt(other: Expr) = Lt(this, other) + fun lt(other: Expr) = Function.lt(this, other) - fun lt(other: Any?) = Lt(this, other) + fun lt(other: Any) = Function.lt(this, other) - fun lte(other: Expr) = Lte(this, other) + fun lte(other: Expr) = Function.lte(this, other) - fun lte(other: Any?) = Lte(this, other) + fun lte(other: Any) = Function.lte(this, other) internal abstract fun toProto(): Value } @@ -308,406 +309,418 @@ class ListOfExprs(val expressions: Array) : Expr() { open class Function protected constructor(private val name: String, private val params: Array) : Expr() { + private constructor(name: String, param: Expr, vararg params: Any) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) + private constructor(name: String, fieldName: String, vararg params: Any) : this(name, arrayOf(Field.of(fieldName), *toArrayOfExprOrConstant(params))) companion object { @JvmStatic - fun and(condition: BooleanExpr, vararg conditions: BooleanExpr) = And(condition, *conditions) + fun and(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("and", condition, *conditions) @JvmStatic - fun or(condition: BooleanExpr, vararg conditions: BooleanExpr) = Or(condition, *conditions) + fun or(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("or", condition, *conditions) @JvmStatic - fun xor(condition: BooleanExpr, vararg conditions: BooleanExpr) = Xor(condition, *conditions) + fun xor(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("xor", condition, *conditions) + + @JvmStatic fun not(condition: BooleanExpr) = BooleanExpr("not", condition) - @JvmStatic fun not(cond: BooleanExpr) = Not(cond) + @JvmStatic fun add(left: Expr, right: Expr) = Function("add", left, right) - @JvmStatic fun add(left: Expr, right: Expr) = Add(left, right) + @JvmStatic fun add(left: Expr, right: Any) = Function("add", left, right) - @JvmStatic fun add(left: Expr, right: Any) = Add(left, right) + @JvmStatic fun add(fieldName: String, other: Expr) = Function("add", fieldName, other) - @JvmStatic fun add(fieldName: String, other: Expr) = Add(fieldName, other) + @JvmStatic fun add(fieldName: String, other: Any) = Function("add", fieldName, other) - @JvmStatic fun add(fieldName: String, other: Any) = Add(fieldName, other) + @JvmStatic fun subtract(left: Expr, right: Expr) = Function("subtract", left, right) - @JvmStatic fun subtract(left: Expr, right: Expr) = Subtract(left, right) + @JvmStatic fun subtract(left: Expr, right: Any) = Function("subtract", left, right) - @JvmStatic fun subtract(left: Expr, right: Any) = Subtract(left, right) + @JvmStatic fun subtract(fieldName: String, other: Expr) = Function("subtract", fieldName, other) - @JvmStatic fun subtract(fieldName: String, other: Expr) = Subtract(fieldName, other) + @JvmStatic fun subtract(fieldName: String, other: Any) = Function("subtract", fieldName, other) - @JvmStatic fun subtract(fieldName: String, other: Any) = Subtract(fieldName, other) + @JvmStatic fun multiply(left: Expr, right: Expr) = Function("multiply", left, right) - @JvmStatic fun multiply(left: Expr, right: Expr) = Multiply(left, right) + @JvmStatic fun multiply(left: Expr, right: Any) = Function("multiply", left, right) - @JvmStatic fun multiply(left: Expr, right: Any) = Multiply(left, right) + @JvmStatic fun multiply(fieldName: String, other: Expr) = Function("multiply", fieldName, other) - @JvmStatic fun multiply(fieldName: String, other: Expr) = Multiply(fieldName, other) + @JvmStatic fun multiply(fieldName: String, other: Any) = Function("multiply", fieldName, other) - @JvmStatic fun multiply(fieldName: String, other: Any) = Multiply(fieldName, other) + @JvmStatic fun divide(left: Expr, right: Expr) = Function("divide", left, right) - @JvmStatic fun divide(left: Expr, right: Expr) = Divide(left, right) + @JvmStatic fun divide(left: Expr, right: Any) = Function("divide", left, right) - @JvmStatic fun divide(left: Expr, right: Any) = Divide(left, right) + @JvmStatic fun divide(fieldName: String, other: Expr) = Function("divide", fieldName, other) - @JvmStatic fun divide(fieldName: String, other: Expr) = Divide(fieldName, other) + @JvmStatic fun divide(fieldName: String, other: Any) = Function("divide", fieldName, other) - @JvmStatic fun divide(fieldName: String, other: Any) = Divide(fieldName, other) + @JvmStatic fun mod(left: Expr, right: Expr) = Function("mod", left, right) - @JvmStatic fun mod(left: Expr, right: Expr) = Mod(left, right) + @JvmStatic fun mod(left: Expr, right: Any) = Function("mod", left, right) - @JvmStatic fun mod(left: Expr, right: Any) = Mod(left, right) + @JvmStatic fun mod(fieldName: String, other: Expr) = Function("mod", fieldName, other) - @JvmStatic fun mod(fieldName: String, other: Expr) = Mod(fieldName, other) + @JvmStatic fun mod(fieldName: String, other: Any) = Function("mod", fieldName, other) - @JvmStatic fun mod(fieldName: String, other: Any) = Mod(fieldName, other) + @JvmStatic fun inAny(array: Expr, values: List) = BooleanExpr("in", array, ListOfExprs(toArrayOfExprOrConstant(values))) - @JvmStatic fun `in`(array: Expr, values: List) = In(array, values) + @JvmStatic fun inAny(fieldName: String, values: List) = BooleanExpr("in", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) - @JvmStatic fun `in`(fieldName: String, values: List) = In(fieldName, values) + @JvmStatic fun notInAny(array: Expr, values: List) = not(inAny(array, values)) - @JvmStatic fun isNan(expr: Expr) = IsNan(expr) + @JvmStatic fun notInAny(fieldName: String, values: List) = not(inAny(fieldName, values)) - @JvmStatic fun isNan(fieldName: String) = IsNan(fieldName) + @JvmStatic fun isNan(expr: Expr) = BooleanExpr("is_nan", expr) + + @JvmStatic fun isNan(fieldName: String) = BooleanExpr("is_nan", fieldName) + + @JvmStatic fun isNull(expr: Expr) = BooleanExpr("is_null", expr) + + @JvmStatic fun isNull(fieldName: String) = BooleanExpr("is_null", fieldName) @JvmStatic - fun replaceFirst(value: Expr, find: Expr, replace: Expr) = ReplaceFirst(value, find, replace) + fun replaceFirst(value: Expr, find: Expr, replace: Expr) = Function("replace_first", value, find, replace) @JvmStatic fun replaceFirst(value: Expr, find: String, replace: String) = - ReplaceFirst(value, find, replace) + Function("replace_first", value, find, replace) @JvmStatic fun replaceFirst(fieldName: String, find: String, replace: String) = - ReplaceFirst(fieldName, find, replace) + Function("replace_first", fieldName, find, replace) @JvmStatic - fun replaceAll(value: Expr, find: Expr, replace: Expr) = ReplaceAll(value, find, replace) + fun replaceAll(value: Expr, find: Expr, replace: Expr) = Function("replace_all", value, find, replace) @JvmStatic - fun replaceAll(value: Expr, find: String, replace: String) = ReplaceAll(value, find, replace) + fun replaceAll(value: Expr, find: String, replace: String) = Function("replace_all", value, find, replace) @JvmStatic fun replaceAll(fieldName: String, find: String, replace: String) = - ReplaceAll(fieldName, find, replace) + Function("replace_all", fieldName, find, replace) + + @JvmStatic fun charLength(value: Expr) = Function("char_length", value) - @JvmStatic fun charLength(value: Expr) = CharLength(value) + @JvmStatic fun charLength(fieldName: String) = Function("char_length", fieldName) - @JvmStatic fun charLength(fieldName: String) = CharLength(fieldName) + @JvmStatic fun byteLength(value: Expr) = Function("byte_length", value) - @JvmStatic fun byteLength(value: Expr) = ByteLength(value) + @JvmStatic fun byteLength(fieldName: String) = Function("byte_length", fieldName) - @JvmStatic fun byteLength(fieldName: String) = ByteLength(fieldName) + @JvmStatic fun like(expr: Expr, pattern: Expr) = BooleanExpr("like", expr, pattern) - @JvmStatic fun like(expr: Expr, pattern: Expr) = Like(expr, pattern) + @JvmStatic fun like(expr: Expr, pattern: String) = BooleanExpr("like", expr, pattern) - @JvmStatic fun like(fieldName: String, pattern: String) = Like(fieldName, pattern) + @JvmStatic fun like(fieldName: String, pattern: Expr) = BooleanExpr("like", fieldName, pattern) - @JvmStatic fun regexContains(expr: Expr, pattern: Expr) = RegexContains(expr, pattern) + @JvmStatic fun like(fieldName: String, pattern: String) = BooleanExpr("like", fieldName, pattern) - @JvmStatic fun regexContains(expr: Expr, pattern: String) = RegexContains(expr, pattern) + @JvmStatic fun regexContains(expr: Expr, pattern: Expr) = BooleanExpr("regex_contains", expr, pattern) + + @JvmStatic fun regexContains(expr: Expr, pattern: String) = BooleanExpr("regex_contains", expr, pattern) @JvmStatic - fun regexContains(fieldName: String, pattern: Expr) = RegexContains(fieldName, pattern) + fun regexContains(fieldName: String, pattern: Expr) = BooleanExpr("regex_contains", fieldName, pattern) @JvmStatic - fun regexContains(fieldName: String, pattern: String) = RegexContains(fieldName, pattern) + fun regexContains(fieldName: String, pattern: String) = BooleanExpr("regex_contains", fieldName, pattern) - @JvmStatic fun regexMatch(expr: Expr, pattern: Expr) = RegexMatch(expr, pattern) + @JvmStatic fun regexMatch(expr: Expr, pattern: Expr) = BooleanExpr("regex_match", expr, pattern) - @JvmStatic fun regexMatch(expr: Expr, pattern: String) = RegexMatch(expr, pattern) + @JvmStatic fun regexMatch(expr: Expr, pattern: String) = BooleanExpr("regex_match", expr, pattern) - @JvmStatic fun regexMatch(fieldName: String, pattern: Expr) = RegexMatch(fieldName, pattern) + @JvmStatic fun regexMatch(fieldName: String, pattern: Expr) = BooleanExpr("regex_match", fieldName, pattern) - @JvmStatic fun regexMatch(fieldName: String, pattern: String) = RegexMatch(fieldName, pattern) + @JvmStatic fun regexMatch(fieldName: String, pattern: String) = BooleanExpr("regex_match", fieldName, pattern) - @JvmStatic fun logicalMax(left: Expr, right: Expr) = LogicalMax(left, right) + @JvmStatic fun logicalMax(left: Expr, right: Expr) = Function("logical_max", left, right) - @JvmStatic fun logicalMax(left: Expr, right: Any?) = LogicalMax(left, right) + @JvmStatic fun logicalMax(left: Expr, right: Any) = Function("logical_max", left, right) - @JvmStatic fun logicalMax(fieldName: String, other: Expr) = LogicalMax(fieldName, other) + @JvmStatic fun logicalMax(fieldName: String, other: Expr) = Function("logical_max", fieldName, other) - @JvmStatic fun logicalMax(fieldName: String, other: Any?) = LogicalMax(fieldName, other) + @JvmStatic fun logicalMax(fieldName: String, other: Any) = Function("logical_max", fieldName, other) - @JvmStatic fun logicalMin(left: Expr, right: Expr) = LogicalMin(left, right) + @JvmStatic fun logicalMin(left: Expr, right: Expr) = Function("logical_min", left, right) - @JvmStatic fun logicalMin(left: Expr, right: Any?) = LogicalMin(left, right) + @JvmStatic fun logicalMin(left: Expr, right: Any) = Function("logical_min", left, right) - @JvmStatic fun logicalMin(fieldName: String, other: Expr) = LogicalMin(fieldName, other) + @JvmStatic fun logicalMin(fieldName: String, other: Expr) = Function("logical_min", fieldName, other) - @JvmStatic fun logicalMin(fieldName: String, other: Any?) = LogicalMin(fieldName, other) + @JvmStatic fun logicalMin(fieldName: String, other: Any) = Function("logical_min", fieldName, other) - @JvmStatic fun reverse(expr: Expr) = Reverse(expr) + @JvmStatic fun reverse(expr: Expr) = Function("reverse", expr) - @JvmStatic fun reverse(fieldName: String) = Reverse(fieldName) + @JvmStatic fun reverse(fieldName: String) = Function("reverse", fieldName) - @JvmStatic fun strContains(expr: Expr, substring: Expr) = StrContains(expr, substring) + @JvmStatic fun strContains(expr: Expr, substring: Expr) = BooleanExpr("str_contains", expr, substring) - @JvmStatic fun strContains(expr: Expr, substring: String) = StrContains(expr, substring) + @JvmStatic fun strContains(expr: Expr, substring: String) = BooleanExpr("str_contains", expr, substring) @JvmStatic - fun strContains(fieldName: String, substring: Expr) = StrContains(fieldName, substring) + fun strContains(fieldName: String, substring: Expr) = BooleanExpr("str_contains", fieldName, substring) @JvmStatic - fun strContains(fieldName: String, substring: String) = StrContains(fieldName, substring) + fun strContains(fieldName: String, substring: String) = BooleanExpr("str_contains", fieldName, substring) - @JvmStatic fun startsWith(expr: Expr, prefix: Expr) = StartsWith(expr, prefix) + @JvmStatic fun startsWith(expr: Expr, prefix: Expr) = BooleanExpr("starts_with", expr, prefix) - @JvmStatic fun startsWith(expr: Expr, prefix: String) = StartsWith(expr, prefix) + @JvmStatic fun startsWith(expr: Expr, prefix: String) = BooleanExpr("starts_with", expr, prefix) - @JvmStatic fun startsWith(fieldName: String, prefix: Expr) = StartsWith(fieldName, prefix) + @JvmStatic fun startsWith(fieldName: String, prefix: Expr) = BooleanExpr("starts_with", fieldName, prefix) - @JvmStatic fun startsWith(fieldName: String, prefix: String) = StartsWith(fieldName, prefix) + @JvmStatic fun startsWith(fieldName: String, prefix: String) = BooleanExpr("starts_with", fieldName, prefix) - @JvmStatic fun endsWith(expr: Expr, suffix: Expr) = EndsWith(expr, suffix) + @JvmStatic fun endsWith(expr: Expr, suffix: Expr) = BooleanExpr("ends_with", expr, suffix) - @JvmStatic fun endsWith(expr: Expr, suffix: String) = EndsWith(expr, suffix) + @JvmStatic fun endsWith(expr: Expr, suffix: String) = BooleanExpr("ends_with", expr, suffix) - @JvmStatic fun endsWith(fieldName: String, suffix: Expr) = EndsWith(fieldName, suffix) + @JvmStatic fun endsWith(fieldName: String, suffix: Expr) = BooleanExpr("ends_with", fieldName, suffix) - @JvmStatic fun endsWith(fieldName: String, suffix: String) = EndsWith(fieldName, suffix) + @JvmStatic fun endsWith(fieldName: String, suffix: String) = BooleanExpr("ends_with", fieldName, suffix) - @JvmStatic fun toLower(expr: Expr) = ToLower(expr) + @JvmStatic fun toLower(expr: Expr) = Function("to_lower", expr) @JvmStatic fun toLower( fieldName: String, - ) = ToLower(fieldName) + ) = Function("to_lower", fieldName) - @JvmStatic fun toUpper(expr: Expr) = ToUpper(expr) + @JvmStatic fun toUpper(expr: Expr) = Function("to_upper", expr) @JvmStatic fun toUpper( fieldName: String, - ) = ToUpper(fieldName) + ) = Function("to_upper", fieldName) - @JvmStatic fun trim(expr: Expr) = Trim(expr) + @JvmStatic fun trim(expr: Expr) = Function("trim", expr) - @JvmStatic fun trim(fieldName: String) = Trim(fieldName) + @JvmStatic fun trim(fieldName: String) = Function("trim", fieldName) - @JvmStatic fun strConcat(first: Expr, vararg rest: Expr) = StrConcat(first, *rest) + @JvmStatic fun strConcat(first: Expr, vararg rest: Expr) = Function("str_concat", first, *rest) - @JvmStatic fun strConcat(first: Expr, vararg rest: Any) = StrConcat(first, *rest) + @JvmStatic fun strConcat(first: Expr, vararg rest: Any) = Function("str_concat", first, *rest) - @JvmStatic fun strConcat(fieldName: String, vararg rest: Expr) = StrConcat(fieldName, *rest) + @JvmStatic fun strConcat(fieldName: String, vararg rest: Expr) = Function("str_concat", fieldName, *rest) - @JvmStatic fun strConcat(fieldName: String, vararg rest: Any) = StrConcat(fieldName, *rest) + @JvmStatic fun strConcat(fieldName: String, vararg rest: Any) = Function("str_concat", fieldName, *rest) - @JvmStatic fun mapGet(map: Expr, key: Expr) = MapGet(map, key) + @JvmStatic fun mapGet(map: Expr, key: Expr) = Function("map_get", map, key) - @JvmStatic fun mapGet(map: Expr, key: String) = MapGet(map, key) + @JvmStatic fun mapGet(map: Expr, key: String) = Function("map_get", map, key) - @JvmStatic fun mapGet(fieldName: String, key: Expr) = MapGet(fieldName, key) + @JvmStatic fun mapGet(fieldName: String, key: Expr) = Function("map_get", fieldName, key) - @JvmStatic fun mapGet(fieldName: String, key: String) = MapGet(fieldName, key) + @JvmStatic fun mapGet(fieldName: String, key: String) = Function("map_get", fieldName, key) - @JvmStatic fun cosineDistance(vector1: Expr, vector2: Expr) = CosineDistance(vector1, vector2) + @JvmStatic fun cosineDistance(vector1: Expr, vector2: Expr) = Function("cosine_distance", vector1, vector2) @JvmStatic - fun cosineDistance(vector1: Expr, vector2: DoubleArray) = CosineDistance(vector1, vector2) + fun cosineDistance(vector1: Expr, vector2: DoubleArray) = Function("cosine_distance", vector1, Constant.vector(vector2)) @JvmStatic - fun cosineDistance(vector1: Expr, vector2: VectorValue) = CosineDistance(vector1, vector2) + fun cosineDistance(vector1: Expr, vector2: VectorValue) = Function("cosine_distance", vector1, vector2) @JvmStatic - fun cosineDistance(fieldName: String, vector: Expr) = CosineDistance(fieldName, vector) + fun cosineDistance(fieldName: String, vector: Expr) = Function("cosine_distance", fieldName, vector) @JvmStatic - fun cosineDistance(fieldName: String, vector: DoubleArray) = CosineDistance(fieldName, vector) + fun cosineDistance(fieldName: String, vector: DoubleArray) = Function("cosine_distance", fieldName, Constant.vector(vector)) @JvmStatic - fun cosineDistance(fieldName: String, vector: VectorValue) = CosineDistance(fieldName, vector) + fun cosineDistance(fieldName: String, vector: VectorValue) = Function("cosine_distance", fieldName, vector) - @JvmStatic fun dotProduct(vector1: Expr, vector2: Expr) = DotProduct(vector1, vector2) + @JvmStatic fun dotProduct(vector1: Expr, vector2: Expr) = Function("dot_product", vector1, vector2) - @JvmStatic fun dotProduct(vector1: Expr, vector2: DoubleArray) = DotProduct(vector1, vector2) + @JvmStatic fun dotProduct(vector1: Expr, vector2: DoubleArray) = Function("dot_product", vector1, Constant.vector(vector2)) - @JvmStatic fun dotProduct(vector1: Expr, vector2: VectorValue) = DotProduct(vector1, vector2) + @JvmStatic fun dotProduct(vector1: Expr, vector2: VectorValue) = Function("dot_product", vector1, vector2) - @JvmStatic fun dotProduct(fieldName: String, vector: Expr) = DotProduct(fieldName, vector) + @JvmStatic fun dotProduct(fieldName: String, vector: Expr) = Function("dot_product", fieldName, vector) @JvmStatic - fun dotProduct(fieldName: String, vector: DoubleArray) = DotProduct(fieldName, vector) + fun dotProduct(fieldName: String, vector: DoubleArray) = Function("dot_product", fieldName, Constant.vector(vector)) @JvmStatic - fun dotProduct(fieldName: String, vector: VectorValue) = DotProduct(fieldName, vector) + fun dotProduct(fieldName: String, vector: VectorValue) = Function("dot_product", fieldName, vector) @JvmStatic - fun euclideanDistance(vector1: Expr, vector2: Expr) = EuclideanDistance(vector1, vector2) + fun euclideanDistance(vector1: Expr, vector2: Expr) = Function("euclidean_distance", vector1, vector2) @JvmStatic - fun euclideanDistance(vector1: Expr, vector2: DoubleArray) = EuclideanDistance(vector1, vector2) + fun euclideanDistance(vector1: Expr, vector2: DoubleArray) = Function("euclidean_distance", vector1, Constant.vector(vector2)) @JvmStatic - fun euclideanDistance(vector1: Expr, vector2: VectorValue) = EuclideanDistance(vector1, vector2) + fun euclideanDistance(vector1: Expr, vector2: VectorValue) = Function("euclidean_distance", vector1, vector2) @JvmStatic - fun euclideanDistance(fieldName: String, vector: Expr) = EuclideanDistance(fieldName, vector) + fun euclideanDistance(fieldName: String, vector: Expr) = Function("euclidean_distance", fieldName, vector) @JvmStatic fun euclideanDistance(fieldName: String, vector: DoubleArray) = - EuclideanDistance(fieldName, vector) + Function("euclidean_distance", fieldName, Constant.vector(vector)) @JvmStatic fun euclideanDistance(fieldName: String, vector: VectorValue) = - EuclideanDistance(fieldName, vector) + Function("euclidean_distance", fieldName, vector) - @JvmStatic fun vectorLength(vector: Expr) = VectorLength(vector) + @JvmStatic fun vectorLength(vector: Expr) = Function("vector_length", vector) - @JvmStatic fun vectorLength(fieldName: String) = VectorLength(fieldName) + @JvmStatic fun vectorLength(fieldName: String) = Function("vector_length", fieldName) - @JvmStatic fun unixMicrosToTimestamp(input: Expr) = UnixMicrosToTimestamp(input) + @JvmStatic fun unixMicrosToTimestamp(input: Expr) = Function("unix_micros_to_timestamp", input) - @JvmStatic fun unixMicrosToTimestamp(fieldName: String) = UnixMicrosToTimestamp(fieldName) + @JvmStatic fun unixMicrosToTimestamp(fieldName: String) = Function("unix_micros_to_timestamp", fieldName) - @JvmStatic fun timestampToUnixMicros(input: Expr) = TimestampToUnixMicros(input) + @JvmStatic fun timestampToUnixMicros(input: Expr) = Function("timestamp_to_unix_micros", input) - @JvmStatic fun timestampToUnixMicros(fieldName: String) = TimestampToUnixMicros(fieldName) + @JvmStatic fun timestampToUnixMicros(fieldName: String) = Function("timestamp_to_unix_micros", fieldName) - @JvmStatic fun unixMillisToTimestamp(input: Expr) = UnixMillisToTimestamp(input) + @JvmStatic fun unixMillisToTimestamp(input: Expr) = Function("unix_millis_to_timestamp", input) - @JvmStatic fun unixMillisToTimestamp(fieldName: String) = UnixMillisToTimestamp(fieldName) + @JvmStatic fun unixMillisToTimestamp(fieldName: String) = Function("unix_millis_to_timestamp", fieldName) - @JvmStatic fun timestampToUnixMillis(input: Expr) = TimestampToUnixMillis(input) + @JvmStatic fun timestampToUnixMillis(input: Expr) = Function("timestamp_to_unix_millis", input) - @JvmStatic fun timestampToUnixMillis(fieldName: String) = TimestampToUnixMillis(fieldName) + @JvmStatic fun timestampToUnixMillis(fieldName: String) = Function("timestamp_to_unix_millis", fieldName) - @JvmStatic fun unixSecondsToTimestamp(input: Expr) = UnixSecondsToTimestamp(input) + @JvmStatic fun unixSecondsToTimestamp(input: Expr) = Function("unix_seconds_to_timestamp", input) - @JvmStatic fun unixSecondsToTimestamp(fieldName: String) = UnixSecondsToTimestamp(fieldName) + @JvmStatic fun unixSecondsToTimestamp(fieldName: String) = Function("unix_seconds_to_timestamp", fieldName) - @JvmStatic fun timestampToUnixSeconds(input: Expr) = TimestampToUnixSeconds(input) + @JvmStatic fun timestampToUnixSeconds(input: Expr) = Function("timestamp_to_unix_seconds", input) - @JvmStatic fun timestampToUnixSeconds(fieldName: String) = TimestampToUnixSeconds(fieldName) + @JvmStatic fun timestampToUnixSeconds(fieldName: String) = Function("timestamp_to_unix_seconds", fieldName) @JvmStatic fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr) = - TimestampAdd(timestamp, unit, amount) + Function("timestamp_add", timestamp, unit, amount) @JvmStatic fun timestampAdd(timestamp: Expr, unit: String, amount: Double) = - TimestampAdd(timestamp, unit, amount) + Function("timestamp_add", timestamp, unit, amount) @JvmStatic fun timestampAdd(fieldName: String, unit: Expr, amount: Expr) = - TimestampAdd(fieldName, unit, amount) + Function("timestamp_add", fieldName, unit, amount) @JvmStatic fun timestampAdd(fieldName: String, unit: String, amount: Double) = - TimestampAdd(fieldName, unit, amount) + Function("timestamp_add", fieldName, unit, amount) @JvmStatic fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr) = - TimestampSub(timestamp, unit, amount) + Function("timestamp_sub", timestamp, unit, amount) @JvmStatic fun timestampSub(timestamp: Expr, unit: String, amount: Double) = - TimestampSub(timestamp, unit, amount) + Function("timestamp_sub", timestamp, unit, amount) @JvmStatic fun timestampSub(fieldName: String, unit: Expr, amount: Expr) = - TimestampSub(fieldName, unit, amount) + Function("timestamp_sub", fieldName, unit, amount) @JvmStatic fun timestampSub(fieldName: String, unit: String, amount: Double) = - TimestampSub(fieldName, unit, amount) + Function("timestamp_sub", fieldName, unit, amount) - @JvmStatic fun eq(left: Expr, right: Expr) = Eq(left, right) + @JvmStatic fun eq(left: Expr, right: Expr) = BooleanExpr("eq", left, right) - @JvmStatic fun eq(left: Expr, right: Any?) = Eq(left, right) + @JvmStatic fun eq(left: Expr, right: Any) = BooleanExpr("eq", left, right) - @JvmStatic fun eq(fieldName: String, right: Expr) = Eq(fieldName, right) + @JvmStatic fun eq(fieldName: String, right: Expr) = BooleanExpr("eq", fieldName, right) - @JvmStatic fun eq(fieldName: String, right: Any?) = Eq(fieldName, right) + @JvmStatic fun eq(fieldName: String, right: Any) = BooleanExpr("eq", fieldName, right) - @JvmStatic fun neq(left: Expr, right: Expr) = Neq(left, right) + @JvmStatic fun neq(left: Expr, right: Expr) = BooleanExpr("neq", left, right) - @JvmStatic fun neq(left: Expr, right: Any?) = Neq(left, right) + @JvmStatic fun neq(left: Expr, right: Any) = BooleanExpr("neq", left, right) - @JvmStatic fun neq(fieldName: String, right: Expr) = Neq(fieldName, right) + @JvmStatic fun neq(fieldName: String, right: Expr) = BooleanExpr("neq", fieldName, right) - @JvmStatic fun neq(fieldName: String, right: Any?) = Neq(fieldName, right) + @JvmStatic fun neq(fieldName: String, right: Any) = BooleanExpr("neq", fieldName, right) - @JvmStatic fun gt(left: Expr, right: Expr) = Gt(left, right) + @JvmStatic fun gt(left: Expr, right: Expr) = BooleanExpr("gt", left, right) - @JvmStatic fun gt(left: Expr, right: Any?) = Gt(left, right) + @JvmStatic fun gt(left: Expr, right: Any) = BooleanExpr("gt", left, right) - @JvmStatic fun gt(fieldName: String, right: Expr) = Gt(fieldName, right) + @JvmStatic fun gt(fieldName: String, right: Expr) = BooleanExpr("gt", fieldName, right) - @JvmStatic fun gt(fieldName: String, right: Any?) = Gt(fieldName, right) + @JvmStatic fun gt(fieldName: String, right: Any) = BooleanExpr("gt", fieldName, right) - @JvmStatic fun gte(left: Expr, right: Expr) = Gte(left, right) + @JvmStatic fun gte(left: Expr, right: Expr) = BooleanExpr("gte", left, right) - @JvmStatic fun gte(left: Expr, right: Any?) = Gte(left, right) + @JvmStatic fun gte(left: Expr, right: Any) = BooleanExpr("gte", left, right) - @JvmStatic fun gte(fieldName: String, right: Expr) = Gte(fieldName, right) + @JvmStatic fun gte(fieldName: String, right: Expr) = BooleanExpr("gte", fieldName, right) - @JvmStatic fun gte(fieldName: String, right: Any?) = Gte(fieldName, right) + @JvmStatic fun gte(fieldName: String, right: Any) = BooleanExpr("gte", fieldName, right) - @JvmStatic fun lt(left: Expr, right: Expr) = Lt(left, right) + @JvmStatic fun lt(left: Expr, right: Expr) = BooleanExpr("lt", left, right) - @JvmStatic fun lt(left: Expr, right: Any?) = Lt(left, right) + @JvmStatic fun lt(left: Expr, right: Any) = BooleanExpr("lt", left, right) - @JvmStatic fun lt(fieldName: String, right: Expr) = Lt(fieldName, right) + @JvmStatic fun lt(fieldName: String, right: Expr) = BooleanExpr("lt", fieldName, right) - @JvmStatic fun lt(fieldName: String, right: Any?) = Lt(fieldName, right) + @JvmStatic fun lt(fieldName: String, right: Any) = BooleanExpr("lt", fieldName, right) - @JvmStatic fun lte(left: Expr, right: Expr) = Lte(left, right) + @JvmStatic fun lte(left: Expr, right: Expr) = BooleanExpr("lte", left, right) - @JvmStatic fun lte(left: Expr, right: Any?) = Lte(left, right) + @JvmStatic fun lte(left: Expr, right: Any) = BooleanExpr("lte", left, right) - @JvmStatic fun lte(fieldName: String, right: Expr) = Lte(fieldName, right) + @JvmStatic fun lte(fieldName: String, right: Expr) = BooleanExpr("lte", fieldName, right) - @JvmStatic fun lte(fieldName: String, right: Any?) = Lte(fieldName, right) + @JvmStatic fun lte(fieldName: String, right: Any) = BooleanExpr("lte", fieldName, right) - @JvmStatic fun arrayConcat(array: Expr, vararg arrays: Expr) = ArrayConcat(array, *arrays) + @JvmStatic fun arrayConcat(array: Expr, vararg arrays: Expr) = Function("array_concat", array, *arrays) @JvmStatic - fun arrayConcat(fieldName: String, vararg arrays: Expr) = ArrayConcat(fieldName, *arrays) + fun arrayConcat(fieldName: String, vararg arrays: Expr) = Function("array_concat", fieldName, *arrays) - @JvmStatic fun arrayConcat(array: Expr, arrays: List) = ArrayConcat(array, arrays) + @JvmStatic fun arrayConcat(array: Expr, arrays: List) = Function("array_concat", array, ListOfExprs(toArrayOfExprOrConstant(arrays))) @JvmStatic - fun arrayConcat(fieldName: String, arrays: List) = ArrayConcat(fieldName, arrays) + fun arrayConcat(fieldName: String, arrays: List) = Function("array_concat", fieldName, ListOfExprs(toArrayOfExprOrConstant(arrays))) - @JvmStatic fun arrayReverse(array: Expr) = ArrayReverse(array) + @JvmStatic fun arrayReverse(array: Expr) = Function("array_reverse", array) - @JvmStatic fun arrayReverse(fieldName: String) = ArrayReverse(fieldName) + @JvmStatic fun arrayReverse(fieldName: String) = Function("array_reverse", fieldName) - @JvmStatic fun arrayContains(array: Expr, value: Expr) = ArrayContains(array, value) + @JvmStatic fun arrayContains(array: Expr, value: Expr) = BooleanExpr("array_contains", array, value) - @JvmStatic fun arrayContains(fieldName: String, value: Expr) = ArrayContains(fieldName, value) + @JvmStatic fun arrayContains(fieldName: String, value: Expr) = BooleanExpr("array_contains", fieldName, value) - @JvmStatic fun arrayContains(array: Expr, value: Any?) = ArrayContains(array, value) + @JvmStatic fun arrayContains(array: Expr, value: Any) = BooleanExpr("array_contains", array, value) - @JvmStatic fun arrayContains(fieldName: String, value: Any?) = ArrayContains(fieldName, value) + @JvmStatic fun arrayContains(fieldName: String, value: Any) = BooleanExpr("array_contains", fieldName, value) @JvmStatic - fun arrayContainsAll(array: Expr, values: List) = ArrayContainsAll(array, values) + fun arrayContainsAll(array: Expr, values: List) = BooleanExpr("array_contains_all", array, ListOfExprs(toArrayOfExprOrConstant(values))) @JvmStatic - fun arrayContainsAll(fieldName: String, values: List) = ArrayContainsAll(fieldName, values) + fun arrayContainsAll(fieldName: String, values: List) = BooleanExpr("array_contains_all", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) @JvmStatic - fun arrayContainsAny(array: Expr, values: List) = ArrayContainsAny(array, values) + fun arrayContainsAny(array: Expr, values: List) = BooleanExpr("array_contains_any", array, ListOfExprs(toArrayOfExprOrConstant(values))) @JvmStatic - fun arrayContainsAny(fieldName: String, values: List) = ArrayContainsAny(fieldName, values) + fun arrayContainsAny(fieldName: String, values: List) = BooleanExpr("array_contains_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + + @JvmStatic fun arrayLength(array: Expr) = Function("array_length", array) + + @JvmStatic fun arrayLength(fieldName: String) = Function("array_length", fieldName) + + @JvmStatic fun ifThen(condition: BooleanExpr, then: Expr) = + Function("if", condition, then, Constant.NULL) - @JvmStatic fun arrayLength(array: Expr) = ArrayLength(array) + @JvmStatic fun ifThen(condition: BooleanExpr, then: Any) = Function("if", condition, then, Constant.NULL) - @JvmStatic fun arrayLength(fieldName: String) = ArrayLength(fieldName) + @JvmStatic fun ifThenElse(condition: BooleanExpr, then: Expr, `else`: Expr) = Function("if", condition, then, `else`) + + @JvmStatic fun ifThenElse(condition: BooleanExpr, then: Any, `else`: Any) = Function("if", condition, then, `else`) } - protected constructor(name: String, param1: Expr) : this(name, arrayOf(param1)) - protected constructor( - name: String, - param1: Expr, - param2: Expr - ) : this(name, arrayOf(param1, param2)) - protected constructor( - name: String, - param1: Expr, - param2: Expr, - param3: Expr - ) : this(name, arrayOf(param1, param2, param3)) + override fun toProto(): Value { val builder = com.google.firestore.v1.Function.newBuilder() builder.setName(name) @@ -718,16 +731,23 @@ protected constructor(private val name: String, private val params: Array) : Function(name, params) { + internal constructor(name: String, param: Expr, vararg params: Any) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) + internal constructor(name: String, fieldName: String, vararg params: Any) : this(name, arrayOf(Field.of(fieldName), *toArrayOfExprOrConstant(params))) + + fun not() = Function.not(this) + + fun countIf(): Accumulator = Accumulator.countIf(this) - fun and(vararg conditions: BooleanExpr): And = And(this, *conditions) - fun or(vararg conditions: BooleanExpr): Or = Or(this, *conditions) + fun ifThen(then: Expr) = Function.ifThen(this, then) - fun xor(vararg conditions: BooleanExpr): Xor = Xor(this, *conditions) + fun ifThen(then: Any) = Function.ifThen(this, then) - fun not(): Not = Not(this) + fun ifThenElse(then: Expr, `else`: Expr) = Function.ifThenElse(this, then, `else`) + + fun ifThenElse(then: Any, `else`: Any) = Function.ifThenElse(this, then, `else`) } class Ordering private constructor(private val expr: Expr, private val dir: Direction) { @@ -760,36 +780,6 @@ class Ordering private constructor(private val expr: Expr, private val dir: Dire .build() } -class Add(left: Expr, right: Expr) : Function("add", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -} - -class Subtract(left: Expr, right: Expr) : Function("subtract", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -} - -class Multiply(left: Expr, right: Expr) : Function("multiply", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -} - -class Divide(left: Expr, right: Expr) : Function("divide", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -} - -class Mod(left: Expr, right: Expr) : Function("mod", left, right) { - constructor(left: Expr, right: Any) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -} - // class BitAnd(left: Expr, right: Expr) : Function("bit_and", left, right) { // constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) // constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) @@ -825,298 +815,3 @@ class Mod(left: Expr, right: Expr) : Function("mod", left, right) { // constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) // constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) // } - -class Eq(left: Expr, right: Expr) : BooleanExpr("eq", left, right) { - constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) -} - -class Neq(left: Expr, right: Expr) : BooleanExpr("neq", left, right) { - constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) -} - -class Lt(left: Expr, right: Expr) : BooleanExpr("lt", left, right) { - constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) -} - -class Lte(left: Expr, right: Expr) : BooleanExpr("lte", left, right) { - constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) -} - -class Gt(left: Expr, right: Expr) : BooleanExpr("gt", left, right) { - constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) -} - -class Gte(left: Expr, right: Expr) : BooleanExpr("gte", left, right) { - constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) -} - -class ArrayConcat(array: Expr, vararg arrays: Expr) : - Function("array_concat", arrayOf(array, *arrays)) { - constructor( - array: Expr, - arrays: List - ) : this(array, ListOfExprs(toArrayOfExprOrConstant(arrays))) - constructor(fieldName: String, vararg arrays: Expr) : this(Field.of(fieldName), *arrays) - constructor(fieldName: String, right: List) : this(Field.of(fieldName), right) -} - -class ArrayReverse(array: Expr) : Function("array_reverse", array) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class ArrayContains(array: Expr, value: Expr) : BooleanExpr("array_contains", array, value) { - constructor(array: Expr, right: Any?) : this(array, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) -} - -class ArrayContainsAll(array: Expr, values: List) : - BooleanExpr("array_contains_all", array, ListOfExprs(toArrayOfExprOrConstant(values))) { - constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) -} - -class ArrayContainsAny(array: Expr, values: List) : - BooleanExpr("array_contains_any", array, ListOfExprs(toArrayOfExprOrConstant(values))) { - constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) -} - -class ArrayLength(array: Expr) : Function("array_length", array) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class In(array: Expr, values: List) : - BooleanExpr("in", array, ListOfExprs(toArrayOfExprOrConstant(values))) { - constructor(fieldName: String, values: List) : this(Field.of(fieldName), values) -} - -class IsNan(expr: Expr) : BooleanExpr("is_nan", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class Exists(expr: Expr) : BooleanExpr("exists", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class Not(cond: BooleanExpr) : BooleanExpr("not", cond) - -class And(condition: BooleanExpr, vararg conditions: BooleanExpr) : - BooleanExpr("and", condition, *conditions) - -class Or(condition: BooleanExpr, vararg conditions: BooleanExpr) : - BooleanExpr("or", condition, *conditions) - -class Xor(condition: BooleanExpr, vararg conditions: Expr) : - BooleanExpr("xor", condition, *conditions) - -class If(condition: BooleanExpr, thenExpr: Expr, elseExpr: Expr) : - Function("if", condition, thenExpr, elseExpr) - -class LogicalMax(left: Expr, right: Expr) : Function("logical_max", left, right) { - constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) -} - -class LogicalMin(left: Expr, right: Expr) : Function("logical_min", left, right) { - constructor(left: Expr, right: Any?) : this(left, toExprOrConstant(right)) - constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) - constructor(fieldName: String, right: Any?) : this(Field.of(fieldName), right) -} - -class Reverse(expr: Expr) : Function("reverse", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class ReplaceFirst(value: Expr, find: Expr, replace: Expr) : - Function("replace_first", value, find, replace) { - constructor( - value: Expr, - find: String, - replace: String - ) : this(value, Constant.of(find), Constant.of(replace)) - constructor( - fieldName: String, - find: String, - replace: String - ) : this(Field.of(fieldName), find, replace) -} - -class ReplaceAll(value: Expr, find: Expr, replace: Expr) : - Function("replace_all", value, find, replace) { - constructor( - value: Expr, - find: String, - replace: String - ) : this(value, Constant.of(find), Constant.of(replace)) - constructor( - fieldName: String, - find: String, - replace: String - ) : this(Field.of(fieldName), find, replace) -} - -class CharLength(value: Expr) : Function("char_length", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class ByteLength(value: Expr) : Function("byte_length", value) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class Like(expr: Expr, pattern: Expr) : BooleanExpr("like", expr, pattern) { - constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) - constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) - constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) -} - -class RegexContains(expr: Expr, pattern: Expr) : BooleanExpr("regex_contains", expr, pattern) { - constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) - constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) - constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) -} - -class RegexMatch(expr: Expr, pattern: Expr) : BooleanExpr("regex_match", expr, pattern) { - constructor(expr: Expr, pattern: String) : this(expr, Constant.of(pattern)) - constructor(fieldName: String, pattern: Expr) : this(Field.of(fieldName), pattern) - constructor(fieldName: String, pattern: String) : this(Field.of(fieldName), pattern) -} - -class StrContains(expr: Expr, substring: Expr) : BooleanExpr("str_contains", expr, substring) { - constructor(expr: Expr, substring: String) : this(expr, Constant.of(substring)) - constructor(fieldName: String, substring: Expr) : this(Field.of(fieldName), substring) - constructor(fieldName: String, substring: String) : this(Field.of(fieldName), substring) -} - -class StartsWith(expr: Expr, prefix: Expr) : BooleanExpr("starts_with", expr, prefix) { - constructor(expr: Expr, prefix: String) : this(expr, Constant.of(prefix)) - constructor(fieldName: String, prefix: Expr) : this(Field.of(fieldName), prefix) - constructor(fieldName: String, prefix: String) : this(Field.of(fieldName), prefix) -} - -class EndsWith(expr: Expr, suffix: Expr) : BooleanExpr("ends_with", expr, suffix) { - constructor(expr: Expr, suffix: String) : this(expr, Constant.of(suffix)) - constructor(fieldName: String, suffix: Expr) : this(Field.of(fieldName), suffix) - constructor(fieldName: String, suffix: String) : this(Field.of(fieldName), suffix) -} - -class ToLower(expr: Expr) : Function("to_lower", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class ToUpper(expr: Expr) : Function("to_upper", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class Trim(expr: Expr) : Function("trim", expr) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class StrConcat internal constructor(first: Expr, vararg rest: Expr) : - Function("str_concat", arrayOf(first, *rest)) { - constructor(first: Expr, vararg rest: String) : this(first, *toArrayOfExprOrConstant(rest)) - constructor(first: Expr, vararg rest: Any) : this(first, *toArrayOfExprOrConstant(rest)) - constructor(fieldName: String, vararg rest: Expr) : this(Field.of(fieldName), *rest) - constructor(fieldName: String, vararg rest: Any) : this(Field.of(fieldName), *rest) - constructor(fieldName: String, vararg rest: String) : this(Field.of(fieldName), *rest) -} - -class MapGet(map: Expr, key: Expr) : Function("map_get", map, key) { - constructor(map: Expr, key: String) : this(map, Constant.of(key)) - constructor(fieldName: String, key: Expr) : this(Field.of(fieldName), key) - constructor(fieldName: String, key: String) : this(Field.of(fieldName), key) -} - -class CosineDistance(vector1: Expr, vector2: Expr) : Function("cosine_distance", vector1, vector2) { - constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) - constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) - constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) -} - -class DotProduct(vector1: Expr, vector2: Expr) : Function("dot_product", vector1, vector2) { - constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) - constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) - constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) -} - -class EuclideanDistance(vector1: Expr, vector2: Expr) : - Function("euclidean_distance", vector1, vector2) { - constructor(vector1: Expr, vector2: DoubleArray) : this(vector1, Constant.vector(vector2)) - constructor(vector1: Expr, vector2: VectorValue) : this(vector1, Constant.of(vector2)) - constructor(fieldName: String, vector2: Expr) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: DoubleArray) : this(Field.of(fieldName), vector2) - constructor(fieldName: String, vector2: VectorValue) : this(Field.of(fieldName), vector2) -} - -class VectorLength(vector: Expr) : Function("vector_length", vector) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class UnixMicrosToTimestamp(input: Expr) : Function("unix_micros_to_timestamp", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class TimestampToUnixMicros(input: Expr) : Function("timestamp_to_unix_micros", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class UnixMillisToTimestamp(input: Expr) : Function("unix_millis_to_timestamp", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class TimestampToUnixMillis(input: Expr) : Function("timestamp_to_unix_millis", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class UnixSecondsToTimestamp(input: Expr) : Function("unix_seconds_to_timestamp", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class TimestampToUnixSeconds(input: Expr) : Function("timestamp_to_unix_seconds", input) { - constructor(fieldName: String) : this(Field.of(fieldName)) -} - -class TimestampAdd(timestamp: Expr, unit: Expr, amount: Expr) : - Function("timestamp_add", timestamp, unit, amount) { - constructor( - timestamp: Expr, - unit: String, - amount: Double - ) : this(timestamp, Constant.of(unit), Constant.of(amount)) - constructor( - fieldName: String, - unit: String, - amount: Double - ) : this(Field.of(fieldName), unit, amount) - constructor(fieldName: String, unit: Expr, amount: Expr) : this(Field.of(fieldName), unit, amount) -} - -class TimestampSub(timestamp: Expr, unit: Expr, amount: Expr) : - Function("timestamp_sub", timestamp, unit, amount) { - constructor( - timestamp: Expr, - unit: String, - amount: Double - ) : this(timestamp, Constant.of(unit), Constant.of(amount)) - constructor( - fieldName: String, - unit: String, - amount: Double - ) : this(Field.of(fieldName), unit, amount) - constructor(fieldName: String, unit: Expr, amount: Expr) : this(Field.of(fieldName), unit, amount) -} From ef7cbdafdf4bcfe99bd8775635fc6dd808410f7c Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 20 Feb 2025 11:06:38 -0500 Subject: [PATCH 019/152] Add tests --- .../firebase/firestore/PipelineTest.java | 49 +++++++++++++++---- .../com/google/firebase/firestore/Pipeline.kt | 8 +++ .../firebase/firestore/pipeline/expression.kt | 28 ++++++++--- .../firebase/firestore/pipeline/stage.kt | 20 +++++--- 4 files changed, 83 insertions(+), 22 deletions(-) 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 94842ba009d..21968864d48 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 @@ -28,6 +28,7 @@ import static com.google.firebase.firestore.pipeline.Function.logicalMin; import static com.google.firebase.firestore.pipeline.Function.lt; import static com.google.firebase.firestore.pipeline.Function.lte; +import static com.google.firebase.firestore.pipeline.Function.mapGet; import static com.google.firebase.firestore.pipeline.Function.neq; import static com.google.firebase.firestore.pipeline.Function.not; import static com.google.firebase.firestore.pipeline.Function.or; @@ -105,7 +106,10 @@ public void tearDown() { entry("published", 1979), entry("rating", 4.2), entry("tags", ImmutableList.of("comedy", "space", "adventure")), - entry("awards", ImmutableMap.of("hugo", true, "nebula", false)))), + entry("awards", ImmutableMap.of("hugo", true, "nebula", false)), + entry( + "nestedField", + ImmutableMap.of("level.1", ImmutableMap.of("level.2", true))))), entry( "book2", mapOfEntries( @@ -714,12 +718,6 @@ public void testMapGet() { ImmutableMap.of("hugoAward", true, "title", "Dune")); } - @Test - public void testParent() {} - - @Test - public void testCollectionId() {} - @Test public void testDistanceFunctions() { double[] sourceVector = {0.1, 0.1}; @@ -745,10 +743,43 @@ public void testDistanceFunctions() { } @Test - public void testNestedFields() {} + public void testNestedFields() { + Task execute = + randomCol + .pipeline() + .where(eq("awards.hugo", true)) + .select("title", "awards.hugo") + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + ImmutableMap.of("title", "The Hitchhiker's Guide to the Galaxy", "awards.hugo", true), + ImmutableMap.of("title", "Dune", "awards.hugo", true)); + } @Test - public void testMapGetWithFieldNameIncludingNotation() {} + public void testMapGetWithFieldNameIncludingNotation() { + Task execute = + randomCol + .pipeline() + .where(eq("awards.hugo", true)) + .select( + "title", + Field.of("nestedField.level.1"), + mapGet("nestedField", "level.1").mapGet("level.2").as("nested")) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + mapOfEntries( + entry("title", "The Hitchhiker's Guide to the Galaxy"), + entry("nestedField.level.`1`", null), + entry("nested", true)), + mapOfEntries( + entry("title", "Dune"), + entry("nestedField.level.`1`", null), + entry("nested", null))); + } static Map.Entry entry(String key, T value) { return new Map.Entry() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 75825ad2cec..4840084233b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -107,6 +107,10 @@ internal constructor( return append(SelectStage(fields.map(Field::of).toTypedArray())) } + fun select(vararg fields: Any): Pipeline { + return append(SelectStage(fields.map(Selectable::toSelectable).toTypedArray())) + } + fun sort(vararg orders: Ordering): Pipeline { return append(SortStage(orders)) } @@ -131,6 +135,10 @@ internal constructor( return append(DistinctStage(groups.map(Field::of).toTypedArray())) } + fun distinct(vararg groups: Any): Pipeline { + return append(DistinctStage(groups.map(Selectable::toSelectable).toTypedArray())) + } + fun aggregate(vararg accumulators: AccumulatorWithAlias): Pipeline { return append(AggregateStage.withAccumulators(*accumulators)) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt index 88a7595cc7f..24e6e494193 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt @@ -266,15 +266,29 @@ abstract class Expr protected constructor() { internal abstract fun toProto(): Value } -abstract class Selectable(internal val alias: String) : Expr() +abstract class Selectable() : Expr() { + internal abstract fun getAlias(): String -open class ExprWithAlias internal constructor(alias: String, private val expr: Expr) : - Selectable(alias) { + internal companion object { + fun toSelectable(o: Any): Selectable { + return when (o) { + is Selectable -> o + is String -> Field.of(o) + is FieldPath -> Field.of(o) + else -> throw IllegalArgumentException("Unknown Selectable type: $o") + } + } + } +} + +open class ExprWithAlias internal constructor(private val alias: String, private val expr: Expr) : + Selectable() { + override fun getAlias() = alias override fun toProto(): Value = expr.toProto() } class Field private constructor(private val fieldPath: ModelFieldPath) : - Selectable(fieldPath.canonicalString()) { + Selectable() { companion object { @JvmStatic @@ -293,11 +307,14 @@ class Field private constructor(private val fieldPath: ModelFieldPath) : return Field(fieldPath.internalPath) } } + + override fun getAlias(): String = fieldPath.canonicalString() + override fun toProto() = Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() } -class ListOfExprs(val expressions: Array) : Expr() { +class ListOfExprs(private val expressions: Array) : Expr() { override fun toProto(): Value { val builder = ArrayValue.newBuilder() for (expr in expressions) { @@ -740,7 +757,6 @@ class BooleanExpr internal constructor(name: String, params: Array) : fun countIf(): Accumulator = Accumulator.countIf(this) - fun ifThen(then: Expr) = Function.ifThen(this, then) fun ifThen(then: Any) = Function.ifThen(this, then) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index dd9c48ab5bf..81fc2ef7499 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -57,7 +57,7 @@ class DocumentsSource internal constructor(private val documents: Array) : Stage("add_fields") { override fun args(): Sequence = - sequenceOf(encodeValue(fields.associate { it.alias to it.toProto() })) + sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto() })) } class AggregateStage @@ -65,7 +65,7 @@ internal constructor( private val accumulators: Map, private val groups: Map ) : Stage("aggregate") { - internal constructor(accumulators: Map) : this(accumulators, emptyMap()) + private constructor(accumulators: Map) : this(accumulators, emptyMap()) companion object { @JvmStatic fun withAccumulators(vararg accumulators: AccumulatorWithAlias): AggregateStage { @@ -78,12 +78,18 @@ internal constructor( } } - fun withGroups(vararg selectable: Selectable) = - AggregateStage(accumulators, selectable.associateBy(Selectable::alias)) + fun withGroups(vararg groups: Selectable) = + AggregateStage(accumulators, groups.associateBy(Selectable::getAlias)) fun withGroups(vararg fields: String) = AggregateStage(accumulators, fields.associateWith(Field::of)) + fun withGroups(vararg selectable: Any) = + AggregateStage( + accumulators, + selectable.map(Selectable::toSelectable).associateBy(Selectable::getAlias) + ) + override fun args(): Sequence = sequenceOf( encodeValue(accumulators.mapValues { entry -> entry.value.toProto() }), @@ -141,7 +147,7 @@ class OffsetStage internal constructor(private val offset: Long) : Stage("offset class SelectStage internal constructor(private val fields: Array) : Stage("select") { override fun args(): Sequence = - sequenceOf(encodeValue(fields.associate { it.alias to it.toProto() })) + sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto() })) } class SortStage internal constructor(private val orders: Array) : Stage("sort") { @@ -151,7 +157,7 @@ class SortStage internal constructor(private val orders: Array) : class DistinctStage internal constructor(private val groups: Array) : Stage("distinct") { override fun args(): Sequence = - sequenceOf(encodeValue(groups.associate { it.alias to it.toProto() })) + sequenceOf(encodeValue(groups.associate { it.getAlias() to it.toProto() })) } class RemoveFieldsStage internal constructor(private val fields: Array) : @@ -192,5 +198,5 @@ class UnionStage internal constructor(private val other: com.google.firebase.fir class UnnestStage internal constructor(private val selectable: Selectable) : Stage("unnest") { override fun args(): Sequence = - sequenceOf(encodeValue(selectable.alias), selectable.toProto()) + sequenceOf(encodeValue(selectable.getAlias()), selectable.toProto()) } From ca31612434dc1f95d75c6e2eacea155d703a5c39 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 20 Feb 2025 11:18:53 -0500 Subject: [PATCH 020/152] Cleanup --- .../google/firebase/firestore/Firestore.kt | 1 - .../com/google/firebase/firestore/Pipeline.kt | 84 ++++++------------- .../firestore/pipeline/accumulators.kt | 64 +++----------- .../firebase/firestore/pipeline/expression.kt | 14 ++-- 4 files changed, 47 insertions(+), 116 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Firestore.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Firestore.kt index 9f5027b5e29..e2ccf89637d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Firestore.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Firestore.kt @@ -21,7 +21,6 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.components.Component import com.google.firebase.components.ComponentRegistrar -import com.google.firebase.firestore.* import com.google.firebase.firestore.util.Executors.BACKGROUND_EXECUTOR import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.awaitClose diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 4840084233b..6fa92b88b8b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -47,7 +47,7 @@ import com.google.firestore.v1.Value class Pipeline internal constructor( internal val firestore: FirebaseFirestore, - internal val stages: FluentIterable + private val stages: FluentIterable ) { internal constructor( firestore: FirebaseFirestore, @@ -68,7 +68,7 @@ internal constructor( return DocumentReference(key, firestore) } - internal fun toProto(): ExecutePipelineRequest { + private fun toProto(): ExecutePipelineRequest { val database = firestore.databaseId val builder = ExecutePipelineRequest.newBuilder() builder.database = "projects/${database.projectId}/databases/${database.databaseId}" @@ -87,67 +87,43 @@ internal constructor( .addAllStages(stages.map(Stage::toProtoStage)) .build() - fun addFields(vararg fields: Selectable): Pipeline { - return append(AddFieldsStage(fields)) - } + fun addFields(vararg fields: Selectable): Pipeline = append(AddFieldsStage(fields)) - fun removeFields(vararg fields: Field): Pipeline { - return append(RemoveFieldsStage(fields)) - } + fun removeFields(vararg fields: Field): Pipeline = append(RemoveFieldsStage(fields)) - fun removeFields(vararg fields: String): Pipeline { - return append(RemoveFieldsStage(fields.map(Field::of).toTypedArray())) - } + fun removeFields(vararg fields: String): Pipeline = + append(RemoveFieldsStage(fields.map(Field::of).toTypedArray())) - fun select(vararg fields: Selectable): Pipeline { - return append(SelectStage(fields)) - } + fun select(vararg fields: Selectable): Pipeline = append(SelectStage(fields)) - fun select(vararg fields: String): Pipeline { - return append(SelectStage(fields.map(Field::of).toTypedArray())) - } + fun select(vararg fields: String): Pipeline = + append(SelectStage(fields.map(Field::of).toTypedArray())) - fun select(vararg fields: Any): Pipeline { - return append(SelectStage(fields.map(Selectable::toSelectable).toTypedArray())) - } + fun select(vararg fields: Any): Pipeline = + append(SelectStage(fields.map(Selectable::toSelectable).toTypedArray())) - fun sort(vararg orders: Ordering): Pipeline { - return append(SortStage(orders)) - } + fun sort(vararg orders: Ordering): Pipeline = append(SortStage(orders)) - fun where(condition: BooleanExpr): Pipeline { - return append(WhereStage(condition)) - } + fun where(condition: BooleanExpr): Pipeline = append(WhereStage(condition)) - fun offset(offset: Long): Pipeline { - return append(OffsetStage(offset)) - } + fun offset(offset: Long): Pipeline = append(OffsetStage(offset)) - fun limit(limit: Long): Pipeline { - return append(LimitStage(limit)) - } + fun limit(limit: Long): Pipeline = append(LimitStage(limit)) - fun distinct(vararg groups: Selectable): Pipeline { - return append(DistinctStage(groups)) - } + fun distinct(vararg groups: Selectable): Pipeline = append(DistinctStage(groups)) - fun distinct(vararg groups: String): Pipeline { - return append(DistinctStage(groups.map(Field::of).toTypedArray())) - } + fun distinct(vararg groups: String): Pipeline = + append(DistinctStage(groups.map(Field::of).toTypedArray())) - fun distinct(vararg groups: Any): Pipeline { - return append(DistinctStage(groups.map(Selectable::toSelectable).toTypedArray())) - } + fun distinct(vararg groups: Any): Pipeline = + append(DistinctStage(groups.map(Selectable::toSelectable).toTypedArray())) - fun aggregate(vararg accumulators: AccumulatorWithAlias): Pipeline { - return append(AggregateStage.withAccumulators(*accumulators)) - } + fun aggregate(vararg accumulators: AccumulatorWithAlias): Pipeline = + append(AggregateStage.withAccumulators(*accumulators)) - fun aggregate(aggregateStage: AggregateStage): Pipeline { - return append(aggregateStage) - } + fun aggregate(aggregateStage: AggregateStage): Pipeline = append(aggregateStage) - private inner class ObserverSnapshotTask() : PipelineResultObserver { + private inner class ObserverSnapshotTask : PipelineResultObserver { private val taskCompletionSource = TaskCompletionSource() private val results: ImmutableList.Builder = ImmutableList.builder() override fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) { @@ -197,9 +173,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto return Pipeline(firestore, CollectionGroupSource(collectionId)) } - fun database(): Pipeline { - return Pipeline(firestore, DatabaseSource()) - } + fun database(): Pipeline = Pipeline(firestore, DatabaseSource()) fun documents(vararg documents: String): Pipeline { // Validate document path by converting to DocumentReference @@ -233,16 +207,12 @@ internal constructor( private val version: SnapshotVersion, ) { - fun getData(): Map { - return userDataWriter().convertObject(fields) - } + fun getData(): Map = userDataWriter().convertObject(fields) private fun userDataWriter(): UserDataWriter = UserDataWriter(firestore, DocumentSnapshot.ServerTimestampBehavior.DEFAULT) - override fun toString(): String { - return "PipelineResult{ref=$ref, version=$version}, data=${getData()}" - } + override fun toString() = "PipelineResult{ref=$ref, version=$version}, data=${getData()}" } internal interface PipelineResultObserver { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt index b89c318636a..7ea8668b1b2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt @@ -26,70 +26,32 @@ private constructor(private val name: String, private val params: Array) : internal constructor(name: String, param: Expr, vararg params: Any) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) internal constructor(name: String, fieldName: String, vararg params: Any) : this(name, arrayOf(Field.of(fieldName), *toArrayOfExprOrConstant(params))) - fun not() = Function.not(this) + fun not() = not(this) fun countIf(): Accumulator = Accumulator.countIf(this) - fun ifThen(then: Expr) = Function.ifThen(this, then) + fun ifThen(then: Expr) = ifThen(this, then) - fun ifThen(then: Any) = Function.ifThen(this, then) + fun ifThen(then: Any) = ifThen(this, then) - fun ifThenElse(then: Expr, `else`: Expr) = Function.ifThenElse(this, then, `else`) + fun ifThenElse(then: Expr, `else`: Expr) = ifThenElse(this, then, `else`) - fun ifThenElse(then: Any, `else`: Any) = Function.ifThenElse(this, then, `else`) + fun ifThenElse(then: Any, `else`: Any) = ifThenElse(this, then, `else`) } class Ordering private constructor(private val expr: Expr, private val dir: Direction) { @@ -779,7 +779,7 @@ class Ordering private constructor(private val expr: Expr, private val dir: Dire fun descending(fieldName: String): Ordering = Ordering(Field.of(fieldName), Direction.DESCENDING) } - private class Direction private constructor(internal val proto: Value) { + private class Direction private constructor(val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) companion object { val ASCENDING = Direction("ascending") From c7605b653ae9a1dfb2c8ae51ddb6dd86cb5f7a4c Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 28 Feb 2025 13:11:33 -0500 Subject: [PATCH 021/152] Generic Stage and Refactor --- .../firebase/firestore/PipelineTest.java | 59 +++++++-- .../firebase/firestore/FirebaseFirestore.java | 5 +- .../com/google/firebase/firestore/Pipeline.kt | 11 +- .../firebase/firestore/UserDataReader.java | 22 ++++ .../google/firebase/firestore/model/Values.kt | 10 +- .../firebase/firestore/pipeline/Constant.kt | 58 ++++----- .../{accumulators.kt => aggregates.kt} | 37 +++--- .../{expression.kt => expressions.kt} | 78 ++++++++---- .../firebase/firestore/pipeline/stage.kt | 115 +++++++++++++----- 9 files changed, 274 insertions(+), 121 deletions(-) rename firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/{accumulators.kt => aggregates.kt} (50%) rename firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/{expression.kt => expressions.kt} (92%) 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 21968864d48..50df7ef880d 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 @@ -37,14 +37,13 @@ import static com.google.firebase.firestore.pipeline.Function.subtract; import static com.google.firebase.firestore.pipeline.Ordering.ascending; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor; -import static java.util.Map.entry; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.gms.tasks.Task; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.truth.Correspondence; -import com.google.firebase.firestore.pipeline.Accumulator; +import com.google.firebase.firestore.pipeline.AggregateExpr; import com.google.firebase.firestore.pipeline.AggregateStage; import com.google.firebase.firestore.pipeline.Constant; import com.google.firebase.firestore.pipeline.Field; @@ -227,7 +226,7 @@ public void aggregateResultsCountAll() { firestore .pipeline() .collection(randomCol) - .aggregate(Accumulator.countAll().as("count")) + .aggregate(AggregateExpr.countAll().as("count")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -243,8 +242,8 @@ public void aggregateResultsMany() { .collection(randomCol) .where(Function.eq("genre", "Science Fiction")) .aggregate( - Accumulator.countAll().as("count"), - Accumulator.avg("rating").as("avgRating"), + AggregateExpr.countAll().as("count"), + AggregateExpr.avg("rating").as("avgRating"), Field.of("rating").max().as("maxRating")) .execute(); assertThat(waitFor(execute).getResults()) @@ -261,7 +260,7 @@ public void groupAndAccumulateResults() { .collection(randomCol) .where(lt(Field.of("published"), 1984)) .aggregate( - AggregateStage.withAccumulators(Accumulator.avg("rating").as("avgRating")) + AggregateStage.withAccumulators(AggregateExpr.avg("rating").as("avgRating")) .withGroups("genre")) .where(gt("avgRating", 4.3)) .sort(Field.of("avgRating").descending()) @@ -274,6 +273,28 @@ public void groupAndAccumulateResults() { mapOfEntries(entry("avgRating", 4.4), entry("genre", "Science Fiction"))); } + @Test + public void groupAndAccumulateResultsGeneric() { + Task execute = + firestore + .pipeline() + .collection(randomCol) + .genericStage("where", lt(Field.of("published"), 1984)) + .genericStage( + "aggregate", + ImmutableMap.of("avgRating", AggregateExpr.avg("rating")), + ImmutableMap.of("genre", Field.of("genre"))) + .genericStage("where", gt("avgRating", 4.3)) + .genericStage("sort", Field.of("avgRating").descending()) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(DATA_CORRESPONDENCE) + .containsExactly( + mapOfEntries(entry("avgRating", 4.7), entry("genre", "Fantasy")), + mapOfEntries(entry("avgRating", 4.5), entry("genre", "Romance")), + mapOfEntries(entry("avgRating", 4.4), entry("genre", "Science Fiction"))); + } + @Test @Ignore("Not supported yet") public void minAndMaxAccumulations() { @@ -282,7 +303,7 @@ public void minAndMaxAccumulations() { .pipeline() .collection(randomCol) .aggregate( - Accumulator.countAll().as("count"), + AggregateExpr.countAll().as("count"), Field.of("rating").max().as("maxRating"), Field.of("published").min().as("minPublished")) .execute(); @@ -781,6 +802,30 @@ public void testMapGetWithFieldNameIncludingNotation() { entry("nested", null))); } + @Test + public void testListEquals() { + Task execute = + randomCol + .pipeline() + .where(eq("tags", ImmutableList.of("philosophy", "crime", "redemption"))) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(ID_CORRESPONDENCE) + .containsExactly("book6"); + } + + @Test + public void testMapEquals() { + Task execute = + randomCol + .pipeline() + .where(eq("awards", ImmutableMap.of("nobel", true, "nebula", false))) + .execute(); + assertThat(waitFor(execute).getResults()) + .comparingElementsUsing(ID_CORRESPONDENCE) + .containsExactly("book3"); + } + static Map.Entry entry(String key, T value) { return new Map.Entry() { private String k = key; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 114fc18da95..932be5983f5 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -23,6 +23,7 @@ import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; @@ -855,7 +856,9 @@ DatabaseId getDatabaseId() { return databaseId; } - UserDataReader getUserDataReader() { + @NonNull + @RestrictTo(RestrictTo.Scope.LIBRARY) + public UserDataReader getUserDataReader() { return userDataReader; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 6fa92b88b8b..6a3f765fd38 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -20,9 +20,9 @@ import com.google.common.collect.FluentIterable import com.google.common.collect.ImmutableList import com.google.firebase.firestore.model.DocumentKey import com.google.firebase.firestore.model.SnapshotVersion -import com.google.firebase.firestore.pipeline.AccumulatorWithAlias import com.google.firebase.firestore.pipeline.AddFieldsStage import com.google.firebase.firestore.pipeline.AggregateStage +import com.google.firebase.firestore.pipeline.AggregateWithAlias import com.google.firebase.firestore.pipeline.BooleanExpr import com.google.firebase.firestore.pipeline.CollectionGroupSource import com.google.firebase.firestore.pipeline.CollectionSource @@ -30,6 +30,8 @@ import com.google.firebase.firestore.pipeline.DatabaseSource import com.google.firebase.firestore.pipeline.DistinctStage import com.google.firebase.firestore.pipeline.DocumentsSource import com.google.firebase.firestore.pipeline.Field +import com.google.firebase.firestore.pipeline.GenericArg +import com.google.firebase.firestore.pipeline.GenericStage import com.google.firebase.firestore.pipeline.LimitStage import com.google.firebase.firestore.pipeline.OffsetStage import com.google.firebase.firestore.pipeline.Ordering @@ -84,9 +86,12 @@ internal constructor( internal fun toPipelineProto(): com.google.firestore.v1.Pipeline = com.google.firestore.v1.Pipeline.newBuilder() - .addAllStages(stages.map(Stage::toProtoStage)) + .addAllStages(stages.map { it.toProtoStage(firestore.userDataReader) }) .build() + fun genericStage(name: String, vararg params: Any) = + append(GenericStage(name, params.map(GenericArg::from))) + fun addFields(vararg fields: Selectable): Pipeline = append(AddFieldsStage(fields)) fun removeFields(vararg fields: Field): Pipeline = append(RemoveFieldsStage(fields)) @@ -118,7 +123,7 @@ internal constructor( fun distinct(vararg groups: Any): Pipeline = append(DistinctStage(groups.map(Selectable::toSelectable).toTypedArray())) - fun aggregate(vararg accumulators: AccumulatorWithAlias): Pipeline = + fun aggregate(vararg accumulators: AggregateWithAlias): Pipeline = append(AggregateStage.withAccumulators(*accumulators)) fun aggregate(aggregateStage: AggregateStage): Pipeline = append(aggregateStage) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java index b1462ed9f74..3ce7cfec87c 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; +import com.google.common.base.Function; import com.google.firebase.firestore.FieldValue.ArrayRemoveFieldValue; import com.google.firebase.firestore.FieldValue.ArrayUnionFieldValue; import com.google.firebase.firestore.FieldValue.DeleteFieldValue; @@ -36,6 +37,7 @@ import com.google.firebase.firestore.model.mutation.FieldMask; import com.google.firebase.firestore.model.mutation.NumericIncrementTransformOperation; import com.google.firebase.firestore.model.mutation.ServerTimestampOperation; +import com.google.firebase.firestore.pipeline.Expr; import com.google.firebase.firestore.util.Assert; import com.google.firebase.firestore.util.CustomClassMapper; import com.google.firebase.firestore.util.Util; @@ -389,6 +391,12 @@ public Value parseScalarValue(Object input, ParseContext context) { return Values.NULL_VALUE; } else if (input.getClass().isArray()) { throw context.createError("Arrays are not supported; use a List instead"); + } else if (input instanceof DocumentReference) { + DocumentReference ref = (DocumentReference) input; + validateDocumentReference(ref, context::createError); + return Values.encodeValue(ref); + } else if (input instanceof Expr) { + throw context.createError("Pipeline expressions are not supported user objects"); } else { try { return Values.encodeAnyValue(input); @@ -398,6 +406,20 @@ public Value parseScalarValue(Object input, ParseContext context) { } } + public void validateDocumentReference( + DocumentReference ref, Function createError) { + DatabaseId otherDb = ref.getFirestore().getDatabaseId(); + if (!otherDb.equals(databaseId)) { + throw createError.apply( + String.format( + "Document reference is for database %s/%s but should be for database %s/%s", + otherDb.getProjectId(), + otherDb.getDatabaseId(), + databaseId.getProjectId(), + databaseId.getDatabaseId())); + } + } + private List parseArrayTransformElements(List elements) { ParseAccumulator accumulator = new ParseAccumulator(UserData.Source.Argument); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 3b9701bc4ec..d5ae4064d95 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -609,9 +609,7 @@ internal object Values { return Value.newBuilder() .setTimestampValue( - com.google.protobuf.Timestamp.newBuilder() - .setSeconds(timestamp.seconds) - .setNanos(truncatedNanoseconds) + Timestamp.newBuilder().setSeconds(timestamp.seconds).setNanos(truncatedNanoseconds) ) .build() } @@ -665,6 +663,11 @@ internal object Values { return Value.newBuilder().setMapValue(MapValue.newBuilder().putAllFields(map)).build() } + @JvmStatic + fun encodeValue(values: Iterable): Value { + return Value.newBuilder().setArrayValue(ArrayValue.newBuilder().addAllValues(values)).build() + } + @JvmStatic fun encodeAnyValue(value: Any?): Value { return when (value) { @@ -676,7 +679,6 @@ internal object Values { is Boolean -> encodeValue(value) is GeoPoint -> encodeValue(value) is Blob -> encodeValue(value) - is DocumentReference -> encodeValue(value) is VectorValue -> encodeValue(value) else -> throw IllegalArgumentException("Unexpected type: $value") } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt index bc9a39a50d4..e9bcf7aac43 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt @@ -18,76 +18,70 @@ import com.google.firebase.Timestamp import com.google.firebase.firestore.Blob import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.GeoPoint +import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.VectorValue import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue import com.google.firestore.v1.Value import java.util.Date -class Constant internal constructor(val value: Value) : Expr() { +abstract class Constant internal constructor() : Expr() { + + private class ValueConstant(val value: Value) : Constant() { + override fun toProto(userDataReader: UserDataReader): Value = value + } companion object { - internal val NULL = Constant(Values.NULL_VALUE) - - fun of(value: Any): Constant { - return when (value) { - is String -> of(value) - is Number -> of(value) - is Date -> of(value) - is Timestamp -> of(value) - is Boolean -> of(value) - is GeoPoint -> of(value) - is Blob -> of(value) - is DocumentReference -> of(value) - is Value -> of(value) - is VectorValue -> of(value) - else -> throw IllegalArgumentException("Unknown type: $value") - } - } + internal val NULL: Constant = ValueConstant(Values.NULL_VALUE) @JvmStatic fun of(value: String): Constant { - return Constant(encodeValue(value)) + return ValueConstant(encodeValue(value)) } @JvmStatic fun of(value: Number): Constant { - return Constant(encodeValue(value)) + return ValueConstant(encodeValue(value)) } @JvmStatic fun of(value: Date): Constant { - return Constant(encodeValue(value)) + return ValueConstant(encodeValue(value)) } @JvmStatic fun of(value: Timestamp): Constant { - return Constant(encodeValue(value)) + return ValueConstant(encodeValue(value)) } @JvmStatic fun of(value: Boolean): Constant { - return Constant(encodeValue(value)) + return ValueConstant(encodeValue(value)) } @JvmStatic fun of(value: GeoPoint): Constant { - return Constant(encodeValue(value)) + return ValueConstant(encodeValue(value)) } @JvmStatic fun of(value: Blob): Constant { - return Constant(encodeValue(value)) + return ValueConstant(encodeValue(value)) } @JvmStatic - fun of(value: DocumentReference): Constant { - return Constant(encodeValue(value)) + fun of(ref: DocumentReference): Constant { + return object : Constant() { + override fun toProto(userDataReader: UserDataReader): Value { + userDataReader.validateDocumentReference(ref, ::IllegalArgumentException) + return encodeValue(ref) + } + } } @JvmStatic fun of(value: VectorValue): Constant { - return Constant(encodeValue(value)) + return ValueConstant(encodeValue(value)) } @JvmStatic @@ -97,16 +91,12 @@ class Constant internal constructor(val value: Value) : Expr() { @JvmStatic fun vector(value: DoubleArray): Constant { - return Constant(Values.encodeVectorValue(value)) + return ValueConstant(Values.encodeVectorValue(value)) } @JvmStatic fun vector(value: VectorValue): Constant { - return Constant(encodeValue(value)) + return ValueConstant(encodeValue(value)) } } - - override fun toProto(): Value { - return value - } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt similarity index 50% rename from firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt rename to firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt index 7ea8668b1b2..c363c28be3e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/accumulators.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt @@ -14,50 +14,51 @@ package com.google.firebase.firestore.pipeline +import com.google.firebase.firestore.UserDataReader import com.google.firestore.v1.Value -class AccumulatorWithAlias -internal constructor(internal val alias: String, internal val accumulator: Accumulator) +class AggregateWithAlias +internal constructor(internal val alias: String, internal val expr: AggregateExpr) -class Accumulator +class AggregateExpr private constructor(private val name: String, private val params: Array) { private constructor(name: String) : this(name, emptyArray()) private constructor(name: String, expr: Expr) : this(name, arrayOf(expr)) private constructor(name: String, fieldName: String) : this(name, Field.of(fieldName)) companion object { - @JvmStatic fun countAll() = Accumulator("count") + @JvmStatic fun countAll() = AggregateExpr("count") - @JvmStatic fun count(fieldName: String) = Accumulator("count", fieldName) + @JvmStatic fun count(fieldName: String) = AggregateExpr("count", fieldName) - @JvmStatic fun count(expr: Expr) = Accumulator("count", expr) + @JvmStatic fun count(expr: Expr) = AggregateExpr("count", expr) - @JvmStatic fun countIf(condition: BooleanExpr) = Accumulator("countIf", condition) + @JvmStatic fun countIf(condition: BooleanExpr) = AggregateExpr("countIf", condition) - @JvmStatic fun sum(fieldName: String) = Accumulator("sum", fieldName) + @JvmStatic fun sum(fieldName: String) = AggregateExpr("sum", fieldName) - @JvmStatic fun sum(expr: Expr) = Accumulator("sum", expr) + @JvmStatic fun sum(expr: Expr) = AggregateExpr("sum", expr) - @JvmStatic fun avg(fieldName: String) = Accumulator("avg", fieldName) + @JvmStatic fun avg(fieldName: String) = AggregateExpr("avg", fieldName) - @JvmStatic fun avg(expr: Expr) = Accumulator("avg", expr) + @JvmStatic fun avg(expr: Expr) = AggregateExpr("avg", expr) - @JvmStatic fun min(fieldName: String) = Accumulator("min", fieldName) + @JvmStatic fun min(fieldName: String) = AggregateExpr("min", fieldName) - @JvmStatic fun min(expr: Expr) = Accumulator("min", expr) + @JvmStatic fun min(expr: Expr) = AggregateExpr("min", expr) - @JvmStatic fun max(fieldName: String) = Accumulator("max", fieldName) + @JvmStatic fun max(fieldName: String) = AggregateExpr("max", fieldName) - @JvmStatic fun max(expr: Expr) = Accumulator("max", expr) + @JvmStatic fun max(expr: Expr) = AggregateExpr("max", expr) } - fun `as`(alias: String) = AccumulatorWithAlias(alias, this) + fun `as`(alias: String) = AggregateWithAlias(alias, this) - fun toProto(): Value { + internal fun toProto(userDataReader: UserDataReader): Value { val builder = com.google.firestore.v1.Function.newBuilder() builder.setName(name) for (param in params) { - builder.addArgs(param.toProto()) + builder.addArgs(param.toProto(userDataReader)) } return Value.newBuilder().setFunctionValue(builder).build() } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt similarity index 92% rename from firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt rename to firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt index afc796742b0..a4a6912fbce 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expression.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt @@ -14,21 +14,49 @@ package com.google.firebase.firestore.pipeline +import com.google.firebase.Timestamp +import com.google.firebase.firestore.Blob +import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.FieldPath +import com.google.firebase.firestore.GeoPoint +import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.VectorValue import com.google.firebase.firestore.model.DocumentKey -import com.google.firebase.firestore.model.FieldPath as ModelFieldPath import com.google.firebase.firestore.model.Values.encodeValue -import com.google.firestore.v1.ArrayValue +import com.google.firebase.firestore.pipeline.Constant.Companion.of +import com.google.firebase.firestore.util.CustomClassMapper import com.google.firestore.v1.MapValue import com.google.firestore.v1.Value +import java.util.Date +import kotlin.reflect.KFunction1 +import com.google.firebase.firestore.model.FieldPath as ModelFieldPath -abstract class Expr protected constructor() { +abstract class Expr internal constructor() { internal companion object { - internal fun toExprOrConstant(value: Any): Expr { + internal fun toExprOrConstant(value: Any?): Expr = toExpr(value, ::toExprOrConstant) ?: pojoToExprOrConstant(CustomClassMapper.convertToPlainJavaTypes(value)) + + private fun pojoToExprOrConstant(value: Any?): Expr = toExpr(value, ::pojoToExprOrConstant) ?: throw IllegalArgumentException("Unknown type: $value") + + private fun toExpr(value: Any?, toExpr: KFunction1): Expr? { + if (value == null) return Constant.nullValue() return when (value) { is Expr -> value - else -> Constant.of(value) + is String -> of(value) + is Number -> of(value) + is Date -> of(value) + is Timestamp -> of(value) + is Boolean -> of(value) + is GeoPoint -> of(value) + is Blob -> of(value) + is DocumentReference -> of(value) + is VectorValue -> of(value) + is Map<*, *> -> MapOfExpr(value.entries.associate { + val key = it.key + if (key is String) key to toExpr(it.value) else + throw IllegalArgumentException("Maps with non-string keys are not supported") + }) + is List<*> -> ListOfExprs(value.map(toExpr).toTypedArray()) + else -> null } } @@ -227,13 +255,13 @@ abstract class Expr protected constructor() { fun arrayLength() = Function.arrayLength(this) - fun sum() = Accumulator.sum(this) + fun sum() = AggregateExpr.sum(this) - fun avg() = Accumulator.avg(this) + fun avg() = AggregateExpr.avg(this) - fun min() = Accumulator.min(this) + fun min() = AggregateExpr.min(this) - fun max() = Accumulator.max(this) + fun max() = AggregateExpr.max(this) fun ascending() = Ordering.ascending(this) @@ -263,7 +291,7 @@ abstract class Expr protected constructor() { fun lte(other: Any) = Function.lte(this, other) - internal abstract fun toProto(): Value + internal abstract fun toProto(userDataReader: UserDataReader): Value } abstract class Selectable : Expr() { @@ -284,7 +312,7 @@ abstract class Selectable : Expr() { open class ExprWithAlias internal constructor(private val alias: String, private val expr: Expr) : Selectable() { override fun getAlias() = alias - override fun toProto(): Value = expr.toProto() + override fun toProto(userDataReader: UserDataReader): Value = expr.toProto(userDataReader) } class Field private constructor(private val fieldPath: ModelFieldPath) : @@ -310,20 +338,26 @@ class Field private constructor(private val fieldPath: ModelFieldPath) : override fun getAlias(): String = fieldPath.canonicalString() - override fun toProto() = + override fun toProto(userDataReader: UserDataReader) = toProto() + + internal fun toProto(): Value = Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() } -class ListOfExprs(private val expressions: Array) : Expr() { - override fun toProto(): Value { - val builder = ArrayValue.newBuilder() +class MapOfExpr(private val expressions: Map) : Expr() { + override fun toProto(userDataReader: UserDataReader): Value { + val builder = MapValue.newBuilder() for (expr in expressions) { - builder.addValues(expr.toProto()) + builder.putFields(expr.key, expr.value.toProto(userDataReader)) } - return Value.newBuilder().setArrayValue(builder).build() + return Value.newBuilder().setMapValue(builder).build() } } +class ListOfExprs(private val expressions: Array) : Expr() { + override fun toProto(userDataReader: UserDataReader): Value = encodeValue(expressions.map{it.toProto(userDataReader)}) +} + open class Function protected constructor(private val name: String, private val params: Array) : Expr() { private constructor(name: String, param: Expr, vararg params: Any) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) @@ -738,11 +772,11 @@ protected constructor(private val name: String, private val params: Array) : fun not() = not(this) - fun countIf(): Accumulator = Accumulator.countIf(this) + fun countIf(): AggregateExpr = AggregateExpr.countIf(this) fun ifThen(then: Expr) = ifThen(this, then) @@ -786,12 +820,12 @@ class Ordering private constructor(private val expr: Expr, private val dir: Dire val DESCENDING = Direction("descending") } } - internal fun toProto(): Value = + internal fun toProto(userDataReader: UserDataReader): Value = Value.newBuilder() .setMapValue( MapValue.newBuilder() .putFields("direction", dir.proto) - .putFields("expression", expr.toProto()) + .putFields("expression", expr.toProto(userDataReader)) ) .build() } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index 81fc2ef7499..fa8026fb1ad 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -15,6 +15,7 @@ package com.google.firebase.firestore.pipeline import com.google.common.collect.ImmutableMap +import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.model.Values.encodeVectorValue import com.google.firestore.v1.Pipeline @@ -23,58 +24,101 @@ import com.google.firestore.v1.Value abstract class Stage internal constructor(private val name: String, private val options: Map) { internal constructor(name: String) : this(name, emptyMap()) - internal fun toProtoStage(): Pipeline.Stage { + internal fun toProtoStage(userDataReader: UserDataReader): Pipeline.Stage { val builder = Pipeline.Stage.newBuilder() builder.setName(name) - args().forEach { arg -> builder.addArgs(arg) } + args(userDataReader).forEach { arg -> builder.addArgs(arg) } builder.putAllOptions(options) return builder.build() } - protected abstract fun args(): Sequence + protected abstract fun args(userDataReader: UserDataReader): Sequence +} + +class GenericStage internal constructor(name: String, private val params: List) : + Stage(name) { + override fun args(userDataReader: UserDataReader): Sequence = + params.asSequence().map { it.toProto(userDataReader) } +} + +internal sealed class GenericArg { + companion object { + fun from(arg: Any?): GenericArg = + when (arg) { + is AggregateExpr -> AggregateArg(arg) + is Ordering -> OrderingArg(arg) + is Map<*, *> -> MapArg(arg.asIterable().associate { it.key as String to from(it.value) }) + is List<*> -> ListArg(arg.map(::from)) + else -> ExprArg(Expr.toExprOrConstant(arg)) + } + } + abstract fun toProto(userDataReader: UserDataReader): Value + + data class AggregateArg(val aggregate: AggregateExpr) : GenericArg() { + override fun toProto(userDataReader: UserDataReader) = aggregate.toProto(userDataReader) + } + + data class ExprArg(val expr: Expr) : GenericArg() { + override fun toProto(userDataReader: UserDataReader) = expr.toProto(userDataReader) + } + + data class OrderingArg(val ordering: Ordering) : GenericArg() { + override fun toProto(userDataReader: UserDataReader) = ordering.toProto(userDataReader) + } + + data class MapArg(val args: Map) : GenericArg() { + override fun toProto(userDataReader: UserDataReader) = + encodeValue(args.mapValues { it.value.toProto(userDataReader) }) + } + + data class ListArg(val args: List) : GenericArg() { + override fun toProto(userDataReader: UserDataReader) = + encodeValue(args.map { it.toProto(userDataReader) }) + } } class DatabaseSource : Stage("database") { - override fun args(): Sequence = emptySequence() + override fun args(userDataReader: UserDataReader): Sequence = emptySequence() } class CollectionSource internal constructor(path: String) : Stage("collection") { private val path: String = if (path.startsWith("/")) path else "/" + path - override fun args(): Sequence = + override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setReferenceValue(path).build()) } class CollectionGroupSource internal constructor(val collectionId: String) : Stage("collection_group") { - override fun args(): Sequence = + override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setReferenceValue("").build(), encodeValue(collectionId)) } class DocumentsSource internal constructor(private val documents: Array) : Stage("documents") { - override fun args(): Sequence = documents.asSequence().map(::encodeValue) + override fun args(userDataReader: UserDataReader): Sequence = + documents.asSequence().map(::encodeValue) } class AddFieldsStage internal constructor(private val fields: Array) : Stage("add_fields") { - override fun args(): Sequence = - sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto() })) + override fun args(userDataReader: UserDataReader): Sequence = + sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto(userDataReader) })) } class AggregateStage internal constructor( - private val accumulators: Map, + private val accumulators: Map, private val groups: Map ) : Stage("aggregate") { - private constructor(accumulators: Map) : this(accumulators, emptyMap()) + private constructor(accumulators: Map) : this(accumulators, emptyMap()) companion object { @JvmStatic - fun withAccumulators(vararg accumulators: AccumulatorWithAlias): AggregateStage { + fun withAccumulators(vararg accumulators: AggregateWithAlias): AggregateStage { if (accumulators.isEmpty()) { throw IllegalArgumentException( "Must specify at least one accumulator for aggregate() stage. There is a distinct() stage if only distinct group values are needed." ) } - return AggregateStage(accumulators.associate { it.alias to it.accumulator }) + return AggregateStage(accumulators.associate { it.alias to it.expr }) } } @@ -90,15 +134,16 @@ internal constructor( selectable.map(Selectable::toSelectable).associateBy(Selectable::getAlias) ) - override fun args(): Sequence = + override fun args(userDataReader: UserDataReader): Sequence = sequenceOf( - encodeValue(accumulators.mapValues { entry -> entry.value.toProto() }), - encodeValue(groups.mapValues { entry -> entry.value.toProto() }) + encodeValue(accumulators.mapValues { entry -> entry.value.toProto(userDataReader) }), + encodeValue(groups.mapValues { entry -> entry.value.toProto(userDataReader) }) ) } class WhereStage internal constructor(private val condition: BooleanExpr) : Stage("where") { - override fun args(): Sequence = sequenceOf(condition.toProto()) + override fun args(userDataReader: UserDataReader): Sequence = + sequenceOf(condition.toProto(userDataReader)) } class FindNearestStage @@ -118,8 +163,8 @@ internal constructor( } } - override fun args(): Sequence = - sequenceOf(property.toProto(), encodeVectorValue(vector), distanceMeasure.proto) + override fun args(userDataReader: UserDataReader): Sequence = + sequenceOf(property.toProto(userDataReader), encodeVectorValue(vector), distanceMeasure.proto) } class FindNearestOptions @@ -137,32 +182,36 @@ internal constructor(private val limit: Long?, private val distanceField: Field? } class LimitStage internal constructor(private val limit: Long) : Stage("limit") { - override fun args(): Sequence = sequenceOf(encodeValue(limit)) + override fun args(userDataReader: UserDataReader): Sequence = + sequenceOf(encodeValue(limit)) } class OffsetStage internal constructor(private val offset: Long) : Stage("offset") { - override fun args(): Sequence = sequenceOf(encodeValue(offset)) + override fun args(userDataReader: UserDataReader): Sequence = + sequenceOf(encodeValue(offset)) } class SelectStage internal constructor(private val fields: Array) : Stage("select") { - override fun args(): Sequence = - sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto() })) + override fun args(userDataReader: UserDataReader): Sequence = + sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto(userDataReader) })) } class SortStage internal constructor(private val orders: Array) : Stage("sort") { - override fun args(): Sequence = orders.asSequence().map(Ordering::toProto) + override fun args(userDataReader: UserDataReader): Sequence = + orders.asSequence().map { it.toProto(userDataReader) } } class DistinctStage internal constructor(private val groups: Array) : Stage("distinct") { - override fun args(): Sequence = - sequenceOf(encodeValue(groups.associate { it.getAlias() to it.toProto() })) + override fun args(userDataReader: UserDataReader): Sequence = + sequenceOf(encodeValue(groups.associate { it.getAlias() to it.toProto(userDataReader) })) } class RemoveFieldsStage internal constructor(private val fields: Array) : Stage("remove_fields") { - override fun args(): Sequence = fields.asSequence().map(Field::toProto) + override fun args(userDataReader: UserDataReader): Sequence = + fields.asSequence().map(Field::toProto) } class ReplaceStage internal constructor(private val field: Selectable, private val mode: Mode) : @@ -175,7 +224,8 @@ class ReplaceStage internal constructor(private val field: Selectable, private v val MERGE_PREFER_PARENT = Mode("merge_prefer_parent") } } - override fun args(): Sequence = sequenceOf(field.toProto(), mode.proto) + override fun args(userDataReader: UserDataReader): Sequence = + sequenceOf(field.toProto(userDataReader), mode.proto) } class SampleStage internal constructor(private val size: Number, private val mode: Mode) : @@ -187,16 +237,17 @@ class SampleStage internal constructor(private val size: Number, private val mod val PERCENT = Mode("percent") } } - override fun args(): Sequence = sequenceOf(encodeValue(size), mode.proto) + override fun args(userDataReader: UserDataReader): Sequence = + sequenceOf(encodeValue(size), mode.proto) } class UnionStage internal constructor(private val other: com.google.firebase.firestore.Pipeline) : Stage("union") { - override fun args(): Sequence = + override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setPipelineValue(other.toPipelineProto()).build()) } class UnnestStage internal constructor(private val selectable: Selectable) : Stage("unnest") { - override fun args(): Sequence = - sequenceOf(encodeValue(selectable.getAlias()), selectable.toProto()) + override fun args(userDataReader: UserDataReader): Sequence = + sequenceOf(encodeValue(selectable.getAlias()), selectable.toProto(userDataReader)) } From 6842a83387ab4d053563a1fe1004c1772643d91b Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 28 Feb 2025 15:04:31 -0500 Subject: [PATCH 022/152] Fix docStubs task --- .../firebase/firestore/CollectionReference.java | 2 +- .../firebase/firestore/FirebaseFirestore.java | 3 +-- .../com/google/firebase/firestore/Pipeline.kt | 15 +++++++++------ .../firebase/firestore/pipeline/expressions.kt | 9 +++++++++ .../google/firebase/firestore/pipeline/stage.kt | 2 +- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java index 297d018c9d6..8f4e1a00d79 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java @@ -131,6 +131,6 @@ public Task add(@NonNull Object data) { @NonNull public Pipeline pipeline() { - return new Pipeline(firestore, new CollectionSource(getPath())); + return new Pipeline(firestore, firestore.getUserDataReader(), new CollectionSource(getPath())); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 932be5983f5..c2c26447ec9 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -857,8 +857,7 @@ DatabaseId getDatabaseId() { } @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY) - public UserDataReader getUserDataReader() { + UserDataReader getUserDataReader() { return userDataReader; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 6a3f765fd38..ccb20b24d12 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -49,15 +49,17 @@ import com.google.firestore.v1.Value class Pipeline internal constructor( internal val firestore: FirebaseFirestore, + internal val userDataReader: UserDataReader, private val stages: FluentIterable ) { internal constructor( firestore: FirebaseFirestore, + userDataReader: UserDataReader, stage: Stage - ) : this(firestore, FluentIterable.of(stage)) + ) : this(firestore, userDataReader, FluentIterable.of(stage)) private fun append(stage: Stage): Pipeline { - return Pipeline(firestore, stages.append(stage)) + return Pipeline(firestore, userDataReader, stages.append(stage)) } fun execute(): Task { @@ -86,7 +88,7 @@ internal constructor( internal fun toPipelineProto(): com.google.firestore.v1.Pipeline = com.google.firestore.v1.Pipeline.newBuilder() - .addAllStages(stages.map { it.toProtoStage(firestore.userDataReader) }) + .addAllStages(stages.map { it.toProtoStage(userDataReader) }) .build() fun genericStage(name: String, vararg params: Any) = @@ -167,7 +169,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto "Provided collection reference is from a different Firestore instance." ) } - return Pipeline(firestore, CollectionSource(ref.path)) + return Pipeline(firestore, firestore.userDataReader, CollectionSource(ref.path)) } fun collectionGroup(collectionId: String): Pipeline { @@ -175,10 +177,10 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto require(!collectionId.contains("/")) { "Invalid collectionId '$collectionId'. Collection IDs must not contain '/'." } - return Pipeline(firestore, CollectionGroupSource(collectionId)) + return Pipeline(firestore, firestore.userDataReader, CollectionGroupSource(collectionId)) } - fun database(): Pipeline = Pipeline(firestore, DatabaseSource()) + fun database(): Pipeline = Pipeline(firestore, firestore.userDataReader, DatabaseSource()) fun documents(vararg documents: String): Pipeline { // Validate document path by converting to DocumentReference @@ -196,6 +198,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto } return Pipeline( firestore, + firestore.userDataReader, DocumentsSource(documents.map { docRef -> "/" + docRef.path }.toTypedArray()) ) } 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 a4a6912fbce..8fb82718b69 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 @@ -363,6 +363,9 @@ protected constructor(private val name: String, private val params: Array) : internal constructor(name: String, param: Expr, vararg params: Any) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) internal constructor(name: String, fieldName: String, vararg params: Any) : this(name, arrayOf(Field.of(fieldName), *toArrayOfExprOrConstant(params))) + companion object { + + @JvmStatic fun generic(name: String, vararg expr: Expr) = BooleanExpr(name, expr) + + } + fun not() = not(this) fun countIf(): AggregateExpr = AggregateExpr.countIf(this) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index fa8026fb1ad..82264a39ec3 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -31,7 +31,7 @@ internal constructor(private val name: String, private val options: Map + internal abstract fun args(userDataReader: UserDataReader): Sequence } class GenericStage internal constructor(name: String, private val params: List) : From 08af748e02b4934945bc1eaa20fb2b128da82bb3 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 28 Feb 2025 15:14:45 -0500 Subject: [PATCH 023/152] Spotless fix --- .../java/com/google/firebase/firestore/FirebaseFirestore.java | 1 - 1 file changed, 1 deletion(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index c2c26447ec9..a2797e72399 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -23,7 +23,6 @@ import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; From fea5b06d1f1acc6318150c94aaf3f1a298bb6299 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 28 Feb 2025 16:14:20 -0500 Subject: [PATCH 024/152] Generate api.txt --- firebase-firestore/api.txt | 744 +++++++++++++++++- .../com/google/firebase/firestore/Pipeline.kt | 26 + 2 files changed, 769 insertions(+), 1 deletion(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index e3a55cf729c..a3aa673735e 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -54,6 +54,7 @@ package com.google.firebase.firestore { method public String getId(); method public com.google.firebase.firestore.DocumentReference? getParent(); method public String getPath(); + method public com.google.firebase.firestore.Pipeline pipeline(); } public class DocumentChange { @@ -72,7 +73,7 @@ package com.google.firebase.firestore { @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public @interface DocumentId { } - public class DocumentReference { + public final class DocumentReference { method public com.google.firebase.firestore.ListenerRegistration addSnapshotListener(android.app.Activity, com.google.firebase.firestore.EventListener); method public com.google.firebase.firestore.ListenerRegistration addSnapshotListener(android.app.Activity, com.google.firebase.firestore.MetadataChanges, com.google.firebase.firestore.EventListener); method public com.google.firebase.firestore.ListenerRegistration addSnapshotListener(com.google.firebase.firestore.EventListener); @@ -85,6 +86,7 @@ package com.google.firebase.firestore { method public com.google.android.gms.tasks.Task get(); method public com.google.android.gms.tasks.Task get(com.google.firebase.firestore.Source); method public com.google.firebase.firestore.FirebaseFirestore getFirestore(); + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public String getFullPath(); method public String getId(); method public com.google.firebase.firestore.CollectionReference getParent(); method public String getPath(); @@ -143,6 +145,7 @@ package com.google.firebase.firestore { public final class FieldPath { method public static com.google.firebase.firestore.FieldPath documentId(); + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static com.google.firebase.firestore.FieldPath fromDotSeparatedPath(String); method public static com.google.firebase.firestore.FieldPath of(java.lang.String!...!); } @@ -204,6 +207,7 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.LoadBundleTask loadBundle(byte[]); method public com.google.firebase.firestore.LoadBundleTask loadBundle(java.io.InputStream); method public com.google.firebase.firestore.LoadBundleTask loadBundle(java.nio.ByteBuffer); + method public com.google.firebase.firestore.PipelineSource pipeline(); method public com.google.android.gms.tasks.Task runBatch(com.google.firebase.firestore.WriteBatch.Function); method public com.google.android.gms.tasks.Task runTransaction(com.google.firebase.firestore.Transaction.Function); method public com.google.android.gms.tasks.Task runTransaction(com.google.firebase.firestore.TransactionOptions, com.google.firebase.firestore.Transaction.Function); @@ -416,6 +420,52 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.PersistentCacheSettings.Builder setSizeBytes(long); } + public final class Pipeline { + method public com.google.firebase.firestore.Pipeline addFields(com.google.firebase.firestore.pipeline.Selectable... fields); + method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateStage aggregateStage); + method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateWithAlias... accumulators); + method public com.google.firebase.firestore.Pipeline distinct(com.google.firebase.firestore.pipeline.Selectable... groups); + method public com.google.firebase.firestore.Pipeline distinct(java.lang.Object... groups); + method public com.google.firebase.firestore.Pipeline distinct(java.lang.String... groups); + method public com.google.android.gms.tasks.Task execute(); + method public com.google.firebase.firestore.Pipeline genericStage(String name, java.lang.Object... params); + method public com.google.firebase.firestore.Pipeline limit(long limit); + method public com.google.firebase.firestore.Pipeline offset(long offset); + method public com.google.firebase.firestore.Pipeline removeFields(com.google.firebase.firestore.pipeline.Field... fields); + method public com.google.firebase.firestore.Pipeline removeFields(java.lang.String... fields); + method public com.google.firebase.firestore.Pipeline replace(com.google.firebase.firestore.pipeline.Selectable field); + method public com.google.firebase.firestore.Pipeline replace(String field); + method public com.google.firebase.firestore.Pipeline sample(int documents); + method public com.google.firebase.firestore.Pipeline select(com.google.firebase.firestore.pipeline.Selectable... fields); + method public com.google.firebase.firestore.Pipeline select(java.lang.Object... fields); + method public com.google.firebase.firestore.Pipeline select(java.lang.String... fields); + method public com.google.firebase.firestore.Pipeline sort(com.google.firebase.firestore.pipeline.Ordering... orders); + method public com.google.firebase.firestore.Pipeline union(com.google.firebase.firestore.Pipeline other); + method public com.google.firebase.firestore.Pipeline unnest(com.google.firebase.firestore.pipeline.Selectable selectable); + method public com.google.firebase.firestore.Pipeline unnest(String field); + method public com.google.firebase.firestore.Pipeline where(com.google.firebase.firestore.pipeline.BooleanExpr condition); + } + + public final class PipelineResult { + method public java.util.Map getData(); + method public com.google.firebase.firestore.DocumentReference? getRef(); + property public final com.google.firebase.firestore.DocumentReference? ref; + } + + public final class PipelineSnapshot { + method public java.util.List getResults(); + property public final java.util.List results; + } + + public final class PipelineSource { + method public com.google.firebase.firestore.Pipeline collection(com.google.firebase.firestore.CollectionReference ref); + method public com.google.firebase.firestore.Pipeline collection(String path); + method public com.google.firebase.firestore.Pipeline collectionGroup(String collectionId); + method public com.google.firebase.firestore.Pipeline database(); + method public com.google.firebase.firestore.Pipeline documents(com.google.firebase.firestore.DocumentReference... documents); + method public com.google.firebase.firestore.Pipeline documents(java.lang.String... documents); + } + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD}) public @interface PropertyName { method public abstract String value(); } @@ -606,3 +656,695 @@ package com.google.firebase.firestore.ktx { } +package com.google.firebase.firestore.pipeline { + + public final class AddFieldsStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public final class AggregateExpr { + method public com.google.firebase.firestore.pipeline.AggregateWithAlias as(String alias); + method public static com.google.firebase.firestore.pipeline.AggregateExpr avg(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateExpr avg(String fieldName); + method public static com.google.firebase.firestore.pipeline.AggregateExpr count(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateExpr count(String fieldName); + method public static com.google.firebase.firestore.pipeline.AggregateExpr countAll(); + method public static com.google.firebase.firestore.pipeline.AggregateExpr countIf(com.google.firebase.firestore.pipeline.BooleanExpr condition); + method public static com.google.firebase.firestore.pipeline.AggregateExpr max(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateExpr max(String fieldName); + method public static com.google.firebase.firestore.pipeline.AggregateExpr min(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateExpr min(String fieldName); + method public static com.google.firebase.firestore.pipeline.AggregateExpr sum(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateExpr sum(String fieldName); + field public static final com.google.firebase.firestore.pipeline.AggregateExpr.Companion Companion; + } + + public static final class AggregateExpr.Companion { + method public com.google.firebase.firestore.pipeline.AggregateExpr avg(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateExpr avg(String fieldName); + method public com.google.firebase.firestore.pipeline.AggregateExpr count(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateExpr count(String fieldName); + method public com.google.firebase.firestore.pipeline.AggregateExpr countAll(); + method public com.google.firebase.firestore.pipeline.AggregateExpr countIf(com.google.firebase.firestore.pipeline.BooleanExpr condition); + method public com.google.firebase.firestore.pipeline.AggregateExpr max(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateExpr max(String fieldName); + method public com.google.firebase.firestore.pipeline.AggregateExpr min(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateExpr min(String fieldName); + method public com.google.firebase.firestore.pipeline.AggregateExpr sum(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateExpr sum(String fieldName); + } + + public final class AggregateStage extends com.google.firebase.firestore.pipeline.Stage { + method public static com.google.firebase.firestore.pipeline.AggregateStage withAccumulators(com.google.firebase.firestore.pipeline.AggregateWithAlias... accumulators); + method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(com.google.firebase.firestore.pipeline.Selectable... groups); + method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(java.lang.Object... selectable); + method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(java.lang.String... fields); + field public static final com.google.firebase.firestore.pipeline.AggregateStage.Companion Companion; + } + + public static final class AggregateStage.Companion { + method public com.google.firebase.firestore.pipeline.AggregateStage withAccumulators(com.google.firebase.firestore.pipeline.AggregateWithAlias... accumulators); + } + + public final class AggregateWithAlias { + } + + public final class BooleanExpr extends com.google.firebase.firestore.pipeline.Function { + method public com.google.firebase.firestore.pipeline.AggregateExpr countIf(); + method public static com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.Expr then); + method public com.google.firebase.firestore.pipeline.Function ifThen(Object then); + method public com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr else); + method public com.google.firebase.firestore.pipeline.Function ifThenElse(Object then, Object else); + method public com.google.firebase.firestore.pipeline.BooleanExpr not(); + field public static final com.google.firebase.firestore.pipeline.BooleanExpr.Companion Companion; + } + + public static final class BooleanExpr.Companion { + method public com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + } + + public final class CollectionGroupSource extends com.google.firebase.firestore.pipeline.Stage { + method public String getCollectionId(); + property public final String collectionId; + } + + public final class CollectionSource extends com.google.firebase.firestore.pipeline.Stage { + } + + public abstract class Constant extends com.google.firebase.firestore.pipeline.Expr { + method public static final com.google.firebase.firestore.pipeline.Constant nullValue(); + method public static final com.google.firebase.firestore.pipeline.Constant of(boolean value); + method public static final com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.Blob value); + method public static final com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.DocumentReference ref); + method public static final com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.GeoPoint value); + method public static final com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.VectorValue value); + method public static final com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.Timestamp value); + method public static final com.google.firebase.firestore.pipeline.Constant of(Number value); + method public static final com.google.firebase.firestore.pipeline.Constant of(String value); + method public static final com.google.firebase.firestore.pipeline.Constant of(java.util.Date value); + method public static final com.google.firebase.firestore.pipeline.Constant vector(com.google.firebase.firestore.VectorValue value); + method public static final com.google.firebase.firestore.pipeline.Constant vector(double[] value); + field public static final com.google.firebase.firestore.pipeline.Constant.Companion Companion; + } + + public static final class Constant.Companion { + method public com.google.firebase.firestore.pipeline.Constant nullValue(); + method public com.google.firebase.firestore.pipeline.Constant of(boolean value); + method public com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.Blob value); + method public com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.DocumentReference ref); + method public com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.GeoPoint value); + method public com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.VectorValue value); + method public com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.Timestamp value); + method public com.google.firebase.firestore.pipeline.Constant of(Number value); + method public com.google.firebase.firestore.pipeline.Constant of(String value); + method public com.google.firebase.firestore.pipeline.Constant of(java.util.Date value); + method public com.google.firebase.firestore.pipeline.Constant vector(com.google.firebase.firestore.VectorValue value); + method public com.google.firebase.firestore.pipeline.Constant vector(double[] value); + } + + public final class DatabaseSource extends com.google.firebase.firestore.pipeline.Stage { + ctor public DatabaseSource(); + } + + public final class DistinctStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public final class DocumentsSource extends com.google.firebase.firestore.pipeline.Stage { + } + + public abstract class Expr { + method public final com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.Function add(Object other); + method public final com.google.firebase.firestore.pipeline.Function arrayConcat(com.google.firebase.firestore.pipeline.Expr... arrays); + method public final com.google.firebase.firestore.pipeline.Function arrayConcat(java.util.List arrays); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr value); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(Object value); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(java.util.List values); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(java.util.List values); + method public final com.google.firebase.firestore.pipeline.Function arrayLength(); + method public final com.google.firebase.firestore.pipeline.Function arrayReverse(); + method public com.google.firebase.firestore.pipeline.ExprWithAlias as(String alias); + method public final com.google.firebase.firestore.pipeline.Ordering ascending(); + method public final com.google.firebase.firestore.pipeline.AggregateExpr avg(); + method public final com.google.firebase.firestore.pipeline.Function byteLength(); + method public final com.google.firebase.firestore.pipeline.Function charLength(); + method public final com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector); + method public final com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.VectorValue vector); + method public final com.google.firebase.firestore.pipeline.Function cosineDistance(double[] vector); + method public final com.google.firebase.firestore.pipeline.Ordering descending(); + method public final com.google.firebase.firestore.pipeline.Function divide(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.Function divide(Object other); + method public final com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector); + method public final com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.VectorValue vector); + method public final com.google.firebase.firestore.pipeline.Function dotProduct(double[] vector); + method public final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr suffix); + method public final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String suffix); + method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(Object other); + method public final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector); + method public final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.VectorValue vector); + method public final com.google.firebase.firestore.pipeline.Function euclideanDistance(double[] vector); + method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(Object other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(Object other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr inAny(java.util.List values); + method public final com.google.firebase.firestore.pipeline.BooleanExpr isNan(); + method public final com.google.firebase.firestore.pipeline.BooleanExpr isNull(); + method public final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr pattern); + method public final com.google.firebase.firestore.pipeline.BooleanExpr like(String pattern); + method public final com.google.firebase.firestore.pipeline.Function logicalMax(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.Function logicalMax(Object other); + method public final com.google.firebase.firestore.pipeline.Function logicalMin(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.Function logicalMin(Object other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(Object other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(Object other); + method public final com.google.firebase.firestore.pipeline.Function mapGet(com.google.firebase.firestore.pipeline.Expr key); + method public final com.google.firebase.firestore.pipeline.Function mapGet(String key); + method public final com.google.firebase.firestore.pipeline.AggregateExpr max(); + method public final com.google.firebase.firestore.pipeline.AggregateExpr min(); + method public final com.google.firebase.firestore.pipeline.Function mod(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.Function mod(Object other); + method public final com.google.firebase.firestore.pipeline.Function multiply(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.Function multiply(Object other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(Object other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr notInAny(java.util.List values); + method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr pattern); + method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String pattern); + method public final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr pattern); + method public final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String pattern); + method public final com.google.firebase.firestore.pipeline.Function replaceAll(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public final com.google.firebase.firestore.pipeline.Function replaceAll(String find, String replace); + method public final com.google.firebase.firestore.pipeline.Function replaceFirst(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public final com.google.firebase.firestore.pipeline.Function replaceFirst(String find, String replace); + method public final com.google.firebase.firestore.pipeline.Function reverse(); + method public final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr prefix); + method public final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String prefix); + method public final com.google.firebase.firestore.pipeline.Function strConcat(com.google.firebase.firestore.pipeline.Expr... expr); + method public final com.google.firebase.firestore.pipeline.Function strConcat(java.lang.Object... string); + method public final com.google.firebase.firestore.pipeline.Function strConcat(java.lang.String... string); + method public final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr substring); + method public final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String substring); + method public final com.google.firebase.firestore.pipeline.Function subtract(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.Function subtract(Object other); + method public final com.google.firebase.firestore.pipeline.AggregateExpr sum(); + method public final com.google.firebase.firestore.pipeline.Function timestampAdd(com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public final com.google.firebase.firestore.pipeline.Function timestampAdd(String unit, double amount); + method public final com.google.firebase.firestore.pipeline.Function timestampSub(com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public final com.google.firebase.firestore.pipeline.Function timestampSub(String unit, double amount); + method public final com.google.firebase.firestore.pipeline.Function timestampToUnixMicros(); + method public final com.google.firebase.firestore.pipeline.Function timestampToUnixMillis(); + method public final com.google.firebase.firestore.pipeline.Function timestampToUnixSeconds(); + method public final com.google.firebase.firestore.pipeline.Function toLower(); + method public final com.google.firebase.firestore.pipeline.Function toUpper(); + method public final com.google.firebase.firestore.pipeline.Function trim(); + method public final com.google.firebase.firestore.pipeline.Function unixMicrosToTimestamp(); + method public final com.google.firebase.firestore.pipeline.Function unixMillisToTimestamp(); + method public final com.google.firebase.firestore.pipeline.Function unixSecondsToTimestamp(); + method public final com.google.firebase.firestore.pipeline.Function vectorLength(); + } + + public class ExprWithAlias extends com.google.firebase.firestore.pipeline.Selectable { + } + + public final class Field extends com.google.firebase.firestore.pipeline.Selectable { + method public static com.google.firebase.firestore.pipeline.Field of(com.google.firebase.firestore.FieldPath fieldPath); + method public static com.google.firebase.firestore.pipeline.Field of(String name); + field public static final com.google.firebase.firestore.pipeline.Field.Companion Companion; + } + + public static final class Field.Companion { + method public com.google.firebase.firestore.pipeline.Field of(com.google.firebase.firestore.FieldPath fieldPath); + method public com.google.firebase.firestore.pipeline.Field of(String name); + } + + public final class FindNearestOptions { + method public java.util.Map toProto(); + } + + public final class FindNearestStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public static final class FindNearestStage.DistanceMeasure { + field public static final com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure.Companion Companion; + } + + public static final class FindNearestStage.DistanceMeasure.Companion { + method public error.NonExistentClass getCOSINE(); + method public error.NonExistentClass getDOT_PRODUCT(); + method public error.NonExistentClass getEUCLIDEAN(); + property public final error.NonExistentClass COSINE; + property public final error.NonExistentClass DOT_PRODUCT; + property public final error.NonExistentClass EUCLIDEAN; + } + + public class Function extends com.google.firebase.firestore.pipeline.Expr { + ctor protected Function(String name, com.google.firebase.firestore.pipeline.Expr[] params); + method public static final com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.Function add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Function add(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); + method public static final com.google.firebase.firestore.pipeline.Function arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); + method public static final com.google.firebase.firestore.pipeline.Function arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); + method public static final com.google.firebase.firestore.pipeline.Function arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); + method public static final com.google.firebase.firestore.pipeline.Function arrayConcat(String fieldName, java.util.List arrays); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, Object value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, com.google.firebase.firestore.pipeline.Expr value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, Object value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String fieldName, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String fieldName, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.Function arrayLength(com.google.firebase.firestore.pipeline.Expr array); + method public static final com.google.firebase.firestore.pipeline.Function arrayLength(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function arrayReverse(com.google.firebase.firestore.pipeline.Expr array); + method public static final com.google.firebase.firestore.pipeline.Function arrayReverse(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function byteLength(com.google.firebase.firestore.pipeline.Expr value); + method public static final com.google.firebase.firestore.pipeline.Function byteLength(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function charLength(com.google.firebase.firestore.pipeline.Expr value); + method public static final com.google.firebase.firestore.pipeline.Function charLength(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, double[] vector); + method public static final com.google.firebase.firestore.pipeline.Function divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function divide(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.Function divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Function divide(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public static final com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public static final com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public static final com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, double[] vector); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr suffix); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, String suffix); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr suffix); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, String suffix); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, double[] vector); + method public static final com.google.firebase.firestore.pipeline.Function generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then); + method public static final com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then); + method public static final com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr else); + method public static final com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object else); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr inAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr inAny(String fieldName, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(String fieldName); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNull(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNull(String fieldName); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, String pattern); + method public static final com.google.firebase.firestore.pipeline.Function logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.Function logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Function logicalMax(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.Function logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.Function logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Function logicalMin(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.Function mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.Function mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); + method public static final com.google.firebase.firestore.pipeline.Function mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.Function mapGet(String fieldName, String key); + method public static final com.google.firebase.firestore.pipeline.Function mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function mod(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.Function mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Function mod(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.Function multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.Function multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Function multiply(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr not(com.google.firebase.firestore.pipeline.BooleanExpr condition); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr notInAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr notInAny(String fieldName, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr or(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String fieldName, String pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, String pattern); + method public static final com.google.firebase.firestore.pipeline.Function replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public static final com.google.firebase.firestore.pipeline.Function replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.Function replaceAll(String fieldName, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.Function replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public static final com.google.firebase.firestore.pipeline.Function replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.Function replaceFirst(String fieldName, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.Function reverse(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Function reverse(String fieldName); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr prefix); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, String prefix); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr prefix); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, String prefix); + method public static final com.google.firebase.firestore.pipeline.Function strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); + method public static final com.google.firebase.firestore.pipeline.Function strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); + method public static final com.google.firebase.firestore.pipeline.Function strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); + method public static final com.google.firebase.firestore.pipeline.Function strConcat(String fieldName, java.lang.Object... rest); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr substring); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, String substring); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, com.google.firebase.firestore.pipeline.Expr substring); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, String substring); + method public static final com.google.firebase.firestore.pipeline.Function subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.Function subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Function subtract(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.Function timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.Function timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.Function timestampAdd(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.Function timestampAdd(String fieldName, String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.Function timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.Function timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.Function timestampSub(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.Function timestampSub(String fieldName, String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixMicros(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixMillis(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixSeconds(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function toLower(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Function toLower(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function toUpper(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Function toUpper(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function trim(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Function trim(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Function unixMicrosToTimestamp(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Function unixMillisToTimestamp(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Function unixSecondsToTimestamp(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function vectorLength(com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Function vectorLength(String fieldName); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr xor(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); + field public static final com.google.firebase.firestore.pipeline.Function.Companion Companion; + } + + public static final class Function.Companion { + method public com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Function add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Function add(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); + method public com.google.firebase.firestore.pipeline.Function arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); + method public com.google.firebase.firestore.pipeline.Function arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); + method public com.google.firebase.firestore.pipeline.Function arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); + method public com.google.firebase.firestore.pipeline.Function arrayConcat(String fieldName, java.util.List arrays); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr value); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, Object value); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, com.google.firebase.firestore.pipeline.Expr value); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, Object value); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String fieldName, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String fieldName, java.util.List values); + method public com.google.firebase.firestore.pipeline.Function arrayLength(com.google.firebase.firestore.pipeline.Expr array); + method public com.google.firebase.firestore.pipeline.Function arrayLength(String fieldName); + method public com.google.firebase.firestore.pipeline.Function arrayReverse(com.google.firebase.firestore.pipeline.Expr array); + method public com.google.firebase.firestore.pipeline.Function arrayReverse(String fieldName); + method public com.google.firebase.firestore.pipeline.Function byteLength(com.google.firebase.firestore.pipeline.Expr value); + method public com.google.firebase.firestore.pipeline.Function byteLength(String fieldName); + method public com.google.firebase.firestore.pipeline.Function charLength(com.google.firebase.firestore.pipeline.Expr value); + method public com.google.firebase.firestore.pipeline.Function charLength(String fieldName); + method public com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.Function divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function divide(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Function divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Function divide(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr suffix); + method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, String suffix); + method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr suffix); + method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, String suffix); + method public com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.Function generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then); + method public com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then); + method public com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr else); + method public com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object else); + method public com.google.firebase.firestore.pipeline.BooleanExpr inAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr inAny(String fieldName, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr isNan(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.BooleanExpr isNan(String fieldName); + method public com.google.firebase.firestore.pipeline.BooleanExpr isNull(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.BooleanExpr isNull(String fieldName); + method public com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, String pattern); + method public com.google.firebase.firestore.pipeline.Function logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Function logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Function logicalMax(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Function logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Function logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Function logicalMin(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.Function mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.Function mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); + method public com.google.firebase.firestore.pipeline.Function mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.Function mapGet(String fieldName, String key); + method public com.google.firebase.firestore.pipeline.Function mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function mod(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Function mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Function mod(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Function multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Function multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Function multiply(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr not(com.google.firebase.firestore.pipeline.BooleanExpr condition); + method public com.google.firebase.firestore.pipeline.BooleanExpr notInAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr notInAny(String fieldName, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr or(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String fieldName, String pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, String pattern); + method public com.google.firebase.firestore.pipeline.Function replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public com.google.firebase.firestore.pipeline.Function replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public com.google.firebase.firestore.pipeline.Function replaceAll(String fieldName, String find, String replace); + method public com.google.firebase.firestore.pipeline.Function replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public com.google.firebase.firestore.pipeline.Function replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public com.google.firebase.firestore.pipeline.Function replaceFirst(String fieldName, String find, String replace); + method public com.google.firebase.firestore.pipeline.Function reverse(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Function reverse(String fieldName); + method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr prefix); + method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, String prefix); + method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr prefix); + method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, String prefix); + method public com.google.firebase.firestore.pipeline.Function strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); + method public com.google.firebase.firestore.pipeline.Function strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); + method public com.google.firebase.firestore.pipeline.Function strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); + method public com.google.firebase.firestore.pipeline.Function strConcat(String fieldName, java.lang.Object... rest); + method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr substring); + method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, String substring); + method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, com.google.firebase.firestore.pipeline.Expr substring); + method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, String substring); + method public com.google.firebase.firestore.pipeline.Function subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Function subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Function subtract(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Function timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.Function timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public com.google.firebase.firestore.pipeline.Function timestampAdd(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.Function timestampAdd(String fieldName, String unit, double amount); + method public com.google.firebase.firestore.pipeline.Function timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.Function timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public com.google.firebase.firestore.pipeline.Function timestampSub(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.Function timestampSub(String fieldName, String unit, double amount); + method public com.google.firebase.firestore.pipeline.Function timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Function timestampToUnixMicros(String fieldName); + method public com.google.firebase.firestore.pipeline.Function timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Function timestampToUnixMillis(String fieldName); + method public com.google.firebase.firestore.pipeline.Function timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Function timestampToUnixSeconds(String fieldName); + method public com.google.firebase.firestore.pipeline.Function toLower(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Function toLower(String fieldName); + method public com.google.firebase.firestore.pipeline.Function toUpper(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Function toUpper(String fieldName); + method public com.google.firebase.firestore.pipeline.Function trim(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Function trim(String fieldName); + method public com.google.firebase.firestore.pipeline.Function unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Function unixMicrosToTimestamp(String fieldName); + method public com.google.firebase.firestore.pipeline.Function unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Function unixMillisToTimestamp(String fieldName); + method public com.google.firebase.firestore.pipeline.Function unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Function unixSecondsToTimestamp(String fieldName); + method public com.google.firebase.firestore.pipeline.Function vectorLength(com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Function vectorLength(String fieldName); + method public com.google.firebase.firestore.pipeline.BooleanExpr xor(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); + } + + public final class GenericStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public final class LimitStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public final class ListOfExprs extends com.google.firebase.firestore.pipeline.Expr { + ctor public ListOfExprs(com.google.firebase.firestore.pipeline.Expr[] expressions); + } + + public final class MapOfExpr extends com.google.firebase.firestore.pipeline.Expr { + ctor public MapOfExpr(java.util.Map expressions); + } + + public final class OffsetStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public final class Ordering { + method public static com.google.firebase.firestore.pipeline.Ordering ascending(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.Ordering ascending(String fieldName); + method public static com.google.firebase.firestore.pipeline.Ordering descending(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.Ordering descending(String fieldName); + field public static final com.google.firebase.firestore.pipeline.Ordering.Companion Companion; + } + + public static final class Ordering.Companion { + method public com.google.firebase.firestore.pipeline.Ordering ascending(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Ordering ascending(String fieldName); + method public com.google.firebase.firestore.pipeline.Ordering descending(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Ordering descending(String fieldName); + } + + public final class RemoveFieldsStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public final class ReplaceStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public static final class ReplaceStage.Mode { + field public static final com.google.firebase.firestore.pipeline.ReplaceStage.Mode.Companion Companion; + } + + public static final class ReplaceStage.Mode.Companion { + method public error.NonExistentClass getFULL_REPLACE(); + method public error.NonExistentClass getMERGE_PREFER_NEXT(); + method public error.NonExistentClass getMERGE_PREFER_PARENT(); + property public final error.NonExistentClass FULL_REPLACE; + property public final error.NonExistentClass MERGE_PREFER_NEXT; + property public final error.NonExistentClass MERGE_PREFER_PARENT; + } + + public final class SampleStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public static final class SampleStage.Mode { + field public static final com.google.firebase.firestore.pipeline.SampleStage.Mode.Companion Companion; + } + + public static final class SampleStage.Mode.Companion { + method public error.NonExistentClass getDOCUMENTS(); + method public error.NonExistentClass getPERCENT(); + property public final error.NonExistentClass DOCUMENTS; + property public final error.NonExistentClass PERCENT; + } + + public final class SelectStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public abstract class Selectable extends com.google.firebase.firestore.pipeline.Expr { + ctor public Selectable(); + } + + public final class SortStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public abstract class Stage { + } + + public final class UnionStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public final class UnnestStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public final class WhereStage extends com.google.firebase.firestore.pipeline.Stage { + } + +} + diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index ccb20b24d12..662563488fa 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -36,10 +36,14 @@ import com.google.firebase.firestore.pipeline.LimitStage import com.google.firebase.firestore.pipeline.OffsetStage import com.google.firebase.firestore.pipeline.Ordering import com.google.firebase.firestore.pipeline.RemoveFieldsStage +import com.google.firebase.firestore.pipeline.ReplaceStage +import com.google.firebase.firestore.pipeline.SampleStage import com.google.firebase.firestore.pipeline.SelectStage import com.google.firebase.firestore.pipeline.Selectable import com.google.firebase.firestore.pipeline.SortStage import com.google.firebase.firestore.pipeline.Stage +import com.google.firebase.firestore.pipeline.UnionStage +import com.google.firebase.firestore.pipeline.UnnestStage import com.google.firebase.firestore.pipeline.WhereStage import com.google.firebase.firestore.util.Preconditions import com.google.firestore.v1.ExecutePipelineRequest @@ -130,6 +134,28 @@ internal constructor( fun aggregate(aggregateStage: AggregateStage): Pipeline = append(aggregateStage) + // fun findNearest() + + fun replace(field: String): Pipeline = replace(Field.of(field)) + + fun replace(field: Selectable): Pipeline = + append(ReplaceStage(field, ReplaceStage.Mode.FULL_REPLACE)) + + fun sample(documents: Int): Pipeline = append(SampleStage(documents, SampleStage.Mode.DOCUMENTS)) + + // fun sample(options: SampleOptions): Pipeline + + fun union(other: Pipeline): Pipeline = append(UnionStage(other)) + + fun unnest(field: String): Pipeline = unnest(Field.of(field)) + + // fun unnest(field: String, options: UnnestOptions): Pipeline = unnest(Field.of(field), options) + + fun unnest(selectable: Selectable): Pipeline = append(UnnestStage(selectable)) + + // fun unnest(selectable: Selectable, options: UnnestOptions): Pipeline = + // append(UnnestStage(selectable)) + private inner class ObserverSnapshotTask : PipelineResultObserver { private val taskCompletionSource = TaskCompletionSource() private val results: ImmutableList.Builder = ImmutableList.builder() From 711b852d3258598ec55010884bab89d90f0a642e Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 28 Feb 2025 16:52:14 -0500 Subject: [PATCH 025/152] Make more of the API internal --- firebase-firestore/api.txt | 94 ------------------- .../firebase/firestore/pipeline/stage.kt | 45 ++++----- 2 files changed, 24 insertions(+), 115 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index a3aa673735e..6b9af468a9e 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -658,9 +658,6 @@ package com.google.firebase.firestore.ktx { package com.google.firebase.firestore.pipeline { - public final class AddFieldsStage extends com.google.firebase.firestore.pipeline.Stage { - } - public final class AggregateExpr { method public com.google.firebase.firestore.pipeline.AggregateWithAlias as(String alias); method public static com.google.firebase.firestore.pipeline.AggregateExpr avg(com.google.firebase.firestore.pipeline.Expr expr); @@ -723,14 +720,6 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); } - public final class CollectionGroupSource extends com.google.firebase.firestore.pipeline.Stage { - method public String getCollectionId(); - property public final String collectionId; - } - - public final class CollectionSource extends com.google.firebase.firestore.pipeline.Stage { - } - public abstract class Constant extends com.google.firebase.firestore.pipeline.Expr { method public static final com.google.firebase.firestore.pipeline.Constant nullValue(); method public static final com.google.firebase.firestore.pipeline.Constant of(boolean value); @@ -762,16 +751,6 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Constant vector(double[] value); } - public final class DatabaseSource extends com.google.firebase.firestore.pipeline.Stage { - ctor public DatabaseSource(); - } - - public final class DistinctStage extends com.google.firebase.firestore.pipeline.Stage { - } - - public final class DocumentsSource extends com.google.firebase.firestore.pipeline.Stage { - } - public abstract class Expr { method public final com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr other); method public final com.google.firebase.firestore.pipeline.Function add(Object other); @@ -885,22 +864,6 @@ package com.google.firebase.firestore.pipeline { method public java.util.Map toProto(); } - public final class FindNearestStage extends com.google.firebase.firestore.pipeline.Stage { - } - - public static final class FindNearestStage.DistanceMeasure { - field public static final com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure.Companion Companion; - } - - public static final class FindNearestStage.DistanceMeasure.Companion { - method public error.NonExistentClass getCOSINE(); - method public error.NonExistentClass getDOT_PRODUCT(); - method public error.NonExistentClass getEUCLIDEAN(); - property public final error.NonExistentClass COSINE; - property public final error.NonExistentClass DOT_PRODUCT; - property public final error.NonExistentClass EUCLIDEAN; - } - public class Function extends com.google.firebase.firestore.pipeline.Expr { ctor protected Function(String name, com.google.firebase.firestore.pipeline.Expr[] params); method public static final com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); @@ -1259,12 +1222,6 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr xor(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); } - public final class GenericStage extends com.google.firebase.firestore.pipeline.Stage { - } - - public final class LimitStage extends com.google.firebase.firestore.pipeline.Stage { - } - public final class ListOfExprs extends com.google.firebase.firestore.pipeline.Expr { ctor public ListOfExprs(com.google.firebase.firestore.pipeline.Expr[] expressions); } @@ -1273,9 +1230,6 @@ package com.google.firebase.firestore.pipeline { ctor public MapOfExpr(java.util.Map expressions); } - public final class OffsetStage extends com.google.firebase.firestore.pipeline.Stage { - } - public final class Ordering { method public static com.google.firebase.firestore.pipeline.Ordering ascending(com.google.firebase.firestore.pipeline.Expr expr); method public static com.google.firebase.firestore.pipeline.Ordering ascending(String fieldName); @@ -1291,60 +1245,12 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Ordering descending(String fieldName); } - public final class RemoveFieldsStage extends com.google.firebase.firestore.pipeline.Stage { - } - - public final class ReplaceStage extends com.google.firebase.firestore.pipeline.Stage { - } - - public static final class ReplaceStage.Mode { - field public static final com.google.firebase.firestore.pipeline.ReplaceStage.Mode.Companion Companion; - } - - public static final class ReplaceStage.Mode.Companion { - method public error.NonExistentClass getFULL_REPLACE(); - method public error.NonExistentClass getMERGE_PREFER_NEXT(); - method public error.NonExistentClass getMERGE_PREFER_PARENT(); - property public final error.NonExistentClass FULL_REPLACE; - property public final error.NonExistentClass MERGE_PREFER_NEXT; - property public final error.NonExistentClass MERGE_PREFER_PARENT; - } - - public final class SampleStage extends com.google.firebase.firestore.pipeline.Stage { - } - - public static final class SampleStage.Mode { - field public static final com.google.firebase.firestore.pipeline.SampleStage.Mode.Companion Companion; - } - - public static final class SampleStage.Mode.Companion { - method public error.NonExistentClass getDOCUMENTS(); - method public error.NonExistentClass getPERCENT(); - property public final error.NonExistentClass DOCUMENTS; - property public final error.NonExistentClass PERCENT; - } - - public final class SelectStage extends com.google.firebase.firestore.pipeline.Stage { - } - public abstract class Selectable extends com.google.firebase.firestore.pipeline.Expr { ctor public Selectable(); } - public final class SortStage extends com.google.firebase.firestore.pipeline.Stage { - } - public abstract class Stage { } - public final class UnionStage extends com.google.firebase.firestore.pipeline.Stage { - } - - public final class UnnestStage extends com.google.firebase.firestore.pipeline.Stage { - } - - public final class WhereStage extends com.google.firebase.firestore.pipeline.Stage { - } - } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index 82264a39ec3..7e8ab87ee1b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -34,8 +34,8 @@ internal constructor(private val name: String, private val options: Map } -class GenericStage internal constructor(name: String, private val params: List) : - Stage(name) { +internal class GenericStage +internal constructor(name: String, private val params: List) : Stage(name) { override fun args(userDataReader: UserDataReader): Sequence = params.asSequence().map { it.toProto(userDataReader) } } @@ -76,29 +76,29 @@ internal sealed class GenericArg { } } -class DatabaseSource : Stage("database") { +internal class DatabaseSource : Stage("database") { override fun args(userDataReader: UserDataReader): Sequence = emptySequence() } -class CollectionSource internal constructor(path: String) : Stage("collection") { +internal class CollectionSource internal constructor(path: String) : Stage("collection") { private val path: String = if (path.startsWith("/")) path else "/" + path override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setReferenceValue(path).build()) } -class CollectionGroupSource internal constructor(val collectionId: String) : +internal class CollectionGroupSource internal constructor(val collectionId: String) : Stage("collection_group") { override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setReferenceValue("").build(), encodeValue(collectionId)) } -class DocumentsSource internal constructor(private val documents: Array) : +internal class DocumentsSource internal constructor(private val documents: Array) : Stage("documents") { override fun args(userDataReader: UserDataReader): Sequence = documents.asSequence().map(::encodeValue) } -class AddFieldsStage internal constructor(private val fields: Array) : +internal class AddFieldsStage internal constructor(private val fields: Array) : Stage("add_fields") { override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto(userDataReader) })) @@ -141,12 +141,13 @@ internal constructor( ) } -class WhereStage internal constructor(private val condition: BooleanExpr) : Stage("where") { +internal class WhereStage internal constructor(private val condition: BooleanExpr) : + Stage("where") { override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(condition.toProto(userDataReader)) } -class FindNearestStage +internal class FindNearestStage internal constructor( private val property: Expr, private val vector: DoubleArray, @@ -181,41 +182,42 @@ internal constructor(private val limit: Long?, private val distanceField: Field? } } -class LimitStage internal constructor(private val limit: Long) : Stage("limit") { +internal class LimitStage internal constructor(private val limit: Long) : Stage("limit") { override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(limit)) } -class OffsetStage internal constructor(private val offset: Long) : Stage("offset") { +internal class OffsetStage internal constructor(private val offset: Long) : Stage("offset") { override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(offset)) } -class SelectStage internal constructor(private val fields: Array) : +internal class SelectStage internal constructor(private val fields: Array) : Stage("select") { override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto(userDataReader) })) } -class SortStage internal constructor(private val orders: Array) : Stage("sort") { +internal class SortStage internal constructor(private val orders: Array) : + Stage("sort") { override fun args(userDataReader: UserDataReader): Sequence = orders.asSequence().map { it.toProto(userDataReader) } } -class DistinctStage internal constructor(private val groups: Array) : +internal class DistinctStage internal constructor(private val groups: Array) : Stage("distinct") { override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(groups.associate { it.getAlias() to it.toProto(userDataReader) })) } -class RemoveFieldsStage internal constructor(private val fields: Array) : +internal class RemoveFieldsStage internal constructor(private val fields: Array) : Stage("remove_fields") { override fun args(userDataReader: UserDataReader): Sequence = fields.asSequence().map(Field::toProto) } -class ReplaceStage internal constructor(private val field: Selectable, private val mode: Mode) : - Stage("replace") { +internal class ReplaceStage +internal constructor(private val field: Selectable, private val mode: Mode) : Stage("replace") { class Mode private constructor(internal val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) companion object { @@ -228,7 +230,7 @@ class ReplaceStage internal constructor(private val field: Selectable, private v sequenceOf(field.toProto(userDataReader), mode.proto) } -class SampleStage internal constructor(private val size: Number, private val mode: Mode) : +internal class SampleStage internal constructor(private val size: Number, private val mode: Mode) : Stage("sample") { class Mode private constructor(internal val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) @@ -241,13 +243,14 @@ class SampleStage internal constructor(private val size: Number, private val mod sequenceOf(encodeValue(size), mode.proto) } -class UnionStage internal constructor(private val other: com.google.firebase.firestore.Pipeline) : - Stage("union") { +internal class UnionStage +internal constructor(private val other: com.google.firebase.firestore.Pipeline) : Stage("union") { override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setPipelineValue(other.toPipelineProto()).build()) } -class UnnestStage internal constructor(private val selectable: Selectable) : Stage("unnest") { +internal class UnnestStage internal constructor(private val selectable: Selectable) : + Stage("unnest") { override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(selectable.getAlias()), selectable.toProto(userDataReader)) } From 1ccea4d91682e0c3a8c21285657d64ef727c20dd Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 28 Feb 2025 17:15:16 -0500 Subject: [PATCH 026/152] Make more of the API internal --- firebase-firestore/api.txt | 10 +--------- .../google/firebase/firestore/pipeline/expressions.kt | 6 +++--- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 6b9af468a9e..63d017b6288 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -846,7 +846,7 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.Function vectorLength(); } - public class ExprWithAlias extends com.google.firebase.firestore.pipeline.Selectable { + public final class ExprWithAlias extends com.google.firebase.firestore.pipeline.Selectable { } public final class Field extends com.google.firebase.firestore.pipeline.Selectable { @@ -1222,14 +1222,6 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr xor(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); } - public final class ListOfExprs extends com.google.firebase.firestore.pipeline.Expr { - ctor public ListOfExprs(com.google.firebase.firestore.pipeline.Expr[] expressions); - } - - public final class MapOfExpr extends com.google.firebase.firestore.pipeline.Expr { - ctor public MapOfExpr(java.util.Map expressions); - } - public final class Ordering { method public static com.google.firebase.firestore.pipeline.Ordering ascending(com.google.firebase.firestore.pipeline.Expr expr); method public static com.google.firebase.firestore.pipeline.Ordering ascending(String fieldName); 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 8fb82718b69..cfe0d810454 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 @@ -309,7 +309,7 @@ abstract class Selectable : Expr() { } } -open class ExprWithAlias internal constructor(private val alias: String, private val expr: Expr) : +class ExprWithAlias internal constructor(private val alias: String, private val expr: Expr) : Selectable() { override fun getAlias() = alias override fun toProto(userDataReader: UserDataReader): Value = expr.toProto(userDataReader) @@ -344,7 +344,7 @@ class Field private constructor(private val fieldPath: ModelFieldPath) : Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() } -class MapOfExpr(private val expressions: Map) : Expr() { +internal class MapOfExpr(private val expressions: Map) : Expr() { override fun toProto(userDataReader: UserDataReader): Value { val builder = MapValue.newBuilder() for (expr in expressions) { @@ -354,7 +354,7 @@ class MapOfExpr(private val expressions: Map) : Expr() { } } -class ListOfExprs(private val expressions: Array) : Expr() { +internal class ListOfExprs(private val expressions: Array) : Expr() { override fun toProto(userDataReader: UserDataReader): Value = encodeValue(expressions.map{it.toProto(userDataReader)}) } From 9553abcacf2df421efa6f74b6bbfc5b20cfdb9d9 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 3 Mar 2025 12:19:16 -0500 Subject: [PATCH 027/152] Add options --- firebase-firestore/api.txt | 74 ++++++++++++++++- .../com/google/firebase/firestore/Pipeline.kt | 30 +++++-- .../firebase/firestore/pipeline/options.kt | 81 +++++++++++++++++++ .../firebase/firestore/pipeline/stage.kt | 65 ++++++++++----- 4 files changed, 219 insertions(+), 31 deletions(-) create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 63d017b6288..7a960e3061f 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -428,6 +428,8 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Pipeline distinct(java.lang.Object... groups); method public com.google.firebase.firestore.Pipeline distinct(java.lang.String... groups); method public com.google.android.gms.tasks.Task execute(); + method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Expr property, double[] vector, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Expr property, double[] vector, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure, com.google.firebase.firestore.pipeline.FindNearestOptions options); method public com.google.firebase.firestore.Pipeline genericStage(String name, java.lang.Object... params); method public com.google.firebase.firestore.Pipeline limit(long limit); method public com.google.firebase.firestore.Pipeline offset(long offset); @@ -435,6 +437,7 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Pipeline removeFields(java.lang.String... fields); method public com.google.firebase.firestore.Pipeline replace(com.google.firebase.firestore.pipeline.Selectable field); method public com.google.firebase.firestore.Pipeline replace(String field); + method public com.google.firebase.firestore.Pipeline sample(com.google.firebase.firestore.pipeline.SampleStage sample); method public com.google.firebase.firestore.Pipeline sample(int documents); method public com.google.firebase.firestore.Pipeline select(com.google.firebase.firestore.pipeline.Selectable... fields); method public com.google.firebase.firestore.Pipeline select(java.lang.Object... fields); @@ -442,7 +445,9 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Pipeline sort(com.google.firebase.firestore.pipeline.Ordering... orders); method public com.google.firebase.firestore.Pipeline union(com.google.firebase.firestore.Pipeline other); method public com.google.firebase.firestore.Pipeline unnest(com.google.firebase.firestore.pipeline.Selectable selectable); - method public com.google.firebase.firestore.Pipeline unnest(String field); + method public com.google.firebase.firestore.Pipeline unnest(com.google.firebase.firestore.pipeline.Selectable selectable, com.google.firebase.firestore.pipeline.UnnestOptions options); + method public com.google.firebase.firestore.Pipeline unnest(String field, String alias); + method public com.google.firebase.firestore.Pipeline unnest(String field, String alias, com.google.firebase.firestore.pipeline.UnnestOptions options); method public com.google.firebase.firestore.Pipeline where(com.google.firebase.firestore.pipeline.BooleanExpr condition); } @@ -658,6 +663,15 @@ package com.google.firebase.firestore.ktx { package com.google.firebase.firestore.pipeline { + public abstract class AbstractOptions> { + method public final T with(String key, boolean value); + method public final T with(String key, com.google.firebase.firestore.pipeline.Field value); + method public final T with(String key, double value); + method protected final T with(String key, error.NonExistentClass value); + method public final T with(String key, String value); + method public final T with(String key, long value); + } + public final class AggregateExpr { method public com.google.firebase.firestore.pipeline.AggregateWithAlias as(String alias); method public static com.google.firebase.firestore.pipeline.AggregateExpr avg(com.google.firebase.firestore.pipeline.Expr expr); @@ -860,8 +874,31 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Field of(String name); } - public final class FindNearestOptions { - method public java.util.Map toProto(); + public final class FindNearestOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { + method public com.google.firebase.firestore.pipeline.FindNearestOptions withDistanceField(com.google.firebase.firestore.pipeline.Field distanceField); + method public com.google.firebase.firestore.pipeline.FindNearestOptions withDistanceField(String distanceField); + method public com.google.firebase.firestore.pipeline.FindNearestOptions withLimit(long limit); + field public static final com.google.firebase.firestore.pipeline.FindNearestOptions.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.FindNearestOptions DEFAULT; + } + + public static final class FindNearestOptions.Companion { + } + + public final class FindNearestStage extends com.google.firebase.firestore.pipeline.Stage { + } + + public static final class FindNearestStage.DistanceMeasure { + field public static final com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure.Companion Companion; + } + + public static final class FindNearestStage.DistanceMeasure.Companion { + method public error.NonExistentClass getCOSINE(); + method public error.NonExistentClass getDOT_PRODUCT(); + method public error.NonExistentClass getEUCLIDEAN(); + property public final error.NonExistentClass COSINE; + property public final error.NonExistentClass DOT_PRODUCT; + property public final error.NonExistentClass EUCLIDEAN; } public class Function extends com.google.firebase.firestore.pipeline.Expr { @@ -1237,6 +1274,28 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Ordering descending(String fieldName); } + public final class SampleStage extends com.google.firebase.firestore.pipeline.Stage { + method public static com.google.firebase.firestore.pipeline.SampleStage withDocLimit(int documents); + method public static com.google.firebase.firestore.pipeline.SampleStage withPercentage(double percentage); + field public static final com.google.firebase.firestore.pipeline.SampleStage.Companion Companion; + } + + public static final class SampleStage.Companion { + method public com.google.firebase.firestore.pipeline.SampleStage withDocLimit(int documents); + method public com.google.firebase.firestore.pipeline.SampleStage withPercentage(double percentage); + } + + public static final class SampleStage.Mode { + field public static final com.google.firebase.firestore.pipeline.SampleStage.Mode.Companion Companion; + } + + public static final class SampleStage.Mode.Companion { + method public error.NonExistentClass getDOCUMENTS(); + method public error.NonExistentClass getPERCENT(); + property public final error.NonExistentClass DOCUMENTS; + property public final error.NonExistentClass PERCENT; + } + public abstract class Selectable extends com.google.firebase.firestore.pipeline.Expr { ctor public Selectable(); } @@ -1244,5 +1303,14 @@ package com.google.firebase.firestore.pipeline { public abstract class Stage { } + public final class UnnestOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { + method public com.google.firebase.firestore.pipeline.UnnestOptions withIndexField(String indexField); + field public static final com.google.firebase.firestore.pipeline.UnnestOptions.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.UnnestOptions DEFAULT; + } + + public static final class UnnestOptions.Companion { + } + } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 662563488fa..e88fc324c0d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -29,7 +29,10 @@ import com.google.firebase.firestore.pipeline.CollectionSource import com.google.firebase.firestore.pipeline.DatabaseSource import com.google.firebase.firestore.pipeline.DistinctStage import com.google.firebase.firestore.pipeline.DocumentsSource +import com.google.firebase.firestore.pipeline.Expr import com.google.firebase.firestore.pipeline.Field +import com.google.firebase.firestore.pipeline.FindNearestOptions +import com.google.firebase.firestore.pipeline.FindNearestStage import com.google.firebase.firestore.pipeline.GenericArg import com.google.firebase.firestore.pipeline.GenericStage import com.google.firebase.firestore.pipeline.LimitStage @@ -43,6 +46,7 @@ import com.google.firebase.firestore.pipeline.Selectable import com.google.firebase.firestore.pipeline.SortStage import com.google.firebase.firestore.pipeline.Stage import com.google.firebase.firestore.pipeline.UnionStage +import com.google.firebase.firestore.pipeline.UnnestOptions import com.google.firebase.firestore.pipeline.UnnestStage import com.google.firebase.firestore.pipeline.WhereStage import com.google.firebase.firestore.util.Preconditions @@ -134,27 +138,39 @@ internal constructor( fun aggregate(aggregateStage: AggregateStage): Pipeline = append(aggregateStage) - // fun findNearest() + fun findNearest( + property: Expr, + vector: DoubleArray, + distanceMeasure: FindNearestStage.DistanceMeasure + ) = append(FindNearestStage(property, vector, distanceMeasure, FindNearestOptions.DEFAULT)) + + fun findNearest( + property: Expr, + vector: DoubleArray, + distanceMeasure: FindNearestStage.DistanceMeasure, + options: FindNearestOptions + ) = append(FindNearestStage(property, vector, distanceMeasure, options)) fun replace(field: String): Pipeline = replace(Field.of(field)) fun replace(field: Selectable): Pipeline = append(ReplaceStage(field, ReplaceStage.Mode.FULL_REPLACE)) - fun sample(documents: Int): Pipeline = append(SampleStage(documents, SampleStage.Mode.DOCUMENTS)) + fun sample(documents: Int): Pipeline = append(SampleStage.withDocLimit(documents)) - // fun sample(options: SampleOptions): Pipeline + fun sample(sample: SampleStage): Pipeline = append(sample) fun union(other: Pipeline): Pipeline = append(UnionStage(other)) - fun unnest(field: String): Pipeline = unnest(Field.of(field)) + fun unnest(field: String, alias: String): Pipeline = unnest(Field.of(field).`as`(alias)) - // fun unnest(field: String, options: UnnestOptions): Pipeline = unnest(Field.of(field), options) + fun unnest(field: String, alias: String, options: UnnestOptions): Pipeline = + unnest(Field.of(field).`as`(alias), options) fun unnest(selectable: Selectable): Pipeline = append(UnnestStage(selectable)) - // fun unnest(selectable: Selectable, options: UnnestOptions): Pipeline = - // append(UnnestStage(selectable)) + fun unnest(selectable: Selectable, options: UnnestOptions): Pipeline = + append(UnnestStage(selectable)) private inner class ObserverSnapshotTask : PipelineResultObserver { private val taskCompletionSource = TaskCompletionSource() diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt new file mode 100644 index 00000000000..5695a9dff91 --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt @@ -0,0 +1,81 @@ +// Copyright 2025 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.firebase.firestore.pipeline + +import com.google.common.collect.ImmutableMap +import com.google.firebase.firestore.model.Values +import com.google.firestore.v1.ArrayValue +import com.google.firestore.v1.MapValue +import com.google.firestore.v1.Value + +/** + * Wither style Key/Value options object. + * + * Basic `wither` functionality built upon `ImmutableMap, Value>`. Exposes methods + * to construct, augment, and encode Kay/Value pairs. The wrapped collection + * `ImmutableMap, Value>` is an implementation detail, not to be exposed, since + * more efficient implementations are possible. + */ +internal class InternalOptions +internal constructor(private val options: ImmutableMap) { + internal fun with(key: String, value: Value): InternalOptions { + val builder = ImmutableMap.builderWithExpectedSize(options.size + 1) + builder.putAll(options) + builder.put(key, value) + return InternalOptions(builder.buildKeepingLast()) + } + + internal fun with(key: String, values: Iterable): InternalOptions { + val arrayValue = ArrayValue.newBuilder().addAllValues(values).build() + return with(key, Value.newBuilder().setArrayValue(arrayValue).build()) + } + + internal fun with(key: String, value: InternalOptions): InternalOptions { + return with(key, value.toValue()) + } + + internal fun forEach(f: (String, Value) -> Unit) = options.forEach(f) + + private fun toValue(): Value { + val mapValue = MapValue.newBuilder().putAllFields(options).build() + return Value.newBuilder().setMapValue(mapValue).build() + } + + internal companion object { + internal val EMPTY: InternalOptions = InternalOptions(ImmutableMap.of()) + + internal fun of(key: String, value: Value): InternalOptions { + return InternalOptions(ImmutableMap.of(key, value)) + } + } +} + +abstract class AbstractOptions> +internal constructor(internal val options: InternalOptions) { + + internal abstract fun self(options: InternalOptions): T + + protected fun with(key: String, value: Value): T = self(options.with(key, value)) + + fun with(key: String, value: String): T = with(key, Values.encodeValue(value)) + + fun with(key: String, value: Boolean): T = with(key, Values.encodeValue(value)) + + fun with(key: String, value: Long): T = with(key, Values.encodeValue(value)) + + fun with(key: String, value: Double): T = with(key, Values.encodeValue(value)) + + fun with(key: String, value: Field): T = with(key, value.toProto()) +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index 7e8ab87ee1b..b1477409eda 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -14,21 +14,22 @@ package com.google.firebase.firestore.pipeline -import com.google.common.collect.ImmutableMap import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.model.Values.encodeVectorValue +import com.google.firebase.firestore.pipeline.Field.Companion.of import com.google.firestore.v1.Pipeline import com.google.firestore.v1.Value abstract class Stage -internal constructor(private val name: String, private val options: Map) { - internal constructor(name: String) : this(name, emptyMap()) +private constructor(private val name: String, private val options: InternalOptions) { + internal constructor(name: String) : this(name, InternalOptions.EMPTY) + internal constructor(name: String, options: AbstractOptions<*>) : this(name, options.options) internal fun toProtoStage(userDataReader: UserDataReader): Pipeline.Stage { val builder = Pipeline.Stage.newBuilder() builder.setName(name) args(userDataReader).forEach { arg -> builder.addArgs(arg) } - builder.putAllOptions(options) + options.forEach(builder::putOptions) return builder.build() } internal abstract fun args(userDataReader: UserDataReader): Sequence @@ -147,13 +148,13 @@ internal class WhereStage internal constructor(private val condition: BooleanExp sequenceOf(condition.toProto(userDataReader)) } -internal class FindNearestStage +class FindNearestStage internal constructor( private val property: Expr, private val vector: DoubleArray, private val distanceMeasure: DistanceMeasure, - private val options: FindNearestOptions -) : Stage("find_nearest", options.toProto()) { + options: FindNearestOptions +) : Stage("find_nearest", options) { class DistanceMeasure private constructor(internal val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) @@ -168,18 +169,21 @@ internal constructor( sequenceOf(property.toProto(userDataReader), encodeVectorValue(vector), distanceMeasure.proto) } -class FindNearestOptions -internal constructor(private val limit: Long?, private val distanceField: Field?) { - fun toProto(): Map { - val builder = ImmutableMap.builder() - if (limit != null) { - builder.put("limit", encodeValue(limit)) - } - if (distanceField != null) { - builder.put("distance_field", distanceField.toProto()) - } - return builder.build() +class FindNearestOptions private constructor(options: InternalOptions) : + AbstractOptions(options) { + companion object { + @JvmField val DEFAULT = FindNearestOptions(InternalOptions.EMPTY) } + + override fun self(options: InternalOptions): FindNearestOptions = FindNearestOptions(options) + + fun withLimit(limit: Long): FindNearestOptions = with("limit", limit) + + fun withDistanceField(distanceField: Field): FindNearestOptions = + with("distance_field", distanceField) + + fun withDistanceField(distanceField: String): FindNearestOptions = + withDistanceField(of(distanceField)) } internal class LimitStage internal constructor(private val limit: Long) : Stage("limit") { @@ -230,7 +234,7 @@ internal constructor(private val field: Selectable, private val mode: Mode) : St sequenceOf(field.toProto(userDataReader), mode.proto) } -internal class SampleStage internal constructor(private val size: Number, private val mode: Mode) : +class SampleStage private constructor(private val size: Number, private val mode: Mode) : Stage("sample") { class Mode private constructor(internal val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) @@ -239,6 +243,11 @@ internal class SampleStage internal constructor(private val size: Number, privat val PERCENT = Mode("percent") } } + companion object { + @JvmStatic fun withPercentage(percentage: Double) = SampleStage(percentage, Mode.PERCENT) + + @JvmStatic fun withDocLimit(documents: Int) = SampleStage(documents, Mode.DOCUMENTS) + } override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(size), mode.proto) } @@ -249,8 +258,22 @@ internal constructor(private val other: com.google.firebase.firestore.Pipeline) sequenceOf(Value.newBuilder().setPipelineValue(other.toPipelineProto()).build()) } -internal class UnnestStage internal constructor(private val selectable: Selectable) : - Stage("unnest") { +internal class UnnestStage +internal constructor(private val selectable: Selectable, options: UnnestOptions) : + Stage("unnest", options) { + internal constructor(selectable: Selectable) : this(selectable, UnnestOptions.DEFAULT) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(selectable.getAlias()), selectable.toProto(userDataReader)) } + +class UnnestOptions private constructor(options: InternalOptions) : + AbstractOptions(options) { + + fun withIndexField(indexField: String): UnnestOptions = with("index_field", indexField) + + override fun self(options: InternalOptions) = UnnestOptions(options) + + companion object { + @JvmField val DEFAULT: UnnestOptions = UnnestOptions(InternalOptions.EMPTY) + } +} From b2f0b3f8d6432dcc915138ed48575617d22272a3 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 3 Mar 2025 15:09:27 -0500 Subject: [PATCH 028/152] GenericOptions --- .../firebase/firestore/PipelineTest.java | 3 ++- .../com/google/firebase/firestore/Pipeline.kt | 6 +++-- .../firebase/firestore/pipeline/stage.kt | 26 ++++++++++++++++--- 3 files changed, 28 insertions(+), 7 deletions(-) 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 50df7ef880d..7498bfccff2 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 @@ -48,6 +48,7 @@ import com.google.firebase.firestore.pipeline.Constant; import com.google.firebase.firestore.pipeline.Field; import com.google.firebase.firestore.pipeline.Function; +import com.google.firebase.firestore.pipeline.GenericStage; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import java.util.Collections; import java.util.LinkedHashMap; @@ -284,7 +285,7 @@ public void groupAndAccumulateResultsGeneric() { "aggregate", ImmutableMap.of("avgRating", AggregateExpr.avg("rating")), ImmutableMap.of("genre", Field.of("genre"))) - .genericStage("where", gt("avgRating", 4.3)) + .genericStage(GenericStage.of("where").withArguments(gt("avgRating", 4.3))) .genericStage("sort", Field.of("avgRating").descending()) .execute(); assertThat(waitFor(execute).getResults()) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index e88fc324c0d..29b81683b61 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -99,8 +99,10 @@ internal constructor( .addAllStages(stages.map { it.toProtoStage(userDataReader) }) .build() - fun genericStage(name: String, vararg params: Any) = - append(GenericStage(name, params.map(GenericArg::from))) + fun genericStage(name: String, vararg arguments: Any): Pipeline = + append(GenericStage(name, arguments.map(GenericArg::from))) + + fun genericStage(stage: GenericStage): Pipeline = append(stage) fun addFields(vararg fields: Selectable): Pipeline = append(AddFieldsStage(fields)) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index b1477409eda..7ab8ec02b07 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -22,7 +22,7 @@ import com.google.firestore.v1.Pipeline import com.google.firestore.v1.Value abstract class Stage -private constructor(private val name: String, private val options: InternalOptions) { +private constructor(protected val name: String, private val options: InternalOptions) { internal constructor(name: String) : this(name, InternalOptions.EMPTY) internal constructor(name: String, options: AbstractOptions<*>) : this(name, options.options) internal fun toProtoStage(userDataReader: UserDataReader): Pipeline.Stage { @@ -35,10 +35,20 @@ private constructor(private val name: String, private val options: InternalOptio internal abstract fun args(userDataReader: UserDataReader): Sequence } -internal class GenericStage -internal constructor(name: String, private val params: List) : Stage(name) { +class GenericStage +private constructor(name: String, private val arguments: List, private val options: GenericOptions) : Stage(name, options) { + internal constructor(name: String, arguments: List) : this(name, arguments, GenericOptions.DEFAULT) + companion object { + @JvmStatic + fun of(name: String) = GenericStage(name, emptyList()) + } + + fun withArguments(vararg arguments: Any): GenericStage = GenericStage(name, arguments.map(GenericArg::from), options) + + fun withOptions(options: GenericOptions): GenericStage = GenericStage(name, arguments, options) + override fun args(userDataReader: UserDataReader): Sequence = - params.asSequence().map { it.toProto(userDataReader) } + arguments.asSequence().map { it.toProto(userDataReader) } } internal sealed class GenericArg { @@ -77,6 +87,14 @@ internal sealed class GenericArg { } } +class GenericOptions private constructor(options: InternalOptions) : AbstractOptions(options) { + companion object { + @JvmField + val DEFAULT = GenericOptions(InternalOptions.EMPTY) + } + override fun self(options: InternalOptions) = GenericOptions(options) +} + internal class DatabaseSource : Stage("database") { override fun args(userDataReader: UserDataReader): Sequence = emptySequence() } From 5e8f7f024a604a4dc8c6c07c17dcc772e2c7205c Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 3 Mar 2025 15:37:48 -0500 Subject: [PATCH 029/152] Spotless --- firebase-firestore/CHANGELOG.md | 2 +- firebase-firestore/api.txt | 24 ++++++++++++++++++- .../testutil/IntegrationTestUtil.java | 2 +- .../firebase/firestore/pipeline/stage.kt | 23 +++++++++++------- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 66fce5b35ce..d2f756372e9 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,5 +1,5 @@ # Unreleased - +* [feature] Pipelines # 25.1.2 * [fixed] Fixed a server and sdk mismatch in unicode string sorting. [#6615](//github.com/firebase/firebase-android-sdk/pull/6615) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 7a960e3061f..db0a902420e 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -430,7 +430,8 @@ package com.google.firebase.firestore { method public com.google.android.gms.tasks.Task execute(); method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Expr property, double[] vector, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Expr property, double[] vector, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure, com.google.firebase.firestore.pipeline.FindNearestOptions options); - method public com.google.firebase.firestore.Pipeline genericStage(String name, java.lang.Object... params); + method public com.google.firebase.firestore.Pipeline genericStage(com.google.firebase.firestore.pipeline.GenericStage stage); + method public com.google.firebase.firestore.Pipeline genericStage(String name, java.lang.Object... arguments); method public com.google.firebase.firestore.Pipeline limit(long limit); method public com.google.firebase.firestore.Pipeline offset(long offset); method public com.google.firebase.firestore.Pipeline removeFields(com.google.firebase.firestore.pipeline.Field... fields); @@ -1259,6 +1260,25 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr xor(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); } + public final class GenericOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { + field public static final com.google.firebase.firestore.pipeline.GenericOptions.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.GenericOptions DEFAULT; + } + + public static final class GenericOptions.Companion { + } + + public final class GenericStage extends com.google.firebase.firestore.pipeline.Stage { + method public static com.google.firebase.firestore.pipeline.GenericStage of(String name); + method public com.google.firebase.firestore.pipeline.GenericStage withArguments(java.lang.Object... arguments); + method public com.google.firebase.firestore.pipeline.GenericStage withOptions(com.google.firebase.firestore.pipeline.GenericOptions options); + field public static final com.google.firebase.firestore.pipeline.GenericStage.Companion Companion; + } + + public static final class GenericStage.Companion { + method public com.google.firebase.firestore.pipeline.GenericStage of(String name); + } + public final class Ordering { method public static com.google.firebase.firestore.pipeline.Ordering ascending(com.google.firebase.firestore.pipeline.Expr expr); method public static com.google.firebase.firestore.pipeline.Ordering ascending(String fieldName); @@ -1301,6 +1321,8 @@ package com.google.firebase.firestore.pipeline { } public abstract class Stage { + method protected final String getName(); + property protected final String name; } public final class UnnestOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index a7417d96563..d11c27828cf 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -98,7 +98,7 @@ public enum TargetBackend { // Set this to the desired enum value to change the target backend when running tests locally. // Note: DO NOT change this variable except for local testing. - private static final TargetBackend backendForLocalTesting = null; + private static final TargetBackend backendForLocalTesting = TargetBackend.NIGHTLY; private static final TargetBackend backend = getTargetBackend(); private static final String EMULATOR_HOST = "10.0.2.2"; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index 7ab8ec02b07..e4b0345f662 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -36,14 +36,21 @@ private constructor(protected val name: String, private val options: InternalOpt } class GenericStage -private constructor(name: String, private val arguments: List, private val options: GenericOptions) : Stage(name, options) { - internal constructor(name: String, arguments: List) : this(name, arguments, GenericOptions.DEFAULT) +private constructor( + name: String, + private val arguments: List, + private val options: GenericOptions +) : Stage(name, options) { + internal constructor( + name: String, + arguments: List + ) : this(name, arguments, GenericOptions.DEFAULT) companion object { - @JvmStatic - fun of(name: String) = GenericStage(name, emptyList()) + @JvmStatic fun of(name: String) = GenericStage(name, emptyList()) } - fun withArguments(vararg arguments: Any): GenericStage = GenericStage(name, arguments.map(GenericArg::from), options) + fun withArguments(vararg arguments: Any): GenericStage = + GenericStage(name, arguments.map(GenericArg::from), options) fun withOptions(options: GenericOptions): GenericStage = GenericStage(name, arguments, options) @@ -87,10 +94,10 @@ internal sealed class GenericArg { } } -class GenericOptions private constructor(options: InternalOptions) : AbstractOptions(options) { +class GenericOptions private constructor(options: InternalOptions) : + AbstractOptions(options) { companion object { - @JvmField - val DEFAULT = GenericOptions(InternalOptions.EMPTY) + @JvmField val DEFAULT = GenericOptions(InternalOptions.EMPTY) } override fun self(options: InternalOptions) = GenericOptions(options) } From 8d049e061091f00992ae6d94a48f14748b56e8ef Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 19 Mar 2025 11:51:49 -0400 Subject: [PATCH 030/152] Bit Operators --- .../firestore/pipeline/expressions.kt | 404 ++++++++++++------ 1 file changed, 276 insertions(+), 128 deletions(-) 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 cfe0d810454..401aa2ff20a 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 @@ -22,6 +22,7 @@ import com.google.firebase.firestore.GeoPoint import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.VectorValue import com.google.firebase.firestore.model.DocumentKey +import com.google.firebase.firestore.model.FieldPath as ModelFieldPath import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.pipeline.Constant.Companion.of import com.google.firebase.firestore.util.CustomClassMapper @@ -29,13 +30,16 @@ import com.google.firestore.v1.MapValue import com.google.firestore.v1.Value import java.util.Date import kotlin.reflect.KFunction1 -import com.google.firebase.firestore.model.FieldPath as ModelFieldPath abstract class Expr internal constructor() { internal companion object { - internal fun toExprOrConstant(value: Any?): Expr = toExpr(value, ::toExprOrConstant) ?: pojoToExprOrConstant(CustomClassMapper.convertToPlainJavaTypes(value)) + internal fun toExprOrConstant(value: Any?): Expr = + toExpr(value, ::toExprOrConstant) + ?: pojoToExprOrConstant(CustomClassMapper.convertToPlainJavaTypes(value)) - private fun pojoToExprOrConstant(value: Any?): Expr = toExpr(value, ::pojoToExprOrConstant) ?: throw IllegalArgumentException("Unknown type: $value") + private fun pojoToExprOrConstant(value: Any?): Expr = + toExpr(value, ::pojoToExprOrConstant) + ?: throw IllegalArgumentException("Unknown type: $value") private fun toExpr(value: Any?, toExpr: KFunction1): Expr? { if (value == null) return Constant.nullValue() @@ -50,11 +54,15 @@ abstract class Expr internal constructor() { is Blob -> of(value) is DocumentReference -> of(value) is VectorValue -> of(value) - is Map<*, *> -> MapOfExpr(value.entries.associate { - val key = it.key - if (key is String) key to toExpr(it.value) else - throw IllegalArgumentException("Maps with non-string keys are not supported") - }) + is Value -> of(value) + is Map<*, *> -> + MapOfExpr( + value.entries.associate { + val key = it.key + if (key is String) key to toExpr(it.value) + else throw IllegalArgumentException("Maps with non-string keys are not supported") + } + ) is List<*> -> ListOfExprs(value.map(toExpr).toTypedArray()) else -> null } @@ -67,6 +75,28 @@ abstract class Expr internal constructor() { others.map(::toExprOrConstant).toTypedArray() } + fun bitAnd(right: Expr) = Function.bitAnd(this, right) + + fun bitAnd(right: Any) = Function.bitAnd(this, right) + + fun bitOr(right: Expr) = Function.bitOr(this, right) + + fun bitOr(right: Any) = Function.bitOr(this, right) + + fun bitXor(right: Expr) = Function.bitXor(this, right) + + fun bitXor(right: Any) = Function.bitXor(this, right) + + fun bitNot() = Function.bitNot(this) + + fun bitLeftShift(numberExpr: Expr) = Function.bitLeftShift(this, numberExpr) + + fun bitLeftShift(number: Int) = Function.bitLeftShift(this, number) + + fun bitRightShift(numberExpr: Expr) = Function.bitRightShift(this, numberExpr) + + fun bitRightShift(number: Int) = Function.bitRightShift(this, number) + /** * Assigns an alias to this expression. * @@ -291,6 +321,8 @@ abstract class Expr internal constructor() { fun lte(other: Any) = Function.lte(this, other) + fun exists() = Function.exists(this) + internal abstract fun toProto(userDataReader: UserDataReader): Value } @@ -315,8 +347,7 @@ class ExprWithAlias internal constructor(private val alias: String, private val override fun toProto(userDataReader: UserDataReader): Value = expr.toProto(userDataReader) } -class Field private constructor(private val fieldPath: ModelFieldPath) : - Selectable() { +class Field internal constructor(private val fieldPath: ModelFieldPath) : Selectable() { companion object { @JvmStatic @@ -329,9 +360,6 @@ class Field private constructor(private val fieldPath: ModelFieldPath) : @JvmStatic fun of(fieldPath: FieldPath): Field { - if (fieldPath == FieldPath.documentId()) { - return Field(FieldPath.documentId().internalPath) - } return Field(fieldPath.internalPath) } } @@ -347,36 +375,102 @@ class Field private constructor(private val fieldPath: ModelFieldPath) : internal class MapOfExpr(private val expressions: Map) : Expr() { override fun toProto(userDataReader: UserDataReader): Value { val builder = MapValue.newBuilder() - for (expr in expressions) { - builder.putFields(expr.key, expr.value.toProto(userDataReader)) + for ((key, value) in expressions) { + builder.putFields(key, value.toProto(userDataReader)) } return Value.newBuilder().setMapValue(builder).build() } } internal class ListOfExprs(private val expressions: Array) : Expr() { - override fun toProto(userDataReader: UserDataReader): Value = encodeValue(expressions.map{it.toProto(userDataReader)}) + override fun toProto(userDataReader: UserDataReader): Value = + encodeValue(expressions.map { it.toProto(userDataReader) }) } open class Function protected constructor(private val name: String, private val params: Array) : Expr() { - private constructor(name: String, param: Expr, vararg params: Any) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) - private constructor(name: String, fieldName: String, vararg params: Any) : this(name, arrayOf(Field.of(fieldName), *toArrayOfExprOrConstant(params))) + private constructor( + name: String, + param: Expr, + vararg params: Any + ) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) + private constructor( + name: String, + fieldName: String, + vararg params: Any + ) : this(name, arrayOf(Field.of(fieldName), *toArrayOfExprOrConstant(params))) companion object { @JvmStatic fun generic(name: String, vararg expr: Expr) = Function(name, expr) @JvmStatic - fun and(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("and", condition, *conditions) + fun and(condition: BooleanExpr, vararg conditions: BooleanExpr) = + BooleanExpr("and", condition, *conditions) @JvmStatic - fun or(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("or", condition, *conditions) + fun or(condition: BooleanExpr, vararg conditions: BooleanExpr) = + BooleanExpr("or", condition, *conditions) @JvmStatic - fun xor(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("xor", condition, *conditions) + fun xor(condition: BooleanExpr, vararg conditions: BooleanExpr) = + BooleanExpr("xor", condition, *conditions) @JvmStatic fun not(condition: BooleanExpr) = BooleanExpr("not", condition) + @JvmStatic fun bitAnd(left: Expr, right: Expr) = Function("bit_and", left, right) + + @JvmStatic fun bitAnd(left: Expr, right: Any) = Function("bit_and", left, right) + + @JvmStatic fun bitAnd(fieldName: String, right: Expr) = Function("bit_and", fieldName, right) + + @JvmStatic fun bitAnd(fieldName: String, right: Any) = Function("bit_and", fieldName, right) + + @JvmStatic fun bitOr(left: Expr, right: Expr) = Function("bit_or", left, right) + + @JvmStatic fun bitOr(left: Expr, right: Any) = Function("bit_or", left, right) + + @JvmStatic fun bitOr(fieldName: String, right: Expr) = Function("bit_or", fieldName, right) + + @JvmStatic fun bitOr(fieldName: String, right: Any) = Function("bit_or", fieldName, right) + + @JvmStatic fun bitXor(left: Expr, right: Expr) = Function("bit_xor", left, right) + + @JvmStatic fun bitXor(left: Expr, right: Any) = Function("bit_xor", left, right) + + @JvmStatic fun bitXor(fieldName: String, right: Expr) = Function("bit_xor", fieldName, right) + + @JvmStatic fun bitXor(fieldName: String, right: Any) = Function("bit_xor", fieldName, right) + + @JvmStatic fun bitNot(left: Expr) = Function("bit_not", left) + + @JvmStatic fun bitNot(fieldName: String) = Function("bit_not", fieldName) + + @JvmStatic + fun bitLeftShift(left: Expr, numberExpr: Expr) = Function("bit_left_shift", left, numberExpr) + + @JvmStatic fun bitLeftShift(left: Expr, number: Int) = Function("bit_left_shift", left, number) + + @JvmStatic + fun bitLeftShift(fieldName: String, numberExpr: Expr) = + Function("bit_left_shift", fieldName, numberExpr) + + @JvmStatic + fun bitLeftShift(fieldName: String, number: Int) = Function("bit_left_shift", fieldName, number) + + @JvmStatic + fun bitRightShift(left: Expr, numberExpr: Expr) = Function("bit_right_shift", left, numberExpr) + + @JvmStatic + fun bitRightShift(left: Expr, number: Int) = Function("bit_right_shift", left, number) + + @JvmStatic + fun bitRightShift(fieldName: String, numberExpr: Expr) = + Function("bit_right_shift", fieldName, numberExpr) + + @JvmStatic + fun bitRightShift(fieldName: String, number: Int) = + Function("bit_right_shift", fieldName, number) + @JvmStatic fun add(left: Expr, right: Expr) = Function("add", left, right) @JvmStatic fun add(left: Expr, right: Any) = Function("add", left, right) @@ -417,9 +511,13 @@ protected constructor(private val name: String, private val params: Array) = BooleanExpr("in", array, ListOfExprs(toArrayOfExprOrConstant(values))) + @JvmStatic + fun inAny(array: Expr, values: List) = + BooleanExpr("in", array, ListOfExprs(toArrayOfExprOrConstant(values))) - @JvmStatic fun inAny(fieldName: String, values: List) = BooleanExpr("in", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + @JvmStatic + fun inAny(fieldName: String, values: List) = + BooleanExpr("in", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) @JvmStatic fun notInAny(array: Expr, values: List) = not(inAny(array, values)) @@ -434,7 +532,8 @@ protected constructor(private val name: String, private val params: Array) = Function("array_concat", array, ListOfExprs(toArrayOfExprOrConstant(arrays))) + @JvmStatic + fun arrayConcat(array: Expr, arrays: List) = + Function("array_concat", array, ListOfExprs(toArrayOfExprOrConstant(arrays))) @JvmStatic - fun arrayConcat(fieldName: String, arrays: List) = Function("array_concat", fieldName, ListOfExprs(toArrayOfExprOrConstant(arrays))) + fun arrayConcat(fieldName: String, arrays: List) = + Function("array_concat", fieldName, ListOfExprs(toArrayOfExprOrConstant(arrays))) @JvmStatic fun arrayReverse(array: Expr) = Function("array_reverse", array) @JvmStatic fun arrayReverse(fieldName: String) = Function("array_reverse", fieldName) - @JvmStatic fun arrayContains(array: Expr, value: Expr) = BooleanExpr("array_contains", array, value) + @JvmStatic + fun arrayContains(array: Expr, value: Expr) = BooleanExpr("array_contains", array, value) - @JvmStatic fun arrayContains(fieldName: String, value: Expr) = BooleanExpr("array_contains", fieldName, value) + @JvmStatic + fun arrayContains(fieldName: String, value: Expr) = + BooleanExpr("array_contains", fieldName, value) - @JvmStatic fun arrayContains(array: Expr, value: Any) = BooleanExpr("array_contains", array, value) + @JvmStatic + fun arrayContains(array: Expr, value: Any) = BooleanExpr("array_contains", array, value) - @JvmStatic fun arrayContains(fieldName: String, value: Any) = BooleanExpr("array_contains", fieldName, value) + @JvmStatic + fun arrayContains(fieldName: String, value: Any) = + BooleanExpr("array_contains", fieldName, value) @JvmStatic - fun arrayContainsAll(array: Expr, values: List) = BooleanExpr("array_contains_all", array, ListOfExprs(toArrayOfExprOrConstant(values))) + fun arrayContainsAll(array: Expr, values: List) = + BooleanExpr("array_contains_all", array, ListOfExprs(toArrayOfExprOrConstant(values))) @JvmStatic - fun arrayContainsAll(fieldName: String, values: List) = BooleanExpr("array_contains_all", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + fun arrayContainsAll(fieldName: String, values: List) = + BooleanExpr("array_contains_all", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) @JvmStatic - fun arrayContainsAny(array: Expr, values: List) = BooleanExpr("array_contains_any", array, ListOfExprs(toArrayOfExprOrConstant(values))) + fun arrayContainsAny(array: Expr, values: List) = + BooleanExpr("array_contains_any", array, ListOfExprs(toArrayOfExprOrConstant(values))) @JvmStatic - fun arrayContainsAny(fieldName: String, values: List) = BooleanExpr("array_contains_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + fun arrayContainsAny(fieldName: String, values: List) = + BooleanExpr("array_contains_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) @JvmStatic fun arrayLength(array: Expr) = Function("array_length", array) @JvmStatic fun arrayLength(fieldName: String) = Function("array_length", fieldName) - @JvmStatic fun ifThen(condition: BooleanExpr, then: Expr) = - Function("if", condition, then, Constant.NULL) + @JvmStatic + fun ifThen(condition: BooleanExpr, then: Expr) = Function("if", condition, then, Constant.NULL) - @JvmStatic fun ifThen(condition: BooleanExpr, then: Any) = Function("if", condition, then, Constant.NULL) + @JvmStatic + fun ifThen(condition: BooleanExpr, then: Any) = Function("if", condition, then, Constant.NULL) - @JvmStatic fun ifThenElse(condition: BooleanExpr, then: Expr, `else`: Expr) = Function("if", condition, then, `else`) + @JvmStatic + fun ifThenElse(condition: BooleanExpr, then: Expr, `else`: Expr) = + Function("if", condition, then, `else`) + + @JvmStatic + fun ifThenElse(condition: BooleanExpr, then: Any, `else`: Any) = + Function("if", condition, then, `else`) - @JvmStatic fun ifThenElse(condition: BooleanExpr, then: Any, `else`: Any) = Function("if", condition, then, `else`) + @JvmStatic fun exists(expr: Expr) = BooleanExpr("exists", expr) } override fun toProto(userDataReader: UserDataReader): Value { @@ -787,13 +960,24 @@ protected constructor(private val name: String, private val params: Array) : Function(name, params) { - internal constructor(name: String, param: Expr, vararg params: Any) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) - internal constructor(name: String, fieldName: String, vararg params: Any) : this(name, arrayOf(Field.of(fieldName), *toArrayOfExprOrConstant(params))) + internal constructor( + name: String, + params: List + ) : this(name, toArrayOfExprOrConstant(params)) + internal constructor( + name: String, + param: Expr, + vararg params: Any + ) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) + internal constructor( + name: String, + fieldName: String, + vararg params: Any + ) : this(name, arrayOf(Field.of(fieldName), *toArrayOfExprOrConstant(params))) companion object { @JvmStatic fun generic(name: String, vararg expr: Expr) = BooleanExpr(name, expr) - } fun not() = not(this) @@ -838,39 +1022,3 @@ class Ordering private constructor(private val expr: Expr, private val dir: Dire ) .build() } - -// class BitAnd(left: Expr, right: Expr) : Function("bit_and", left, right) { -// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) -// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) -// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -// } - -// class BitOr(left: Expr, right: Expr) : Function("bit_or", left, right) { -// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) -// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) -// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -// } - -// class BitXor(left: Expr, right: Expr) : Function("bit_xor", left, right) { -// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) -// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) -// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -// } - -// class BitNot(left: Expr, right: Expr) : Function("bit_not", left, right) { -// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) -// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) -// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -// } - -// class BitLeftShift(left: Expr, right: Expr) : Function("bit_left_shift", left, right) { -// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) -// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) -// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -// } - -// class BitRightShift(left: Expr, right: Expr) : Function("bit_right_shift", left, right) { -// constructor(left: Expr, right: Any) : this(left, castToExprOrConvertToConstant(right)) -// constructor(fieldName: String, right: Expr) : this(Field.of(fieldName), right) -// constructor(fieldName: String, right: Any) : this(Field.of(fieldName), right) -// } From fcae38059aaffe89bed9ad915f9d9fafd9e3a5cf Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 24 Mar 2025 15:41:48 -0400 Subject: [PATCH 031/152] Convert query to pipeline --- .../firestore/QueryToPipelineTest.java | 889 ++++++++++++++++++ .../testutil/IntegrationTestUtil.java | 39 + .../firestore/CollectionReference.java | 1 + .../com/google/firebase/firestore/Pipeline.kt | 48 +- .../com/google/firebase/firestore/Query.java | 5 + .../firebase/firestore/UserDataReader.java | 2 +- .../firebase/firestore/UserDataWriter.java | 2 +- .../firestore/core/CompositeFilter.java | 14 + .../firebase/firestore/core/FieldFilter.java | 47 + .../firebase/firestore/core/Filter.java | 3 + .../firebase/firestore/core/OrderBy.java | 2 +- .../google/firebase/firestore/core/Query.java | 104 ++ .../firebase/firestore/model/BasePath.java | 9 +- .../firebase/firestore/model/ObjectValue.java | 9 +- .../google/firebase/firestore/model/Values.kt | 24 +- .../firebase/firestore/pipeline/Constant.kt | 4 + .../firestore/pipeline/expressions.kt | 46 +- .../firebase/firestore/pipeline/stage.kt | 12 +- .../firestore/remote/RemoteSerializer.java | 46 +- 19 files changed, 1241 insertions(+), 65 deletions(-) create mode 100644 firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java new file mode 100644 index 00000000000..c4f8f33bb01 --- /dev/null +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java @@ -0,0 +1,889 @@ +// Copyright 2018 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.firebase.firestore; + +import static com.google.common.io.Files.map; +import static com.google.firebase.firestore.Filter.and; +import static com.google.firebase.firestore.Filter.arrayContains; +import static com.google.firebase.firestore.Filter.arrayContainsAny; +import static com.google.firebase.firestore.Filter.equalTo; +import static com.google.firebase.firestore.Filter.inArray; +import static com.google.firebase.firestore.Filter.or; +import static com.google.firebase.firestore.pipeline.Function.ifThen; +import static com.google.firebase.firestore.testutil.IntegrationTestUtil.checkQueryAndPipelineResultsMatch; +import static com.google.firebase.firestore.testutil.IntegrationTestUtil.nullList; +import static com.google.firebase.firestore.testutil.IntegrationTestUtil.pipelineSnapshotToIds; +import static com.google.firebase.firestore.testutil.IntegrationTestUtil.pipelineSnapshotToValues; +import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollection; +import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollectionWithDocs; +import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testFirestore; +import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor; +import static com.google.firebase.firestore.testutil.TestUtil.expectError; +import static com.google.firebase.firestore.testutil.TestUtil.map; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.gms.tasks.Task; +import com.google.common.collect.Lists; +import com.google.firebase.firestore.Query.Direction; +import com.google.firebase.firestore.pipeline.Constant; +import com.google.firebase.firestore.pipeline.Field; +import com.google.firebase.firestore.testutil.IntegrationTestUtil; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class QueryToPipelineTest { + + @After + public void tearDown() { + IntegrationTestUtil.tearDown(); + } + + @Test + public void testLimitQueries() { + CollectionReference collection = + testCollectionWithDocs( + map( + "a", map("k", "a"), + "b", map("k", "b"), + "c", map("k", "c"))); + + Query query = collection.limit(2); + PipelineSnapshot set = waitFor(query.pipeline().execute()); + List> data = pipelineSnapshotToValues(set); + assertEquals(asList(map("k", "a"), map("k", "b")), data); + } + + @Test + public void testLimitQueriesUsingDescendingSortOrder() { + CollectionReference collection = + testCollectionWithDocs( + map( + "a", map("k", "a", "sort", 0), + "b", map("k", "b", "sort", 1), + "c", map("k", "c", "sort", 1), + "d", map("k", "d", "sort", 2))); + + Query query = collection.limit(2).orderBy("sort", Direction.DESCENDING); + PipelineSnapshot set = waitFor(query.pipeline().execute()); + List> data = pipelineSnapshotToValues(set); + assertEquals(asList(map("k", "d", "sort", 2L), map("k", "c", "sort", 1L)), data); + } + + @Test + public void testLimitToLastMustAlsoHaveExplicitOrderBy() { + CollectionReference collection = testCollectionWithDocs(map()); + + Query query = collection.limitToLast(2); + expectError( + () -> waitFor(query.pipeline().execute()), + "limitToLast() queries require specifying at least one orderBy() clause"); + } + + @Test + public void testLimitToLastQueriesWithCursors() { + CollectionReference collection = + testCollectionWithDocs( + map( + "a", map("k", "a", "sort", 0), + "b", map("k", "b", "sort", 1), + "c", map("k", "c", "sort", 1), + "d", map("k", "d", "sort", 2))); + + Query query = collection.limitToLast(3).orderBy("sort").endBefore(2); + PipelineSnapshot set = waitFor(query.pipeline().execute()); + List> data = pipelineSnapshotToValues(set); + assertEquals( + asList(map("k", "a", "sort", 0L), map("k", "b", "sort", 1L), map("k", "c", "sort", 1L)), + data); + + query = collection.limitToLast(3).orderBy("sort").endAt(1); + set = waitFor(query.pipeline().execute()); + data = pipelineSnapshotToValues(set); + assertEquals( + asList(map("k", "a", "sort", 0L), map("k", "b", "sort", 1L), map("k", "c", "sort", 1L)), + data); + + query = collection.limitToLast(3).orderBy("sort").startAt(2); + set = waitFor(query.pipeline().execute()); + data = pipelineSnapshotToValues(set); + assertEquals(asList(map("k", "d", "sort", 2L)), data); + + query = collection.limitToLast(3).orderBy("sort").startAfter(0); + set = waitFor(query.pipeline().execute()); + data = pipelineSnapshotToValues(set); + assertEquals( + asList(map("k", "b", "sort", 1L), map("k", "c", "sort", 1L), map("k", "d", "sort", 2L)), + data); + + query = collection.limitToLast(3).orderBy("sort").startAfter(-1); + set = waitFor(query.pipeline().execute()); + data = pipelineSnapshotToValues(set); + assertEquals( + asList(map("k", "b", "sort", 1L), map("k", "c", "sort", 1L), map("k", "d", "sort", 2L)), + data); + } + + @Test + public void testKeyOrderIsDescendingForDescendingInequality() { + CollectionReference collection = + testCollectionWithDocs( + map( + "a", map("foo", 42), + "b", map("foo", 42.0), + "c", map("foo", 42), + "d", map("foo", 21), + "e", map("foo", 21.0), + "f", map("foo", 66), + "g", map("foo", 66.0))); + + Query query = collection.whereGreaterThan("foo", 21.0).orderBy("foo", Direction.DESCENDING); + PipelineSnapshot result = waitFor(query.pipeline().execute()); + assertEquals(asList("g", "f", "c", "b", "a"), pipelineSnapshotToIds(result)); + } + + @Test + public void testUnaryFilterQueries() { + CollectionReference collection = + testCollectionWithDocs( + map( + "a", map("null", null, "nan", Double.NaN), + "b", map("null", null, "nan", 0), + "c", map("null", false, "nan", Double.NaN))); + PipelineSnapshot results = + waitFor( + collection + .whereEqualTo("null", null) + .whereEqualTo("nan", Double.NaN) + .pipeline() + .execute()); + assertEquals(1, results.getResults().size()); + PipelineResult result = results.getResults().get(0); + // Can't use assertEquals() since NaN != NaN. + assertEquals(null, result.get("null")); + assertTrue(((Double) result.get("nan")).isNaN()); + } + + @Test + public void testFilterOnInfinity() { + CollectionReference collection = + testCollectionWithDocs( + map( + "a", map("inf", Double.POSITIVE_INFINITY), + "b", map("inf", Double.NEGATIVE_INFINITY))); + PipelineSnapshot results = + waitFor(collection.whereEqualTo("inf", Double.POSITIVE_INFINITY).pipeline().execute()); + assertEquals(1, results.getResults().size()); + assertEquals(asList(map("inf", Double.POSITIVE_INFINITY)), pipelineSnapshotToValues(results)); + } + + @Test + public void testCanExplicitlySortByDocumentId() { + Map> testDocs = + map( + "a", map("key", "a"), + "b", map("key", "b"), + "c", map("key", "c")); + CollectionReference collection = testCollectionWithDocs(testDocs); + // Ideally this would be descending to validate it's different than + // the default, but that requires an extra index + PipelineSnapshot docs = + waitFor(collection.orderBy(FieldPath.documentId()).pipeline().execute()); + assertEquals( + asList(testDocs.get("a"), testDocs.get("b"), testDocs.get("c")), + pipelineSnapshotToValues(docs)); + } + + @Test + public void testCanQueryByDocumentId() { + Map> testDocs = + map( + "aa", map("key", "aa"), + "ab", map("key", "ab"), + "ba", map("key", "ba"), + "bb", map("key", "bb")); + CollectionReference collection = testCollectionWithDocs(testDocs); + PipelineSnapshot docs = + waitFor(collection.whereEqualTo(FieldPath.documentId(), "ab").pipeline().execute()); + assertEquals(singletonList(testDocs.get("ab")), pipelineSnapshotToValues(docs)); + + docs = + waitFor( + collection + .whereGreaterThan(FieldPath.documentId(), "aa") + .whereLessThanOrEqualTo(FieldPath.documentId(), "ba") + .pipeline() + .execute()); + assertEquals(asList(testDocs.get("ab"), testDocs.get("ba")), pipelineSnapshotToValues(docs)); + } + + @Test + public void testCanQueryByDocumentIdUsingRefs() { + Map> testDocs = + map( + "aa", map("key", "aa"), + "ab", map("key", "ab"), + "ba", map("key", "ba"), + "bb", map("key", "bb")); + CollectionReference collection = testCollectionWithDocs(testDocs); + PipelineSnapshot docs = + waitFor( + collection + .whereEqualTo(FieldPath.documentId(), collection.document("ab")) + .pipeline() + .execute()); + assertEquals(singletonList(testDocs.get("ab")), pipelineSnapshotToValues(docs)); + + docs = + waitFor( + collection + .whereGreaterThan(FieldPath.documentId(), collection.document("aa")) + .whereLessThanOrEqualTo(FieldPath.documentId(), collection.document("ba")) + .pipeline() + .execute()); + assertEquals(asList(testDocs.get("ab"), testDocs.get("ba")), pipelineSnapshotToValues(docs)); + } + + @Test + public void testCanQueryWithAndWithoutDocumentKey() { + CollectionReference collection = testCollection(); + collection.add(map()); + Task query1 = + collection.orderBy(FieldPath.documentId(), Direction.ASCENDING).pipeline().execute(); + Task query2 = collection.pipeline().execute(); + + waitFor(query1); + waitFor(query2); + + assertEquals( + pipelineSnapshotToValues(query1.getResult()), pipelineSnapshotToValues(query2.getResult())); + } + + @Test + public void testQueriesCanUseNotEqualFilters() { + // These documents are ordered by value in "zip" since the notEquals filter is an inequality, + // which results in documents being sorted by value. + Map docA = map("zip", Double.NaN); + Map docB = map("zip", 91102L); + Map docC = map("zip", 98101L); + Map docD = map("zip", "98101"); + Map docE = map("zip", asList(98101L)); + Map docF = map("zip", asList(98101L, 98102L)); + Map docG = map("zip", asList("98101", map("zip", 98101L))); + Map docH = map("zip", map("code", 500L)); + Map docI = map("code", 500L); + Map docJ = map("zip", null); + + Map> allDocs = + map( + "a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG, "h", docH, + "i", docI, "j", docJ); + CollectionReference collection = testCollectionWithDocs(allDocs); + + // Search for zips not matching 98101. + Map> expectedDocsMap = new LinkedHashMap<>(allDocs); + expectedDocsMap.remove("c"); + expectedDocsMap.remove("i"); + expectedDocsMap.remove("j"); + + PipelineSnapshot snapshot = + waitFor(collection.whereNotEqualTo("zip", 98101L).pipeline().execute()); + assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); + + // With objects. + expectedDocsMap = new LinkedHashMap<>(allDocs); + expectedDocsMap.remove("h"); + expectedDocsMap.remove("i"); + expectedDocsMap.remove("j"); + snapshot = waitFor(collection.whereNotEqualTo("zip", map("code", 500)).pipeline().execute()); + assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); + + // With Null. + expectedDocsMap = new LinkedHashMap<>(allDocs); + expectedDocsMap.remove("i"); + expectedDocsMap.remove("j"); + snapshot = waitFor(collection.whereNotEqualTo("zip", null).pipeline().execute()); + assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); + + List pipelineResults = waitFor(collection.pipeline() + .addFields( + Field.of("zip").isNan().as("isNan1"), + ifThen(Field.of("zip").isNan(), Constant.of(true)).as("isNan2"), + ifThen(Field.of("zip").isNan(), Constant.of(true)).isNotNull().as("isNan3") + ).execute()).getResults(); + + // With NaN. + expectedDocsMap = new LinkedHashMap<>(allDocs); + expectedDocsMap.remove("a"); + expectedDocsMap.remove("i"); + expectedDocsMap.remove("j"); + snapshot = waitFor(collection.whereEqualTo("zip", Double.NaN).pipeline().execute()); + assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); + } + + @Test + public void testQueriesCanUseNotEqualFiltersWithDocIds() { + Map docA = map("key", "aa"); + Map docB = map("key", "ab"); + Map docC = map("key", "ba"); + Map docD = map("key", "bb"); + Map> testDocs = + map( + "aa", docA, + "ab", docB, + "ba", docC, + "bb", docD); + CollectionReference collection = testCollectionWithDocs(testDocs); + PipelineSnapshot docs = + waitFor(collection.whereNotEqualTo(FieldPath.documentId(), "aa").pipeline().execute()); + assertEquals(asList(docB, docC, docD), pipelineSnapshotToValues(docs)); + } + + @Test + public void testQueriesCanUseArrayContainsFilters() { + Map docA = map("array", asList(42L)); + Map docB = map("array", asList("a", 42L, "c")); + Map docC = map("array", asList(41.999, "42", map("a", asList(42)))); + Map docD = map("array", asList(42L), "array2", asList("bingo")); + Map docE = map("array", nullList()); + Map docF = map("array", asList(Double.NaN)); + CollectionReference collection = + testCollectionWithDocs( + map("a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF)); + + // Search for "array" to contain 42 + PipelineSnapshot snapshot = + waitFor(collection.whereArrayContains("array", 42L).pipeline().execute()); + assertEquals(asList(docA, docB, docD), pipelineSnapshotToValues(snapshot)); + + // Note: whereArrayContains() requires a non-null value parameter, so no null test is needed. + // With NaN. + snapshot = waitFor(collection.whereArrayContains("array", Double.NaN).pipeline().execute()); + assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); + } + + @Test + public void testQueriesCanUseInFilters() { + Map docA = map("zip", 98101L); + Map docB = map("zip", 91102L); + Map docC = map("zip", 98103L); + Map docD = map("zip", asList(98101L)); + Map docE = map("zip", asList("98101", map("zip", 98101L))); + Map docF = map("zip", map("code", 500L)); + Map docG = map("zip", asList(98101L, 98102L)); + Map docH = map("zip", null); + Map docI = map("zip", Double.NaN); + + CollectionReference collection = + testCollectionWithDocs( + map( + "a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG, "h", + docH, "i", docI)); + + // Search for zips matching 98101, 98103, or [98101, 98102]. + PipelineSnapshot snapshot = + waitFor( + collection + .whereIn("zip", asList(98101L, 98103L, asList(98101L, 98102L))) + .pipeline() + .execute()); + assertEquals(asList(docA, docC, docG), pipelineSnapshotToValues(snapshot)); + + // With objects. + snapshot = waitFor(collection.whereIn("zip", asList(map("code", 500L))).pipeline().execute()); + assertEquals(asList(docF), pipelineSnapshotToValues(snapshot)); + + // With null. + snapshot = waitFor(collection.whereIn("zip", nullList()).pipeline().execute()); + assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); + + // With null and a value. + List inputList = nullList(); + inputList.add(98101L); + snapshot = waitFor(collection.whereIn("zip", inputList).pipeline().execute()); + assertEquals(asList(docA), pipelineSnapshotToValues(snapshot)); + + // With NaN. + snapshot = waitFor(collection.whereIn("zip", asList(Double.NaN)).pipeline().execute()); + assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); + + // With NaN and a value. + snapshot = waitFor(collection.whereIn("zip", asList(Double.NaN, 98101L)).pipeline().execute()); + assertEquals(asList(docA), pipelineSnapshotToValues(snapshot)); + } + + @Test + public void testQueriesCanUseInFiltersWithDocIds() { + Map docA = map("key", "aa"); + Map docB = map("key", "ab"); + Map docC = map("key", "ba"); + Map docD = map("key", "bb"); + Map> testDocs = + map( + "aa", docA, + "ab", docB, + "ba", docC, + "bb", docD); + CollectionReference collection = testCollectionWithDocs(testDocs); + PipelineSnapshot docs = + waitFor( + collection.whereIn(FieldPath.documentId(), asList("aa", "ab")).pipeline().execute()); + assertEquals(asList(docA, docB), pipelineSnapshotToValues(docs)); + } + + @Test + public void testQueriesCanUseNotInFilters() { + // These documents are ordered by value in "zip" since the notEquals filter is an inequality, + // which results in documents being sorted by value. + Map docA = map("zip", Double.NaN); + Map docB = map("zip", 91102L); + Map docC = map("zip", 98101L); + Map docD = map("zip", 98103L); + Map docE = map("zip", asList(98101L)); + Map docF = map("zip", asList(98101L, 98102L)); + Map docG = map("zip", asList("98101", map("zip", 98101L))); + Map docH = map("zip", map("code", 500L)); + Map docI = map("code", 500L); + Map docJ = map("zip", null); + + Map> allDocs = + map( + "a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG, "h", docH, + "i", docI, "j", docJ); + CollectionReference collection = testCollectionWithDocs(allDocs); + + // Search for zips not matching 98101, 98103, or [98101, 98102]. + Map> expectedDocsMap = new LinkedHashMap<>(allDocs); + expectedDocsMap.remove("c"); + expectedDocsMap.remove("d"); + expectedDocsMap.remove("f"); + expectedDocsMap.remove("i"); + expectedDocsMap.remove("j"); + + PipelineSnapshot snapshot = + waitFor( + collection + .whereNotIn("zip", asList(98101L, 98103L, asList(98101L, 98102L))) + .pipeline() + .execute()); + assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); + + // With objects. + expectedDocsMap = new LinkedHashMap<>(allDocs); + expectedDocsMap.remove("h"); + expectedDocsMap.remove("i"); + expectedDocsMap.remove("j"); + snapshot = + waitFor(collection.whereNotIn("zip", asList(map("code", 500L))).pipeline().execute()); + assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); + + // With Null. + snapshot = waitFor(collection.whereNotIn("zip", nullList()).pipeline().execute()); + assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); + + // With NaN. + expectedDocsMap = new LinkedHashMap<>(allDocs); + expectedDocsMap.remove("a"); + expectedDocsMap.remove("i"); + expectedDocsMap.remove("j"); + snapshot = waitFor(collection.whereNotIn("zip", asList(Double.NaN)).pipeline().execute()); + assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); + + // With NaN and a number. + expectedDocsMap = new LinkedHashMap<>(allDocs); + expectedDocsMap.remove("a"); + expectedDocsMap.remove("c"); + expectedDocsMap.remove("i"); + expectedDocsMap.remove("j"); + snapshot = + waitFor(collection.whereNotIn("zip", asList(Float.NaN, 98101L)).pipeline().execute()); + assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); + } + + @Test + public void testQueriesCanUseNotInFiltersWithDocIds() { + Map docA = map("key", "aa"); + Map docB = map("key", "ab"); + Map docC = map("key", "ba"); + Map docD = map("key", "bb"); + Map> testDocs = + map( + "aa", docA, + "ab", docB, + "ba", docC, + "bb", docD); + CollectionReference collection = testCollectionWithDocs(testDocs); + PipelineSnapshot docs = + waitFor( + collection.whereNotIn(FieldPath.documentId(), asList("aa", "ab")).pipeline().execute()); + assertEquals(asList(docC, docD), pipelineSnapshotToValues(docs)); + } + + @Test + public void testQueriesCanUseArrayContainsAnyFilters() { + Map docA = map("array", asList(42L)); + Map docB = map("array", asList("a", 42L, "c")); + Map docC = map("array", asList(41.999, "42", map("a", asList(42)))); + Map docD = map("array", asList(42L), "array2", asList("bingo")); + Map docE = map("array", asList(43L)); + Map docF = map("array", asList(map("a", 42L))); + Map docG = map("array", 42L); + Map docH = map("array", nullList()); + Map docI = map("array", asList(Double.NaN)); + + CollectionReference collection = + testCollectionWithDocs( + map( + "a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG, "h", + docH, "i", docI)); + + // Search for "array" to contain [42, 43]. + PipelineSnapshot snapshot = + waitFor(collection.whereArrayContainsAny("array", asList(42L, 43L)).pipeline().execute()); + assertEquals(asList(docA, docB, docD, docE), pipelineSnapshotToValues(snapshot)); + + // With objects. + snapshot = + waitFor( + collection.whereArrayContainsAny("array", asList(map("a", 42L))).pipeline().execute()); + assertEquals(asList(docF), pipelineSnapshotToValues(snapshot)); + + // With null. + snapshot = waitFor(collection.whereArrayContainsAny("array", nullList()).pipeline().execute()); + assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); + + // With null and a value. + List inputList = nullList(); + inputList.add(43L); + snapshot = waitFor(collection.whereArrayContainsAny("array", inputList).pipeline().execute()); + assertEquals(asList(docE), pipelineSnapshotToValues(snapshot)); + + // With NaN. + snapshot = + waitFor(collection.whereArrayContainsAny("array", asList(Double.NaN)).pipeline().execute()); + assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); + + // With NaN and a value. + snapshot = + waitFor( + collection + .whereArrayContainsAny("array", asList(Double.NaN, 43L)) + .pipeline() + .execute()); + assertEquals(asList(docE), pipelineSnapshotToValues(snapshot)); + } + + @Test + public void testCollectionGroupQueries() { + FirebaseFirestore db = testFirestore(); + // Use .document() to get a random collection group name to use but ensure it starts with 'b' + // for predictable ordering. + String collectionGroup = "b" + db.collection("foo").document().getId(); + + String[] docPaths = + new String[] { + "abc/123/${collectionGroup}/cg-doc1", + "abc/123/${collectionGroup}/cg-doc2", + "${collectionGroup}/cg-doc3", + "${collectionGroup}/cg-doc4", + "def/456/${collectionGroup}/cg-doc5", + "${collectionGroup}/virtual-doc/nested-coll/not-cg-doc", + "x${collectionGroup}/not-cg-doc", + "${collectionGroup}x/not-cg-doc", + "abc/123/${collectionGroup}x/not-cg-doc", + "abc/123/x${collectionGroup}/not-cg-doc", + "abc/${collectionGroup}" + }; + WriteBatch batch = db.batch(); + for (String path : docPaths) { + batch.set(db.document(path.replace("${collectionGroup}", collectionGroup)), map("x", 1)); + } + waitFor(batch.commit()); + + PipelineSnapshot snapshot = waitFor(db.collectionGroup(collectionGroup).pipeline().execute()); + assertEquals( + asList("cg-doc1", "cg-doc2", "cg-doc3", "cg-doc4", "cg-doc5"), + pipelineSnapshotToIds(snapshot)); + } + + @Test + public void testCollectionGroupQueriesWithStartAtEndAtWithArbitraryDocumentIds() { + FirebaseFirestore db = testFirestore(); + // Use .document() to get a random collection group name to use but ensure it starts with 'b' + // for predictable ordering. + String collectionGroup = "b" + db.collection("foo").document().getId(); + + String[] docPaths = + new String[] { + "a/a/${collectionGroup}/cg-doc1", + "a/b/a/b/${collectionGroup}/cg-doc2", + "a/b/${collectionGroup}/cg-doc3", + "a/b/c/d/${collectionGroup}/cg-doc4", + "a/c/${collectionGroup}/cg-doc5", + "${collectionGroup}/cg-doc6", + "a/b/nope/nope" + }; + WriteBatch batch = db.batch(); + for (String path : docPaths) { + batch.set(db.document(path.replace("${collectionGroup}", collectionGroup)), map("x", 1)); + } + waitFor(batch.commit()); + + PipelineSnapshot snapshot = + waitFor( + db.collectionGroup(collectionGroup) + .orderBy(FieldPath.documentId()) + .startAt("a/b") + .endAt("a/b0") + .pipeline() + .execute()); + assertEquals(asList("cg-doc2", "cg-doc3", "cg-doc4"), pipelineSnapshotToIds(snapshot)); + + snapshot = + waitFor( + db.collectionGroup(collectionGroup) + .orderBy(FieldPath.documentId()) + .startAfter("a/b") + .endBefore("a/b/" + collectionGroup + "/cg-doc3") + .pipeline() + .execute()); + assertEquals(asList("cg-doc2"), pipelineSnapshotToIds(snapshot)); + } + + @Test + public void testCollectionGroupQueriesWithWhereFiltersOnArbitraryDocumentIds() { + FirebaseFirestore db = testFirestore(); + // Use .document() to get a random collection group name to use but ensure it starts with 'b' + // for predictable ordering. + String collectionGroup = "b" + db.collection("foo").document().getId(); + + String[] docPaths = + new String[] { + "a/a/${collectionGroup}/cg-doc1", + "a/b/a/b/${collectionGroup}/cg-doc2", + "a/b/${collectionGroup}/cg-doc3", + "a/b/c/d/${collectionGroup}/cg-doc4", + "a/c/${collectionGroup}/cg-doc5", + "${collectionGroup}/cg-doc6", + "a/b/nope/nope" + }; + WriteBatch batch = db.batch(); + for (String path : docPaths) { + batch.set(db.document(path.replace("${collectionGroup}", collectionGroup)), map("x", 1)); + } + waitFor(batch.commit()); + + PipelineSnapshot snapshot = + waitFor( + db.collectionGroup(collectionGroup) + .whereGreaterThanOrEqualTo(FieldPath.documentId(), "a/b") + .whereLessThanOrEqualTo(FieldPath.documentId(), "a/b0") + .pipeline() + .execute()); + assertEquals(asList("cg-doc2", "cg-doc3", "cg-doc4"), pipelineSnapshotToIds(snapshot)); + + snapshot = + waitFor( + db.collectionGroup(collectionGroup) + .whereGreaterThan(FieldPath.documentId(), "a/b") + .whereLessThan(FieldPath.documentId(), "a/b/" + collectionGroup + "/cg-doc3") + .pipeline() + .execute()); + assertEquals(asList("cg-doc2"), pipelineSnapshotToIds(snapshot)); + } + + @Test + public void testOrQueries() { + Map> testDocs = + map( + "doc1", map("a", 1, "b", 0), + "doc2", map("a", 2, "b", 1), + "doc3", map("a", 3, "b", 2), + "doc4", map("a", 1, "b", 3), + "doc5", map("a", 1, "b", 1)); + CollectionReference collection = testCollectionWithDocs(testDocs); + + // Two equalities: a==1 || b==1. + checkQueryAndPipelineResultsMatch( + collection.where(or(equalTo("a", 1), equalTo("b", 1))), "doc1", "doc2", "doc4", "doc5"); + + // (a==1 && b==0) || (a==3 && b==2) + checkQueryAndPipelineResultsMatch( + collection.where( + or(and(equalTo("a", 1), equalTo("b", 0)), and(equalTo("a", 3), equalTo("b", 2)))), + "doc1", + "doc3"); + + // a==1 && (b==0 || b==3). + checkQueryAndPipelineResultsMatch( + collection.where(and(equalTo("a", 1), or(equalTo("b", 0), equalTo("b", 3)))), + "doc1", + "doc4"); + + // (a==2 || b==2) && (a==3 || b==3) + checkQueryAndPipelineResultsMatch( + collection.where( + and(or(equalTo("a", 2), equalTo("b", 2)), or(equalTo("a", 3), equalTo("b", 3)))), + "doc3"); + + // Test with limits without orderBy (the __name__ ordering is the tie breaker). + checkQueryAndPipelineResultsMatch( + collection.where(or(equalTo("a", 2), equalTo("b", 1))).limit(1), "doc2"); + } + + @Test + public void testOrQueriesWithIn() { + Map> testDocs = + map( + "doc1", map("a", 1, "b", 0), + "doc2", map("b", 1), + "doc3", map("a", 3, "b", 2), + "doc4", map("a", 1, "b", 3), + "doc5", map("a", 1), + "doc6", map("a", 2)); + CollectionReference collection = testCollectionWithDocs(testDocs); + + // a==2 || b in [2,3] + checkQueryAndPipelineResultsMatch( + collection.where(or(equalTo("a", 2), inArray("b", asList(2, 3)))), "doc3", "doc4", "doc6"); + } + + @Test + public void testOrQueriesWithArrayMembership() { + Map> testDocs = + map( + "doc1", map("a", 1, "b", asList(0)), + "doc2", map("b", asList(1)), + "doc3", map("a", 3, "b", asList(2, 7)), + "doc4", map("a", 1, "b", asList(3, 7)), + "doc5", map("a", 1), + "doc6", map("a", 2)); + CollectionReference collection = testCollectionWithDocs(testDocs); + + // a==2 || b array-contains 7 + checkQueryAndPipelineResultsMatch( + collection.where(or(equalTo("a", 2), arrayContains("b", 7))), "doc3", "doc4", "doc6"); + + // a==2 || b array-contains-any [0, 3] + checkQueryAndPipelineResultsMatch( + collection.where(or(equalTo("a", 2), arrayContainsAny("b", asList(0, 3)))), + "doc1", + "doc4", + "doc6"); + } + + @Test + public void testMultipleInOps() { + Map> testDocs = + map( + "doc1", map("a", 1, "b", 0), + "doc2", map("b", 1), + "doc3", map("a", 3, "b", 2), + "doc4", map("a", 1, "b", 3), + "doc5", map("a", 1), + "doc6", map("a", 2)); + CollectionReference collection = testCollectionWithDocs(testDocs); + + // Two IN operations on different fields with disjunction. + Query query1 = collection.where(or(inArray("a", asList(2, 3)), inArray("b", asList(0, 2)))); + checkQueryAndPipelineResultsMatch(query1, "doc1", "doc3", "doc6"); + + // Two IN operations on the same field with disjunction. + // a IN [0,3] || a IN [0,2] should union them (similar to: a IN [0,2,3]). + Query query2 = collection.where(or(inArray("a", asList(0, 3)), inArray("a", asList(0, 2)))); + checkQueryAndPipelineResultsMatch(query2, "doc3", "doc6"); + } + + @Test + public void testUsingInWithArrayContainsAny() { + Map> testDocs = + map( + "doc1", map("a", 1, "b", asList(0)), + "doc2", map("b", asList(1)), + "doc3", map("a", 3, "b", asList(2, 7), "c", 10), + "doc4", map("a", 1, "b", asList(3, 7)), + "doc5", map("a", 1), + "doc6", map("a", 2, "c", 20)); + CollectionReference collection = testCollectionWithDocs(testDocs); + + Query query1 = + collection.where(or(inArray("a", asList(2, 3)), arrayContainsAny("b", asList(0, 7)))); + checkQueryAndPipelineResultsMatch(query1, "doc1", "doc3", "doc4", "doc6"); + + Query query2 = + collection.where( + or( + and(inArray("a", asList(2, 3)), equalTo("c", 10)), + arrayContainsAny("b", asList(0, 7)))); + checkQueryAndPipelineResultsMatch(query2, "doc1", "doc3", "doc4"); + } + + @Test + public void testUsingInWithArrayContains() { + Map> testDocs = + map( + "doc1", map("a", 1, "b", asList(0)), + "doc2", map("b", asList(1)), + "doc3", map("a", 3, "b", asList(2, 7)), + "doc4", map("a", 1, "b", asList(3, 7)), + "doc5", map("a", 1), + "doc6", map("a", 2)); + CollectionReference collection = testCollectionWithDocs(testDocs); + + Query query1 = collection.where(or(inArray("a", asList(2, 3)), arrayContains("b", 3))); + checkQueryAndPipelineResultsMatch(query1, "doc3", "doc4", "doc6"); + + Query query2 = collection.where(and(inArray("a", asList(2, 3)), arrayContains("b", 7))); + checkQueryAndPipelineResultsMatch(query2, "doc3"); + + Query query3 = + collection.where( + or(inArray("a", asList(2, 3)), and(arrayContains("b", 3), equalTo("a", 1)))); + checkQueryAndPipelineResultsMatch(query3, "doc3", "doc4", "doc6"); + + Query query4 = + collection.where( + and(inArray("a", asList(2, 3)), or(arrayContains("b", 7), equalTo("a", 1)))); + checkQueryAndPipelineResultsMatch(query4, "doc3"); + } + + @Test + public void testOrderByEquality() { + Map> testDocs = + map( + "doc1", map("a", 1, "b", asList(0)), + "doc2", map("b", asList(1)), + "doc3", map("a", 3, "b", asList(2, 7), "c", 10), + "doc4", map("a", 1, "b", asList(3, 7)), + "doc5", map("a", 1), + "doc6", map("a", 2, "c", 20)); + CollectionReference collection = testCollectionWithDocs(testDocs); + + Query query1 = collection.where(equalTo("a", 1)).orderBy("a"); + checkQueryAndPipelineResultsMatch(query1, "doc1", "doc4", "doc5"); + + Query query2 = collection.where(inArray("a", asList(2, 3))).orderBy("a"); + checkQueryAndPipelineResultsMatch(query2, "doc6", "doc3"); + } +} diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index d11c27828cf..5a8a9ceb67c 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -38,6 +38,8 @@ import com.google.firebase.firestore.FirebaseFirestoreSettings; import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.MetadataChanges; +import com.google.firebase.firestore.PipelineResult; +import com.google.firebase.firestore.PipelineSnapshot; import com.google.firebase.firestore.Query; import com.google.firebase.firestore.QuerySnapshot; import com.google.firebase.firestore.Source; @@ -465,6 +467,15 @@ public static List> querySnapshotToValues(QuerySnapshot quer return res; } + public static List> pipelineSnapshotToValues( + PipelineSnapshot pipelineSnapshot) { + List> res = new ArrayList<>(); + for (PipelineResult result : pipelineSnapshot) { + res.add(result.getData()); + } + return res; + } + public static List querySnapshotToIds(QuerySnapshot querySnapshot) { List res = new ArrayList<>(); for (DocumentSnapshot doc : querySnapshot) { @@ -473,6 +484,15 @@ public static List querySnapshotToIds(QuerySnapshot querySnapshot) { return res; } + public static List pipelineSnapshotToIds(PipelineSnapshot pipelineResults) { + List res = new ArrayList<>(); + for (PipelineResult result : pipelineResults) { + DocumentReference ref = result.getRef(); + res.add(ref == null ? null : ref.getId()); + } + return res; + } + public static void disableNetwork(FirebaseFirestore firestore) { if (firestoreStatus.get(firestore)) { waitFor(firestore.disableNetwork()); @@ -537,4 +557,23 @@ public static void checkOnlineAndOfflineResultsMatch(Query query, String... expe assertEquals(expected, querySnapshotToIds(docsFromCache)); } } + + /** + * Checks that running the query while online (against the backend/emulator) results in the same + * documents as running the query while offline. If `expectedDocs` is provided, it also checks + * that both online and offline query result is equal to the expected documents. + * + * @param query The query to check + * @param expectedDocs Ordered list of document keys that are expected to match the query + */ + public static void checkQueryAndPipelineResultsMatch(Query query, String... expectedDocs) { + QuerySnapshot docsFromQuery = waitFor(query.get(Source.SERVER)); + PipelineSnapshot docsFromPipeline = waitFor(query.pipeline().execute()); + + assertEquals(querySnapshotToIds(docsFromQuery), pipelineSnapshotToIds(docsFromPipeline)); + List expected = asList(expectedDocs); + if (!expected.isEmpty()) { + assertEquals(expected, querySnapshotToIds(docsFromQuery)); + } + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java index 8f4e1a00d79..c8b9cfaa90a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java @@ -130,6 +130,7 @@ public Task add(@NonNull Object data) { } @NonNull + @Override public Pipeline pipeline() { return new Pipeline(firestore, firestore.getUserDataReader(), new CollectionSource(getPath())); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 29b81683b61..8f3ca218061 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -20,6 +20,7 @@ import com.google.common.collect.FluentIterable import com.google.common.collect.ImmutableList import com.google.firebase.firestore.model.DocumentKey import com.google.firebase.firestore.model.SnapshotVersion +import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.pipeline.AddFieldsStage import com.google.firebase.firestore.pipeline.AggregateStage import com.google.firebase.firestore.pipeline.AggregateWithAlias @@ -123,9 +124,9 @@ internal constructor( fun where(condition: BooleanExpr): Pipeline = append(WhereStage(condition)) - fun offset(offset: Long): Pipeline = append(OffsetStage(offset)) + fun offset(offset: Int): Pipeline = append(OffsetStage(offset)) - fun limit(limit: Long): Pipeline = append(LimitStage(limit)) + fun limit(limit: Int): Pipeline = append(LimitStage(limit)) fun distinct(vararg groups: Selectable): Pipeline = append(DistinctStage(groups)) @@ -175,12 +176,15 @@ internal constructor( append(UnnestStage(selectable)) private inner class ObserverSnapshotTask : PipelineResultObserver { + private val userDataWriter = + UserDataWriter(firestore, DocumentSnapshot.ServerTimestampBehavior.DEFAULT) private val taskCompletionSource = TaskCompletionSource() private val results: ImmutableList.Builder = ImmutableList.builder() override fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) { results.add( PipelineResult( firestore, + userDataWriter, if (key == null) null else DocumentReference(key, firestore), data, version @@ -249,20 +253,52 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto } class PipelineSnapshot -internal constructor(private val executionTime: SnapshotVersion, val results: List) +internal constructor( + private val executionTime: SnapshotVersion, + val results: List +) : Iterable { + override fun iterator() = results.iterator() +} class PipelineResult internal constructor( private val firestore: FirebaseFirestore, + private val userDataWriter: UserDataWriter, val ref: DocumentReference?, private val fields: Map, private val version: SnapshotVersion, ) { - fun getData(): Map = userDataWriter().convertObject(fields) + /** + * Returns the ID of the document represented by this result. Returns null if this result is not + * corresponding to a Firestore document. + */ + fun getId(): String? = ref?.id + + fun getData(): Map = userDataWriter.convertObject(fields) + + private fun extractNestedValue(fieldPath: FieldPath): Value? { + val segments = fieldPath.internalPath.iterator() + if (!segments.hasNext()) { + return Values.encodeValue(fields) + } + val firstSegment = segments.next() + if (!fields.containsKey(firstSegment)) { + return null + } + var value: Value? = fields[firstSegment] + for (segment in segments) { + if (value == null || !value.hasMapValue()) { + return null + } + value = value.mapValue.getFieldsOrDefault(segment, null) + } + return value + } + + fun get(field: String): Any? = get(FieldPath.fromDotSeparatedPath(field)) - private fun userDataWriter(): UserDataWriter = - UserDataWriter(firestore, DocumentSnapshot.ServerTimestampBehavior.DEFAULT) + fun get(fieldPath: FieldPath): Any? = userDataWriter.convertValue(extractNestedValue(fieldPath)) override fun toString() = "PipelineResult{ref=$ref, version=$version}, data=${getData()}" } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java index d5fb8a4399b..12ff36bf960 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java @@ -1241,6 +1241,11 @@ public AggregateQuery aggregate( return new AggregateQuery(this, fields); } + @NonNull + public Pipeline pipeline() { + return query.toPipeline(firestore, firestore.getUserDataReader()); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java index 3ce7cfec87c..97c9d9d33e1 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java @@ -230,7 +230,7 @@ private ObjectValue convertAndParseDocumentData(Object input, ParseContext conte Object converted = CustomClassMapper.convertToPlainJavaTypes(input); Value parsedValue = parseData(converted, context); - if (parsedValue.getValueTypeCase() != Value.ValueTypeCase.MAP_VALUE) { + if (!parsedValue.hasMapValue()) { throw new IllegalArgumentException(badDocReason + "of type: " + Util.typeName(input)); } return new ObjectValue(parsedValue); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataWriter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataWriter.java index d6ac7b90bba..8c90a4d02e4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataWriter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataWriter.java @@ -78,7 +78,7 @@ public Object convertValue(Value value) { case TYPE_ORDER_BOOLEAN: return value.getBooleanValue(); case TYPE_ORDER_NUMBER: - return value.getValueTypeCase().equals(Value.ValueTypeCase.INTEGER_VALUE) + return value.hasIntegerValue() ? (Object) value.getIntegerValue() // Cast to Object to prevent type coercion to double : (Object) value.getDoubleValue(); case TYPE_ORDER_STRING: diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java index 26654f7a1ba..c9ed9c6c0e3 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java @@ -17,6 +17,7 @@ import android.text.TextUtils; import androidx.annotation.Nullable; import com.google.firebase.firestore.model.Document; +import com.google.firebase.firestore.pipeline.BooleanExpr; import com.google.firebase.firestore.util.Function; import java.util.ArrayList; import java.util.Collections; @@ -167,6 +168,19 @@ public String getCanonicalId() { return builder.toString(); } + @Override + BooleanExpr toPipelineExpr() { + BooleanExpr[] booleanExprs = filters.stream().map(Filter::toPipelineExpr).toArray(BooleanExpr[]::new); + switch (operator) { + case AND: + return new BooleanExpr("and", booleanExprs); + case OR: + return new BooleanExpr("or", booleanExprs); + } + // Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed + throw new IllegalArgumentException("Unsupported operator: " + operator); + } + @Override public String toString() { return getCanonicalId(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java index 6ebf26d0718..4d2a5a404c0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java @@ -14,11 +14,15 @@ package com.google.firebase.firestore.core; +import static com.google.firebase.firestore.pipeline.Function.and; import static com.google.firebase.firestore.util.Assert.hardAssert; +import static java.lang.Double.isNaN; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.FieldPath; import com.google.firebase.firestore.model.Values; +import com.google.firebase.firestore.pipeline.BooleanExpr; +import com.google.firebase.firestore.pipeline.Field; import com.google.firebase.firestore.util.Assert; import com.google.firestore.v1.Value; import java.util.Arrays; @@ -172,6 +176,49 @@ public List getFilters() { return Collections.singletonList(this); } + @Override + BooleanExpr toPipelineExpr() { + Field x = new Field(field); + BooleanExpr exists = x.exists(); + switch (operator) { + case LESS_THAN: + return and(exists, x.lt(value)); + case LESS_THAN_OR_EQUAL: + return and(exists, x.lte(value)); + case EQUAL: + if (value.hasNullValue()) { + return and(exists, x.isNull()); + } else if (value.hasDoubleValue() && isNaN(value.getDoubleValue())) { + return and(exists, x.isNan()); + } else { + return and(exists, x.eq(value)); + } + case NOT_EQUAL: + if (value.hasNullValue()) { + return and(exists, x.isNotNull()); + } else if (value.hasDoubleValue() && isNaN(value.getDoubleValue())) { + return and(exists, x.isNotNan()); + } else { + return and(exists, x.neq(value)); + } + case GREATER_THAN: + return and(exists, x.gt(value)); + case GREATER_THAN_OR_EQUAL: + return and(exists, x.gte(value)); + case ARRAY_CONTAINS: + return and(exists, x.arrayContains(value)); + case ARRAY_CONTAINS_ANY: + return and(exists, x.arrayContainsAny(value.getArrayValue().getValuesList())); + case IN: + return and(exists, x.eqAny(value.getArrayValue().getValuesList())); + case NOT_IN: + return and(exists, x.notEqAny(value.getArrayValue().getValuesList())); + default: + // Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed + throw new IllegalArgumentException("Unsupported operator: " + operator); + } + } + @Override public String toString() { return getCanonicalId(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Filter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Filter.java index 063b994f7a8..3f33bd3d5bc 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Filter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Filter.java @@ -15,6 +15,7 @@ package com.google.firebase.firestore.core; import com.google.firebase.firestore.model.Document; +import com.google.firebase.firestore.pipeline.BooleanExpr; import java.util.List; public abstract class Filter { @@ -29,4 +30,6 @@ public abstract class Filter { /** Returns a list of all filters that are contained within this filter */ public abstract List getFilters(); + + abstract BooleanExpr toPipelineExpr(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/OrderBy.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/OrderBy.java index 8636fd0498a..1fd8a42ada5 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/OrderBy.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/OrderBy.java @@ -21,7 +21,7 @@ import com.google.firestore.v1.Value; /** Represents a sort order for a Firestore Query */ -public class OrderBy { +public final class OrderBy { /** The direction of the ordering */ public enum Direction { ASCENDING(1), diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index bf57dfdb7a3..a923be48e8d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -14,21 +14,41 @@ package com.google.firebase.firestore.core; +import static com.google.firebase.firestore.pipeline.Function.and; +import static com.google.firebase.firestore.pipeline.Function.or; import static com.google.firebase.firestore.util.Assert.hardAssert; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.Pipeline; +import com.google.firebase.firestore.UserDataReader; import com.google.firebase.firestore.core.OrderBy.Direction; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldPath; import com.google.firebase.firestore.model.ResourcePath; +import com.google.firebase.firestore.pipeline.BooleanExpr; +import com.google.firebase.firestore.pipeline.CollectionGroupSource; +import com.google.firebase.firestore.pipeline.CollectionSource; +import com.google.firebase.firestore.pipeline.DocumentsSource; +import com.google.firebase.firestore.pipeline.Expr; +import com.google.firebase.firestore.pipeline.Field; +import com.google.firebase.firestore.pipeline.Function; +import com.google.firebase.firestore.pipeline.Ordering; +import com.google.firebase.firestore.pipeline.Stage; +import com.google.firestore.v1.Value; + import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; +import java.util.function.BiFunction; +import java.util.stream.Collectors; /** * Encapsulates all the query attributes we support in the SDK. It can be run against the @@ -502,6 +522,90 @@ private synchronized Target toTarget(List orderBys) { } } + @NonNull + public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataReader) { + Pipeline p = new Pipeline(firestore, userDataReader, pipelineSource()); + + // Filters + for (Filter filter : filters) { + p = p.where(filter.toPipelineExpr()); + } + + // Orders + List normalizedOrderBy = getNormalizedOrderBy(); + int size = normalizedOrderBy.size(); + List fields = new ArrayList<>(size); + List orderings = new ArrayList<>(size); + for (OrderBy order : normalizedOrderBy) { + Field field = new Field(order.getField()); + fields.add(field); + if (order.getDirection() == Direction.ASCENDING) { + orderings.add(field.ascending()); + } else { + orderings.add(field.descending()); + } + } + + if (fields.size() == 1) { + p = p.where(fields.get(0).exists()); + } else { + p = p.where(and(fields.get(0).exists(), fields.stream().skip(1).map(Expr::exists).toArray(BooleanExpr[]::new))); + } + + if (startAt != null) { + p = p.where(whereConditionsFromCursor(startAt, fields, Function::gt)); + } + + if (endAt != null) { + p = p.where(whereConditionsFromCursor(endAt, fields, Function::lt)); + } + + // Cursors, Limit, Offset + if (hasLimit()) { + // TODO: Handle situation where user enters limit larger than integer. + if (limitType == LimitType.LIMIT_TO_FIRST) { + p = p.sort(orderings.toArray(Ordering[]::new)); + p = p.limit((int) limit); + } else { + p = p.sort(orderings.stream().map(Ordering::reverse).toArray(Ordering[]::new)); + p = p.limit((int) limit); + p = p.sort(orderings.toArray(Ordering[]::new)); + } + } else { + p = p.sort(orderings.toArray(Ordering[]::new)); + } + + return p; + } + + private static BooleanExpr whereConditionsFromCursor(Bound bound, List fields, BiFunction cmp) { + List boundPosition = bound.getPosition(); + int size = boundPosition.size(); + hardAssert(size <= fields.size(), "Bound positions must not exceed order fields."); + int last = size - 1; + BooleanExpr condition = cmp.apply(fields.get(last), boundPosition.get(last)); + if (bound.isInclusive()) { + condition = or(condition, Function.eq(fields.get(last), boundPosition.get(last))); + } + for (int i = size - 2; i >=0; i--) { + final Field field = fields.get(i); + final Value value = boundPosition.get(i); + condition = or(cmp.apply(field, value), and(field.eq(value), condition)); + } + return condition; + } + + @NonNull + private Stage pipelineSource() { + if (isDocumentQuery()) { + return new DocumentsSource(path.canonicalString()); + } else if (isCollectionGroupQuery()) { + return new CollectionGroupSource(collectionGroup); + } else { + return new CollectionSource(path.canonicalString()); + } + } + /** * This method is marked as synchronized because it modifies the internal state in some cases. * diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/BasePath.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/BasePath.java index 66356e12595..6d800de05d4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/BasePath.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/BasePath.java @@ -19,19 +19,26 @@ import androidx.annotation.NonNull; import com.google.firebase.firestore.util.Util; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; /** * BasePath represents a path sequence in the Firestore database. It is composed of an ordered * sequence of string segments. */ -public abstract class BasePath> implements Comparable { +public abstract class BasePath> implements Comparable, Iterable { final List segments; BasePath(List segments) { this.segments = segments; } + @NonNull + @Override + public Iterator iterator() { + return segments.iterator(); + } + public String getSegment(int index) { return segments.get(index); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java index a7ea2997618..581bbf8481b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java @@ -47,9 +47,7 @@ public static ObjectValue fromMap(Map value) { } public ObjectValue(Value value) { - hardAssert( - value.getValueTypeCase() == Value.ValueTypeCase.MAP_VALUE, - "ObjectValues should be backed by a MapValue"); + hardAssert(value.hasMapValue(), "ObjectValues should be backed by a MapValue"); hardAssert( !ServerTimestamps.isServerTimestamp(value), "ServerTimestamps should not be used as an ObjectValue"); @@ -103,7 +101,7 @@ private FieldMask extractFieldMask(MapValue value) { } @Nullable - private Value extractNestedValue(Value value, FieldPath fieldPath) { + private static Value extractNestedValue(Value value, FieldPath fieldPath) { if (fieldPath.isEmpty()) { return value; } else { @@ -180,8 +178,7 @@ private void setOverlay(FieldPath path, @Nullable Value value) { if (currentValue instanceof Map) { // Re-use a previously created map currentLevel = (Map) currentValue; - } else if (currentValue instanceof Value - && ((Value) currentValue).getValueTypeCase() == Value.ValueTypeCase.MAP_VALUE) { + } else if (currentValue instanceof Value && ((Value) currentValue).hasMapValue()) { // Convert the existing Protobuf MapValue into a Java map Map nextLevel = new HashMap<>(((Value) currentValue).getMapValue().getFieldsMap()); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index d5ae4064d95..afda3ca32ae 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -267,16 +267,16 @@ internal object Values { } private fun compareNumbers(left: Value, right: Value): Int { - if (left.valueTypeCase == ValueTypeCase.DOUBLE_VALUE) { - if (right.valueTypeCase == ValueTypeCase.DOUBLE_VALUE) { + if (left.hasDoubleValue()) { + if (right.hasDoubleValue()) { return Util.compareDoubles(left.doubleValue, right.doubleValue) - } else if (right.valueTypeCase == ValueTypeCase.INTEGER_VALUE) { + } else if (right.hasIntegerValue()) { return Util.compareMixed(left.doubleValue, right.integerValue) } - } else if (left.valueTypeCase == ValueTypeCase.INTEGER_VALUE) { - if (right.valueTypeCase == ValueTypeCase.INTEGER_VALUE) { + } else if (left.hasIntegerValue()) { + if (right.hasIntegerValue()) { return Util.compareLongs(left.integerValue, right.integerValue) - } else if (right.valueTypeCase == ValueTypeCase.DOUBLE_VALUE) { + } else if (right.hasDoubleValue()) { return -1 * Util.compareMixed(right.doubleValue, left.integerValue) } } @@ -435,13 +435,13 @@ internal object Values { /** Returns true if `value` is a INTEGER_VALUE. */ @JvmStatic fun isInteger(value: Value?): Boolean { - return value != null && value.valueTypeCase == ValueTypeCase.INTEGER_VALUE + return value != null && value.hasIntegerValue() } /** Returns true if `value` is a DOUBLE_VALUE. */ @JvmStatic fun isDouble(value: Value?): Boolean { - return value != null && value.valueTypeCase == ValueTypeCase.DOUBLE_VALUE + return value != null && value.hasDoubleValue() } /** Returns true if `value` is either a INTEGER_VALUE or a DOUBLE_VALUE. */ @@ -453,17 +453,17 @@ internal object Values { /** Returns true if `value` is an ARRAY_VALUE. */ @JvmStatic fun isArray(value: Value?): Boolean { - return value != null && value.valueTypeCase == ValueTypeCase.ARRAY_VALUE + return value != null && value.hasArrayValue() } @JvmStatic fun isReferenceValue(value: Value?): Boolean { - return value != null && value.valueTypeCase == ValueTypeCase.REFERENCE_VALUE + return value != null && value.hasReferenceValue() } @JvmStatic fun isNullValue(value: Value?): Boolean { - return value != null && value.valueTypeCase == ValueTypeCase.NULL_VALUE + return value != null && value.hasNullValue() } @JvmStatic @@ -473,7 +473,7 @@ internal object Values { @JvmStatic fun isMapValue(value: Value?): Boolean { - return value != null && value.valueTypeCase == ValueTypeCase.MAP_VALUE + return value != null && value.hasMapValue() } @JvmStatic diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt index e9bcf7aac43..0edf481cf16 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt @@ -34,6 +34,10 @@ abstract class Constant internal constructor() : Expr() { companion object { internal val NULL: Constant = ValueConstant(Values.NULL_VALUE) + internal fun of(value: Value): Constant { + return ValueConstant(value) + } + @JvmStatic fun of(value: String): Constant { return ValueConstant(encodeValue(value)) 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 401aa2ff20a..452d60e6787 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 @@ -27,11 +27,13 @@ import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.pipeline.Constant.Companion.of import com.google.firebase.firestore.util.CustomClassMapper import com.google.firestore.v1.MapValue +import com.google.firestore.v1.StructuredQuery.Order import com.google.firestore.v1.Value import java.util.Date import kotlin.reflect.KFunction1 abstract class Expr internal constructor() { + internal companion object { internal fun toExprOrConstant(value: Any?): Expr = toExpr(value, ::toExprOrConstant) @@ -159,14 +161,18 @@ abstract class Expr internal constructor() { fun mod(other: Any) = Function.mod(this, other) - fun inAny(values: List) = Function.inAny(this, values) + fun eqAny(values: List) = Function.eqAny(this, values) - fun notInAny(values: List) = Function.notInAny(this, values) + fun notEqAny(values: List) = Function.notEqAny(this, values) fun isNan() = Function.isNan(this) + fun isNotNan() = Function.isNotNan(this) + fun isNull() = Function.isNull(this) + fun isNotNull() = Function.isNotNull(this) + fun replaceFirst(find: Expr, replace: Expr) = Function.replaceFirst(this, find, replace) fun replaceFirst(find: String, replace: String) = Function.replaceFirst(this, find, replace) @@ -349,6 +355,8 @@ class ExprWithAlias internal constructor(private val alias: String, private val class Field internal constructor(private val fieldPath: ModelFieldPath) : Selectable() { companion object { + @JvmField + val DOCUMENT_ID: Field = of(FieldPath.documentId()) @JvmStatic fun of(name: String): Field { @@ -512,25 +520,35 @@ protected constructor(private val name: String, private val params: Array) = - BooleanExpr("in", array, ListOfExprs(toArrayOfExprOrConstant(values))) + fun eqAny(value: Expr, values: List) = + BooleanExpr("eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) @JvmStatic - fun inAny(fieldName: String, values: List) = - BooleanExpr("in", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + fun eqAny(fieldName: String, values: List) = + BooleanExpr("eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) - @JvmStatic fun notInAny(array: Expr, values: List) = not(inAny(array, values)) + @JvmStatic fun notEqAny(value: Expr, values: List) = + BooleanExpr("not_eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) - @JvmStatic fun notInAny(fieldName: String, values: List) = not(inAny(fieldName, values)) + @JvmStatic fun notEqAny(fieldName: String, values: List) = + BooleanExpr("not_eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) @JvmStatic fun isNan(expr: Expr) = BooleanExpr("is_nan", expr) @JvmStatic fun isNan(fieldName: String) = BooleanExpr("is_nan", fieldName) + @JvmStatic fun isNotNan(expr: Expr) = BooleanExpr("is_not_nan", expr) + + @JvmStatic fun isNotNan(fieldName: String) = BooleanExpr("is_not_nan", fieldName) + @JvmStatic fun isNull(expr: Expr) = BooleanExpr("is_null", expr) @JvmStatic fun isNull(fieldName: String) = BooleanExpr("is_null", fieldName) + @JvmStatic fun isNotNull(expr: Expr) = BooleanExpr("is_not_null", expr) + + @JvmStatic fun isNotNull(fieldName: String) = BooleanExpr("is_not_null", fieldName) + @JvmStatic fun replaceFirst(value: Expr, find: Expr, replace: Expr) = Function("replace_first", value, find, replace) @@ -932,18 +950,18 @@ protected constructor(private val name: String, private val params: Array) : fun ifThenElse(then: Any, `else`: Any) = ifThenElse(this, then, `else`) } -class Ordering private constructor(private val expr: Expr, private val dir: Direction) { +class Ordering private constructor(val expr: Expr, private val dir: Direction) { companion object { @JvmStatic fun ascending(expr: Expr): Ordering = Ordering(expr, Direction.ASCENDING) @@ -1021,4 +1039,6 @@ class Ordering private constructor(private val expr: Expr, private val dir: Dire .putFields("expression", expr.toProto(userDataReader)) ) .build() + + fun reverse(): Ordering = Ordering(expr, if (dir == Direction.ASCENDING) Direction.DESCENDING else Direction.ASCENDING) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index e4b0345f662..f72ede652ad 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -28,7 +28,7 @@ private constructor(protected val name: String, private val options: InternalOpt internal fun toProtoStage(userDataReader: UserDataReader): Pipeline.Stage { val builder = Pipeline.Stage.newBuilder() builder.setName(name) - args(userDataReader).forEach { arg -> builder.addArgs(arg) } + args(userDataReader).forEach(builder::addArgs) options.forEach(builder::putOptions) return builder.build() } @@ -64,7 +64,8 @@ internal sealed class GenericArg { when (arg) { is AggregateExpr -> AggregateArg(arg) is Ordering -> OrderingArg(arg) - is Map<*, *> -> MapArg(arg.asIterable().associate { it.key as String to from(it.value) }) + is Map<*, *> -> + MapArg(arg.asIterable().associate { (key, value) -> key as String to from(value) }) is List<*> -> ListArg(arg.map(::from)) else -> ExprArg(Expr.toExprOrConstant(arg)) } @@ -120,8 +121,9 @@ internal class CollectionGroupSource internal constructor(val collectionId: Stri internal class DocumentsSource internal constructor(private val documents: Array) : Stage("documents") { + internal constructor(document: String) : this(arrayOf(document)) override fun args(userDataReader: UserDataReader): Sequence = - documents.asSequence().map(::encodeValue) + documents.asSequence().map { if (it.startsWith("/")) it else "/" + it }.map(::encodeValue) } internal class AddFieldsStage internal constructor(private val fields: Array) : @@ -211,12 +213,12 @@ class FindNearestOptions private constructor(options: InternalOptions) : withDistanceField(of(distanceField)) } -internal class LimitStage internal constructor(private val limit: Long) : Stage("limit") { +internal class LimitStage internal constructor(private val limit: Int) : Stage("limit") { override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(limit)) } -internal class OffsetStage internal constructor(private val offset: Long) : Stage("offset") { +internal class OffsetStage internal constructor(private val offset: Int) : Stage("offset") { override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(offset)) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java index 8e509247524..2e813ac3b98 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java @@ -727,31 +727,39 @@ StructuredQuery.Filter encodeFilter(com.google.firebase.firestore.core.Filter fi @VisibleForTesting StructuredQuery.Filter encodeUnaryOrFieldFilter(FieldFilter filter) { - if (filter.getOperator() == FieldFilter.Operator.EQUAL - || filter.getOperator() == FieldFilter.Operator.NOT_EQUAL) { - UnaryFilter.Builder unaryProto = UnaryFilter.newBuilder(); - unaryProto.setField(encodeFieldPath(filter.getField())); - if (Values.isNanValue(filter.getValue())) { - unaryProto.setOp( - filter.getOperator() == FieldFilter.Operator.EQUAL - ? UnaryFilter.Operator.IS_NAN - : UnaryFilter.Operator.IS_NOT_NAN); - return StructuredQuery.Filter.newBuilder().setUnaryFilter(unaryProto).build(); - } else if (Values.isNullValue(filter.getValue())) { - unaryProto.setOp( - filter.getOperator() == FieldFilter.Operator.EQUAL - ? UnaryFilter.Operator.IS_NULL - : UnaryFilter.Operator.IS_NOT_NULL); - return StructuredQuery.Filter.newBuilder().setUnaryFilter(unaryProto).build(); + FieldFilter.Operator op = filter.getOperator(); + Value value = filter.getValue(); + FieldReference fieldReference = encodeFieldPath(filter.getField()); + if (op == FieldFilter.Operator.EQUAL) { + if (Values.isNanValue(value)) { + return encodeUnaryFilter(fieldReference, UnaryFilter.Operator.IS_NAN); + } + if (Values.isNullValue(value)) { + return encodeUnaryFilter(fieldReference, UnaryFilter.Operator.IS_NULL); + } + } else if (op == FieldFilter.Operator.NOT_EQUAL) { + if (Values.isNanValue(value)) { + return encodeUnaryFilter(fieldReference, UnaryFilter.Operator.IS_NOT_NAN); + } + if (Values.isNullValue(value)) { + return encodeUnaryFilter(fieldReference, UnaryFilter.Operator.IS_NOT_NULL); } } StructuredQuery.FieldFilter.Builder proto = StructuredQuery.FieldFilter.newBuilder(); - proto.setField(encodeFieldPath(filter.getField())); - proto.setOp(encodeFieldFilterOperator(filter.getOperator())); - proto.setValue(filter.getValue()); + proto.setField(fieldReference); + proto.setOp(encodeFieldFilterOperator(op)); + proto.setValue(value); return StructuredQuery.Filter.newBuilder().setFieldFilter(proto).build(); } + private StructuredQuery.Filter encodeUnaryFilter( + FieldReference fieldReference, UnaryFilter.Operator op) { + UnaryFilter.Builder unaryProto = UnaryFilter.newBuilder(); + unaryProto.setField(fieldReference); + unaryProto.setOp(op); + return StructuredQuery.Filter.newBuilder().setUnaryFilter(unaryProto).build(); + } + StructuredQuery.CompositeFilter.Operator encodeCompositeFilterOperator( com.google.firebase.firestore.core.CompositeFilter.Operator op) { switch (op) { From 35a9116d59a45e9f3728cf8dd2aa0a40f750ba12 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 24 Mar 2025 18:38:46 -0400 Subject: [PATCH 032/152] Spotless and Generate API --- firebase-firestore/api.txt | 104 +++++++++++++++--- .../firestore/core/CompositeFilter.java | 3 +- .../google/firebase/firestore/core/Query.java | 12 +- .../firestore/pipeline/expressions.kt | 16 +-- 4 files changed, 107 insertions(+), 28 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index db0a902420e..c3ef4cac64a 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -54,7 +54,6 @@ package com.google.firebase.firestore { method public String getId(); method public com.google.firebase.firestore.DocumentReference? getParent(); method public String getPath(); - method public com.google.firebase.firestore.Pipeline pipeline(); } public class DocumentChange { @@ -432,8 +431,8 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Expr property, double[] vector, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure, com.google.firebase.firestore.pipeline.FindNearestOptions options); method public com.google.firebase.firestore.Pipeline genericStage(com.google.firebase.firestore.pipeline.GenericStage stage); method public com.google.firebase.firestore.Pipeline genericStage(String name, java.lang.Object... arguments); - method public com.google.firebase.firestore.Pipeline limit(long limit); - method public com.google.firebase.firestore.Pipeline offset(long offset); + method public com.google.firebase.firestore.Pipeline limit(int limit); + method public com.google.firebase.firestore.Pipeline offset(int offset); method public com.google.firebase.firestore.Pipeline removeFields(com.google.firebase.firestore.pipeline.Field... fields); method public com.google.firebase.firestore.Pipeline removeFields(java.lang.String... fields); method public com.google.firebase.firestore.Pipeline replace(com.google.firebase.firestore.pipeline.Selectable field); @@ -453,13 +452,17 @@ package com.google.firebase.firestore { } public final class PipelineResult { + method public Object? get(com.google.firebase.firestore.FieldPath fieldPath); + method public Object? get(String field); method public java.util.Map getData(); + method public String? getId(); method public com.google.firebase.firestore.DocumentReference? getRef(); property public final com.google.firebase.firestore.DocumentReference? ref; } - public final class PipelineSnapshot { + public final class PipelineSnapshot implements java.lang.Iterable kotlin.jvm.internal.markers.KMappedMarker { method public java.util.List getResults(); + method public java.util.Iterator iterator(); property public final java.util.List results; } @@ -499,6 +502,7 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Query orderBy(com.google.firebase.firestore.FieldPath, com.google.firebase.firestore.Query.Direction); method public com.google.firebase.firestore.Query orderBy(String); method public com.google.firebase.firestore.Query orderBy(String, com.google.firebase.firestore.Query.Direction); + method public com.google.firebase.firestore.Pipeline pipeline(); method public com.google.firebase.firestore.Query startAfter(com.google.firebase.firestore.DocumentSnapshot); method public com.google.firebase.firestore.Query startAfter(java.lang.Object!...!); method public com.google.firebase.firestore.Query startAt(com.google.firebase.firestore.DocumentSnapshot); @@ -780,6 +784,17 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.ExprWithAlias as(String alias); method public final com.google.firebase.firestore.pipeline.Ordering ascending(); method public final com.google.firebase.firestore.pipeline.AggregateExpr avg(); + method public final com.google.firebase.firestore.pipeline.Function bitAnd(com.google.firebase.firestore.pipeline.Expr right); + method public final com.google.firebase.firestore.pipeline.Function bitAnd(Object right); + method public final com.google.firebase.firestore.pipeline.Function bitLeftShift(com.google.firebase.firestore.pipeline.Expr numberExpr); + method public final com.google.firebase.firestore.pipeline.Function bitLeftShift(int number); + method public final com.google.firebase.firestore.pipeline.Function bitNot(); + method public final com.google.firebase.firestore.pipeline.Function bitOr(com.google.firebase.firestore.pipeline.Expr right); + method public final com.google.firebase.firestore.pipeline.Function bitOr(Object right); + method public final com.google.firebase.firestore.pipeline.Function bitRightShift(com.google.firebase.firestore.pipeline.Expr numberExpr); + method public final com.google.firebase.firestore.pipeline.Function bitRightShift(int number); + method public final com.google.firebase.firestore.pipeline.Function bitXor(com.google.firebase.firestore.pipeline.Expr right); + method public final com.google.firebase.firestore.pipeline.Function bitXor(Object right); method public final com.google.firebase.firestore.pipeline.Function byteLength(); method public final com.google.firebase.firestore.pipeline.Function charLength(); method public final com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector); @@ -795,15 +810,18 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String suffix); method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr other); method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(Object other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(java.util.List values); method public final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector); method public final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.VectorValue vector); method public final com.google.firebase.firestore.pipeline.Function euclideanDistance(double[] vector); + method public final com.google.firebase.firestore.pipeline.BooleanExpr exists(); method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr other); method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(Object other); method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr other); method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(Object other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr inAny(java.util.List values); method public final com.google.firebase.firestore.pipeline.BooleanExpr isNan(); + method public final com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(); + method public final com.google.firebase.firestore.pipeline.BooleanExpr isNotNull(); method public final com.google.firebase.firestore.pipeline.BooleanExpr isNull(); method public final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr pattern); method public final com.google.firebase.firestore.pipeline.BooleanExpr like(String pattern); @@ -825,7 +843,7 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.Function multiply(Object other); method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr other); method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(Object other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr notInAny(java.util.List values); + method public final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(java.util.List values); method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr pattern); method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String pattern); method public final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr pattern); @@ -868,6 +886,7 @@ package com.google.firebase.firestore.pipeline { method public static com.google.firebase.firestore.pipeline.Field of(com.google.firebase.firestore.FieldPath fieldPath); method public static com.google.firebase.firestore.pipeline.Field of(String name); field public static final com.google.firebase.firestore.pipeline.Field.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.Field DOCUMENT_ID; } public static final class Field.Companion { @@ -925,6 +944,28 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.Function arrayLength(String fieldName); method public static final com.google.firebase.firestore.pipeline.Function arrayReverse(com.google.firebase.firestore.pipeline.Expr array); method public static final com.google.firebase.firestore.pipeline.Function arrayReverse(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.Function bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function bitAnd(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.Function bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Function bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public static final com.google.firebase.firestore.pipeline.Function bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Function bitLeftShift(String fieldName, int number); + method public static final com.google.firebase.firestore.pipeline.Function bitNot(com.google.firebase.firestore.pipeline.Expr left); + method public static final com.google.firebase.firestore.pipeline.Function bitNot(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Function bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.Function bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function bitOr(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.Function bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Function bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public static final com.google.firebase.firestore.pipeline.Function bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Function bitRightShift(String fieldName, int number); + method public static final com.google.firebase.firestore.pipeline.Function bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.Function bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Function bitXor(String fieldName, Object right); method public static final com.google.firebase.firestore.pipeline.Function byteLength(com.google.firebase.firestore.pipeline.Expr value); method public static final com.google.firebase.firestore.pipeline.Function byteLength(String fieldName); method public static final com.google.firebase.firestore.pipeline.Function charLength(com.google.firebase.firestore.pipeline.Expr value); @@ -953,12 +994,15 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, Object right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(String fieldName, java.util.List values); method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, double[] vector); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr exists(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.Function generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); @@ -972,10 +1016,12 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then); method public static final com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr else); method public static final com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object else); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr inAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr inAny(String fieldName, java.util.List values); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(String fieldName); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(String fieldName); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNotNull(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNotNull(String fieldName); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNull(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNull(String fieldName); method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); @@ -1015,8 +1061,8 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, Object right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr not(com.google.firebase.firestore.pipeline.BooleanExpr condition); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr notInAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr notInAny(String fieldName, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(String fieldName, java.util.List values); method public static final com.google.firebase.firestore.pipeline.BooleanExpr or(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, String pattern); @@ -1104,6 +1150,28 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Function arrayLength(String fieldName); method public com.google.firebase.firestore.pipeline.Function arrayReverse(com.google.firebase.firestore.pipeline.Expr array); method public com.google.firebase.firestore.pipeline.Function arrayReverse(String fieldName); + method public com.google.firebase.firestore.pipeline.Function bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Function bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function bitAnd(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.Function bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Function bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public com.google.firebase.firestore.pipeline.Function bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Function bitLeftShift(String fieldName, int number); + method public com.google.firebase.firestore.pipeline.Function bitNot(com.google.firebase.firestore.pipeline.Expr left); + method public com.google.firebase.firestore.pipeline.Function bitNot(String fieldName); + method public com.google.firebase.firestore.pipeline.Function bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Function bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function bitOr(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.Function bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Function bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public com.google.firebase.firestore.pipeline.Function bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Function bitRightShift(String fieldName, int number); + method public com.google.firebase.firestore.pipeline.Function bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Function bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Function bitXor(String fieldName, Object right); method public com.google.firebase.firestore.pipeline.Function byteLength(com.google.firebase.firestore.pipeline.Expr value); method public com.google.firebase.firestore.pipeline.Function byteLength(String fieldName); method public com.google.firebase.firestore.pipeline.Function charLength(com.google.firebase.firestore.pipeline.Expr value); @@ -1132,12 +1200,15 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr eqAny(String fieldName, java.util.List values); method public com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); method public com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); method public com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); method public com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); method public com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); method public com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.BooleanExpr exists(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.Function generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); @@ -1151,10 +1222,12 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then); method public com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr else); method public com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object else); - method public com.google.firebase.firestore.pipeline.BooleanExpr inAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); - method public com.google.firebase.firestore.pipeline.BooleanExpr inAny(String fieldName, java.util.List values); method public com.google.firebase.firestore.pipeline.BooleanExpr isNan(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.BooleanExpr isNan(String fieldName); + method public com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(String fieldName); + method public com.google.firebase.firestore.pipeline.BooleanExpr isNotNull(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.BooleanExpr isNotNull(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr isNull(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.BooleanExpr isNull(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); @@ -1194,8 +1267,8 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr not(com.google.firebase.firestore.pipeline.BooleanExpr condition); - method public com.google.firebase.firestore.pipeline.BooleanExpr notInAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); - method public com.google.firebase.firestore.pipeline.BooleanExpr notInAny(String fieldName, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(String fieldName, java.util.List values); method public com.google.firebase.firestore.pipeline.BooleanExpr or(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, String pattern); @@ -1284,6 +1357,9 @@ package com.google.firebase.firestore.pipeline { method public static com.google.firebase.firestore.pipeline.Ordering ascending(String fieldName); method public static com.google.firebase.firestore.pipeline.Ordering descending(com.google.firebase.firestore.pipeline.Expr expr); method public static com.google.firebase.firestore.pipeline.Ordering descending(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr getExpr(); + method public com.google.firebase.firestore.pipeline.Ordering reverse(); + property public final com.google.firebase.firestore.pipeline.Expr expr; field public static final com.google.firebase.firestore.pipeline.Ordering.Companion Companion; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java index c9ed9c6c0e3..ee471318f80 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java @@ -170,7 +170,8 @@ public String getCanonicalId() { @Override BooleanExpr toPipelineExpr() { - BooleanExpr[] booleanExprs = filters.stream().map(Filter::toPipelineExpr).toArray(BooleanExpr[]::new); + BooleanExpr[] booleanExprs = + filters.stream().map(Filter::toPipelineExpr).toArray(BooleanExpr[]::new); switch (operator) { case AND: return new BooleanExpr("and", booleanExprs); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index a923be48e8d..266b280d41f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -38,9 +38,7 @@ import com.google.firebase.firestore.pipeline.Ordering; import com.google.firebase.firestore.pipeline.Stage; import com.google.firestore.v1.Value; - import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; @@ -48,7 +46,6 @@ import java.util.SortedSet; import java.util.TreeSet; import java.util.function.BiFunction; -import java.util.stream.Collectors; /** * Encapsulates all the query attributes we support in the SDK. It can be run against the @@ -549,7 +546,9 @@ public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataR if (fields.size() == 1) { p = p.where(fields.get(0).exists()); } else { - p = p.where(and(fields.get(0).exists(), fields.stream().skip(1).map(Expr::exists).toArray(BooleanExpr[]::new))); + BooleanExpr[] conditions = + fields.stream().skip(1).map(Expr::exists).toArray(BooleanExpr[]::new); + p = p.where(and(fields.get(0).exists(), conditions)); } if (startAt != null) { @@ -578,7 +577,8 @@ public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataR return p; } - private static BooleanExpr whereConditionsFromCursor(Bound bound, List fields, BiFunction cmp) { + private static BooleanExpr whereConditionsFromCursor( + Bound bound, List fields, BiFunction cmp) { List boundPosition = bound.getPosition(); int size = boundPosition.size(); hardAssert(size <= fields.size(), "Bound positions must not exceed order fields."); @@ -587,7 +587,7 @@ private static BooleanExpr whereConditionsFromCursor(Bound bound, List fi if (bound.isInclusive()) { condition = or(condition, Function.eq(fields.get(last), boundPosition.get(last))); } - for (int i = size - 2; i >=0; i--) { + for (int i = size - 2; i >= 0; i--) { final Field field = fields.get(i); final Value value = boundPosition.get(i); condition = or(cmp.apply(field, value), and(field.eq(value), condition)); 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 452d60e6787..005fdbd9068 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 @@ -27,7 +27,6 @@ import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.pipeline.Constant.Companion.of import com.google.firebase.firestore.util.CustomClassMapper import com.google.firestore.v1.MapValue -import com.google.firestore.v1.StructuredQuery.Order import com.google.firestore.v1.Value import java.util.Date import kotlin.reflect.KFunction1 @@ -355,8 +354,7 @@ class ExprWithAlias internal constructor(private val alias: String, private val class Field internal constructor(private val fieldPath: ModelFieldPath) : Selectable() { companion object { - @JvmField - val DOCUMENT_ID: Field = of(FieldPath.documentId()) + @JvmField val DOCUMENT_ID: Field = of(FieldPath.documentId()) @JvmStatic fun of(name: String): Field { @@ -527,10 +525,12 @@ protected constructor(private val name: String, private val params: Array) = BooleanExpr("eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) - @JvmStatic fun notEqAny(value: Expr, values: List) = + @JvmStatic + fun notEqAny(value: Expr, values: List) = BooleanExpr("not_eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) - @JvmStatic fun notEqAny(fieldName: String, values: List) = + @JvmStatic + fun notEqAny(fieldName: String, values: List) = BooleanExpr("not_eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) @JvmStatic fun isNan(expr: Expr) = BooleanExpr("is_nan", expr) @@ -950,7 +950,8 @@ protected constructor(private val name: String, private val params: Array Date: Wed, 2 Apr 2025 11:03:08 -0400 Subject: [PATCH 033/152] Fixups --- .../firebase/firestore/PipelineTest.java | 144 ++++++---- .../firestore/QueryToPipelineTest.java | 252 +++++++++++------- .../firebase/firestore/AggregateField.java | 24 ++ .../firestore/CollectionReference.java | 7 - .../com/google/firebase/firestore/Pipeline.kt | 27 +- .../com/google/firebase/firestore/Query.java | 5 - .../firebase/firestore/pipeline/aggregates.kt | 2 +- .../firestore/pipeline/expressions.kt | 78 +++--- 8 files changed, 335 insertions(+), 204 deletions(-) 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 7498bfccff2..7b69f8f216b 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 @@ -25,7 +25,6 @@ import static com.google.firebase.firestore.pipeline.Function.euclideanDistance; import static com.google.firebase.firestore.pipeline.Function.gt; import static com.google.firebase.firestore.pipeline.Function.logicalMax; -import static com.google.firebase.firestore.pipeline.Function.logicalMin; import static com.google.firebase.firestore.pipeline.Function.lt; import static com.google.firebase.firestore.pipeline.Function.lte; import static com.google.firebase.firestore.pipeline.Function.mapGet; @@ -227,7 +226,7 @@ public void aggregateResultsCountAll() { firestore .pipeline() .collection(randomCol) - .aggregate(AggregateExpr.countAll().as("count")) + .aggregate(AggregateExpr.countAll().alias("count")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -243,9 +242,9 @@ public void aggregateResultsMany() { .collection(randomCol) .where(Function.eq("genre", "Science Fiction")) .aggregate( - AggregateExpr.countAll().as("count"), - AggregateExpr.avg("rating").as("avgRating"), - Field.of("rating").max().as("maxRating")) + AggregateExpr.countAll().alias("count"), + AggregateExpr.avg("rating").alias("avgRating"), + Field.of("rating").max().alias("maxRating")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -261,7 +260,7 @@ public void groupAndAccumulateResults() { .collection(randomCol) .where(lt(Field.of("published"), 1984)) .aggregate( - AggregateStage.withAccumulators(AggregateExpr.avg("rating").as("avgRating")) + AggregateStage.withAccumulators(AggregateExpr.avg("rating").alias("avgRating")) .withGroups("genre")) .where(gt("avgRating", 4.3)) .sort(Field.of("avgRating").descending()) @@ -304,9 +303,9 @@ public void minAndMaxAccumulations() { .pipeline() .collection(randomCol) .aggregate( - AggregateExpr.countAll().as("count"), - Field.of("rating").max().as("maxRating"), - Field.of("published").min().as("minPublished")) + AggregateExpr.countAll().alias("count"), + Field.of("rating").max().alias("maxRating"), + Field.of("published").min().alias("minPublished")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -443,9 +442,10 @@ public void arrayContainsAllWorks() { @Test public void arrayLengthWorks() { Task execute = - randomCol + firestore .pipeline() - .select(Field.of("tags").arrayLength().as("tagsCount")) + .collection(randomCol) + .select(Field.of("tags").arrayLength().alias("tagsCount")) .where(eq("tagsCount", 3)) .execute(); assertThat(waitFor(execute).getResults()).hasSize(10); @@ -462,7 +462,7 @@ public void arrayConcatWorks() { .select( Field.of("tags") .arrayConcat(ImmutableList.of("newTag1", "newTag2")) - .as("modifiedTags")) + .alias("modifiedTags")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -476,9 +476,10 @@ public void arrayConcatWorks() { @Test public void testStrConcat() { Task execute = - randomCol + firestore .pipeline() - .select(Field.of("author").strConcat(" - ", Field.of("title")).as("bookInfo")) + .collection(randomCol) + .select(Field.of("author").strConcat(" - ", Field.of("title")).alias("bookInfo")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -490,8 +491,9 @@ public void testStrConcat() { @Test public void testStartsWith() { Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .where(startsWith("title", "The")) .select("title") .sort(Field.of("title").ascending()) @@ -508,8 +510,9 @@ public void testStartsWith() { @Test public void testEndsWith() { Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .where(endsWith("title", "y")) .select("title") .sort(Field.of("title").descending()) @@ -524,9 +527,10 @@ public void testEndsWith() { @Test public void testLength() { Task execute = - randomCol + firestore .pipeline() - .select(Field.of("title").charLength().as("titleLength"), Field.of("title")) + .collection(randomCol) + .select(Field.of("title").charLength().alias("titleLength"), Field.of("title")) .where(gt("titleLength", 20)) .sort(Field.of("title").ascending()) .execute(); @@ -543,9 +547,10 @@ public void testLength() { @Ignore("Not supported yet") public void testToLowercase() { Task execute = - randomCol + firestore .pipeline() - .select(Field.of("title").toLower().as("lowercaseTitle")) + .collection(randomCol) + .select(Field.of("title").toLower().alias("lowercaseTitle")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -557,9 +562,10 @@ public void testToLowercase() { @Ignore("Not supported yet") public void testToUppercase() { Task execute = - randomCol + firestore .pipeline() - .select(Field.of("author").toLower().as("uppercaseAuthor")) + .collection(randomCol) + .select(Field.of("author").toLower().alias("uppercaseAuthor")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -571,10 +577,11 @@ public void testToUppercase() { @Ignore("Not supported yet") public void testTrim() { Task execute = - randomCol + firestore .pipeline() - .addFields(strConcat(" ", Field.of("title"), " ").as("spacedTitle")) - .select(Field.of("spacedTitle").trim().as("trimmedTitle")) + .collection(randomCol) + .addFields(strConcat(" ", Field.of("title"), " ").alias("spacedTitle")) + .select(Field.of("spacedTitle").trim().alias("trimmedTitle")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -590,7 +597,12 @@ public void testTrim() { @Test public void testLike() { Task execute = - randomCol.pipeline().where(Function.like("title", "%Guide%")).select("title").execute(); + firestore + .pipeline() + .collection(randomCol) + .where(Function.like("title", "%Guide%")) + .select("title") + .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly(ImmutableMap.of("title", "The Hitchhiker's Guide to the Galaxy")); @@ -599,27 +611,36 @@ public void testLike() { @Test public void testRegexContains() { Task execute = - randomCol.pipeline().where(Function.regexContains("title", "(?i)(the|of)")).execute(); + firestore + .pipeline() + .collection(randomCol) + .where(Function.regexContains("title", "(?i)(the|of)")) + .execute(); assertThat(waitFor(execute).getResults()).hasSize(5); } @Test public void testRegexMatches() { Task execute = - randomCol.pipeline().where(Function.regexContains("title", ".*(?i)(the|of).*")).execute(); + firestore + .pipeline() + .collection(randomCol) + .where(Function.regexContains("title", ".*(?i)(the|of).*")) + .execute(); assertThat(waitFor(execute).getResults()).hasSize(5); } @Test public void testArithmeticOperations() { Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .select( - add(Field.of("rating"), 1).as("ratingPlusOne"), - subtract(Field.of("published"), 1900).as("yearsSince1900"), - Field.of("rating").multiply(10).as("ratingTimesTen"), - Field.of("rating").divide(2).as("ratingDividedByTwo")) + add(Field.of("rating"), 1).alias("ratingPlusOne"), + subtract(Field.of("published"), 1900).alias("yearsSince1900"), + Field.of("rating").multiply(10).alias("ratingTimesTen"), + Field.of("rating").divide(2).alias("ratingDividedByTwo")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -635,8 +656,9 @@ public void testArithmeticOperations() { @Test public void testComparisonOperators() { Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .where( and( gt("rating", 4.2), @@ -656,8 +678,9 @@ public void testComparisonOperators() { @Test public void testLogicalOperators() { Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .where( or( and(gt("rating", 4.5), eq("genre", "Science Fiction")), @@ -676,13 +699,14 @@ public void testLogicalOperators() { @Test public void testChecks() { Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .where(not(Field.of("rating").isNan())) .select( - Field.of("rating").isNull().as("ratingIsNull"), - Field.of("rating").eq(Constant.nullValue()).as("ratingEqNull"), - not(Field.of("rating").isNan()).as("ratingIsNotNan")) + Field.of("rating").isNull().alias("ratingIsNull"), + Field.of("rating").eq(Constant.nullValue()).alias("ratingEqNull"), + not(Field.of("rating").isNan()).alias("ratingIsNotNan")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -698,12 +722,13 @@ public void testChecks() { @Ignore("Not supported yet") public void testLogicalMax() { Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .where(Field.of("author").eq("Douglas Adams")) .select( - Field.of("rating").logicalMax(4.5).as("max_rating"), - logicalMax(Field.of("published"), 1900).as("max_published")) + Field.of("rating").logicalMax(4.5).alias("max_rating"), + logicalMax(Field.of("published"), 1900).alias("max_published")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -714,12 +739,7 @@ public void testLogicalMax() { @Ignore("Not supported yet") public void testLogicalMin() { Task execute = - randomCol - .pipeline() - .select( - Field.of("rating").logicalMin(4.5).as("min_rating"), - logicalMin(Field.of("published"), 1900).as("min_published")) - .execute(); + firestore.pipeline().collection(randomCol).sort(Field.of("rating").ascending()).execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly(ImmutableMap.of("min_rating", 4.2, "min_published", 1900)); @@ -728,9 +748,10 @@ public void testLogicalMin() { @Test public void testMapGet() { Task execute = - randomCol + firestore .pipeline() - .select(Field.of("awards").mapGet("hugo").as("hugoAward"), Field.of("title")) + .collection(randomCol) + .select(Field.of("awards").mapGet("hugo").alias("hugoAward"), Field.of("title")) .where(eq("hugoAward", true)) .execute(); assertThat(waitFor(execute).getResults()) @@ -745,14 +766,15 @@ public void testDistanceFunctions() { double[] sourceVector = {0.1, 0.1}; double[] targetVector = {0.5, 0.8}; Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .select( - cosineDistance(Constant.vector(sourceVector), targetVector).as("cosineDistance"), + cosineDistance(Constant.vector(sourceVector), targetVector).alias("cosineDistance"), Function.dotProduct(Constant.vector(sourceVector), targetVector) - .as("dotProductDistance"), + .alias("dotProductDistance"), euclideanDistance(Constant.vector(sourceVector), targetVector) - .as("euclideanDistance")) + .alias("euclideanDistance")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -767,8 +789,9 @@ public void testDistanceFunctions() { @Test public void testNestedFields() { Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .where(eq("awards.hugo", true)) .select("title", "awards.hugo") .execute(); @@ -782,13 +805,14 @@ public void testNestedFields() { @Test public void testMapGetWithFieldNameIncludingNotation() { Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .where(eq("awards.hugo", true)) .select( "title", Field.of("nestedField.level.1"), - mapGet("nestedField", "level.1").mapGet("level.2").as("nested")) + mapGet("nestedField", "level.1").mapGet("level.2").alias("nested")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -806,8 +830,9 @@ public void testMapGetWithFieldNameIncludingNotation() { @Test public void testListEquals() { Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .where(eq("tags", ImmutableList.of("philosophy", "crime", "redemption"))) .execute(); assertThat(waitFor(execute).getResults()) @@ -818,8 +843,9 @@ public void testListEquals() { @Test public void testMapEquals() { Task execute = - randomCol + firestore .pipeline() + .collection(randomCol) .where(eq("awards", ImmutableMap.of("nobel", true, "nebula", false))) .execute(); assertThat(waitFor(execute).getResults()) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java index c4f8f33bb01..188b047f739 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java @@ -21,7 +21,6 @@ import static com.google.firebase.firestore.Filter.equalTo; import static com.google.firebase.firestore.Filter.inArray; import static com.google.firebase.firestore.Filter.or; -import static com.google.firebase.firestore.pipeline.Function.ifThen; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.checkQueryAndPipelineResultsMatch; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.nullList; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.pipelineSnapshotToIds; @@ -41,8 +40,6 @@ import com.google.android.gms.tasks.Task; import com.google.common.collect.Lists; import com.google.firebase.firestore.Query.Direction; -import com.google.firebase.firestore.pipeline.Constant; -import com.google.firebase.firestore.pipeline.Field; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -70,7 +67,8 @@ public void testLimitQueries() { "c", map("k", "c"))); Query query = collection.limit(2); - PipelineSnapshot set = waitFor(query.pipeline().execute()); + FirebaseFirestore db = collection.firestore; + PipelineSnapshot set = waitFor(db.pipeline().createFrom(query).execute()); List> data = pipelineSnapshotToValues(set); assertEquals(asList(map("k", "a"), map("k", "b")), data); } @@ -86,7 +84,9 @@ public void testLimitQueriesUsingDescendingSortOrder() { "d", map("k", "d", "sort", 2))); Query query = collection.limit(2).orderBy("sort", Direction.DESCENDING); - PipelineSnapshot set = waitFor(query.pipeline().execute()); + FirebaseFirestore db = collection.firestore; + PipelineSnapshot set = waitFor(db.pipeline().createFrom(query).execute()); + List> data = pipelineSnapshotToValues(set); assertEquals(asList(map("k", "d", "sort", 2L), map("k", "c", "sort", 1L)), data); } @@ -94,10 +94,11 @@ public void testLimitQueriesUsingDescendingSortOrder() { @Test public void testLimitToLastMustAlsoHaveExplicitOrderBy() { CollectionReference collection = testCollectionWithDocs(map()); + FirebaseFirestore db = collection.firestore; Query query = collection.limitToLast(2); expectError( - () -> waitFor(query.pipeline().execute()), + () -> waitFor(db.pipeline().createFrom(query).execute()), "limitToLast() queries require specifying at least one orderBy() clause"); } @@ -112,33 +113,35 @@ public void testLimitToLastQueriesWithCursors() { "d", map("k", "d", "sort", 2))); Query query = collection.limitToLast(3).orderBy("sort").endBefore(2); - PipelineSnapshot set = waitFor(query.pipeline().execute()); + FirebaseFirestore db = collection.firestore; + + PipelineSnapshot set = waitFor(db.pipeline().createFrom(query).execute()); List> data = pipelineSnapshotToValues(set); assertEquals( asList(map("k", "a", "sort", 0L), map("k", "b", "sort", 1L), map("k", "c", "sort", 1L)), data); query = collection.limitToLast(3).orderBy("sort").endAt(1); - set = waitFor(query.pipeline().execute()); + set = waitFor(db.pipeline().createFrom(query).execute()); data = pipelineSnapshotToValues(set); assertEquals( asList(map("k", "a", "sort", 0L), map("k", "b", "sort", 1L), map("k", "c", "sort", 1L)), data); query = collection.limitToLast(3).orderBy("sort").startAt(2); - set = waitFor(query.pipeline().execute()); + set = waitFor(db.pipeline().createFrom(query).execute()); data = pipelineSnapshotToValues(set); assertEquals(asList(map("k", "d", "sort", 2L)), data); query = collection.limitToLast(3).orderBy("sort").startAfter(0); - set = waitFor(query.pipeline().execute()); + set = waitFor(db.pipeline().createFrom(query).execute()); data = pipelineSnapshotToValues(set); assertEquals( asList(map("k", "b", "sort", 1L), map("k", "c", "sort", 1L), map("k", "d", "sort", 2L)), data); query = collection.limitToLast(3).orderBy("sort").startAfter(-1); - set = waitFor(query.pipeline().execute()); + set = waitFor(db.pipeline().createFrom(query).execute()); data = pipelineSnapshotToValues(set); assertEquals( asList(map("k", "b", "sort", 1L), map("k", "c", "sort", 1L), map("k", "d", "sort", 2L)), @@ -159,7 +162,8 @@ public void testKeyOrderIsDescendingForDescendingInequality() { "g", map("foo", 66.0))); Query query = collection.whereGreaterThan("foo", 21.0).orderBy("foo", Direction.DESCENDING); - PipelineSnapshot result = waitFor(query.pipeline().execute()); + FirebaseFirestore db = collection.firestore; + PipelineSnapshot result = waitFor(db.pipeline().createFrom(query).execute()); assertEquals(asList("g", "f", "c", "b", "a"), pipelineSnapshotToIds(result)); } @@ -171,12 +175,11 @@ public void testUnaryFilterQueries() { "a", map("null", null, "nan", Double.NaN), "b", map("null", null, "nan", 0), "c", map("null", false, "nan", Double.NaN))); + FirebaseFirestore db = collection.firestore; PipelineSnapshot results = waitFor( - collection - .whereEqualTo("null", null) - .whereEqualTo("nan", Double.NaN) - .pipeline() + db.pipeline() + .createFrom(collection.whereEqualTo("null", null).whereEqualTo("nan", Double.NaN)) .execute()); assertEquals(1, results.getResults().size()); PipelineResult result = results.getResults().get(0); @@ -192,8 +195,12 @@ public void testFilterOnInfinity() { map( "a", map("inf", Double.POSITIVE_INFINITY), "b", map("inf", Double.NEGATIVE_INFINITY))); + FirebaseFirestore db = collection.firestore; PipelineSnapshot results = - waitFor(collection.whereEqualTo("inf", Double.POSITIVE_INFINITY).pipeline().execute()); + waitFor( + db.pipeline() + .createFrom(collection.whereEqualTo("inf", Double.POSITIVE_INFINITY)) + .execute()); assertEquals(1, results.getResults().size()); assertEquals(asList(map("inf", Double.POSITIVE_INFINITY)), pipelineSnapshotToValues(results)); } @@ -206,10 +213,11 @@ public void testCanExplicitlySortByDocumentId() { "b", map("key", "b"), "c", map("key", "c")); CollectionReference collection = testCollectionWithDocs(testDocs); + FirebaseFirestore db = collection.firestore; // Ideally this would be descending to validate it's different than // the default, but that requires an extra index PipelineSnapshot docs = - waitFor(collection.orderBy(FieldPath.documentId()).pipeline().execute()); + waitFor(db.pipeline().createFrom(collection.orderBy(FieldPath.documentId())).execute()); assertEquals( asList(testDocs.get("a"), testDocs.get("b"), testDocs.get("c")), pipelineSnapshotToValues(docs)); @@ -224,16 +232,21 @@ public void testCanQueryByDocumentId() { "ba", map("key", "ba"), "bb", map("key", "bb")); CollectionReference collection = testCollectionWithDocs(testDocs); + FirebaseFirestore db = collection.firestore; PipelineSnapshot docs = - waitFor(collection.whereEqualTo(FieldPath.documentId(), "ab").pipeline().execute()); + waitFor( + db.pipeline() + .createFrom(collection.whereEqualTo(FieldPath.documentId(), "ab")) + .execute()); assertEquals(singletonList(testDocs.get("ab")), pipelineSnapshotToValues(docs)); docs = waitFor( - collection - .whereGreaterThan(FieldPath.documentId(), "aa") - .whereLessThanOrEqualTo(FieldPath.documentId(), "ba") - .pipeline() + db.pipeline() + .createFrom( + collection + .whereGreaterThan(FieldPath.documentId(), "aa") + .whereLessThanOrEqualTo(FieldPath.documentId(), "ba")) .execute()); assertEquals(asList(testDocs.get("ab"), testDocs.get("ba")), pipelineSnapshotToValues(docs)); } @@ -247,20 +260,22 @@ public void testCanQueryByDocumentIdUsingRefs() { "ba", map("key", "ba"), "bb", map("key", "bb")); CollectionReference collection = testCollectionWithDocs(testDocs); + FirebaseFirestore db = collection.firestore; PipelineSnapshot docs = waitFor( - collection - .whereEqualTo(FieldPath.documentId(), collection.document("ab")) - .pipeline() + db.pipeline() + .createFrom( + collection.whereEqualTo(FieldPath.documentId(), collection.document("ab"))) .execute()); assertEquals(singletonList(testDocs.get("ab")), pipelineSnapshotToValues(docs)); docs = waitFor( - collection - .whereGreaterThan(FieldPath.documentId(), collection.document("aa")) - .whereLessThanOrEqualTo(FieldPath.documentId(), collection.document("ba")) - .pipeline() + db.pipeline() + .createFrom( + collection + .whereGreaterThan(FieldPath.documentId(), collection.document("aa")) + .whereLessThanOrEqualTo(FieldPath.documentId(), collection.document("ba"))) .execute()); assertEquals(asList(testDocs.get("ab"), testDocs.get("ba")), pipelineSnapshotToValues(docs)); } @@ -268,10 +283,13 @@ public void testCanQueryByDocumentIdUsingRefs() { @Test public void testCanQueryWithAndWithoutDocumentKey() { CollectionReference collection = testCollection(); + FirebaseFirestore db = collection.firestore; collection.add(map()); Task query1 = - collection.orderBy(FieldPath.documentId(), Direction.ASCENDING).pipeline().execute(); - Task query2 = collection.pipeline().execute(); + db.pipeline() + .createFrom(collection.orderBy(FieldPath.documentId(), Direction.ASCENDING)) + .execute(); + Task query2 = db.pipeline().createFrom(collection).execute(); waitFor(query1); waitFor(query2); @@ -300,6 +318,7 @@ public void testQueriesCanUseNotEqualFilters() { "a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG, "h", docH, "i", docI, "j", docJ); CollectionReference collection = testCollectionWithDocs(allDocs); + FirebaseFirestore db = collection.firestore; // Search for zips not matching 98101. Map> expectedDocsMap = new LinkedHashMap<>(allDocs); @@ -308,7 +327,7 @@ public void testQueriesCanUseNotEqualFilters() { expectedDocsMap.remove("j"); PipelineSnapshot snapshot = - waitFor(collection.whereNotEqualTo("zip", 98101L).pipeline().execute()); + waitFor(db.pipeline().createFrom(collection.whereNotEqualTo("zip", 98101L)).execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); // With objects. @@ -316,29 +335,27 @@ public void testQueriesCanUseNotEqualFilters() { expectedDocsMap.remove("h"); expectedDocsMap.remove("i"); expectedDocsMap.remove("j"); - snapshot = waitFor(collection.whereNotEqualTo("zip", map("code", 500)).pipeline().execute()); + snapshot = + waitFor( + db.pipeline() + .createFrom(collection.whereNotEqualTo("zip", map("code", 500))) + .execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); // With Null. expectedDocsMap = new LinkedHashMap<>(allDocs); expectedDocsMap.remove("i"); expectedDocsMap.remove("j"); - snapshot = waitFor(collection.whereNotEqualTo("zip", null).pipeline().execute()); + snapshot = waitFor(db.pipeline().createFrom(collection.whereNotEqualTo("zip", null)).execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); - List pipelineResults = waitFor(collection.pipeline() - .addFields( - Field.of("zip").isNan().as("isNan1"), - ifThen(Field.of("zip").isNan(), Constant.of(true)).as("isNan2"), - ifThen(Field.of("zip").isNan(), Constant.of(true)).isNotNull().as("isNan3") - ).execute()).getResults(); - // With NaN. expectedDocsMap = new LinkedHashMap<>(allDocs); expectedDocsMap.remove("a"); expectedDocsMap.remove("i"); expectedDocsMap.remove("j"); - snapshot = waitFor(collection.whereEqualTo("zip", Double.NaN).pipeline().execute()); + snapshot = + waitFor(db.pipeline().createFrom(collection.whereEqualTo("zip", Double.NaN)).execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); } @@ -355,8 +372,12 @@ public void testQueriesCanUseNotEqualFiltersWithDocIds() { "ba", docC, "bb", docD); CollectionReference collection = testCollectionWithDocs(testDocs); + FirebaseFirestore db = collection.firestore; PipelineSnapshot docs = - waitFor(collection.whereNotEqualTo(FieldPath.documentId(), "aa").pipeline().execute()); + waitFor( + db.pipeline() + .createFrom(collection.whereNotEqualTo(FieldPath.documentId(), "aa")) + .execute()); assertEquals(asList(docB, docC, docD), pipelineSnapshotToValues(docs)); } @@ -371,15 +392,18 @@ public void testQueriesCanUseArrayContainsFilters() { CollectionReference collection = testCollectionWithDocs( map("a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF)); + FirebaseFirestore db = collection.firestore; // Search for "array" to contain 42 PipelineSnapshot snapshot = - waitFor(collection.whereArrayContains("array", 42L).pipeline().execute()); + waitFor(db.pipeline().createFrom(collection.whereArrayContains("array", 42L)).execute()); assertEquals(asList(docA, docB, docD), pipelineSnapshotToValues(snapshot)); // Note: whereArrayContains() requires a non-null value parameter, so no null test is needed. // With NaN. - snapshot = waitFor(collection.whereArrayContains("array", Double.NaN).pipeline().execute()); + snapshot = + waitFor( + db.pipeline().createFrom(collection.whereArrayContains("array", Double.NaN)).execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); } @@ -400,36 +424,46 @@ public void testQueriesCanUseInFilters() { map( "a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG, "h", docH, "i", docI)); + FirebaseFirestore db = collection.firestore; // Search for zips matching 98101, 98103, or [98101, 98102]. PipelineSnapshot snapshot = waitFor( - collection - .whereIn("zip", asList(98101L, 98103L, asList(98101L, 98102L))) - .pipeline() + db.pipeline() + .createFrom( + collection.whereIn("zip", asList(98101L, 98103L, asList(98101L, 98102L)))) .execute()); assertEquals(asList(docA, docC, docG), pipelineSnapshotToValues(snapshot)); // With objects. - snapshot = waitFor(collection.whereIn("zip", asList(map("code", 500L))).pipeline().execute()); + snapshot = + waitFor( + db.pipeline() + .createFrom(collection.whereIn("zip", asList(map("code", 500L)))) + .execute()); assertEquals(asList(docF), pipelineSnapshotToValues(snapshot)); // With null. - snapshot = waitFor(collection.whereIn("zip", nullList()).pipeline().execute()); + snapshot = waitFor(db.pipeline().createFrom(collection.whereIn("zip", nullList())).execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With null and a value. List inputList = nullList(); inputList.add(98101L); - snapshot = waitFor(collection.whereIn("zip", inputList).pipeline().execute()); + snapshot = waitFor(db.pipeline().createFrom(collection.whereIn("zip", inputList)).execute()); assertEquals(asList(docA), pipelineSnapshotToValues(snapshot)); // With NaN. - snapshot = waitFor(collection.whereIn("zip", asList(Double.NaN)).pipeline().execute()); + snapshot = + waitFor(db.pipeline().createFrom(collection.whereIn("zip", asList(Double.NaN))).execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With NaN and a value. - snapshot = waitFor(collection.whereIn("zip", asList(Double.NaN, 98101L)).pipeline().execute()); + snapshot = + waitFor( + db.pipeline() + .createFrom(collection.whereIn("zip", asList(Double.NaN, 98101L))) + .execute()); assertEquals(asList(docA), pipelineSnapshotToValues(snapshot)); } @@ -446,9 +480,12 @@ public void testQueriesCanUseInFiltersWithDocIds() { "ba", docC, "bb", docD); CollectionReference collection = testCollectionWithDocs(testDocs); + FirebaseFirestore db = collection.firestore; PipelineSnapshot docs = waitFor( - collection.whereIn(FieldPath.documentId(), asList("aa", "ab")).pipeline().execute()); + db.pipeline() + .createFrom(collection.whereIn(FieldPath.documentId(), asList("aa", "ab"))) + .execute()); assertEquals(asList(docA, docB), pipelineSnapshotToValues(docs)); } @@ -472,6 +509,7 @@ public void testQueriesCanUseNotInFilters() { "a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG, "h", docH, "i", docI, "j", docJ); CollectionReference collection = testCollectionWithDocs(allDocs); + FirebaseFirestore db = collection.firestore; // Search for zips not matching 98101, 98103, or [98101, 98102]. Map> expectedDocsMap = new LinkedHashMap<>(allDocs); @@ -483,9 +521,9 @@ public void testQueriesCanUseNotInFilters() { PipelineSnapshot snapshot = waitFor( - collection - .whereNotIn("zip", asList(98101L, 98103L, asList(98101L, 98102L))) - .pipeline() + db.pipeline() + .createFrom( + collection.whereNotIn("zip", asList(98101L, 98103L, asList(98101L, 98102L)))) .execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); @@ -495,11 +533,15 @@ public void testQueriesCanUseNotInFilters() { expectedDocsMap.remove("i"); expectedDocsMap.remove("j"); snapshot = - waitFor(collection.whereNotIn("zip", asList(map("code", 500L))).pipeline().execute()); + waitFor( + db.pipeline() + .createFrom(collection.whereNotIn("zip", asList(map("code", 500L)))) + .execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); // With Null. - snapshot = waitFor(collection.whereNotIn("zip", nullList()).pipeline().execute()); + snapshot = + waitFor(db.pipeline().createFrom(collection.whereNotIn("zip", nullList())).execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With NaN. @@ -507,7 +549,9 @@ public void testQueriesCanUseNotInFilters() { expectedDocsMap.remove("a"); expectedDocsMap.remove("i"); expectedDocsMap.remove("j"); - snapshot = waitFor(collection.whereNotIn("zip", asList(Double.NaN)).pipeline().execute()); + snapshot = + waitFor( + db.pipeline().createFrom(collection.whereNotIn("zip", asList(Double.NaN))).execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); // With NaN and a number. @@ -517,7 +561,10 @@ public void testQueriesCanUseNotInFilters() { expectedDocsMap.remove("i"); expectedDocsMap.remove("j"); snapshot = - waitFor(collection.whereNotIn("zip", asList(Float.NaN, 98101L)).pipeline().execute()); + waitFor( + db.pipeline() + .createFrom(collection.whereNotIn("zip", asList(Float.NaN, 98101L))) + .execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); } @@ -534,9 +581,12 @@ public void testQueriesCanUseNotInFiltersWithDocIds() { "ba", docC, "bb", docD); CollectionReference collection = testCollectionWithDocs(testDocs); + FirebaseFirestore db = collection.firestore; PipelineSnapshot docs = waitFor( - collection.whereNotIn(FieldPath.documentId(), asList("aa", "ab")).pipeline().execute()); + db.pipeline() + .createFrom(collection.whereNotIn(FieldPath.documentId(), asList("aa", "ab"))) + .execute()); assertEquals(asList(docC, docD), pipelineSnapshotToValues(docs)); } @@ -557,39 +607,55 @@ public void testQueriesCanUseArrayContainsAnyFilters() { map( "a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG, "h", docH, "i", docI)); + FirebaseFirestore db = collection.firestore; // Search for "array" to contain [42, 43]. PipelineSnapshot snapshot = - waitFor(collection.whereArrayContainsAny("array", asList(42L, 43L)).pipeline().execute()); + waitFor( + db.pipeline() + .createFrom(collection.whereArrayContainsAny("array", asList(42L, 43L))) + .execute()); assertEquals(asList(docA, docB, docD, docE), pipelineSnapshotToValues(snapshot)); // With objects. snapshot = waitFor( - collection.whereArrayContainsAny("array", asList(map("a", 42L))).pipeline().execute()); + db.pipeline() + .createFrom(collection.whereArrayContainsAny("array", asList(map("a", 42L)))) + .execute()); assertEquals(asList(docF), pipelineSnapshotToValues(snapshot)); // With null. - snapshot = waitFor(collection.whereArrayContainsAny("array", nullList()).pipeline().execute()); + snapshot = + waitFor( + db.pipeline() + .createFrom(collection.whereArrayContainsAny("array", nullList())) + .execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With null and a value. List inputList = nullList(); inputList.add(43L); - snapshot = waitFor(collection.whereArrayContainsAny("array", inputList).pipeline().execute()); + snapshot = + waitFor( + db.pipeline() + .createFrom(collection.whereArrayContainsAny("array", inputList)) + .execute()); assertEquals(asList(docE), pipelineSnapshotToValues(snapshot)); // With NaN. snapshot = - waitFor(collection.whereArrayContainsAny("array", asList(Double.NaN)).pipeline().execute()); + waitFor( + db.pipeline() + .createFrom(collection.whereArrayContainsAny("array", asList(Double.NaN))) + .execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With NaN and a value. snapshot = waitFor( - collection - .whereArrayContainsAny("array", asList(Double.NaN, 43L)) - .pipeline() + db.pipeline() + .createFrom(collection.whereArrayContainsAny("array", asList(Double.NaN, 43L))) .execute()); assertEquals(asList(docE), pipelineSnapshotToValues(snapshot)); } @@ -621,7 +687,8 @@ public void testCollectionGroupQueries() { } waitFor(batch.commit()); - PipelineSnapshot snapshot = waitFor(db.collectionGroup(collectionGroup).pipeline().execute()); + PipelineSnapshot snapshot = + waitFor(db.pipeline().createFrom(db.collectionGroup(collectionGroup)).execute()); assertEquals( asList("cg-doc1", "cg-doc2", "cg-doc3", "cg-doc4", "cg-doc5"), pipelineSnapshotToIds(snapshot)); @@ -652,21 +719,23 @@ public void testCollectionGroupQueriesWithStartAtEndAtWithArbitraryDocumentIds() PipelineSnapshot snapshot = waitFor( - db.collectionGroup(collectionGroup) - .orderBy(FieldPath.documentId()) - .startAt("a/b") - .endAt("a/b0") - .pipeline() + db.pipeline() + .createFrom( + db.collectionGroup(collectionGroup) + .orderBy(FieldPath.documentId()) + .startAt("a/b") + .endAt("a/b0")) .execute()); assertEquals(asList("cg-doc2", "cg-doc3", "cg-doc4"), pipelineSnapshotToIds(snapshot)); snapshot = waitFor( - db.collectionGroup(collectionGroup) - .orderBy(FieldPath.documentId()) - .startAfter("a/b") - .endBefore("a/b/" + collectionGroup + "/cg-doc3") - .pipeline() + db.pipeline() + .createFrom( + db.collectionGroup(collectionGroup) + .orderBy(FieldPath.documentId()) + .startAfter("a/b") + .endBefore("a/b/" + collectionGroup + "/cg-doc3")) .execute()); assertEquals(asList("cg-doc2"), pipelineSnapshotToIds(snapshot)); } @@ -696,19 +765,22 @@ public void testCollectionGroupQueriesWithWhereFiltersOnArbitraryDocumentIds() { PipelineSnapshot snapshot = waitFor( - db.collectionGroup(collectionGroup) - .whereGreaterThanOrEqualTo(FieldPath.documentId(), "a/b") - .whereLessThanOrEqualTo(FieldPath.documentId(), "a/b0") - .pipeline() + db.pipeline() + .createFrom( + db.collectionGroup(collectionGroup) + .whereGreaterThanOrEqualTo(FieldPath.documentId(), "a/b") + .whereLessThanOrEqualTo(FieldPath.documentId(), "a/b0")) .execute()); assertEquals(asList("cg-doc2", "cg-doc3", "cg-doc4"), pipelineSnapshotToIds(snapshot)); snapshot = waitFor( - db.collectionGroup(collectionGroup) - .whereGreaterThan(FieldPath.documentId(), "a/b") - .whereLessThan(FieldPath.documentId(), "a/b/" + collectionGroup + "/cg-doc3") - .pipeline() + db.pipeline() + .createFrom( + db.collectionGroup(collectionGroup) + .whereGreaterThan(FieldPath.documentId(), "a/b") + .whereLessThan( + FieldPath.documentId(), "a/b/" + collectionGroup + "/cg-doc3")) .execute()); assertEquals(asList("cg-doc2"), pipelineSnapshotToIds(snapshot)); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java index 902d515d86f..d9cdff77bfc 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java @@ -17,6 +17,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; +import com.google.firebase.firestore.pipeline.AggregateExpr; +import com.google.firebase.firestore.pipeline.AggregateWithAlias; +import com.google.firebase.firestore.pipeline.Field; import java.util.Objects; /** Represents an aggregation that can be performed by Firestore. */ @@ -61,6 +64,9 @@ public String getOperator() { return operator; } + @NonNull + abstract AggregateWithAlias toPipeline(); + /** * Returns true if the given object is equal to this object. Two `AggregateField` objects are * considered equal if they have the same operator and operate on the same field. @@ -195,6 +201,12 @@ public static class CountAggregateField extends AggregateField { private CountAggregateField() { super(null, "count"); } + + @NonNull + @Override + AggregateWithAlias toPipeline() { + return AggregateExpr.countAll().alias(getAlias()); + } } /** Represents a "sum" aggregation that can be performed by Firestore. */ @@ -202,6 +214,12 @@ public static class SumAggregateField extends AggregateField { private SumAggregateField(@NonNull FieldPath fieldPath) { super(fieldPath, "sum"); } + + @NonNull + @Override + AggregateWithAlias toPipeline() { + return Field.of(getFieldPath()).sum().alias(getAlias()); + } } /** Represents an "average" aggregation that can be performed by Firestore. */ @@ -209,5 +227,11 @@ public static class AverageAggregateField extends AggregateField { private AverageAggregateField(@NonNull FieldPath fieldPath) { super(fieldPath, "average"); } + + @NonNull + @Override + AggregateWithAlias toPipeline() { + return Field.of(getFieldPath()).avg().alias(getAlias()); + } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java index c8b9cfaa90a..d0a358e2233 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java @@ -21,7 +21,6 @@ import com.google.android.gms.tasks.Task; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.ResourcePath; -import com.google.firebase.firestore.pipeline.CollectionSource; import com.google.firebase.firestore.util.Executors; import com.google.firebase.firestore.util.Util; @@ -128,10 +127,4 @@ public Task add(@NonNull Object data) { return ref; }); } - - @NonNull - @Override - public Pipeline pipeline() { - return new Pipeline(firestore, firestore.getUserDataReader(), new CollectionSource(getPath())); - } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 8f3ca218061..3c4932168ce 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -165,10 +165,10 @@ internal constructor( fun union(other: Pipeline): Pipeline = append(UnionStage(other)) - fun unnest(field: String, alias: String): Pipeline = unnest(Field.of(field).`as`(alias)) + fun unnest(field: String, alias: String): Pipeline = unnest(Field.of(field).alias(alias)) fun unnest(field: String, alias: String, options: UnnestOptions): Pipeline = - unnest(Field.of(field).`as`(alias), options) + unnest(Field.of(field).alias(alias), options) fun unnest(selectable: Selectable): Pipeline = append(UnnestStage(selectable)) @@ -206,11 +206,23 @@ internal constructor( } class PipelineSource internal constructor(private val firestore: FirebaseFirestore) { - fun collection(path: String): Pipeline { - // Validate path by converting to CollectionReference - return collection(firestore.collection(path)) + fun createFrom(query: Query): Pipeline { + if (query.firestore.databaseId != firestore.databaseId) { + throw IllegalArgumentException("Provided query is from a different Firestore instance.") + } + return query.query.toPipeline(firestore, firestore.userDataReader) } + fun createFrom(query: AggregateQuery): Pipeline = + createFrom(query.query) + .aggregate( + *query.aggregateFields.map(AggregateField::toPipeline).toTypedArray() + ) + + fun collection(path: String): Pipeline = + // Validate path by converting to CollectionReference + collection(firestore.collection(path)) + fun collection(ref: CollectionReference): Pipeline { if (ref.firestore.databaseId != firestore.databaseId) { throw IllegalArgumentException( @@ -230,10 +242,9 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto fun database(): Pipeline = Pipeline(firestore, firestore.userDataReader, DatabaseSource()) - fun documents(vararg documents: String): Pipeline { + fun documents(vararg documents: String): Pipeline = // Validate document path by converting to DocumentReference - return documents(*documents.map(firestore::document).toTypedArray()) - } + documents(*documents.map(firestore::document).toTypedArray()) fun documents(vararg documents: DocumentReference): Pipeline { val databaseId = firestore.databaseId diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java index 12ff36bf960..d5fb8a4399b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java @@ -1241,11 +1241,6 @@ public AggregateQuery aggregate( return new AggregateQuery(this, fields); } - @NonNull - public Pipeline pipeline() { - return query.toPipeline(firestore, firestore.getUserDataReader()); - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt index c363c28be3e..308b6fb70f0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt @@ -52,7 +52,7 @@ private constructor(private val name: String, private val params: Array of(value) is Value -> of(value) is Map<*, *> -> - MapOfExpr( - value.entries.associate { - val key = it.key - if (key is String) key to toExpr(it.value) - else throw IllegalArgumentException("Maps with non-string keys are not supported") - } + Function.map( + value + .flatMap { + val key = it.key + if (key is String) listOf(of(key), toExpr(it.value)) + else throw IllegalArgumentException("Maps with non-string keys are not supported") + } + .toTypedArray() ) is List<*> -> ListOfExprs(value.map(toExpr).toTypedArray()) else -> null @@ -116,7 +118,7 @@ abstract class Expr internal constructor() { * expression and associates it with the provided alias. * ``` */ - open fun `as`(alias: String) = ExprWithAlias(alias, this) + open fun alias(alias: String) = ExprWithAlias(alias, this) /** * Creates an expression that this expression to another expression. @@ -234,6 +236,13 @@ abstract class Expr internal constructor() { fun mapGet(key: String) = Function.mapGet(this, key) + fun mapMerge(secondMap: Expr, vararg otherMaps: Expr) = + Function.mapMerge(this, secondMap, *otherMaps) + + fun mapRemove(key: Expr) = Function.mapRemove(this, key) + + fun mapRemove(key: String) = Function.mapRemove(this, key) + fun cosineDistance(vector: Expr) = Function.cosineDistance(this, vector) fun cosineDistance(vector: DoubleArray) = Function.cosineDistance(this, vector) @@ -378,16 +387,6 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() } -internal class MapOfExpr(private val expressions: Map) : Expr() { - override fun toProto(userDataReader: UserDataReader): Value { - val builder = MapValue.newBuilder() - for ((key, value) in expressions) { - builder.putFields(key, value.toProto(userDataReader)) - } - return Value.newBuilder().setMapValue(builder).build() - } -} - internal class ListOfExprs(private val expressions: Array) : Expr() { override fun toProto(userDataReader: UserDataReader): Value = encodeValue(expressions.map { it.toProto(userDataReader) }) @@ -704,6 +703,12 @@ protected constructor(private val name: String, private val params: Array) = Function("map", elements) + + @JvmStatic + fun map(elements: Map) = + map(elements.flatMap { listOf(of(it.key), toExprOrConstant(it.value)) }.toTypedArray()) + @JvmStatic fun mapGet(map: Expr, key: Expr) = Function("map_get", map, key) @JvmStatic fun mapGet(map: Expr, key: String) = Function("map_get", map, key) @@ -712,6 +717,22 @@ protected constructor(private val name: String, private val params: Array) : fun countIf(): AggregateExpr = AggregateExpr.countIf(this) - fun ifThen(then: Expr) = ifThen(this, then) - - fun ifThen(then: Any) = ifThen(this, then) - - fun ifThenElse(then: Expr, `else`: Expr) = ifThenElse(this, then, `else`) + fun cond(then: Expr, otherwise: Expr) = cond(this, then, otherwise) - fun ifThenElse(then: Any, `else`: Any) = ifThenElse(this, then, `else`) + fun cond(then: Any, otherwise: Any) = cond(this, then, otherwise) } class Ordering private constructor(val expr: Expr, private val dir: Direction) { From 5e1d84e4964f466a35bb2b9728e5679438814f33 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 8 Apr 2025 15:07:13 -0400 Subject: [PATCH 034/152] Fixups --- .../testutil/IntegrationTestUtil.java | 3 +- .../firebase/firestore/FirebaseFirestore.java | 5 + .../com/google/firebase/firestore/Pipeline.kt | 195 +++++++++++--- .../firebase/firestore/pipeline/options.kt | 25 +- .../firebase/firestore/pipeline/stage.kt | 252 ++++++++++++------ .../firebase/firestore/remote/Datastore.java | 21 +- 6 files changed, 353 insertions(+), 148 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index 5a8a9ceb67c..e94fb3baf35 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -568,7 +568,8 @@ public static void checkOnlineAndOfflineResultsMatch(Query query, String... expe */ public static void checkQueryAndPipelineResultsMatch(Query query, String... expectedDocs) { QuerySnapshot docsFromQuery = waitFor(query.get(Source.SERVER)); - PipelineSnapshot docsFromPipeline = waitFor(query.pipeline().execute()); + PipelineSnapshot docsFromPipeline = + waitFor(query.getFirestore().pipeline().createFrom(query).execute()); assertEquals(querySnapshotToIds(docsFromQuery), pipelineSnapshotToIds(docsFromPipeline)); List expected = asList(expectedDocs); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index a2797e72399..fa0ccf7f90f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -884,6 +884,11 @@ static void setClientLanguage(@NonNull String languageToken) { FirestoreChannel.setClientLanguage(languageToken); } + /** + * Build a new Pipeline + * + * @return {@code PipelineSource} for this Firestore instance. + */ @NonNull public PipelineSource pipeline() { clientProvider.ensureConfigured(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 3c4932168ce..25496f498a9 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -18,8 +18,8 @@ import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.TaskCompletionSource import com.google.common.collect.FluentIterable import com.google.common.collect.ImmutableList +import com.google.firebase.Timestamp import com.google.firebase.firestore.model.DocumentKey -import com.google.firebase.firestore.model.SnapshotVersion import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.pipeline.AddFieldsStage import com.google.firebase.firestore.pipeline.AggregateStage @@ -32,7 +32,6 @@ import com.google.firebase.firestore.pipeline.DistinctStage import com.google.firebase.firestore.pipeline.DocumentsSource import com.google.firebase.firestore.pipeline.Expr import com.google.firebase.firestore.pipeline.Field -import com.google.firebase.firestore.pipeline.FindNearestOptions import com.google.firebase.firestore.pipeline.FindNearestStage import com.google.firebase.firestore.pipeline.GenericArg import com.google.firebase.firestore.pipeline.GenericStage @@ -47,7 +46,6 @@ import com.google.firebase.firestore.pipeline.Selectable import com.google.firebase.firestore.pipeline.SortStage import com.google.firebase.firestore.pipeline.Stage import com.google.firebase.firestore.pipeline.UnionStage -import com.google.firebase.firestore.pipeline.UnnestOptions import com.google.firebase.firestore.pipeline.UnnestStage import com.google.firebase.firestore.pipeline.WhereStage import com.google.firebase.firestore.util.Preconditions @@ -59,15 +57,15 @@ class Pipeline internal constructor( internal val firestore: FirebaseFirestore, internal val userDataReader: UserDataReader, - private val stages: FluentIterable + private val stages: FluentIterable> ) { internal constructor( firestore: FirebaseFirestore, userDataReader: UserDataReader, - stage: Stage + stage: Stage<*> ) : this(firestore, userDataReader, FluentIterable.of(stage)) - private fun append(stage: Stage): Pipeline { + private fun append(stage: Stage<*>): Pipeline { return Pipeline(firestore, userDataReader, stages.append(stage)) } @@ -144,15 +142,28 @@ internal constructor( fun findNearest( property: Expr, vector: DoubleArray, - distanceMeasure: FindNearestStage.DistanceMeasure - ) = append(FindNearestStage(property, vector, distanceMeasure, FindNearestOptions.DEFAULT)) + distanceMeasure: FindNearestStage.DistanceMeasure, + ) = append(FindNearestStage.of(property, vector, distanceMeasure)) fun findNearest( - property: Expr, + propertyField: String, vector: DoubleArray, distanceMeasure: FindNearestStage.DistanceMeasure, - options: FindNearestOptions - ) = append(FindNearestStage(property, vector, distanceMeasure, options)) + ) = append(FindNearestStage.of(propertyField, vector, distanceMeasure)) + + fun findNearest( + property: Expr, + vector: Expr, + distanceMeasure: FindNearestStage.DistanceMeasure, + ) = append(FindNearestStage.of(property, vector, distanceMeasure)) + + fun findNearest( + propertyField: String, + vector: Expr, + distanceMeasure: FindNearestStage.DistanceMeasure, + ) = append(FindNearestStage.of(propertyField, vector, distanceMeasure)) + + fun findNearest(stage: FindNearestStage) = append(stage) fun replace(field: String): Pipeline = replace(Field.of(field)) @@ -165,34 +176,36 @@ internal constructor( fun union(other: Pipeline): Pipeline = append(UnionStage(other)) - fun unnest(field: String, alias: String): Pipeline = unnest(Field.of(field).alias(alias)) - - fun unnest(field: String, alias: String, options: UnnestOptions): Pipeline = - unnest(Field.of(field).alias(alias), options) + fun unnest(field: String, alias: String): Pipeline = unnest(UnnestStage.withField(field, alias)) fun unnest(selectable: Selectable): Pipeline = append(UnnestStage(selectable)) - fun unnest(selectable: Selectable, options: UnnestOptions): Pipeline = - append(UnnestStage(selectable)) + fun unnest(stage: UnnestStage): Pipeline = append(stage) private inner class ObserverSnapshotTask : PipelineResultObserver { private val userDataWriter = UserDataWriter(firestore, DocumentSnapshot.ServerTimestampBehavior.DEFAULT) private val taskCompletionSource = TaskCompletionSource() private val results: ImmutableList.Builder = ImmutableList.builder() - override fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) { + override fun onDocument( + key: DocumentKey?, + data: Map, + createTime: Timestamp?, + updateTime: Timestamp? + ) { results.add( PipelineResult( firestore, userDataWriter, if (key == null) null else DocumentReference(key, firestore), data, - version + createTime, + updateTime ) ) } - override fun onComplete(executionTime: SnapshotVersion) { + override fun onComplete(executionTime: Timestamp) { taskCompletionSource.setResult(PipelineSnapshot(executionTime, results.build())) } @@ -205,7 +218,17 @@ internal constructor( } } +/** Start of a Firestore Pipeline */ class PipelineSource internal constructor(private val firestore: FirebaseFirestore) { + + /** + * Convert the given Query into an equivalent Pipeline. + * + * @param query A Query to be converted into a Pipeline. + * @return Pipeline that is equivalent to [query] + * @throws [IllegalArgumentException] Thrown if the [query] provided targets a different project + * or database than the pipeline. + */ fun createFrom(query: Query): Pipeline { if (query.firestore.databaseId != firestore.databaseId) { throw IllegalArgumentException("Provided query is from a different Firestore instance.") @@ -213,16 +236,40 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto return query.query.toPipeline(firestore, firestore.userDataReader) } - fun createFrom(query: AggregateQuery): Pipeline = - createFrom(query.query) + /** + * Convert the given Aggregate Query into an equivalent Pipeline. + * + * @param aggregateQuery An Aggregate Query to be converted into a Pipeline. + * @return Pipeline that is equivalent to [aggregateQuery] + * @throws [IllegalArgumentException] Thrown if the [aggregateQuery] provided targets a different + * project or database than the pipeline. + */ + fun createFrom(aggregateQuery: AggregateQuery): Pipeline = + createFrom(aggregateQuery.query) .aggregate( - *query.aggregateFields.map(AggregateField::toPipeline).toTypedArray() + *aggregateQuery.aggregateFields + .map(AggregateField::toPipeline) + .toTypedArray() ) + /** + * Set the pipeline's source to the collection specified by the given path. + * + * @param path A path to a collection that will be the source of this pipeline. + * @return Pipeline with documents from target collection. + */ fun collection(path: String): Pipeline = // Validate path by converting to CollectionReference collection(firestore.collection(path)) + /** + * Set the pipeline's source to the collection specified by the given CollectionReference. + * + * @param ref A CollectionReference for a collection that will be the source of this pipeline. + * @return Pipeline with documents from target collection. + * @throws [IllegalArgumentException] Thrown if the [ref] provided targets a different project or + * database than the pipeline. + */ fun collection(ref: CollectionReference): Pipeline { if (ref.firestore.databaseId != firestore.databaseId) { throw IllegalArgumentException( @@ -232,6 +279,11 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto return Pipeline(firestore, firestore.userDataReader, CollectionSource(ref.path)) } + /** + * Set the pipeline's source to the collection group with the given id. + * + * @param collectionid The id of a collection group that will be the source of this pipeline. + */ fun collectionGroup(collectionId: String): Pipeline { Preconditions.checkNotNull(collectionId, "Provided collection ID must not be null.") require(!collectionId.contains("/")) { @@ -240,12 +292,33 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto return Pipeline(firestore, firestore.userDataReader, CollectionGroupSource(collectionId)) } + /** + * Set the pipeline's source to be all documents in this database. + * + * @return Pipeline with all documents in this database. + */ fun database(): Pipeline = Pipeline(firestore, firestore.userDataReader, DatabaseSource()) + /** + * Set the pipeline's source to the documents specified by the given paths. + * + * @param documents Paths specifying the individual documents that will be the source of this + * pipeline. + * @return Pipeline with [documents]. + */ fun documents(vararg documents: String): Pipeline = // Validate document path by converting to DocumentReference documents(*documents.map(firestore::document).toTypedArray()) + /** + * Set the pipeline's source to the documents specified by the given DocumentReferences. + * + * @param documents DocumentReferences specifying the individual documents that will be the source + * of this pipeline. + * @return Pipeline with [documents]. + * @throws [IllegalArgumentException] Thrown if the [documents] provided targets a different + * project or database than the pipeline. + */ fun documents(vararg documents: DocumentReference): Pipeline { val databaseId = firestore.databaseId for (document in documents) { @@ -263,11 +336,18 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto } } +/** + */ class PipelineSnapshot -internal constructor( - private val executionTime: SnapshotVersion, - val results: List -) : Iterable { +internal constructor(executionTime: Timestamp, results: List) : + Iterable { + + /** The time at which the pipeline producing this result is executed. */ + val executionTime: Timestamp = executionTime + + /** List of all the results */ + val results: List = results + override fun iterator() = results.iterator() } @@ -275,17 +355,51 @@ class PipelineResult internal constructor( private val firestore: FirebaseFirestore, private val userDataWriter: UserDataWriter, - val ref: DocumentReference?, + ref: DocumentReference?, private val fields: Map, - private val version: SnapshotVersion, + createTime: Timestamp?, + updateTime: Timestamp?, ) { + /** The time the document was created. Null if this result is not a document. */ + val createTime: Timestamp? = createTime + + /** + * The time the document was last updated (at the time the snapshot was generated). Null if this + * result is not a document. + */ + val updateTime: Timestamp? = updateTime + + /** + * The reference to the document, if the query returns the `__name__` field for a document. The + * name field will be returned by default if querying a document. + * + * The `__name__` field will not be returned if the query projects away this field. For example: + * ``` + * // this query does not select the `__name__` field as part of the select stage, + * // so the __name__ field will not be in the output docs from this stage + * db.pipeline().collection("books").select("title", "desc") + * ``` + * + * The `__name__` field will not be returned from queries with aggregate or distinct stages. + * + * @return [DocumentReference] Reference to the document, if applicable. + */ + val ref: DocumentReference? = ref + /** * Returns the ID of the document represented by this result. Returns null if this result is not * corresponding to a Firestore document. + * + * @return ID of document, if applicable. */ fun getId(): String? = ref?.id + /** + * Retrieves all fields in the result as an object map. + * + * @return Map of field names to objects. + */ fun getData(): Map = userDataWriter.convertObject(fields) private fun extractNestedValue(fieldPath: FieldPath): Value? { @@ -307,15 +421,32 @@ internal constructor( return value } + /** + * Retrieves the field specified by [field]. + * + * @param field The field path (e.g. "foo" or "foo.bar") to a specific field. + * @return The data at the specified field location or null if no such field exists. + */ fun get(field: String): Any? = get(FieldPath.fromDotSeparatedPath(field)) + /** + * Retrieves the field specified by [fieldPath]. + * + * @param fieldPath The field path to a specific field. + * @return The data at the specified field location or null if no such field exists. + */ fun get(fieldPath: FieldPath): Any? = userDataWriter.convertValue(extractNestedValue(fieldPath)) - override fun toString() = "PipelineResult{ref=$ref, version=$version}, data=${getData()}" + override fun toString() = "PipelineResult{ref=$ref, updateTime=$updateTime}, data=${getData()}" } internal interface PipelineResultObserver { - fun onDocument(key: DocumentKey?, data: Map, version: SnapshotVersion) - fun onComplete(executionTime: SnapshotVersion) + fun onDocument( + key: DocumentKey?, + data: Map, + createTime: Timestamp?, + updateTime: Timestamp? + ) + fun onComplete(executionTime: Timestamp) fun onError(exception: FirebaseFirestoreException) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt index 5695a9dff91..af3853f2f4a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt @@ -15,7 +15,6 @@ package com.google.firebase.firestore.pipeline import com.google.common.collect.ImmutableMap -import com.google.firebase.firestore.model.Values import com.google.firestore.v1.ArrayValue import com.google.firestore.v1.MapValue import com.google.firestore.v1.Value @@ -46,7 +45,11 @@ internal constructor(private val options: ImmutableMap) { return with(key, value.toValue()) } - internal fun forEach(f: (String, Value) -> Unit) = options.forEach(f) + internal fun forEach(f: (String, Value) -> Unit) { + for (entry in options.entries) { + f(entry.key, entry.value) + } + } private fun toValue(): Value { val mapValue = MapValue.newBuilder().putAllFields(options).build() @@ -61,21 +64,3 @@ internal constructor(private val options: ImmutableMap) { } } } - -abstract class AbstractOptions> -internal constructor(internal val options: InternalOptions) { - - internal abstract fun self(options: InternalOptions): T - - protected fun with(key: String, value: Value): T = self(options.with(key, value)) - - fun with(key: String, value: String): T = with(key, Values.encodeValue(value)) - - fun with(key: String, value: Boolean): T = with(key, Values.encodeValue(value)) - - fun with(key: String, value: Long): T = with(key, Values.encodeValue(value)) - - fun with(key: String, value: Double): T = with(key, Values.encodeValue(value)) - - fun with(key: String, value: Field): T = with(key, value.toProto()) -} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index f72ede652ad..bcae5dd66e5 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -15,16 +15,15 @@ package com.google.firebase.firestore.pipeline import com.google.firebase.firestore.UserDataReader +import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue -import com.google.firebase.firestore.model.Values.encodeVectorValue import com.google.firebase.firestore.pipeline.Field.Companion.of +import com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure import com.google.firestore.v1.Pipeline import com.google.firestore.v1.Value -abstract class Stage -private constructor(protected val name: String, private val options: InternalOptions) { - internal constructor(name: String) : this(name, InternalOptions.EMPTY) - internal constructor(name: String, options: AbstractOptions<*>) : this(name, options.options) +abstract class Stage> +internal constructor(protected val name: String, internal val options: InternalOptions) { internal fun toProtoStage(userDataReader: UserDataReader): Pipeline.Stage { val builder = Pipeline.Stage.newBuilder() builder.setName(name) @@ -33,27 +32,37 @@ private constructor(protected val name: String, private val options: InternalOpt return builder.build() } internal abstract fun args(userDataReader: UserDataReader): Sequence + + internal abstract fun self(options: InternalOptions): T + + protected fun with(key: String, value: Value): T = self(options.with(key, value)) + + fun with(key: String, value: String): T = with(key, Values.encodeValue(value)) + + fun with(key: String, value: Boolean): T = with(key, Values.encodeValue(value)) + + fun with(key: String, value: Long): T = with(key, Values.encodeValue(value)) + + fun with(key: String, value: Double): T = with(key, Values.encodeValue(value)) + + fun with(key: String, value: Field): T = with(key, value.toProto()) } class GenericStage -private constructor( +internal constructor( name: String, private val arguments: List, - private val options: GenericOptions -) : Stage(name, options) { - internal constructor( - name: String, - arguments: List - ) : this(name, arguments, GenericOptions.DEFAULT) + options: InternalOptions = InternalOptions.EMPTY +) : Stage(name, options) { companion object { - @JvmStatic fun of(name: String) = GenericStage(name, emptyList()) + @JvmStatic fun of(name: String) = GenericStage(name, emptyList(), InternalOptions.EMPTY) } + override fun self(options: InternalOptions) = GenericStage(name, arguments, options) + fun withArguments(vararg arguments: Any): GenericStage = GenericStage(name, arguments.map(GenericArg::from), options) - fun withOptions(options: GenericOptions): GenericStage = GenericStage(name, arguments, options) - override fun args(userDataReader: UserDataReader): Sequence = arguments.asSequence().map { it.toProto(userDataReader) } } @@ -95,39 +104,52 @@ internal sealed class GenericArg { } } -class GenericOptions private constructor(options: InternalOptions) : - AbstractOptions(options) { - companion object { - @JvmField val DEFAULT = GenericOptions(InternalOptions.EMPTY) - } - override fun self(options: InternalOptions) = GenericOptions(options) -} - -internal class DatabaseSource : Stage("database") { +internal class DatabaseSource +@JvmOverloads +internal constructor(options: InternalOptions = InternalOptions.EMPTY) : + Stage("database", options) { + override fun self(options: InternalOptions) = DatabaseSource(options) override fun args(userDataReader: UserDataReader): Sequence = emptySequence() } -internal class CollectionSource internal constructor(path: String) : Stage("collection") { - private val path: String = if (path.startsWith("/")) path else "/" + path +internal class CollectionSource +@JvmOverloads +internal constructor(val path: String, options: InternalOptions = InternalOptions.EMPTY) : + Stage("collection", options) { + override fun self(options: InternalOptions): CollectionSource = CollectionSource(path, options) override fun args(userDataReader: UserDataReader): Sequence = - sequenceOf(Value.newBuilder().setReferenceValue(path).build()) + sequenceOf( + Value.newBuilder().setReferenceValue(if (path.startsWith("/")) path else "/" + path).build() + ) } -internal class CollectionGroupSource internal constructor(val collectionId: String) : - Stage("collection_group") { +internal class CollectionGroupSource +@JvmOverloads +internal constructor(val collectionId: String, options: InternalOptions = InternalOptions.EMPTY) : + Stage("collection_group", options) { + override fun self(options: InternalOptions) = CollectionGroupSource(collectionId, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setReferenceValue("").build(), encodeValue(collectionId)) } -internal class DocumentsSource internal constructor(private val documents: Array) : - Stage("documents") { +internal class DocumentsSource +@JvmOverloads +internal constructor( + private val documents: Array, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("documents", options) { internal constructor(document: String) : this(arrayOf(document)) + override fun self(options: InternalOptions) = DocumentsSource(documents, options) override fun args(userDataReader: UserDataReader): Sequence = documents.asSequence().map { if (it.startsWith("/")) it else "/" + it }.map(::encodeValue) } -internal class AddFieldsStage internal constructor(private val fields: Array) : - Stage("add_fields") { +internal class AddFieldsStage +internal constructor( + private val fields: Array, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("add_fields", options) { + override fun self(options: InternalOptions) = AddFieldsStage(fields, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto(userDataReader) })) } @@ -135,8 +157,9 @@ internal class AddFieldsStage internal constructor(private val fields: Array, - private val groups: Map -) : Stage("aggregate") { + private val groups: Map, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("aggregate", options) { private constructor(accumulators: Map) : this(accumulators, emptyMap()) companion object { @JvmStatic @@ -150,6 +173,8 @@ internal constructor( } } + override fun self(options: InternalOptions) = AggregateStage(accumulators, groups, options) + fun withGroups(vararg groups: Selectable) = AggregateStage(accumulators, groups.associateBy(Selectable::getAlias)) @@ -169,8 +194,12 @@ internal constructor( ) } -internal class WhereStage internal constructor(private val condition: BooleanExpr) : - Stage("where") { +internal class WhereStage +internal constructor( + private val condition: BooleanExpr, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("where", options) { + override fun self(options: InternalOptions) = WhereStage(condition, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(condition.toProto(userDataReader)) } @@ -178,77 +207,122 @@ internal class WhereStage internal constructor(private val condition: BooleanExp class FindNearestStage internal constructor( private val property: Expr, - private val vector: DoubleArray, + private val vector: Expr, private val distanceMeasure: DistanceMeasure, - options: FindNearestOptions -) : Stage("find_nearest", options) { + options: InternalOptions = InternalOptions.EMPTY +) : Stage("find_nearest", options) { + + companion object { + @JvmStatic + fun of(property: Expr, vector: Expr, distanceMeasure: DistanceMeasure) = + FindNearestStage(property, vector, distanceMeasure) + + @JvmStatic + fun of(property: Expr, vector: DoubleArray, distanceMeasure: DistanceMeasure) = + FindNearestStage(property, Constant.vector(vector), distanceMeasure) + + @JvmStatic + fun of(fieldName: String, vector: Expr, distanceMeasure: DistanceMeasure) = + FindNearestStage(Constant.of(fieldName), vector, distanceMeasure) + + @JvmStatic + fun of(fieldName: String, vector: DoubleArray, distanceMeasure: DistanceMeasure) = + FindNearestStage(Constant.of(fieldName), Constant.vector(vector), distanceMeasure) + } class DistanceMeasure private constructor(internal val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) + companion object { - val EUCLIDEAN = DistanceMeasure("euclidean") - val COSINE = DistanceMeasure("cosine") - val DOT_PRODUCT = DistanceMeasure("dot_product") - } - } + @JvmField val EUCLIDEAN = DistanceMeasure("euclidean") - override fun args(userDataReader: UserDataReader): Sequence = - sequenceOf(property.toProto(userDataReader), encodeVectorValue(vector), distanceMeasure.proto) -} + @JvmField val COSINE = DistanceMeasure("cosine") -class FindNearestOptions private constructor(options: InternalOptions) : - AbstractOptions(options) { - companion object { - @JvmField val DEFAULT = FindNearestOptions(InternalOptions.EMPTY) + @JvmField val DOT_PRODUCT = DistanceMeasure("dot_product") + } } - override fun self(options: InternalOptions): FindNearestOptions = FindNearestOptions(options) + override fun self(options: InternalOptions) = + FindNearestStage(property, vector, distanceMeasure, options) - fun withLimit(limit: Long): FindNearestOptions = with("limit", limit) + override fun args(userDataReader: UserDataReader): Sequence = + sequenceOf( + property.toProto(userDataReader), + vector.toProto(userDataReader), + distanceMeasure.proto + ) + + fun withLimit(limit: Long): FindNearestStage = with("limit", limit) - fun withDistanceField(distanceField: Field): FindNearestOptions = + fun withDistanceField(distanceField: Field): FindNearestStage = with("distance_field", distanceField) - fun withDistanceField(distanceField: String): FindNearestOptions = + fun withDistanceField(distanceField: String): FindNearestStage = withDistanceField(of(distanceField)) } -internal class LimitStage internal constructor(private val limit: Int) : Stage("limit") { +internal class LimitStage +internal constructor(private val limit: Int, options: InternalOptions = InternalOptions.EMPTY) : + Stage("limit", options) { + override fun self(options: InternalOptions) = LimitStage(limit, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(limit)) } -internal class OffsetStage internal constructor(private val offset: Int) : Stage("offset") { +internal class OffsetStage +internal constructor(private val offset: Int, options: InternalOptions = InternalOptions.EMPTY) : + Stage("offset", options) { + override fun self(options: InternalOptions) = OffsetStage(offset, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(offset)) } -internal class SelectStage internal constructor(private val fields: Array) : - Stage("select") { +internal class SelectStage +internal constructor( + private val fields: Array, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("select", options) { + override fun self(options: InternalOptions) = SelectStage(fields, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto(userDataReader) })) } -internal class SortStage internal constructor(private val orders: Array) : - Stage("sort") { +internal class SortStage +internal constructor( + private val orders: Array, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("sort", options) { + override fun self(options: InternalOptions) = SortStage(orders, options) override fun args(userDataReader: UserDataReader): Sequence = orders.asSequence().map { it.toProto(userDataReader) } } -internal class DistinctStage internal constructor(private val groups: Array) : - Stage("distinct") { +internal class DistinctStage +internal constructor( + private val groups: Array, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("distinct", options) { + override fun self(options: InternalOptions) = DistinctStage(groups, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(groups.associate { it.getAlias() to it.toProto(userDataReader) })) } -internal class RemoveFieldsStage internal constructor(private val fields: Array) : - Stage("remove_fields") { +internal class RemoveFieldsStage +internal constructor( + private val fields: Array, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("remove_fields", options) { + override fun self(options: InternalOptions) = RemoveFieldsStage(fields, options) override fun args(userDataReader: UserDataReader): Sequence = fields.asSequence().map(Field::toProto) } internal class ReplaceStage -internal constructor(private val field: Selectable, private val mode: Mode) : Stage("replace") { +internal constructor( + private val field: Selectable, + private val mode: Mode, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("replace", options) { class Mode private constructor(internal val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) companion object { @@ -257,12 +331,18 @@ internal constructor(private val field: Selectable, private val mode: Mode) : St val MERGE_PREFER_PARENT = Mode("merge_prefer_parent") } } + override fun self(options: InternalOptions) = ReplaceStage(field, mode, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(field.toProto(userDataReader), mode.proto) } -class SampleStage private constructor(private val size: Number, private val mode: Mode) : - Stage("sample") { +class SampleStage +private constructor( + private val size: Number, + private val mode: Mode, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("sample", options) { + override fun self(options: InternalOptions) = SampleStage(size, mode, options) class Mode private constructor(internal val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) companion object { @@ -280,27 +360,29 @@ class SampleStage private constructor(private val size: Number, private val mode } internal class UnionStage -internal constructor(private val other: com.google.firebase.firestore.Pipeline) : Stage("union") { +internal constructor( + private val other: com.google.firebase.firestore.Pipeline, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("union", options) { + override fun self(options: InternalOptions) = UnionStage(other, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setPipelineValue(other.toPipelineProto()).build()) } -internal class UnnestStage -internal constructor(private val selectable: Selectable, options: UnnestOptions) : - Stage("unnest", options) { - internal constructor(selectable: Selectable) : this(selectable, UnnestOptions.DEFAULT) +class UnnestStage +internal constructor( + private val selectable: Selectable, + options: InternalOptions = InternalOptions.EMPTY +) : Stage("unnest", options) { + companion object { + @JvmStatic fun withField(selectable: Selectable) = UnnestStage(selectable) + @JvmStatic + fun withField(field: String, alias: String): UnnestStage = + UnnestStage(Field.of(field).alias(alias)) + } + override fun self(options: InternalOptions) = UnnestStage(selectable, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(selectable.getAlias()), selectable.toProto(userDataReader)) -} - -class UnnestOptions private constructor(options: InternalOptions) : - AbstractOptions(options) { - - fun withIndexField(indexField: String): UnnestOptions = with("index_field", indexField) - override fun self(options: InternalOptions) = UnnestOptions(options) - - companion object { - @JvmField val DEFAULT: UnnestOptions = UnnestOptions(InternalOptions.EMPTY) - } + fun withIndexField(indexField: String): UnnestStage = with("index_field", indexField) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java index 91086531db1..e3a8da26217 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java @@ -22,6 +22,7 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.common.base.Strings; +import com.google.firebase.Timestamp; import com.google.firebase.firestore.AggregateField; import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.PipelineResultObserver; @@ -248,17 +249,24 @@ public void executePipeline(ExecutePipelineRequest request, PipelineResultObserv request, new FirestoreChannel.StreamingListener() { - private SnapshotVersion executionTime = SnapshotVersion.NONE; + private Timestamp executionTime = null; @Override public void onMessage(ExecutePipelineResponse message) { - setExecutionTime(serializer.decodeVersion(message.getExecutionTime())); + if (message.hasExecutionTime()) { + executionTime = serializer.decodeTimestamp(message.getExecutionTime()); + } for (Document document : message.getResultsList()) { String documentName = document.getName(); observer.onDocument( Strings.isNullOrEmpty(documentName) ? null : serializer.decodeKey(documentName), document.getFieldsMap(), - serializer.decodeVersion(document.getUpdateTime())); + document.hasCreateTime() + ? serializer.decodeTimestamp(document.getCreateTime()) + : null, + document.hasUpdateTime() + ? serializer.decodeTimestamp(document.getUpdateTime()) + : null); } } @@ -274,13 +282,6 @@ public void onClose(Status status) { observer.onError(exception); } } - - private void setExecutionTime(SnapshotVersion executionTime) { - if (executionTime.equals(SnapshotVersion.NONE)) { - return; - } - this.executionTime = executionTime; - } }); } From 8141aa8c024119f5847aeac469535195b1578d40 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 9 Apr 2025 10:03:26 -0400 Subject: [PATCH 035/152] Fixups --- .../firebase/firestore/PipelineTest.java | 66 +-- .../firebase/firestore/AggregateField.java | 4 +- .../firebase/firestore/core/FieldFilter.java | 2 +- .../google/firebase/firestore/core/Query.java | 12 +- .../firebase/firestore/pipeline/aggregates.kt | 31 +- .../firestore/pipeline/expressions.kt | 488 +++++++++--------- .../firebase/firestore/pipeline/stage.kt | 11 +- 7 files changed, 308 insertions(+), 306 deletions(-) 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 7b69f8f216b..cd4fc945221 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 @@ -15,25 +15,25 @@ package com.google.firebase.firestore; import static com.google.common.truth.Truth.assertThat; -import static com.google.firebase.firestore.pipeline.Function.add; -import static com.google.firebase.firestore.pipeline.Function.and; -import static com.google.firebase.firestore.pipeline.Function.arrayContains; -import static com.google.firebase.firestore.pipeline.Function.arrayContainsAny; -import static com.google.firebase.firestore.pipeline.Function.cosineDistance; -import static com.google.firebase.firestore.pipeline.Function.endsWith; -import static com.google.firebase.firestore.pipeline.Function.eq; -import static com.google.firebase.firestore.pipeline.Function.euclideanDistance; -import static com.google.firebase.firestore.pipeline.Function.gt; -import static com.google.firebase.firestore.pipeline.Function.logicalMax; -import static com.google.firebase.firestore.pipeline.Function.lt; -import static com.google.firebase.firestore.pipeline.Function.lte; -import static com.google.firebase.firestore.pipeline.Function.mapGet; -import static com.google.firebase.firestore.pipeline.Function.neq; -import static com.google.firebase.firestore.pipeline.Function.not; -import static com.google.firebase.firestore.pipeline.Function.or; -import static com.google.firebase.firestore.pipeline.Function.startsWith; -import static com.google.firebase.firestore.pipeline.Function.strConcat; -import static com.google.firebase.firestore.pipeline.Function.subtract; +import static com.google.firebase.firestore.pipeline.FunctionExpr.add; +import static com.google.firebase.firestore.pipeline.FunctionExpr.and; +import static com.google.firebase.firestore.pipeline.FunctionExpr.arrayContains; +import static com.google.firebase.firestore.pipeline.FunctionExpr.arrayContainsAny; +import static com.google.firebase.firestore.pipeline.FunctionExpr.cosineDistance; +import static com.google.firebase.firestore.pipeline.FunctionExpr.endsWith; +import static com.google.firebase.firestore.pipeline.FunctionExpr.eq; +import static com.google.firebase.firestore.pipeline.FunctionExpr.euclideanDistance; +import static com.google.firebase.firestore.pipeline.FunctionExpr.gt; +import static com.google.firebase.firestore.pipeline.FunctionExpr.logicalMax; +import static com.google.firebase.firestore.pipeline.FunctionExpr.lt; +import static com.google.firebase.firestore.pipeline.FunctionExpr.lte; +import static com.google.firebase.firestore.pipeline.FunctionExpr.mapGet; +import static com.google.firebase.firestore.pipeline.FunctionExpr.neq; +import static com.google.firebase.firestore.pipeline.FunctionExpr.not; +import static com.google.firebase.firestore.pipeline.FunctionExpr.or; +import static com.google.firebase.firestore.pipeline.FunctionExpr.startsWith; +import static com.google.firebase.firestore.pipeline.FunctionExpr.strConcat; +import static com.google.firebase.firestore.pipeline.FunctionExpr.subtract; import static com.google.firebase.firestore.pipeline.Ordering.ascending; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor; @@ -42,11 +42,11 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.truth.Correspondence; -import com.google.firebase.firestore.pipeline.AggregateExpr; +import com.google.firebase.firestore.pipeline.AggregateFunction; import com.google.firebase.firestore.pipeline.AggregateStage; import com.google.firebase.firestore.pipeline.Constant; import com.google.firebase.firestore.pipeline.Field; -import com.google.firebase.firestore.pipeline.Function; +import com.google.firebase.firestore.pipeline.FunctionExpr; import com.google.firebase.firestore.pipeline.GenericStage; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import java.util.Collections; @@ -226,7 +226,7 @@ public void aggregateResultsCountAll() { firestore .pipeline() .collection(randomCol) - .aggregate(AggregateExpr.countAll().alias("count")) + .aggregate(AggregateFunction.countAll().alias("count")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -240,10 +240,10 @@ public void aggregateResultsMany() { firestore .pipeline() .collection(randomCol) - .where(Function.eq("genre", "Science Fiction")) + .where(FunctionExpr.eq("genre", "Science Fiction")) .aggregate( - AggregateExpr.countAll().alias("count"), - AggregateExpr.avg("rating").alias("avgRating"), + AggregateFunction.countAll().alias("count"), + AggregateFunction.avg("rating").alias("avgRating"), Field.of("rating").max().alias("maxRating")) .execute(); assertThat(waitFor(execute).getResults()) @@ -260,7 +260,7 @@ public void groupAndAccumulateResults() { .collection(randomCol) .where(lt(Field.of("published"), 1984)) .aggregate( - AggregateStage.withAccumulators(AggregateExpr.avg("rating").alias("avgRating")) + AggregateStage.withAccumulators(AggregateFunction.avg("rating").alias("avgRating")) .withGroups("genre")) .where(gt("avgRating", 4.3)) .sort(Field.of("avgRating").descending()) @@ -282,9 +282,9 @@ public void groupAndAccumulateResultsGeneric() { .genericStage("where", lt(Field.of("published"), 1984)) .genericStage( "aggregate", - ImmutableMap.of("avgRating", AggregateExpr.avg("rating")), + ImmutableMap.of("avgRating", AggregateFunction.avg("rating")), ImmutableMap.of("genre", Field.of("genre"))) - .genericStage(GenericStage.of("where").withArguments(gt("avgRating", 4.3))) + .genericStage(GenericStage.ofName("where").withArguments(gt("avgRating", 4.3))) .genericStage("sort", Field.of("avgRating").descending()) .execute(); assertThat(waitFor(execute).getResults()) @@ -303,7 +303,7 @@ public void minAndMaxAccumulations() { .pipeline() .collection(randomCol) .aggregate( - AggregateExpr.countAll().alias("count"), + AggregateFunction.countAll().alias("count"), Field.of("rating").max().alias("maxRating"), Field.of("published").min().alias("minPublished")) .execute(); @@ -600,7 +600,7 @@ public void testLike() { firestore .pipeline() .collection(randomCol) - .where(Function.like("title", "%Guide%")) + .where(FunctionExpr.like("title", "%Guide%")) .select("title") .execute(); assertThat(waitFor(execute).getResults()) @@ -614,7 +614,7 @@ public void testRegexContains() { firestore .pipeline() .collection(randomCol) - .where(Function.regexContains("title", "(?i)(the|of)")) + .where(FunctionExpr.regexContains("title", "(?i)(the|of)")) .execute(); assertThat(waitFor(execute).getResults()).hasSize(5); } @@ -625,7 +625,7 @@ public void testRegexMatches() { firestore .pipeline() .collection(randomCol) - .where(Function.regexContains("title", ".*(?i)(the|of).*")) + .where(FunctionExpr.regexContains("title", ".*(?i)(the|of).*")) .execute(); assertThat(waitFor(execute).getResults()).hasSize(5); } @@ -771,7 +771,7 @@ public void testDistanceFunctions() { .collection(randomCol) .select( cosineDistance(Constant.vector(sourceVector), targetVector).alias("cosineDistance"), - Function.dotProduct(Constant.vector(sourceVector), targetVector) + FunctionExpr.dotProduct(Constant.vector(sourceVector), targetVector) .alias("dotProductDistance"), euclideanDistance(Constant.vector(sourceVector), targetVector) .alias("euclideanDistance")) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java index d9cdff77bfc..d26f82ea7c2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java @@ -17,7 +17,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; -import com.google.firebase.firestore.pipeline.AggregateExpr; +import com.google.firebase.firestore.pipeline.AggregateFunction; import com.google.firebase.firestore.pipeline.AggregateWithAlias; import com.google.firebase.firestore.pipeline.Field; import java.util.Objects; @@ -205,7 +205,7 @@ private CountAggregateField() { @NonNull @Override AggregateWithAlias toPipeline() { - return AggregateExpr.countAll().alias(getAlias()); + return AggregateFunction.countAll().alias(getAlias()); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java index 4d2a5a404c0..cc3c5402eb6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java @@ -14,7 +14,7 @@ package com.google.firebase.firestore.core; -import static com.google.firebase.firestore.pipeline.Function.and; +import static com.google.firebase.firestore.pipeline.FunctionExpr.and; import static com.google.firebase.firestore.util.Assert.hardAssert; import static java.lang.Double.isNaN; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index 266b280d41f..e3045ea2aec 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -14,8 +14,8 @@ package com.google.firebase.firestore.core; -import static com.google.firebase.firestore.pipeline.Function.and; -import static com.google.firebase.firestore.pipeline.Function.or; +import static com.google.firebase.firestore.pipeline.FunctionExpr.and; +import static com.google.firebase.firestore.pipeline.FunctionExpr.or; import static com.google.firebase.firestore.util.Assert.hardAssert; import androidx.annotation.NonNull; @@ -34,7 +34,7 @@ import com.google.firebase.firestore.pipeline.DocumentsSource; import com.google.firebase.firestore.pipeline.Expr; import com.google.firebase.firestore.pipeline.Field; -import com.google.firebase.firestore.pipeline.Function; +import com.google.firebase.firestore.pipeline.FunctionExpr; import com.google.firebase.firestore.pipeline.Ordering; import com.google.firebase.firestore.pipeline.Stage; import com.google.firestore.v1.Value; @@ -552,11 +552,11 @@ public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataR } if (startAt != null) { - p = p.where(whereConditionsFromCursor(startAt, fields, Function::gt)); + p = p.where(whereConditionsFromCursor(startAt, fields, FunctionExpr::gt)); } if (endAt != null) { - p = p.where(whereConditionsFromCursor(endAt, fields, Function::lt)); + p = p.where(whereConditionsFromCursor(endAt, fields, FunctionExpr::lt)); } // Cursors, Limit, Offset @@ -585,7 +585,7 @@ private static BooleanExpr whereConditionsFromCursor( int last = size - 1; BooleanExpr condition = cmp.apply(fields.get(last), boundPosition.get(last)); if (bound.isInclusive()) { - condition = or(condition, Function.eq(fields.get(last), boundPosition.get(last))); + condition = or(condition, FunctionExpr.eq(fields.get(last), boundPosition.get(last))); } for (int i = size - 2; i >= 0; i--) { final Field field = fields.get(i); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt index 308b6fb70f0..0461718d7fa 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt @@ -18,38 +18,41 @@ import com.google.firebase.firestore.UserDataReader import com.google.firestore.v1.Value class AggregateWithAlias -internal constructor(internal val alias: String, internal val expr: AggregateExpr) +internal constructor(internal val alias: String, internal val expr: AggregateFunction) -class AggregateExpr +class AggregateFunction private constructor(private val name: String, private val params: Array) { private constructor(name: String) : this(name, emptyArray()) private constructor(name: String, expr: Expr) : this(name, arrayOf(expr)) private constructor(name: String, fieldName: String) : this(name, Field.of(fieldName)) companion object { - @JvmStatic fun countAll() = AggregateExpr("count") - @JvmStatic fun count(fieldName: String) = AggregateExpr("count", fieldName) + @JvmStatic fun generic(name: String, vararg expr: Expr) = AggregateFunction(name, expr) - @JvmStatic fun count(expr: Expr) = AggregateExpr("count", expr) + @JvmStatic fun countAll() = AggregateFunction("count") - @JvmStatic fun countIf(condition: BooleanExpr) = AggregateExpr("countIf", condition) + @JvmStatic fun count(fieldName: String) = AggregateFunction("count", fieldName) - @JvmStatic fun sum(fieldName: String) = AggregateExpr("sum", fieldName) + @JvmStatic fun count(expr: Expr) = AggregateFunction("count", expr) - @JvmStatic fun sum(expr: Expr) = AggregateExpr("sum", expr) + @JvmStatic fun countIf(condition: BooleanExpr) = AggregateFunction("countIf", condition) - @JvmStatic fun avg(fieldName: String) = AggregateExpr("avg", fieldName) + @JvmStatic fun sum(fieldName: String) = AggregateFunction("sum", fieldName) - @JvmStatic fun avg(expr: Expr) = AggregateExpr("avg", expr) + @JvmStatic fun sum(expr: Expr) = AggregateFunction("sum", expr) - @JvmStatic fun min(fieldName: String) = AggregateExpr("min", fieldName) + @JvmStatic fun avg(fieldName: String) = AggregateFunction("avg", fieldName) - @JvmStatic fun min(expr: Expr) = AggregateExpr("min", expr) + @JvmStatic fun avg(expr: Expr) = AggregateFunction("avg", expr) - @JvmStatic fun max(fieldName: String) = AggregateExpr("max", fieldName) + @JvmStatic fun min(fieldName: String) = AggregateFunction("min", fieldName) - @JvmStatic fun max(expr: Expr) = AggregateExpr("max", expr) + @JvmStatic fun min(expr: Expr) = AggregateFunction("min", expr) + + @JvmStatic fun max(fieldName: String) = AggregateFunction("max", fieldName) + + @JvmStatic fun max(expr: Expr) = AggregateFunction("max", expr) } fun alias(alias: String) = AggregateWithAlias(alias, this) 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 1d84e905723..12e7439d745 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 @@ -57,7 +57,7 @@ abstract class Expr internal constructor() { is VectorValue -> of(value) is Value -> of(value) is Map<*, *> -> - Function.map( + FunctionExpr.map( value .flatMap { val key = it.key @@ -78,27 +78,27 @@ abstract class Expr internal constructor() { others.map(::toExprOrConstant).toTypedArray() } - fun bitAnd(right: Expr) = Function.bitAnd(this, right) + fun bitAnd(right: Expr) = FunctionExpr.bitAnd(this, right) - fun bitAnd(right: Any) = Function.bitAnd(this, right) + fun bitAnd(right: Any) = FunctionExpr.bitAnd(this, right) - fun bitOr(right: Expr) = Function.bitOr(this, right) + fun bitOr(right: Expr) = FunctionExpr.bitOr(this, right) - fun bitOr(right: Any) = Function.bitOr(this, right) + fun bitOr(right: Any) = FunctionExpr.bitOr(this, right) - fun bitXor(right: Expr) = Function.bitXor(this, right) + fun bitXor(right: Expr) = FunctionExpr.bitXor(this, right) - fun bitXor(right: Any) = Function.bitXor(this, right) + fun bitXor(right: Any) = FunctionExpr.bitXor(this, right) - fun bitNot() = Function.bitNot(this) + fun bitNot() = FunctionExpr.bitNot(this) - fun bitLeftShift(numberExpr: Expr) = Function.bitLeftShift(this, numberExpr) + fun bitLeftShift(numberExpr: Expr) = FunctionExpr.bitLeftShift(this, numberExpr) - fun bitLeftShift(number: Int) = Function.bitLeftShift(this, number) + fun bitLeftShift(number: Int) = FunctionExpr.bitLeftShift(this, number) - fun bitRightShift(numberExpr: Expr) = Function.bitRightShift(this, numberExpr) + fun bitRightShift(numberExpr: Expr) = FunctionExpr.bitRightShift(this, numberExpr) - fun bitRightShift(number: Int) = Function.bitRightShift(this, number) + fun bitRightShift(number: Int) = FunctionExpr.bitRightShift(this, number) /** * Assigns an alias to this expression. @@ -131,7 +131,7 @@ abstract class Expr internal constructor() { * @param other The expression to add to this expression. * @return A new {@code Expr} representing the addition operation. */ - fun add(other: Expr) = Function.add(this, other) + fun add(other: Expr) = FunctionExpr.add(this, other) /** * Creates an expression that this expression to another expression. @@ -144,198 +144,198 @@ abstract class Expr internal constructor() { * @param other The constant value to add to this expression. * @return A new {@code Expr} representing the addition operation. */ - fun add(other: Any) = Function.add(this, other) + fun add(other: Any) = FunctionExpr.add(this, other) - fun subtract(other: Expr) = Function.subtract(this, other) + fun subtract(other: Expr) = FunctionExpr.subtract(this, other) - fun subtract(other: Any) = Function.subtract(this, other) + fun subtract(other: Any) = FunctionExpr.subtract(this, other) - fun multiply(other: Expr) = Function.multiply(this, other) + fun multiply(other: Expr) = FunctionExpr.multiply(this, other) - fun multiply(other: Any) = Function.multiply(this, other) + fun multiply(other: Any) = FunctionExpr.multiply(this, other) - fun divide(other: Expr) = Function.divide(this, other) + fun divide(other: Expr) = FunctionExpr.divide(this, other) - fun divide(other: Any) = Function.divide(this, other) + fun divide(other: Any) = FunctionExpr.divide(this, other) - fun mod(other: Expr) = Function.mod(this, other) + fun mod(other: Expr) = FunctionExpr.mod(this, other) - fun mod(other: Any) = Function.mod(this, other) + fun mod(other: Any) = FunctionExpr.mod(this, other) - fun eqAny(values: List) = Function.eqAny(this, values) + fun eqAny(values: List) = FunctionExpr.eqAny(this, values) - fun notEqAny(values: List) = Function.notEqAny(this, values) + fun notEqAny(values: List) = FunctionExpr.notEqAny(this, values) - fun isNan() = Function.isNan(this) + fun isNan() = FunctionExpr.isNan(this) - fun isNotNan() = Function.isNotNan(this) + fun isNotNan() = FunctionExpr.isNotNan(this) - fun isNull() = Function.isNull(this) + fun isNull() = FunctionExpr.isNull(this) - fun isNotNull() = Function.isNotNull(this) + fun isNotNull() = FunctionExpr.isNotNull(this) - fun replaceFirst(find: Expr, replace: Expr) = Function.replaceFirst(this, find, replace) + fun replaceFirst(find: Expr, replace: Expr) = FunctionExpr.replaceFirst(this, find, replace) - fun replaceFirst(find: String, replace: String) = Function.replaceFirst(this, find, replace) + fun replaceFirst(find: String, replace: String) = FunctionExpr.replaceFirst(this, find, replace) - fun replaceAll(find: Expr, replace: Expr) = Function.replaceAll(this, find, replace) + fun replaceAll(find: Expr, replace: Expr) = FunctionExpr.replaceAll(this, find, replace) - fun replaceAll(find: String, replace: String) = Function.replaceAll(this, find, replace) + fun replaceAll(find: String, replace: String) = FunctionExpr.replaceAll(this, find, replace) - fun charLength() = Function.charLength(this) + fun charLength() = FunctionExpr.charLength(this) - fun byteLength() = Function.byteLength(this) + fun byteLength() = FunctionExpr.byteLength(this) - fun like(pattern: Expr) = Function.like(this, pattern) + fun like(pattern: Expr) = FunctionExpr.like(this, pattern) - fun like(pattern: String) = Function.like(this, pattern) + fun like(pattern: String) = FunctionExpr.like(this, pattern) - fun regexContains(pattern: Expr) = Function.regexContains(this, pattern) + fun regexContains(pattern: Expr) = FunctionExpr.regexContains(this, pattern) - fun regexContains(pattern: String) = Function.regexContains(this, pattern) + fun regexContains(pattern: String) = FunctionExpr.regexContains(this, pattern) - fun regexMatch(pattern: Expr) = Function.regexMatch(this, pattern) + fun regexMatch(pattern: Expr) = FunctionExpr.regexMatch(this, pattern) - fun regexMatch(pattern: String) = Function.regexMatch(this, pattern) + fun regexMatch(pattern: String) = FunctionExpr.regexMatch(this, pattern) - fun logicalMax(other: Expr) = Function.logicalMax(this, other) + fun logicalMax(other: Expr) = FunctionExpr.logicalMax(this, other) - fun logicalMax(other: Any) = Function.logicalMax(this, other) + fun logicalMax(other: Any) = FunctionExpr.logicalMax(this, other) - fun logicalMin(other: Expr) = Function.logicalMin(this, other) + fun logicalMin(other: Expr) = FunctionExpr.logicalMin(this, other) - fun logicalMin(other: Any) = Function.logicalMin(this, other) + fun logicalMin(other: Any) = FunctionExpr.logicalMin(this, other) - fun reverse() = Function.reverse(this) + fun reverse() = FunctionExpr.reverse(this) - fun strContains(substring: Expr) = Function.strContains(this, substring) + fun strContains(substring: Expr) = FunctionExpr.strContains(this, substring) - fun strContains(substring: String) = Function.strContains(this, substring) + fun strContains(substring: String) = FunctionExpr.strContains(this, substring) - fun startsWith(prefix: Expr) = Function.startsWith(this, prefix) + fun startsWith(prefix: Expr) = FunctionExpr.startsWith(this, prefix) - fun startsWith(prefix: String) = Function.startsWith(this, prefix) + fun startsWith(prefix: String) = FunctionExpr.startsWith(this, prefix) - fun endsWith(suffix: Expr) = Function.endsWith(this, suffix) + fun endsWith(suffix: Expr) = FunctionExpr.endsWith(this, suffix) - fun endsWith(suffix: String) = Function.endsWith(this, suffix) + fun endsWith(suffix: String) = FunctionExpr.endsWith(this, suffix) - fun toLower() = Function.toLower(this) + fun toLower() = FunctionExpr.toLower(this) - fun toUpper() = Function.toUpper(this) + fun toUpper() = FunctionExpr.toUpper(this) - fun trim() = Function.trim(this) + fun trim() = FunctionExpr.trim(this) - fun strConcat(vararg expr: Expr) = Function.strConcat(this, *expr) + fun strConcat(vararg expr: Expr) = FunctionExpr.strConcat(this, *expr) - fun strConcat(vararg string: String) = Function.strConcat(this, *string) + fun strConcat(vararg string: String) = FunctionExpr.strConcat(this, *string) - fun strConcat(vararg string: Any) = Function.strConcat(this, *string) + fun strConcat(vararg string: Any) = FunctionExpr.strConcat(this, *string) - fun mapGet(key: Expr) = Function.mapGet(this, key) + fun mapGet(key: Expr) = FunctionExpr.mapGet(this, key) - fun mapGet(key: String) = Function.mapGet(this, key) + fun mapGet(key: String) = FunctionExpr.mapGet(this, key) fun mapMerge(secondMap: Expr, vararg otherMaps: Expr) = - Function.mapMerge(this, secondMap, *otherMaps) + FunctionExpr.mapMerge(this, secondMap, *otherMaps) - fun mapRemove(key: Expr) = Function.mapRemove(this, key) + fun mapRemove(key: Expr) = FunctionExpr.mapRemove(this, key) - fun mapRemove(key: String) = Function.mapRemove(this, key) + fun mapRemove(key: String) = FunctionExpr.mapRemove(this, key) - fun cosineDistance(vector: Expr) = Function.cosineDistance(this, vector) + fun cosineDistance(vector: Expr) = FunctionExpr.cosineDistance(this, vector) - fun cosineDistance(vector: DoubleArray) = Function.cosineDistance(this, vector) + fun cosineDistance(vector: DoubleArray) = FunctionExpr.cosineDistance(this, vector) - fun cosineDistance(vector: VectorValue) = Function.cosineDistance(this, vector) + fun cosineDistance(vector: VectorValue) = FunctionExpr.cosineDistance(this, vector) - fun dotProduct(vector: Expr) = Function.dotProduct(this, vector) + fun dotProduct(vector: Expr) = FunctionExpr.dotProduct(this, vector) - fun dotProduct(vector: DoubleArray) = Function.dotProduct(this, vector) + fun dotProduct(vector: DoubleArray) = FunctionExpr.dotProduct(this, vector) - fun dotProduct(vector: VectorValue) = Function.dotProduct(this, vector) + fun dotProduct(vector: VectorValue) = FunctionExpr.dotProduct(this, vector) - fun euclideanDistance(vector: Expr) = Function.euclideanDistance(this, vector) + fun euclideanDistance(vector: Expr) = FunctionExpr.euclideanDistance(this, vector) - fun euclideanDistance(vector: DoubleArray) = Function.euclideanDistance(this, vector) + fun euclideanDistance(vector: DoubleArray) = FunctionExpr.euclideanDistance(this, vector) - fun euclideanDistance(vector: VectorValue) = Function.euclideanDistance(this, vector) + fun euclideanDistance(vector: VectorValue) = FunctionExpr.euclideanDistance(this, vector) - fun vectorLength() = Function.vectorLength(this) + fun vectorLength() = FunctionExpr.vectorLength(this) - fun unixMicrosToTimestamp() = Function.unixMicrosToTimestamp(this) + fun unixMicrosToTimestamp() = FunctionExpr.unixMicrosToTimestamp(this) - fun timestampToUnixMicros() = Function.timestampToUnixMicros(this) + fun timestampToUnixMicros() = FunctionExpr.timestampToUnixMicros(this) - fun unixMillisToTimestamp() = Function.unixMillisToTimestamp(this) + fun unixMillisToTimestamp() = FunctionExpr.unixMillisToTimestamp(this) - fun timestampToUnixMillis() = Function.timestampToUnixMillis(this) + fun timestampToUnixMillis() = FunctionExpr.timestampToUnixMillis(this) - fun unixSecondsToTimestamp() = Function.unixSecondsToTimestamp(this) + fun unixSecondsToTimestamp() = FunctionExpr.unixSecondsToTimestamp(this) - fun timestampToUnixSeconds() = Function.timestampToUnixSeconds(this) + fun timestampToUnixSeconds() = FunctionExpr.timestampToUnixSeconds(this) - fun timestampAdd(unit: Expr, amount: Expr) = Function.timestampAdd(this, unit, amount) + fun timestampAdd(unit: Expr, amount: Expr) = FunctionExpr.timestampAdd(this, unit, amount) - fun timestampAdd(unit: String, amount: Double) = Function.timestampAdd(this, unit, amount) + fun timestampAdd(unit: String, amount: Double) = FunctionExpr.timestampAdd(this, unit, amount) - fun timestampSub(unit: Expr, amount: Expr) = Function.timestampSub(this, unit, amount) + fun timestampSub(unit: Expr, amount: Expr) = FunctionExpr.timestampSub(this, unit, amount) - fun timestampSub(unit: String, amount: Double) = Function.timestampSub(this, unit, amount) + fun timestampSub(unit: String, amount: Double) = FunctionExpr.timestampSub(this, unit, amount) - fun arrayConcat(vararg arrays: Expr) = Function.arrayConcat(this, *arrays) + fun arrayConcat(vararg arrays: Expr) = FunctionExpr.arrayConcat(this, *arrays) - fun arrayConcat(arrays: List) = Function.arrayConcat(this, arrays) + fun arrayConcat(arrays: List) = FunctionExpr.arrayConcat(this, arrays) - fun arrayReverse() = Function.arrayReverse(this) + fun arrayReverse() = FunctionExpr.arrayReverse(this) - fun arrayContains(value: Expr) = Function.arrayContains(this, value) + fun arrayContains(value: Expr) = FunctionExpr.arrayContains(this, value) - fun arrayContains(value: Any) = Function.arrayContains(this, value) + fun arrayContains(value: Any) = FunctionExpr.arrayContains(this, value) - fun arrayContainsAll(values: List) = Function.arrayContainsAll(this, values) + fun arrayContainsAll(values: List) = FunctionExpr.arrayContainsAll(this, values) - fun arrayContainsAny(values: List) = Function.arrayContainsAny(this, values) + fun arrayContainsAny(values: List) = FunctionExpr.arrayContainsAny(this, values) - fun arrayLength() = Function.arrayLength(this) + fun arrayLength() = FunctionExpr.arrayLength(this) - fun sum() = AggregateExpr.sum(this) + fun sum() = AggregateFunction.sum(this) - fun avg() = AggregateExpr.avg(this) + fun avg() = AggregateFunction.avg(this) - fun min() = AggregateExpr.min(this) + fun min() = AggregateFunction.min(this) - fun max() = AggregateExpr.max(this) + fun max() = AggregateFunction.max(this) fun ascending() = Ordering.ascending(this) fun descending() = Ordering.descending(this) - fun eq(other: Expr) = Function.eq(this, other) + fun eq(other: Expr) = FunctionExpr.eq(this, other) - fun eq(other: Any) = Function.eq(this, other) + fun eq(other: Any) = FunctionExpr.eq(this, other) - fun neq(other: Expr) = Function.neq(this, other) + fun neq(other: Expr) = FunctionExpr.neq(this, other) - fun neq(other: Any) = Function.neq(this, other) + fun neq(other: Any) = FunctionExpr.neq(this, other) - fun gt(other: Expr) = Function.gt(this, other) + fun gt(other: Expr) = FunctionExpr.gt(this, other) - fun gt(other: Any) = Function.gt(this, other) + fun gt(other: Any) = FunctionExpr.gt(this, other) - fun gte(other: Expr) = Function.gte(this, other) + fun gte(other: Expr) = FunctionExpr.gte(this, other) - fun gte(other: Any) = Function.gte(this, other) + fun gte(other: Any) = FunctionExpr.gte(this, other) - fun lt(other: Expr) = Function.lt(this, other) + fun lt(other: Expr) = FunctionExpr.lt(this, other) - fun lt(other: Any) = Function.lt(this, other) + fun lt(other: Any) = FunctionExpr.lt(this, other) - fun lte(other: Expr) = Function.lte(this, other) + fun lte(other: Expr) = FunctionExpr.lte(this, other) - fun lte(other: Any) = Function.lte(this, other) + fun lte(other: Any) = FunctionExpr.lte(this, other) - fun exists() = Function.exists(this) + fun exists() = FunctionExpr.exists(this) internal abstract fun toProto(userDataReader: UserDataReader): Value } @@ -392,7 +392,7 @@ internal class ListOfExprs(private val expressions: Array) : Expr() { encodeValue(expressions.map { it.toProto(userDataReader) }) } -open class Function +open class FunctionExpr protected constructor(private val name: String, private val params: Array) : Expr() { private constructor( name: String, @@ -406,7 +406,7 @@ protected constructor(private val name: String, private val params: Array) = @@ -550,35 +550,35 @@ protected constructor(private val name: String, private val params: Array) = Function("map", elements) + internal fun map(elements: Array) = FunctionExpr("map", elements) @JvmStatic fun map(elements: Map) = map(elements.flatMap { listOf(of(it.key), toExprOrConstant(it.value)) }.toTypedArray()) - @JvmStatic fun mapGet(map: Expr, key: Expr) = Function("map_get", map, key) + @JvmStatic fun mapGet(map: Expr, key: Expr) = FunctionExpr("map_get", map, key) - @JvmStatic fun mapGet(map: Expr, key: String) = Function("map_get", map, key) + @JvmStatic fun mapGet(map: Expr, key: String) = FunctionExpr("map_get", map, key) - @JvmStatic fun mapGet(fieldName: String, key: Expr) = Function("map_get", fieldName, key) + @JvmStatic fun mapGet(fieldName: String, key: Expr) = FunctionExpr("map_get", fieldName, key) - @JvmStatic fun mapGet(fieldName: String, key: String) = Function("map_get", fieldName, key) + @JvmStatic fun mapGet(fieldName: String, key: String) = FunctionExpr("map_get", fieldName, key) @JvmStatic fun mapMerge(firstMap: Expr, secondMap: Expr, vararg otherMaps: Expr) = - Function("map_merge", firstMap, secondMap, otherMaps) + FunctionExpr("map_merge", firstMap, secondMap, otherMaps) @JvmStatic fun mapMerge(mapField: String, secondMap: Expr, vararg otherMaps: Expr) = - Function("map_merge", mapField, secondMap, otherMaps) + FunctionExpr("map_merge", mapField, secondMap, otherMaps) - @JvmStatic fun mapRemove(firstMap: Expr, key: Expr) = Function("map_remove", firstMap, key) + @JvmStatic fun mapRemove(firstMap: Expr, key: Expr) = FunctionExpr("map_remove", firstMap, key) - @JvmStatic fun mapRemove(mapField: String, key: Expr) = Function("map_remove", mapField, key) + @JvmStatic fun mapRemove(mapField: String, key: Expr) = FunctionExpr("map_remove", mapField, key) - @JvmStatic fun mapRemove(firstMap: Expr, key: String) = Function("map_remove", firstMap, key) + @JvmStatic fun mapRemove(firstMap: Expr, key: String) = FunctionExpr("map_remove", firstMap, key) - @JvmStatic fun mapRemove(mapField: String, key: String) = Function("map_remove", mapField, key) + @JvmStatic fun mapRemove(mapField: String, key: String) = FunctionExpr("map_remove", mapField, key) @JvmStatic - fun cosineDistance(vector1: Expr, vector2: Expr) = Function("cosine_distance", vector1, vector2) + fun cosineDistance(vector1: Expr, vector2: Expr) = FunctionExpr("cosine_distance", vector1, vector2) @JvmStatic fun cosineDistance(vector1: Expr, vector2: DoubleArray) = - Function("cosine_distance", vector1, Constant.vector(vector2)) + FunctionExpr("cosine_distance", vector1, Constant.vector(vector2)) @JvmStatic fun cosineDistance(vector1: Expr, vector2: VectorValue) = - Function("cosine_distance", vector1, vector2) + FunctionExpr("cosine_distance", vector1, vector2) @JvmStatic fun cosineDistance(fieldName: String, vector: Expr) = - Function("cosine_distance", fieldName, vector) + FunctionExpr("cosine_distance", fieldName, vector) @JvmStatic fun cosineDistance(fieldName: String, vector: DoubleArray) = - Function("cosine_distance", fieldName, Constant.vector(vector)) + FunctionExpr("cosine_distance", fieldName, Constant.vector(vector)) @JvmStatic fun cosineDistance(fieldName: String, vector: VectorValue) = - Function("cosine_distance", fieldName, vector) + FunctionExpr("cosine_distance", fieldName, vector) @JvmStatic - fun dotProduct(vector1: Expr, vector2: Expr) = Function("dot_product", vector1, vector2) + fun dotProduct(vector1: Expr, vector2: Expr) = FunctionExpr("dot_product", vector1, vector2) @JvmStatic fun dotProduct(vector1: Expr, vector2: DoubleArray) = - Function("dot_product", vector1, Constant.vector(vector2)) + FunctionExpr("dot_product", vector1, Constant.vector(vector2)) @JvmStatic - fun dotProduct(vector1: Expr, vector2: VectorValue) = Function("dot_product", vector1, vector2) + fun dotProduct(vector1: Expr, vector2: VectorValue) = FunctionExpr("dot_product", vector1, vector2) @JvmStatic - fun dotProduct(fieldName: String, vector: Expr) = Function("dot_product", fieldName, vector) + fun dotProduct(fieldName: String, vector: Expr) = FunctionExpr("dot_product", fieldName, vector) @JvmStatic fun dotProduct(fieldName: String, vector: DoubleArray) = - Function("dot_product", fieldName, Constant.vector(vector)) + FunctionExpr("dot_product", fieldName, Constant.vector(vector)) @JvmStatic fun dotProduct(fieldName: String, vector: VectorValue) = - Function("dot_product", fieldName, vector) + FunctionExpr("dot_product", fieldName, vector) @JvmStatic fun euclideanDistance(vector1: Expr, vector2: Expr) = - Function("euclidean_distance", vector1, vector2) + FunctionExpr("euclidean_distance", vector1, vector2) @JvmStatic fun euclideanDistance(vector1: Expr, vector2: DoubleArray) = - Function("euclidean_distance", vector1, Constant.vector(vector2)) + FunctionExpr("euclidean_distance", vector1, Constant.vector(vector2)) @JvmStatic fun euclideanDistance(vector1: Expr, vector2: VectorValue) = - Function("euclidean_distance", vector1, vector2) + FunctionExpr("euclidean_distance", vector1, vector2) @JvmStatic fun euclideanDistance(fieldName: String, vector: Expr) = - Function("euclidean_distance", fieldName, vector) + FunctionExpr("euclidean_distance", fieldName, vector) @JvmStatic fun euclideanDistance(fieldName: String, vector: DoubleArray) = - Function("euclidean_distance", fieldName, Constant.vector(vector)) + FunctionExpr("euclidean_distance", fieldName, Constant.vector(vector)) @JvmStatic fun euclideanDistance(fieldName: String, vector: VectorValue) = - Function("euclidean_distance", fieldName, vector) + FunctionExpr("euclidean_distance", fieldName, vector) - @JvmStatic fun vectorLength(vector: Expr) = Function("vector_length", vector) + @JvmStatic fun vectorLength(vector: Expr) = FunctionExpr("vector_length", vector) - @JvmStatic fun vectorLength(fieldName: String) = Function("vector_length", fieldName) + @JvmStatic fun vectorLength(fieldName: String) = FunctionExpr("vector_length", fieldName) - @JvmStatic fun unixMicrosToTimestamp(input: Expr) = Function("unix_micros_to_timestamp", input) + @JvmStatic fun unixMicrosToTimestamp(input: Expr) = FunctionExpr("unix_micros_to_timestamp", input) @JvmStatic - fun unixMicrosToTimestamp(fieldName: String) = Function("unix_micros_to_timestamp", fieldName) + fun unixMicrosToTimestamp(fieldName: String) = FunctionExpr("unix_micros_to_timestamp", fieldName) - @JvmStatic fun timestampToUnixMicros(input: Expr) = Function("timestamp_to_unix_micros", input) + @JvmStatic fun timestampToUnixMicros(input: Expr) = FunctionExpr("timestamp_to_unix_micros", input) @JvmStatic - fun timestampToUnixMicros(fieldName: String) = Function("timestamp_to_unix_micros", fieldName) + fun timestampToUnixMicros(fieldName: String) = FunctionExpr("timestamp_to_unix_micros", fieldName) - @JvmStatic fun unixMillisToTimestamp(input: Expr) = Function("unix_millis_to_timestamp", input) + @JvmStatic fun unixMillisToTimestamp(input: Expr) = FunctionExpr("unix_millis_to_timestamp", input) @JvmStatic - fun unixMillisToTimestamp(fieldName: String) = Function("unix_millis_to_timestamp", fieldName) + fun unixMillisToTimestamp(fieldName: String) = FunctionExpr("unix_millis_to_timestamp", fieldName) - @JvmStatic fun timestampToUnixMillis(input: Expr) = Function("timestamp_to_unix_millis", input) + @JvmStatic fun timestampToUnixMillis(input: Expr) = FunctionExpr("timestamp_to_unix_millis", input) @JvmStatic - fun timestampToUnixMillis(fieldName: String) = Function("timestamp_to_unix_millis", fieldName) + fun timestampToUnixMillis(fieldName: String) = FunctionExpr("timestamp_to_unix_millis", fieldName) @JvmStatic - fun unixSecondsToTimestamp(input: Expr) = Function("unix_seconds_to_timestamp", input) + fun unixSecondsToTimestamp(input: Expr) = FunctionExpr("unix_seconds_to_timestamp", input) @JvmStatic - fun unixSecondsToTimestamp(fieldName: String) = Function("unix_seconds_to_timestamp", fieldName) + fun unixSecondsToTimestamp(fieldName: String) = FunctionExpr("unix_seconds_to_timestamp", fieldName) @JvmStatic - fun timestampToUnixSeconds(input: Expr) = Function("timestamp_to_unix_seconds", input) + fun timestampToUnixSeconds(input: Expr) = FunctionExpr("timestamp_to_unix_seconds", input) @JvmStatic - fun timestampToUnixSeconds(fieldName: String) = Function("timestamp_to_unix_seconds", fieldName) + fun timestampToUnixSeconds(fieldName: String) = FunctionExpr("timestamp_to_unix_seconds", fieldName) @JvmStatic fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr) = - Function("timestamp_add", timestamp, unit, amount) + FunctionExpr("timestamp_add", timestamp, unit, amount) @JvmStatic fun timestampAdd(timestamp: Expr, unit: String, amount: Double) = - Function("timestamp_add", timestamp, unit, amount) + FunctionExpr("timestamp_add", timestamp, unit, amount) @JvmStatic fun timestampAdd(fieldName: String, unit: Expr, amount: Expr) = - Function("timestamp_add", fieldName, unit, amount) + FunctionExpr("timestamp_add", fieldName, unit, amount) @JvmStatic fun timestampAdd(fieldName: String, unit: String, amount: Double) = - Function("timestamp_add", fieldName, unit, amount) + FunctionExpr("timestamp_add", fieldName, unit, amount) @JvmStatic fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr) = - Function("timestamp_sub", timestamp, unit, amount) + FunctionExpr("timestamp_sub", timestamp, unit, amount) @JvmStatic fun timestampSub(timestamp: Expr, unit: String, amount: Double) = - Function("timestamp_sub", timestamp, unit, amount) + FunctionExpr("timestamp_sub", timestamp, unit, amount) @JvmStatic fun timestampSub(fieldName: String, unit: Expr, amount: Expr) = - Function("timestamp_sub", fieldName, unit, amount) + FunctionExpr("timestamp_sub", fieldName, unit, amount) @JvmStatic fun timestampSub(fieldName: String, unit: String, amount: Double) = - Function("timestamp_sub", fieldName, unit, amount) + FunctionExpr("timestamp_sub", fieldName, unit, amount) @JvmStatic fun eq(left: Expr, right: Expr) = BooleanExpr("eq", left, right) @@ -918,23 +918,23 @@ protected constructor(private val name: String, private val params: Array) = - Function("array_concat", array, ListOfExprs(toArrayOfExprOrConstant(arrays))) + FunctionExpr("array_concat", array, ListOfExprs(toArrayOfExprOrConstant(arrays))) @JvmStatic fun arrayConcat(fieldName: String, arrays: List) = - Function("array_concat", fieldName, ListOfExprs(toArrayOfExprOrConstant(arrays))) + FunctionExpr("array_concat", fieldName, ListOfExprs(toArrayOfExprOrConstant(arrays))) - @JvmStatic fun arrayReverse(array: Expr) = Function("array_reverse", array) + @JvmStatic fun arrayReverse(array: Expr) = FunctionExpr("array_reverse", array) - @JvmStatic fun arrayReverse(fieldName: String) = Function("array_reverse", fieldName) + @JvmStatic fun arrayReverse(fieldName: String) = FunctionExpr("array_reverse", fieldName) @JvmStatic fun arrayContains(array: Expr, value: Expr) = BooleanExpr("array_contains", array, value) @@ -966,17 +966,17 @@ protected constructor(private val name: String, private val params: Array) = BooleanExpr("array_contains_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) - @JvmStatic fun arrayLength(array: Expr) = Function("array_length", array) + @JvmStatic fun arrayLength(array: Expr) = FunctionExpr("array_length", array) - @JvmStatic fun arrayLength(fieldName: String) = Function("array_length", fieldName) + @JvmStatic fun arrayLength(fieldName: String) = FunctionExpr("array_length", fieldName) @JvmStatic fun cond(condition: BooleanExpr, then: Expr, otherwise: Expr) = - Function("cond", condition, then, otherwise) + FunctionExpr("cond", condition, then, otherwise) @JvmStatic fun cond(condition: BooleanExpr, then: Any, otherwise: Any) = - Function("cond", condition, then, otherwise) + FunctionExpr("cond", condition, then, otherwise) @JvmStatic fun exists(expr: Expr) = BooleanExpr("exists", expr) } @@ -992,7 +992,7 @@ protected constructor(private val name: String, private val params: Array) : - Function(name, params) { + FunctionExpr(name, params) { internal constructor( name: String, params: List @@ -1015,7 +1015,7 @@ class BooleanExpr internal constructor(name: String, params: Array) : fun not() = not(this) - fun countIf(): AggregateExpr = AggregateExpr.countIf(this) + fun countIf(): AggregateFunction = AggregateFunction.countIf(this) fun cond(then: Expr, otherwise: Expr) = cond(this, then, otherwise) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index bcae5dd66e5..d2a91c3a805 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -18,7 +18,6 @@ import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.pipeline.Field.Companion.of -import com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure import com.google.firestore.v1.Pipeline import com.google.firestore.v1.Value @@ -55,7 +54,7 @@ internal constructor( options: InternalOptions = InternalOptions.EMPTY ) : Stage(name, options) { companion object { - @JvmStatic fun of(name: String) = GenericStage(name, emptyList(), InternalOptions.EMPTY) + @JvmStatic fun ofName(name: String) = GenericStage(name, emptyList(), InternalOptions.EMPTY) } override fun self(options: InternalOptions) = GenericStage(name, arguments, options) @@ -71,7 +70,7 @@ internal sealed class GenericArg { companion object { fun from(arg: Any?): GenericArg = when (arg) { - is AggregateExpr -> AggregateArg(arg) + is AggregateFunction -> AggregateArg(arg) is Ordering -> OrderingArg(arg) is Map<*, *> -> MapArg(arg.asIterable().associate { (key, value) -> key as String to from(value) }) @@ -81,7 +80,7 @@ internal sealed class GenericArg { } abstract fun toProto(userDataReader: UserDataReader): Value - data class AggregateArg(val aggregate: AggregateExpr) : GenericArg() { + data class AggregateArg(val aggregate: AggregateFunction) : GenericArg() { override fun toProto(userDataReader: UserDataReader) = aggregate.toProto(userDataReader) } @@ -156,11 +155,11 @@ internal constructor( class AggregateStage internal constructor( - private val accumulators: Map, + private val accumulators: Map, private val groups: Map, options: InternalOptions = InternalOptions.EMPTY ) : Stage("aggregate", options) { - private constructor(accumulators: Map) : this(accumulators, emptyMap()) + private constructor(accumulators: Map) : this(accumulators, emptyMap()) companion object { @JvmStatic fun withAccumulators(vararg accumulators: AggregateWithAlias): AggregateStage { From 9dbce3f33c90891951a65a4f98c9de6026012b4a Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 9 Apr 2025 22:51:17 -0400 Subject: [PATCH 036/152] Fixups --- .../com/google/firebase/firestore/Pipeline.kt | 303 ++++++++++++++++-- .../firestore/pipeline/expressions.kt | 90 ++++-- .../firebase/firestore/pipeline/stage.kt | 157 +++++++-- 3 files changed, 464 insertions(+), 86 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 25496f498a9..951f0d2f3ec 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -22,6 +22,7 @@ import com.google.firebase.Timestamp import com.google.firebase.firestore.model.DocumentKey import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.pipeline.AddFieldsStage +import com.google.firebase.firestore.pipeline.AggregateFunction import com.google.firebase.firestore.pipeline.AggregateStage import com.google.firebase.firestore.pipeline.AggregateWithAlias import com.google.firebase.firestore.pipeline.BooleanExpr @@ -31,8 +32,10 @@ import com.google.firebase.firestore.pipeline.DatabaseSource import com.google.firebase.firestore.pipeline.DistinctStage import com.google.firebase.firestore.pipeline.DocumentsSource import com.google.firebase.firestore.pipeline.Expr +import com.google.firebase.firestore.pipeline.ExprWithAlias import com.google.firebase.firestore.pipeline.Field import com.google.firebase.firestore.pipeline.FindNearestStage +import com.google.firebase.firestore.pipeline.FunctionExpr import com.google.firebase.firestore.pipeline.GenericArg import com.google.firebase.firestore.pipeline.GenericStage import com.google.firebase.firestore.pipeline.LimitStage @@ -103,67 +106,301 @@ internal constructor( fun genericStage(stage: GenericStage): Pipeline = append(stage) - fun addFields(vararg fields: Selectable): Pipeline = append(AddFieldsStage(fields)) + /** + * Adds new fields to outputs from previous stages. + * + * This stage allows you to compute values on-the-fly based on existing data from previous stages + * or constants. You can use this to create new fields or overwrite existing ones. + * + * The added fields are defined using [Selectable]s, which can be: + * + * - [Field]: References an existing document field. + * - [ExprWithAlias]: Represents the result of a expression with an assigned alias name + * using [Expr.alias] + * + * @param field The first field to add to the documents, specified as a [Selectable]. + * @param additionalFields The fields to add to the documents, specified as [Selectable]s. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun addFields(field: Selectable, vararg additionalFields: Selectable): Pipeline = + append(AddFieldsStage(arrayOf(field, *additionalFields))) fun removeFields(vararg fields: Field): Pipeline = append(RemoveFieldsStage(fields)) fun removeFields(vararg fields: String): Pipeline = append(RemoveFieldsStage(fields.map(Field::of).toTypedArray())) - fun select(vararg fields: Selectable): Pipeline = append(SelectStage(fields)) - - fun select(vararg fields: String): Pipeline = - append(SelectStage(fields.map(Field::of).toTypedArray())) + /** + * Selects or creates a set of fields from the outputs of previous stages. + * + * The selected fields are defined using [Selectable] expressions, which can be: + * + * - [String]: Name of an existing field + * - [Field]: Reference to an existing field. + * - [ExprWithAlias]: Represents the result of a expression with an assigned alias name + * using [Expr.alias] + * + * If no selections are provided, the output of this stage is empty. Use [Pipeline.addFields] + * instead if only additions are desired. + * + * @param selection The first field to include in the output documents, specified as a + * [Selectable] expression. + * @param additionalSelections Optional additional fields to include in the output documents, + * specified as [Selectable] expressions or string values representing field names. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun select(selection: Selectable, vararg additionalSelections: Any): Pipeline = + append( + SelectStage( + arrayOf(selection, *additionalSelections.map(Selectable::toSelectable).toTypedArray()) + ) + ) - fun select(vararg fields: Any): Pipeline = - append(SelectStage(fields.map(Selectable::toSelectable).toTypedArray())) + /** + * Selects or creates a set of fields from the outputs of previous stages. + * + * The selected fields are defined using [Selectable] expressions, which can be: + * + * - [String]: Name of an existing field + * - [Field]: Reference to an existing field. + * - [ExprWithAlias]: Represents the result of a expression with an assigned alias name + * using [Expr.alias] + * + * If no selections are provided, the output of this stage is empty. Use [Pipeline.addFields] + * instead if only additions are desired. + * + * @param fieldName The first field to include in the output documents, specified as a string + * value representing a field names. + * @param additionalSelections Optional additional fields to include in the output documents, + * specified as [Selectable] expressions or string values representing field names. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun select(fieldName: String, vararg additionalSelections: Any): Pipeline = + append( + SelectStage( + arrayOf( + Field.of(fieldName), + *additionalSelections.map(Selectable::toSelectable).toTypedArray() + ) + ) + ) fun sort(vararg orders: Ordering): Pipeline = append(SortStage(orders)) + /** + * Filters the documents from previous stages to only include those matching the specified + * [BooleanExpr]. + * + * This stage allows you to apply conditions to the data, similar to a "WHERE" clause in SQL. + * + * You can filter documents based on their field values, using implementations of + * [BooleanExpr], typically including but not limited to: + * + * - field comparators: [FunctionExpr.eq], [FunctionExpr.lt] (less than), [FunctionExpr.gt] + * (greater than), etc. + * - logical operators: [FunctionExpr.and], [FunctionExpr.or], [FunctionExpr.not], etc. + * - advanced functions: [FunctionExpr.regexMatch], [FunctionExpr.arrayContains[], etc. + * + * @param condition The [BooleanExpr] to apply. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun where(condition: BooleanExpr): Pipeline = append(WhereStage(condition)) + /** + * Skips the first `offset` number of documents from the results of previous stages. + * + * This stage is useful for implementing pagination in your pipelines, allowing you to retrieve + * results in chunks. It is typically used in conjunction with [limit] to control the size + * of each page. + * + * @param offset The number of documents to skip. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun offset(offset: Int): Pipeline = append(OffsetStage(offset)) + /** + * Limits the maximum number of documents returned by previous stages to `limit`. + * + * This stage is particularly useful when you want to retrieve a controlled subset of data + * from a potentially large result set. It's often used for: + * + * - **Pagination:** In combination with [offset] to retrieve specific pages of results. + * - **Limiting Data Retrieval:** To prevent excessive data transfer and improve performance, + * especially when dealing with large collections. + * + * @param limit The maximum number of documents to return. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun limit(limit: Int): Pipeline = append(LimitStage(limit)) - fun distinct(vararg groups: Selectable): Pipeline = append(DistinctStage(groups)) - - fun distinct(vararg groups: String): Pipeline = - append(DistinctStage(groups.map(Field::of).toTypedArray())) + /** + * Returns a set of distinct values from the inputs to this stage. + * + * This stage runs through the results from previous stages to include only results with unique + * combinations of [Expr] values [Field], [FunctionExpr], etc). + * + * The parameters to this stage are defined using [Selectable] expressions or strings: + * + * - [String]: Name of an existing field + * - [Field]: References an existing document field. + * - [ExprWithAlias]: Represents the result of a function with an assigned alias name using + * [Expr.alias] + * + * @param group The [Selectable] expression to consider when determining distinct value + * combinations. + * @param additionalGroups The [Selectable] expressions to consider when determining + * distinct value combinations or [String]s representing field names. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun distinct(group: Selectable, vararg additionalGroups: Any): Pipeline = + append( + DistinctStage(arrayOf(group, *additionalGroups.map(Selectable::toSelectable).toTypedArray())) + ) - fun distinct(vararg groups: Any): Pipeline = - append(DistinctStage(groups.map(Selectable::toSelectable).toTypedArray())) + /** + * Returns a set of distinct values from the inputs to this stage. + * + * This stage runs through the results from previous stages to include only results with unique + * combinations of [Expr] values ([Field], [FunctionExpr], etc). + * + * The parameters to this stage are defined using [Selectable] expressions or strings: + * + * - [String]: Name of an existing field + * - [Field]: References an existing document field. + * - [ExprWithAlias]: Represents the result of a function with an assigned alias name using + * [Expr.alias] + * + * @param groupField The [String] representing field name. + * @param additionalGroups The [Selectable] expressions to consider when determining + * distinct value combinations or [String]s representing field names. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun distinct(groupField: String, vararg additionalGroups: Any): Pipeline = + append( + DistinctStage( + arrayOf( + Field.of(groupField), + *additionalGroups.map(Selectable::toSelectable).toTypedArray() + ) + ) + ) - fun aggregate(vararg accumulators: AggregateWithAlias): Pipeline = - append(AggregateStage.withAccumulators(*accumulators)) + /** + * Performs aggregation operations on the documents from previous stages. + * + * This stage allows you to calculate aggregate values over a set of documents. You define the + * aggregations to perform using [AggregateWithAlias] expressions which are typically + * results of calling [AggregateFunction.alias] on [AggregateFunction] instances. + * + * @param accumulator The first [AggregateWithAlias] expression, wrapping an + * [AggregateFunction] with an alias for the accumulated results. + * @param additionalAccumulators The [AggregateWithAlias] expressions, each wrapping an + * [AggregateFunction] with an alias for the accumulated results. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun aggregate( + accumulator: AggregateWithAlias, + vararg additionalAccumulators: AggregateWithAlias + ): Pipeline = append(AggregateStage.withAccumulators(accumulator, *additionalAccumulators)) + /** + * Performs optionally grouped aggregation operations on the documents from previous stages. + * + * This stage allows you to calculate aggregate values over a set of documents, optionally grouped + * by one or more fields or functions. You can specify: + * + * - **Grouping Fields or Expressions:** One or more fields or functions to group the documents + * by. For each distinct combination of values in these fields, a separate group is created. If no + * grouping fields are provided, a single group containing all documents is used. Not specifying + * groups is the same as putting the entire inputs into one group. + * + * - **AggregateFunctions:** One or more accumulation operations to perform within each group. + * These are defined using [AggregateWithAlias] expressions, which are typically created by + * calling [AggregateFunction.alias] on [AggregateFunction] instances. Each aggregation calculates + * a value (e.g., sum, average, count) based on the documents within its group. + * + * @param aggregateStage An [AggregateStage] object that specifies the grouping fields (if any) + * and the aggregation operations to perform. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun aggregate(aggregateStage: AggregateStage): Pipeline = append(aggregateStage) + /** + * Performs a vector similarity search, ordering the result set by most similar to least + * similar, and returning the first N documents in the result set. + * + * @param vectorField A [Field] that contains vector to search on. + * @param vectorValue The [VectorValue] in array form that is used to measure the distance from + * [vectorField] values in the documents. + * @param distanceMeasure specifies what type of distance is calculated + * when performing the search. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun findNearest( - property: Expr, - vector: DoubleArray, + vectorField: Field, + vectorValue: DoubleArray, distanceMeasure: FindNearestStage.DistanceMeasure, - ) = append(FindNearestStage.of(property, vector, distanceMeasure)) + ): Pipeline = append(FindNearestStage.of(vectorField, vectorValue, distanceMeasure)) + /** + * Performs a vector similarity search, ordering the result set by most similar to least + * similar, and returning the first N documents in the result set. + * + * @param vectorField A [String] specifying the vector field to search on. + * @param vectorValue The [VectorValue] in array form that is used to measure the distance from + * [vectorField] values in the documents. + * @param distanceMeasure specifies what type of distance is calculated + * when performing the search. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun findNearest( - propertyField: String, - vector: DoubleArray, + vectorField: String, + vectorValue: DoubleArray, distanceMeasure: FindNearestStage.DistanceMeasure, - ) = append(FindNearestStage.of(propertyField, vector, distanceMeasure)) + ): Pipeline = append(FindNearestStage.of(vectorField, vectorValue, distanceMeasure)) + /** + * Performs a vector similarity search, ordering the result set by most similar to least + * similar, and returning the first N documents in the result set. + * + * @param vectorField A [Field] that contains vector to search on. + * @param vectorValue The [VectorValue] used to measure the distance from [vectorField] values in + * the documents. + * @param distanceMeasure specifies what type of distance is calculated. + * when performing the search. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun findNearest( - property: Expr, - vector: Expr, + vectorField: Field, + vectorValue: VectorValue, distanceMeasure: FindNearestStage.DistanceMeasure, - ) = append(FindNearestStage.of(property, vector, distanceMeasure)) + ): Pipeline = append(FindNearestStage.of(vectorField, vectorValue, distanceMeasure)) + /** + * Performs a vector similarity search, ordering the result set by most similar to least + * similar, and returning the first N documents in the result set. + * + * @param vectorField A [String] specifying the vector field to search on. + * @param vectorValue The [VectorValue] used to measure the distance from [vectorField] values in + * the documents. + * @param distanceMeasure specifies what type of distance is calculated + * when performing the search. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun findNearest( - propertyField: String, - vector: Expr, + vectorField: String, + vectorValue: VectorValue, distanceMeasure: FindNearestStage.DistanceMeasure, - ) = append(FindNearestStage.of(propertyField, vector, distanceMeasure)) + ): Pipeline = append(FindNearestStage.of(vectorField, vectorValue, distanceMeasure)) - fun findNearest(stage: FindNearestStage) = append(stage) + /** + * Performs a vector similarity search, ordering the result set by most similar to least + * similar, and returning the first N documents in the result set. + * + * @param stage An [FindNearestStage] object that specifies the search parameters. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun findNearest(stage: FindNearestStage): Pipeline = append(stage) fun replace(field: String): Pipeline = replace(Field.of(field)) @@ -244,13 +481,17 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * @throws [IllegalArgumentException] Thrown if the [aggregateQuery] provided targets a different * project or database than the pipeline. */ - fun createFrom(aggregateQuery: AggregateQuery): Pipeline = - createFrom(aggregateQuery.query) + fun createFrom(aggregateQuery: AggregateQuery): Pipeline { + val aggregateFields = aggregateQuery.aggregateFields + return createFrom(aggregateQuery.query) .aggregate( - *aggregateQuery.aggregateFields + aggregateFields.first().toPipeline(), + *aggregateFields + .drop(1) .map(AggregateField::toPipeline) .toTypedArray() ) + } /** * Set the pipeline's source to the collection specified by the given path. 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 12e7439d745..c8e4de0a3ff 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 @@ -342,6 +342,7 @@ abstract class Expr internal constructor() { abstract class Selectable : Expr() { internal abstract fun getAlias(): String + internal abstract fun getExpr(): Expr internal companion object { fun toSelectable(o: Any): Selectable { @@ -358,6 +359,7 @@ abstract class Selectable : Expr() { class ExprWithAlias internal constructor(private val alias: String, private val expr: Expr) : Selectable() { override fun getAlias() = alias + override fun getExpr() = expr override fun toProto(userDataReader: UserDataReader): Value = expr.toProto(userDataReader) } @@ -380,6 +382,7 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select } override fun getAlias(): String = fieldPath.canonicalString() + override fun getExpr(): Expr = this override fun toProto(userDataReader: UserDataReader) = toProto() @@ -426,7 +429,8 @@ protected constructor(private val name: String, private val params: Array) = FunctionExpr("map", elements) @@ -727,14 +744,18 @@ protected constructor(private val name: String, private val params: Array, @@ -161,29 +179,53 @@ internal constructor( ) : Stage("aggregate", options) { private constructor(accumulators: Map) : this(accumulators, emptyMap()) companion object { + + /** + * Create [AggregateStage] with one or more accumulators. + * + * @param accumulator The first [AggregateWithAlias] expression, wrapping an {@link + * AggregateFunction} with an alias for the accumulated results. + * @param additionalAccumulators The [AggregateWithAlias] expressions, each wrapping an + * [AggregateFunction] with an alias for the accumulated results. + * @return Aggregate Stage with specified accumulators. + */ @JvmStatic - fun withAccumulators(vararg accumulators: AggregateWithAlias): AggregateStage { - if (accumulators.isEmpty()) { - throw IllegalArgumentException( - "Must specify at least one accumulator for aggregate() stage. There is a distinct() stage if only distinct group values are needed." - ) - } - return AggregateStage(accumulators.associate { it.alias to it.expr }) + fun withAccumulators( + accumulator: AggregateWithAlias, + vararg additionalAccumulators: AggregateWithAlias + ): AggregateStage { + return AggregateStage( + mapOf(accumulator.alias to accumulator.expr) + .plus(additionalAccumulators.associate { it.alias to it.expr }) + ) } } override fun self(options: InternalOptions) = AggregateStage(accumulators, groups, options) - fun withGroups(vararg groups: Selectable) = - AggregateStage(accumulators, groups.associateBy(Selectable::getAlias)) - - fun withGroups(vararg fields: String) = - AggregateStage(accumulators, fields.associateWith(Field::of)) - - fun withGroups(vararg selectable: Any) = + /** + * Add one or more groups to [AggregateStage] + * + * @param groupField The [String] representing field name. + * @param additionalGroups The [Selectable] expressions to consider when determining + * group value combinations or [String]s representing field names. + * @return Aggregate Stage with specified groups. + */ + fun withGroups(groupField: String, vararg additionalGroups: Any) = withGroups(Field.of(groupField), additionalGroups) + + /** + * Add one or more groups to [AggregateStage] + * + * @param groupField The [Selectable] expression to consider when determining group value + * combinations. + * @param additionalGroups The [Selectable] expressions to consider when determining + * group value combinations or [String]s representing field names. + * @return Aggregate Stage with specified groups. + */ + fun withGroups(group: Selectable, vararg additionalGroups: Any) = AggregateStage( accumulators, - selectable.map(Selectable::toSelectable).associateBy(Selectable::getAlias) + mapOf(group.getAlias() to group.getExpr()).plus(additionalGroups.map(Selectable::toSelectable).associateBy(Selectable::getAlias)) ) override fun args(userDataReader: UserDataReader): Sequence = @@ -203,6 +245,10 @@ internal constructor( sequenceOf(condition.toProto(userDataReader)) } +/** + * Performs a vector similarity search, ordering the result set by most similar to least + * similar, and returning the first N documents in the result set. + */ class FindNearestStage internal constructor( private val property: Expr, @@ -212,21 +258,62 @@ internal constructor( ) : Stage("find_nearest", options) { companion object { - @JvmStatic - fun of(property: Expr, vector: Expr, distanceMeasure: DistanceMeasure) = - FindNearestStage(property, vector, distanceMeasure) + /** + * Create [FindNearestStage]. + * + * @param vectorField A [Field] that contains vector to search on. + * @param vectorValue The [VectorValue] used to measure the distance from [vectorField] values + * in the documents. + * @param distanceMeasure specifies what type of distance is calculated. + * when performing the search. + * @return [FindNearestStage] with specified parameters. + */ @JvmStatic - fun of(property: Expr, vector: DoubleArray, distanceMeasure: DistanceMeasure) = - FindNearestStage(property, Constant.vector(vector), distanceMeasure) - + fun of(vectorField: Field, vectorValue: VectorValue, distanceMeasure: DistanceMeasure) = + FindNearestStage(vectorField, Constant.of(vectorValue), distanceMeasure) + + /** + * Create [FindNearestStage]. + * + * @param vectorField A [Field] that contains vector to search on. + * @param vectorValue The [VectorValue] in array form that is used to measure the distance from + * [vectorField] values in the documents. + * @param distanceMeasure specifies what type of distance is calculated + * when performing the search. + * @return [FindNearestStage] with specified parameters. + */ @JvmStatic - fun of(fieldName: String, vector: Expr, distanceMeasure: DistanceMeasure) = - FindNearestStage(Constant.of(fieldName), vector, distanceMeasure) - + fun of(vectorField: Field, vectorValue: DoubleArray, distanceMeasure: DistanceMeasure) = + FindNearestStage(vectorField, Constant.vector(vectorValue), distanceMeasure) + + /** + * Create [FindNearestStage]. + * + * @param vectorField A [String] specifying the vector field to search on. + * @param vectorValue The [VectorValue] used to measure the distance from [vectorField] values + * in the documents. + * @param distanceMeasure specifies what type of distance is calculated + * when performing the search. + * @return [FindNearestStage] with specified parameters. + */ + @JvmStatic + fun of(vectorField: String, vectorValue: VectorValue, distanceMeasure: DistanceMeasure) = + FindNearestStage(Constant.of(vectorField), Constant.of(vectorValue), distanceMeasure) + + /** + * Create [FindNearestStage]. + * + * @param vectorField A [String] specifying the vector field to search on. + * @param vectorValue The [VectorValue] in array form that is used to measure the distance from + * [vectorField] values in the documents. + * @param distanceMeasure specifies what type of distance is calculated + * when performing the search. + * @return [FindNearestStage] with specified parameters. + */ @JvmStatic - fun of(fieldName: String, vector: DoubleArray, distanceMeasure: DistanceMeasure) = - FindNearestStage(Constant.of(fieldName), Constant.vector(vector), distanceMeasure) + fun of(vectorField: String, vectorValue: DoubleArray, distanceMeasure: DistanceMeasure) = + FindNearestStage(Constant.of(vectorField), Constant.vector(vectorValue), distanceMeasure) } class DistanceMeasure private constructor(internal val proto: Value) { @@ -251,11 +338,29 @@ internal constructor( distanceMeasure.proto ) + /** + * Specifies the upper bound of documents to return. + * + * @param limit must be a positive integer. + * @return [FindNearestStage] with specified [limit]. + */ fun withLimit(limit: Long): FindNearestStage = with("limit", limit) + /** + * Add a field containing the distance to the result. + * + * @param distanceField The [Field] that will be added to the result. + * @return [FindNearestStage] with specified [distanceField]. + */ fun withDistanceField(distanceField: Field): FindNearestStage = with("distance_field", distanceField) + /** + * Add a field containing the distance to the result. + * + * @param distanceField The name of the field that will be added to the result. + * @return [FindNearestStage] with specified [distanceField]. + */ fun withDistanceField(distanceField: String): FindNearestStage = withDistanceField(of(distanceField)) } From 51883b9443cce246425faffd57d33a9bfe7158e6 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 9 Apr 2025 22:54:31 -0400 Subject: [PATCH 037/152] Comments word wrap --- .../com/google/firebase/firestore/Pipeline.kt | 81 +++++++++---------- .../firebase/firestore/pipeline/stage.kt | 47 +++++------ 2 files changed, 63 insertions(+), 65 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 951f0d2f3ec..8f30947e50c 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -115,8 +115,8 @@ internal constructor( * The added fields are defined using [Selectable]s, which can be: * * - [Field]: References an existing document field. - * - [ExprWithAlias]: Represents the result of a expression with an assigned alias name - * using [Expr.alias] + * - [ExprWithAlias]: Represents the result of a expression with an assigned alias name using + * [Expr.alias] * * @param field The first field to add to the documents, specified as a [Selectable]. * @param additionalFields The fields to add to the documents, specified as [Selectable]s. @@ -137,8 +137,8 @@ internal constructor( * * - [String]: Name of an existing field * - [Field]: Reference to an existing field. - * - [ExprWithAlias]: Represents the result of a expression with an assigned alias name - * using [Expr.alias] + * - [ExprWithAlias]: Represents the result of a expression with an assigned alias name using + * [Expr.alias] * * If no selections are provided, the output of this stage is empty. Use [Pipeline.addFields] * instead if only additions are desired. @@ -163,8 +163,8 @@ internal constructor( * * - [String]: Name of an existing field * - [Field]: Reference to an existing field. - * - [ExprWithAlias]: Represents the result of a expression with an assigned alias name - * using [Expr.alias] + * - [ExprWithAlias]: Represents the result of a expression with an assigned alias name using + * [Expr.alias] * * If no selections are provided, the output of this stage is empty. Use [Pipeline.addFields] * instead if only additions are desired. @@ -193,8 +193,8 @@ internal constructor( * * This stage allows you to apply conditions to the data, similar to a "WHERE" clause in SQL. * - * You can filter documents based on their field values, using implementations of - * [BooleanExpr], typically including but not limited to: + * You can filter documents based on their field values, using implementations of [BooleanExpr], + * typically including but not limited to: * * - field comparators: [FunctionExpr.eq], [FunctionExpr.lt] (less than), [FunctionExpr.gt] * (greater than), etc. @@ -210,8 +210,8 @@ internal constructor( * Skips the first `offset` number of documents from the results of previous stages. * * This stage is useful for implementing pagination in your pipelines, allowing you to retrieve - * results in chunks. It is typically used in conjunction with [limit] to control the size - * of each page. + * results in chunks. It is typically used in conjunction with [limit] to control the size of each + * page. * * @param offset The number of documents to skip. * @return A new [Pipeline] object with this stage appended to the stage list. @@ -221,8 +221,8 @@ internal constructor( /** * Limits the maximum number of documents returned by previous stages to `limit`. * - * This stage is particularly useful when you want to retrieve a controlled subset of data - * from a potentially large result set. It's often used for: + * This stage is particularly useful when you want to retrieve a controlled subset of data from a + * potentially large result set. It's often used for: * * - **Pagination:** In combination with [offset] to retrieve specific pages of results. * - **Limiting Data Retrieval:** To prevent excessive data transfer and improve performance, @@ -248,8 +248,8 @@ internal constructor( * * @param group The [Selectable] expression to consider when determining distinct value * combinations. - * @param additionalGroups The [Selectable] expressions to consider when determining - * distinct value combinations or [String]s representing field names. + * @param additionalGroups The [Selectable] expressions to consider when determining distinct + * value combinations or [String]s representing field names. * @return A new [Pipeline] object with this stage appended to the stage list. */ fun distinct(group: Selectable, vararg additionalGroups: Any): Pipeline = @@ -271,8 +271,8 @@ internal constructor( * [Expr.alias] * * @param groupField The [String] representing field name. - * @param additionalGroups The [Selectable] expressions to consider when determining - * distinct value combinations or [String]s representing field names. + * @param additionalGroups The [Selectable] expressions to consider when determining distinct + * value combinations or [String]s representing field names. * @return A new [Pipeline] object with this stage appended to the stage list. */ fun distinct(groupField: String, vararg additionalGroups: Any): Pipeline = @@ -289,11 +289,11 @@ internal constructor( * Performs aggregation operations on the documents from previous stages. * * This stage allows you to calculate aggregate values over a set of documents. You define the - * aggregations to perform using [AggregateWithAlias] expressions which are typically - * results of calling [AggregateFunction.alias] on [AggregateFunction] instances. + * aggregations to perform using [AggregateWithAlias] expressions which are typically results of + * calling [AggregateFunction.alias] on [AggregateFunction] instances. * - * @param accumulator The first [AggregateWithAlias] expression, wrapping an - * [AggregateFunction] with an alias for the accumulated results. + * @param accumulator The first [AggregateWithAlias] expression, wrapping an [AggregateFunction] + * with an alias for the accumulated results. * @param additionalAccumulators The [AggregateWithAlias] expressions, each wrapping an * [AggregateFunction] with an alias for the accumulated results. * @return A new [Pipeline] object with this stage appended to the stage list. @@ -326,14 +326,14 @@ internal constructor( fun aggregate(aggregateStage: AggregateStage): Pipeline = append(aggregateStage) /** - * Performs a vector similarity search, ordering the result set by most similar to least - * similar, and returning the first N documents in the result set. + * Performs a vector similarity search, ordering the result set by most similar to least similar, + * and returning the first N documents in the result set. * * @param vectorField A [Field] that contains vector to search on. * @param vectorValue The [VectorValue] in array form that is used to measure the distance from * [vectorField] values in the documents. - * @param distanceMeasure specifies what type of distance is calculated - * when performing the search. + * @param distanceMeasure specifies what type of distance is calculated when performing the + * search. * @return A new [Pipeline] object with this stage appended to the stage list. */ fun findNearest( @@ -343,14 +343,14 @@ internal constructor( ): Pipeline = append(FindNearestStage.of(vectorField, vectorValue, distanceMeasure)) /** - * Performs a vector similarity search, ordering the result set by most similar to least - * similar, and returning the first N documents in the result set. + * Performs a vector similarity search, ordering the result set by most similar to least similar, + * and returning the first N documents in the result set. * * @param vectorField A [String] specifying the vector field to search on. * @param vectorValue The [VectorValue] in array form that is used to measure the distance from * [vectorField] values in the documents. - * @param distanceMeasure specifies what type of distance is calculated - * when performing the search. + * @param distanceMeasure specifies what type of distance is calculated when performing the + * search. * @return A new [Pipeline] object with this stage appended to the stage list. */ fun findNearest( @@ -360,14 +360,14 @@ internal constructor( ): Pipeline = append(FindNearestStage.of(vectorField, vectorValue, distanceMeasure)) /** - * Performs a vector similarity search, ordering the result set by most similar to least - * similar, and returning the first N documents in the result set. + * Performs a vector similarity search, ordering the result set by most similar to least similar, + * and returning the first N documents in the result set. * * @param vectorField A [Field] that contains vector to search on. * @param vectorValue The [VectorValue] used to measure the distance from [vectorField] values in * the documents. - * @param distanceMeasure specifies what type of distance is calculated. - * when performing the search. + * @param distanceMeasure specifies what type of distance is calculated. when performing the + * search. * @return A new [Pipeline] object with this stage appended to the stage list. */ fun findNearest( @@ -377,14 +377,14 @@ internal constructor( ): Pipeline = append(FindNearestStage.of(vectorField, vectorValue, distanceMeasure)) /** - * Performs a vector similarity search, ordering the result set by most similar to least - * similar, and returning the first N documents in the result set. + * Performs a vector similarity search, ordering the result set by most similar to least similar, + * and returning the first N documents in the result set. * * @param vectorField A [String] specifying the vector field to search on. * @param vectorValue The [VectorValue] used to measure the distance from [vectorField] values in * the documents. - * @param distanceMeasure specifies what type of distance is calculated - * when performing the search. + * @param distanceMeasure specifies what type of distance is calculated when performing the + * search. * @return A new [Pipeline] object with this stage appended to the stage list. */ fun findNearest( @@ -394,8 +394,8 @@ internal constructor( ): Pipeline = append(FindNearestStage.of(vectorField, vectorValue, distanceMeasure)) /** - * Performs a vector similarity search, ordering the result set by most similar to least - * similar, and returning the first N documents in the result set. + * Performs a vector similarity search, ordering the result set by most similar to least similar, + * and returning the first N documents in the result set. * * @param stage An [FindNearestStage] object that specifies the search parameters. * @return A new [Pipeline] object with this stage appended to the stage list. @@ -486,10 +486,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto return createFrom(aggregateQuery.query) .aggregate( aggregateFields.first().toPipeline(), - *aggregateFields - .drop(1) - .map(AggregateField::toPipeline) - .toTypedArray() + *aggregateFields.drop(1).map(AggregateField::toPipeline).toTypedArray() ) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index f6a0ea2b5e8..2083ecc0837 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -160,16 +160,15 @@ internal constructor( * This stage allows you to calculate aggregate values over a set of documents, optionally grouped * by one or more fields or functions. You can specify: * - * - **Grouping Fields or Expressions:** One or more fields or functions to group the documents - * by. For each distinct combination of values in these fields, a separate group is created. If no + * - **Grouping Fields or Expressions:** One or more fields or functions to group the documents by. + * For each distinct combination of values in these fields, a separate group is created. If no * grouping fields are provided, a single group containing all documents is used. Not specifying * groups is the same as putting the entire inputs into one group. * - * - **AggregateFunctions:** One or more accumulation operations to perform within each group. - * These are defined using [AggregateWithAlias] expressions, which are typically created by - * calling [AggregateFunction.alias] on [AggregateFunction] instances. Each aggregation calculates - * a value (e.g., sum, average, count) based on the documents within its - * group. + * - **AggregateFunctions:** One or more accumulation operations to perform within each group. These + * are defined using [AggregateWithAlias] expressions, which are typically created by calling + * [AggregateFunction.alias] on [AggregateFunction] instances. Each aggregation calculates a value + * (e.g., sum, average, count) based on the documents within its group. */ class AggregateStage internal constructor( @@ -207,25 +206,27 @@ internal constructor( * Add one or more groups to [AggregateStage] * * @param groupField The [String] representing field name. - * @param additionalGroups The [Selectable] expressions to consider when determining - * group value combinations or [String]s representing field names. + * @param additionalGroups The [Selectable] expressions to consider when determining group value + * combinations or [String]s representing field names. * @return Aggregate Stage with specified groups. */ - fun withGroups(groupField: String, vararg additionalGroups: Any) = withGroups(Field.of(groupField), additionalGroups) + fun withGroups(groupField: String, vararg additionalGroups: Any) = + withGroups(Field.of(groupField), additionalGroups) /** * Add one or more groups to [AggregateStage] * * @param groupField The [Selectable] expression to consider when determining group value * combinations. - * @param additionalGroups The [Selectable] expressions to consider when determining - * group value combinations or [String]s representing field names. + * @param additionalGroups The [Selectable] expressions to consider when determining group value + * combinations or [String]s representing field names. * @return Aggregate Stage with specified groups. */ fun withGroups(group: Selectable, vararg additionalGroups: Any) = AggregateStage( accumulators, - mapOf(group.getAlias() to group.getExpr()).plus(additionalGroups.map(Selectable::toSelectable).associateBy(Selectable::getAlias)) + mapOf(group.getAlias() to group.getExpr()) + .plus(additionalGroups.map(Selectable::toSelectable).associateBy(Selectable::getAlias)) ) override fun args(userDataReader: UserDataReader): Sequence = @@ -246,8 +247,8 @@ internal constructor( } /** - * Performs a vector similarity search, ordering the result set by most similar to least - * similar, and returning the first N documents in the result set. + * Performs a vector similarity search, ordering the result set by most similar to least similar, + * and returning the first N documents in the result set. */ class FindNearestStage internal constructor( @@ -265,8 +266,8 @@ internal constructor( * @param vectorField A [Field] that contains vector to search on. * @param vectorValue The [VectorValue] used to measure the distance from [vectorField] values * in the documents. - * @param distanceMeasure specifies what type of distance is calculated. - * when performing the search. + * @param distanceMeasure specifies what type of distance is calculated. when performing the + * search. * @return [FindNearestStage] with specified parameters. */ @JvmStatic @@ -279,8 +280,8 @@ internal constructor( * @param vectorField A [Field] that contains vector to search on. * @param vectorValue The [VectorValue] in array form that is used to measure the distance from * [vectorField] values in the documents. - * @param distanceMeasure specifies what type of distance is calculated - * when performing the search. + * @param distanceMeasure specifies what type of distance is calculated when performing the + * search. * @return [FindNearestStage] with specified parameters. */ @JvmStatic @@ -293,8 +294,8 @@ internal constructor( * @param vectorField A [String] specifying the vector field to search on. * @param vectorValue The [VectorValue] used to measure the distance from [vectorField] values * in the documents. - * @param distanceMeasure specifies what type of distance is calculated - * when performing the search. + * @param distanceMeasure specifies what type of distance is calculated when performing the + * search. * @return [FindNearestStage] with specified parameters. */ @JvmStatic @@ -307,8 +308,8 @@ internal constructor( * @param vectorField A [String] specifying the vector field to search on. * @param vectorValue The [VectorValue] in array form that is used to measure the distance from * [vectorField] values in the documents. - * @param distanceMeasure specifies what type of distance is calculated - * when performing the search. + * @param distanceMeasure specifies what type of distance is calculated when performing the + * search. * @return [FindNearestStage] with specified parameters. */ @JvmStatic From daab5a54db961ea592ced2ece812bb403306deb2 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 10 Apr 2025 19:00:28 -0400 Subject: [PATCH 038/152] Comments and alignment across SDKs --- .../firestore/QueryToPipelineTest.java | 101 ++++++----- .../com/google/firebase/firestore/Pipeline.kt | 171 ++++++++++++++++-- .../firebase/firestore/pipeline/Constant.kt | 84 ++++++++- .../firebase/firestore/pipeline/aggregates.kt | 8 + .../firestore/pipeline/expressions.kt | 110 ++++++++++- .../firebase/firestore/pipeline/stage.kt | 142 ++++++++++++++- 6 files changed, 536 insertions(+), 80 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java index 188b047f739..4bf391df465 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java @@ -68,7 +68,7 @@ public void testLimitQueries() { Query query = collection.limit(2); FirebaseFirestore db = collection.firestore; - PipelineSnapshot set = waitFor(db.pipeline().createFrom(query).execute()); + PipelineSnapshot set = waitFor(db.pipeline().convertFrom(query).execute()); List> data = pipelineSnapshotToValues(set); assertEquals(asList(map("k", "a"), map("k", "b")), data); } @@ -85,7 +85,7 @@ public void testLimitQueriesUsingDescendingSortOrder() { Query query = collection.limit(2).orderBy("sort", Direction.DESCENDING); FirebaseFirestore db = collection.firestore; - PipelineSnapshot set = waitFor(db.pipeline().createFrom(query).execute()); + PipelineSnapshot set = waitFor(db.pipeline().convertFrom(query).execute()); List> data = pipelineSnapshotToValues(set); assertEquals(asList(map("k", "d", "sort", 2L), map("k", "c", "sort", 1L)), data); @@ -98,7 +98,7 @@ public void testLimitToLastMustAlsoHaveExplicitOrderBy() { Query query = collection.limitToLast(2); expectError( - () -> waitFor(db.pipeline().createFrom(query).execute()), + () -> waitFor(db.pipeline().convertFrom(query).execute()), "limitToLast() queries require specifying at least one orderBy() clause"); } @@ -115,33 +115,33 @@ public void testLimitToLastQueriesWithCursors() { Query query = collection.limitToLast(3).orderBy("sort").endBefore(2); FirebaseFirestore db = collection.firestore; - PipelineSnapshot set = waitFor(db.pipeline().createFrom(query).execute()); + PipelineSnapshot set = waitFor(db.pipeline().convertFrom(query).execute()); List> data = pipelineSnapshotToValues(set); assertEquals( asList(map("k", "a", "sort", 0L), map("k", "b", "sort", 1L), map("k", "c", "sort", 1L)), data); query = collection.limitToLast(3).orderBy("sort").endAt(1); - set = waitFor(db.pipeline().createFrom(query).execute()); + set = waitFor(db.pipeline().convertFrom(query).execute()); data = pipelineSnapshotToValues(set); assertEquals( asList(map("k", "a", "sort", 0L), map("k", "b", "sort", 1L), map("k", "c", "sort", 1L)), data); query = collection.limitToLast(3).orderBy("sort").startAt(2); - set = waitFor(db.pipeline().createFrom(query).execute()); + set = waitFor(db.pipeline().convertFrom(query).execute()); data = pipelineSnapshotToValues(set); assertEquals(asList(map("k", "d", "sort", 2L)), data); query = collection.limitToLast(3).orderBy("sort").startAfter(0); - set = waitFor(db.pipeline().createFrom(query).execute()); + set = waitFor(db.pipeline().convertFrom(query).execute()); data = pipelineSnapshotToValues(set); assertEquals( asList(map("k", "b", "sort", 1L), map("k", "c", "sort", 1L), map("k", "d", "sort", 2L)), data); query = collection.limitToLast(3).orderBy("sort").startAfter(-1); - set = waitFor(db.pipeline().createFrom(query).execute()); + set = waitFor(db.pipeline().convertFrom(query).execute()); data = pipelineSnapshotToValues(set); assertEquals( asList(map("k", "b", "sort", 1L), map("k", "c", "sort", 1L), map("k", "d", "sort", 2L)), @@ -163,7 +163,7 @@ public void testKeyOrderIsDescendingForDescendingInequality() { Query query = collection.whereGreaterThan("foo", 21.0).orderBy("foo", Direction.DESCENDING); FirebaseFirestore db = collection.firestore; - PipelineSnapshot result = waitFor(db.pipeline().createFrom(query).execute()); + PipelineSnapshot result = waitFor(db.pipeline().convertFrom(query).execute()); assertEquals(asList("g", "f", "c", "b", "a"), pipelineSnapshotToIds(result)); } @@ -179,7 +179,7 @@ public void testUnaryFilterQueries() { PipelineSnapshot results = waitFor( db.pipeline() - .createFrom(collection.whereEqualTo("null", null).whereEqualTo("nan", Double.NaN)) + .convertFrom(collection.whereEqualTo("null", null).whereEqualTo("nan", Double.NaN)) .execute()); assertEquals(1, results.getResults().size()); PipelineResult result = results.getResults().get(0); @@ -199,7 +199,7 @@ public void testFilterOnInfinity() { PipelineSnapshot results = waitFor( db.pipeline() - .createFrom(collection.whereEqualTo("inf", Double.POSITIVE_INFINITY)) + .convertFrom(collection.whereEqualTo("inf", Double.POSITIVE_INFINITY)) .execute()); assertEquals(1, results.getResults().size()); assertEquals(asList(map("inf", Double.POSITIVE_INFINITY)), pipelineSnapshotToValues(results)); @@ -217,7 +217,7 @@ public void testCanExplicitlySortByDocumentId() { // Ideally this would be descending to validate it's different than // the default, but that requires an extra index PipelineSnapshot docs = - waitFor(db.pipeline().createFrom(collection.orderBy(FieldPath.documentId())).execute()); + waitFor(db.pipeline().convertFrom(collection.orderBy(FieldPath.documentId())).execute()); assertEquals( asList(testDocs.get("a"), testDocs.get("b"), testDocs.get("c")), pipelineSnapshotToValues(docs)); @@ -236,14 +236,14 @@ public void testCanQueryByDocumentId() { PipelineSnapshot docs = waitFor( db.pipeline() - .createFrom(collection.whereEqualTo(FieldPath.documentId(), "ab")) + .convertFrom(collection.whereEqualTo(FieldPath.documentId(), "ab")) .execute()); assertEquals(singletonList(testDocs.get("ab")), pipelineSnapshotToValues(docs)); docs = waitFor( db.pipeline() - .createFrom( + .convertFrom( collection .whereGreaterThan(FieldPath.documentId(), "aa") .whereLessThanOrEqualTo(FieldPath.documentId(), "ba")) @@ -264,7 +264,7 @@ public void testCanQueryByDocumentIdUsingRefs() { PipelineSnapshot docs = waitFor( db.pipeline() - .createFrom( + .convertFrom( collection.whereEqualTo(FieldPath.documentId(), collection.document("ab"))) .execute()); assertEquals(singletonList(testDocs.get("ab")), pipelineSnapshotToValues(docs)); @@ -272,7 +272,7 @@ public void testCanQueryByDocumentIdUsingRefs() { docs = waitFor( db.pipeline() - .createFrom( + .convertFrom( collection .whereGreaterThan(FieldPath.documentId(), collection.document("aa")) .whereLessThanOrEqualTo(FieldPath.documentId(), collection.document("ba"))) @@ -287,9 +287,9 @@ public void testCanQueryWithAndWithoutDocumentKey() { collection.add(map()); Task query1 = db.pipeline() - .createFrom(collection.orderBy(FieldPath.documentId(), Direction.ASCENDING)) + .convertFrom(collection.orderBy(FieldPath.documentId(), Direction.ASCENDING)) .execute(); - Task query2 = db.pipeline().createFrom(collection).execute(); + Task query2 = db.pipeline().convertFrom(collection).execute(); waitFor(query1); waitFor(query2); @@ -327,7 +327,7 @@ public void testQueriesCanUseNotEqualFilters() { expectedDocsMap.remove("j"); PipelineSnapshot snapshot = - waitFor(db.pipeline().createFrom(collection.whereNotEqualTo("zip", 98101L)).execute()); + waitFor(db.pipeline().convertFrom(collection.whereNotEqualTo("zip", 98101L)).execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); // With objects. @@ -338,7 +338,7 @@ public void testQueriesCanUseNotEqualFilters() { snapshot = waitFor( db.pipeline() - .createFrom(collection.whereNotEqualTo("zip", map("code", 500))) + .convertFrom(collection.whereNotEqualTo("zip", map("code", 500))) .execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); @@ -346,7 +346,8 @@ public void testQueriesCanUseNotEqualFilters() { expectedDocsMap = new LinkedHashMap<>(allDocs); expectedDocsMap.remove("i"); expectedDocsMap.remove("j"); - snapshot = waitFor(db.pipeline().createFrom(collection.whereNotEqualTo("zip", null)).execute()); + snapshot = + waitFor(db.pipeline().convertFrom(collection.whereNotEqualTo("zip", null)).execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); // With NaN. @@ -355,7 +356,7 @@ public void testQueriesCanUseNotEqualFilters() { expectedDocsMap.remove("i"); expectedDocsMap.remove("j"); snapshot = - waitFor(db.pipeline().createFrom(collection.whereEqualTo("zip", Double.NaN)).execute()); + waitFor(db.pipeline().convertFrom(collection.whereEqualTo("zip", Double.NaN)).execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); } @@ -376,7 +377,7 @@ public void testQueriesCanUseNotEqualFiltersWithDocIds() { PipelineSnapshot docs = waitFor( db.pipeline() - .createFrom(collection.whereNotEqualTo(FieldPath.documentId(), "aa")) + .convertFrom(collection.whereNotEqualTo(FieldPath.documentId(), "aa")) .execute()); assertEquals(asList(docB, docC, docD), pipelineSnapshotToValues(docs)); } @@ -396,14 +397,16 @@ public void testQueriesCanUseArrayContainsFilters() { // Search for "array" to contain 42 PipelineSnapshot snapshot = - waitFor(db.pipeline().createFrom(collection.whereArrayContains("array", 42L)).execute()); + waitFor(db.pipeline().convertFrom(collection.whereArrayContains("array", 42L)).execute()); assertEquals(asList(docA, docB, docD), pipelineSnapshotToValues(snapshot)); // Note: whereArrayContains() requires a non-null value parameter, so no null test is needed. // With NaN. snapshot = waitFor( - db.pipeline().createFrom(collection.whereArrayContains("array", Double.NaN)).execute()); + db.pipeline() + .convertFrom(collection.whereArrayContains("array", Double.NaN)) + .execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); } @@ -430,7 +433,7 @@ public void testQueriesCanUseInFilters() { PipelineSnapshot snapshot = waitFor( db.pipeline() - .createFrom( + .convertFrom( collection.whereIn("zip", asList(98101L, 98103L, asList(98101L, 98102L)))) .execute()); assertEquals(asList(docA, docC, docG), pipelineSnapshotToValues(snapshot)); @@ -439,30 +442,30 @@ public void testQueriesCanUseInFilters() { snapshot = waitFor( db.pipeline() - .createFrom(collection.whereIn("zip", asList(map("code", 500L)))) + .convertFrom(collection.whereIn("zip", asList(map("code", 500L)))) .execute()); assertEquals(asList(docF), pipelineSnapshotToValues(snapshot)); // With null. - snapshot = waitFor(db.pipeline().createFrom(collection.whereIn("zip", nullList())).execute()); + snapshot = waitFor(db.pipeline().convertFrom(collection.whereIn("zip", nullList())).execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With null and a value. List inputList = nullList(); inputList.add(98101L); - snapshot = waitFor(db.pipeline().createFrom(collection.whereIn("zip", inputList)).execute()); + snapshot = waitFor(db.pipeline().convertFrom(collection.whereIn("zip", inputList)).execute()); assertEquals(asList(docA), pipelineSnapshotToValues(snapshot)); // With NaN. snapshot = - waitFor(db.pipeline().createFrom(collection.whereIn("zip", asList(Double.NaN))).execute()); + waitFor(db.pipeline().convertFrom(collection.whereIn("zip", asList(Double.NaN))).execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With NaN and a value. snapshot = waitFor( db.pipeline() - .createFrom(collection.whereIn("zip", asList(Double.NaN, 98101L))) + .convertFrom(collection.whereIn("zip", asList(Double.NaN, 98101L))) .execute()); assertEquals(asList(docA), pipelineSnapshotToValues(snapshot)); } @@ -484,7 +487,7 @@ public void testQueriesCanUseInFiltersWithDocIds() { PipelineSnapshot docs = waitFor( db.pipeline() - .createFrom(collection.whereIn(FieldPath.documentId(), asList("aa", "ab"))) + .convertFrom(collection.whereIn(FieldPath.documentId(), asList("aa", "ab"))) .execute()); assertEquals(asList(docA, docB), pipelineSnapshotToValues(docs)); } @@ -522,7 +525,7 @@ public void testQueriesCanUseNotInFilters() { PipelineSnapshot snapshot = waitFor( db.pipeline() - .createFrom( + .convertFrom( collection.whereNotIn("zip", asList(98101L, 98103L, asList(98101L, 98102L)))) .execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); @@ -535,13 +538,13 @@ public void testQueriesCanUseNotInFilters() { snapshot = waitFor( db.pipeline() - .createFrom(collection.whereNotIn("zip", asList(map("code", 500L)))) + .convertFrom(collection.whereNotIn("zip", asList(map("code", 500L)))) .execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); // With Null. snapshot = - waitFor(db.pipeline().createFrom(collection.whereNotIn("zip", nullList())).execute()); + waitFor(db.pipeline().convertFrom(collection.whereNotIn("zip", nullList())).execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With NaN. @@ -551,7 +554,7 @@ public void testQueriesCanUseNotInFilters() { expectedDocsMap.remove("j"); snapshot = waitFor( - db.pipeline().createFrom(collection.whereNotIn("zip", asList(Double.NaN))).execute()); + db.pipeline().convertFrom(collection.whereNotIn("zip", asList(Double.NaN))).execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); // With NaN and a number. @@ -563,7 +566,7 @@ public void testQueriesCanUseNotInFilters() { snapshot = waitFor( db.pipeline() - .createFrom(collection.whereNotIn("zip", asList(Float.NaN, 98101L))) + .convertFrom(collection.whereNotIn("zip", asList(Float.NaN, 98101L))) .execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); } @@ -585,7 +588,7 @@ public void testQueriesCanUseNotInFiltersWithDocIds() { PipelineSnapshot docs = waitFor( db.pipeline() - .createFrom(collection.whereNotIn(FieldPath.documentId(), asList("aa", "ab"))) + .convertFrom(collection.whereNotIn(FieldPath.documentId(), asList("aa", "ab"))) .execute()); assertEquals(asList(docC, docD), pipelineSnapshotToValues(docs)); } @@ -613,7 +616,7 @@ public void testQueriesCanUseArrayContainsAnyFilters() { PipelineSnapshot snapshot = waitFor( db.pipeline() - .createFrom(collection.whereArrayContainsAny("array", asList(42L, 43L))) + .convertFrom(collection.whereArrayContainsAny("array", asList(42L, 43L))) .execute()); assertEquals(asList(docA, docB, docD, docE), pipelineSnapshotToValues(snapshot)); @@ -621,7 +624,7 @@ public void testQueriesCanUseArrayContainsAnyFilters() { snapshot = waitFor( db.pipeline() - .createFrom(collection.whereArrayContainsAny("array", asList(map("a", 42L)))) + .convertFrom(collection.whereArrayContainsAny("array", asList(map("a", 42L)))) .execute()); assertEquals(asList(docF), pipelineSnapshotToValues(snapshot)); @@ -629,7 +632,7 @@ public void testQueriesCanUseArrayContainsAnyFilters() { snapshot = waitFor( db.pipeline() - .createFrom(collection.whereArrayContainsAny("array", nullList())) + .convertFrom(collection.whereArrayContainsAny("array", nullList())) .execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); @@ -639,7 +642,7 @@ public void testQueriesCanUseArrayContainsAnyFilters() { snapshot = waitFor( db.pipeline() - .createFrom(collection.whereArrayContainsAny("array", inputList)) + .convertFrom(collection.whereArrayContainsAny("array", inputList)) .execute()); assertEquals(asList(docE), pipelineSnapshotToValues(snapshot)); @@ -647,7 +650,7 @@ public void testQueriesCanUseArrayContainsAnyFilters() { snapshot = waitFor( db.pipeline() - .createFrom(collection.whereArrayContainsAny("array", asList(Double.NaN))) + .convertFrom(collection.whereArrayContainsAny("array", asList(Double.NaN))) .execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); @@ -655,7 +658,7 @@ public void testQueriesCanUseArrayContainsAnyFilters() { snapshot = waitFor( db.pipeline() - .createFrom(collection.whereArrayContainsAny("array", asList(Double.NaN, 43L))) + .convertFrom(collection.whereArrayContainsAny("array", asList(Double.NaN, 43L))) .execute()); assertEquals(asList(docE), pipelineSnapshotToValues(snapshot)); } @@ -688,7 +691,7 @@ public void testCollectionGroupQueries() { waitFor(batch.commit()); PipelineSnapshot snapshot = - waitFor(db.pipeline().createFrom(db.collectionGroup(collectionGroup)).execute()); + waitFor(db.pipeline().convertFrom(db.collectionGroup(collectionGroup)).execute()); assertEquals( asList("cg-doc1", "cg-doc2", "cg-doc3", "cg-doc4", "cg-doc5"), pipelineSnapshotToIds(snapshot)); @@ -720,7 +723,7 @@ public void testCollectionGroupQueriesWithStartAtEndAtWithArbitraryDocumentIds() PipelineSnapshot snapshot = waitFor( db.pipeline() - .createFrom( + .convertFrom( db.collectionGroup(collectionGroup) .orderBy(FieldPath.documentId()) .startAt("a/b") @@ -731,7 +734,7 @@ public void testCollectionGroupQueriesWithStartAtEndAtWithArbitraryDocumentIds() snapshot = waitFor( db.pipeline() - .createFrom( + .convertFrom( db.collectionGroup(collectionGroup) .orderBy(FieldPath.documentId()) .startAfter("a/b") @@ -766,7 +769,7 @@ public void testCollectionGroupQueriesWithWhereFiltersOnArbitraryDocumentIds() { PipelineSnapshot snapshot = waitFor( db.pipeline() - .createFrom( + .convertFrom( db.collectionGroup(collectionGroup) .whereGreaterThanOrEqualTo(FieldPath.documentId(), "a/b") .whereLessThanOrEqualTo(FieldPath.documentId(), "a/b0")) @@ -776,7 +779,7 @@ public void testCollectionGroupQueriesWithWhereFiltersOnArbitraryDocumentIds() { snapshot = waitFor( db.pipeline() - .createFrom( + .convertFrom( db.collectionGroup(collectionGroup) .whereGreaterThan(FieldPath.documentId(), "a/b") .whereLessThan( diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 8f30947e50c..1cf45215a80 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -101,9 +101,34 @@ internal constructor( .addAllStages(stages.map { it.toProtoStage(userDataReader) }) .build() + /** + * Adds a stage to the pipeline by specifying the stage name as an argument. This does not offer + * any type safety on the stage params and requires the caller to know the order (and optionally + * names) of parameters accepted by the stage. + * + * This method provides a way to call stages that are supported by the Firestore backend but that + * are not implemented in the SDK version being used. + * + * For stages with named parameters, use the [GenericStage] class instead. + * + * @param name The unique name of the stage to add. + * @param arguments A list of ordered parameters to configure the stage's behavior. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun genericStage(name: String, vararg arguments: Any): Pipeline = append(GenericStage(name, arguments.map(GenericArg::from))) + /** + * Adds a stage to the pipeline by specifying the stage name as an argument. This does not offer + * any type safety on the stage params and requires the caller to know the order (and optionally + * names) of parameters accepted by the stage. + * + * This method provides a way to call stages that are supported by the Firestore backend but that + * are not implemented in the SDK version being used. + * + * @param stage An [GenericStage] object that specifies stage name and parameters. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun genericStage(stage: GenericStage): Pipeline = append(stage) /** @@ -125,10 +150,27 @@ internal constructor( fun addFields(field: Selectable, vararg additionalFields: Selectable): Pipeline = append(AddFieldsStage(arrayOf(field, *additionalFields))) - fun removeFields(vararg fields: Field): Pipeline = append(RemoveFieldsStage(fields)) + /** + * Remove fields from outputs of previous stages. + * + * @param field The first [Field] to remove. + * @param additionalFields Optional additional [Field]s to remove. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun removeFields(field: Field, vararg additionalFields: Field): Pipeline = + append(RemoveFieldsStage(arrayOf(field, *additionalFields))) - fun removeFields(vararg fields: String): Pipeline = - append(RemoveFieldsStage(fields.map(Field::of).toTypedArray())) + /** + * Remove fields from outputs of previous stages. + * + * @param field The first [String] name of field to remove. + * @param additionalFields Optional additional [String] name of fields to remove. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun removeFields(field: String, vararg additionalFields: String): Pipeline = + append( + RemoveFieldsStage(arrayOf(Field.of(field), *additionalFields.map(Field::of).toTypedArray())) + ) /** * Selects or creates a set of fields from the outputs of previous stages. @@ -185,7 +227,22 @@ internal constructor( ) ) - fun sort(vararg orders: Ordering): Pipeline = append(SortStage(orders)) + /** + * Sorts the documents from previous stages based on one or more [Ordering] criteria. + * + * This stage allows you to order the results of your pipeline. You can specify multiple + * [Ordering] instances to sort by multiple fields in ascending or descending order. If documents + * have the same value for a field used for sorting, the next specified ordering will be used. If + * all orderings result in equal comparison, the documents are considered equal and the order is + * unspecified. + * + * @param order The first [Ordering] instance specifying the sorting criteria. + * @param additionalOrders Optional additional [Ordering] instances specifying the sorting + * criteria. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun sort(order: Ordering, vararg additionalOrders: Ordering): Pipeline = + append(SortStage(arrayOf(order, *additionalOrders))) /** * Filters the documents from previous stages to only include those matching the specified @@ -402,22 +459,112 @@ internal constructor( */ fun findNearest(stage: FindNearestStage): Pipeline = append(stage) + /** + * Fully overwrites all fields in a document with those coming from a nested map. + * + * This stage allows you to emit a map value as a document. Each key of the map becomes a field on + * the document that contains the corresponding value. + * + * @param field The [String] specifying the field name containing the nested map. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun replace(field: String): Pipeline = replace(Field.of(field)) - fun replace(field: Selectable): Pipeline = - append(ReplaceStage(field, ReplaceStage.Mode.FULL_REPLACE)) + /** + * Fully overwrites all fields in a document with those coming from a nested map. + * + * This stage allows you to emit a map value as a document. Each key of the map becomes a field on + * the document that contains the corresponding value. + * + * @param mapValue The [Expr] or [Field] containing the nested map. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun replace(mapValue: Expr): Pipeline = + append(ReplaceStage(mapValue, ReplaceStage.Mode.FULL_REPLACE)) + /** + * Performs a pseudo-random sampling of the input documents. + * + * The [documents] parameter represents the target number of documents to produce and must be a + * non-negative integer value. If the previous stage produces less than size documents, the entire + * previous results are returned. If the previous stage produces more than size, this outputs a + * sample of exactly size entries where any sample is equally likely. + * + * @param documents The number of documents to emit. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun sample(documents: Int): Pipeline = append(SampleStage.withDocLimit(documents)) + /** + * Performs a pseudo-random sampling of the input documents. + * + * @param sample An [SampleStage] object that specifies how sampling is performed. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun sample(sample: SampleStage): Pipeline = append(sample) + /** + * Performs union of all documents from two pipelines, including duplicates. + * + * This stage will pass through documents from previous stage, and also pass through documents + * from previous stage of the `other` Pipeline given in parameter. The order of documents emitted + * from this stage is undefined. + * + * @param other The other [Pipeline] that is part of union. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ fun union(other: Pipeline): Pipeline = append(UnionStage(other)) - fun unnest(field: String, alias: String): Pipeline = unnest(UnnestStage.withField(field, alias)) + /** + * Takes a specified array from the input documents and outputs a document for each element with + * the element stored in a field with name specified by the alias. + * + * For each document emitted by the prior stage, this stage will emit zero or more augmented + * documents. The input array found in the previous stage document field specified by the + * [arrayField] parameter, will for each element of the input array produce an augmented document. + * The element of the input array will be stored in a field with name specified by [alias] + * parameter on the augmented document. + * + * @param arrayField The name of the field containing the array. + * @param alias The name of field to store emitted element of array. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun unnest(arrayField: String, alias: String): Pipeline = + unnest(Field.of(arrayField).alias(alias)) - fun unnest(selectable: Selectable): Pipeline = append(UnnestStage(selectable)) + /** + * Takes a specified array from the input documents and outputs a document for each element with + * the element stored in a field with name specified by the alias. + * + * For each document emitted by the prior stage, this stage will emit zero or more augmented + * documents. The input array is found in parameter [arrayWithAlias], which can be an [Expr] with + * an alias specified via [Expr.alias], or a [Field] that can also have alias specified. For each + * element of the input array, an augmented document will be produced. The element of input array + * will be stored in a field with name specified by the alias of the [arrayWithAlias] parameter. + * If the [arrayWithAlias] is a [Field] with no alias, then the original array field will be + * replaced with the individual element. + * + * @param arrayWithAlias The input array with field alias to store output element of array. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun unnest(arrayWithAlias: Selectable): Pipeline = append(UnnestStage(arrayWithAlias)) - fun unnest(stage: UnnestStage): Pipeline = append(stage) + /** + * Takes a specified array from the input documents and outputs a document for each element with + * the element stored in a field with name specified by the alias. + * + * For each document emitted by the prior stage, this stage will emit zero or more augmented + * documents. The input array specified in the [unnestStage] parameter will for each element of + * the input array produce an augmented document. The element of the input array will be stored in + * a field with a name specified by the [unnestStage] parameter. + * + * Optionally, an index field can also be added to emitted documents. See [UnnestStage] for + * further information. + * + * @param unnestStage An [UnnestStage] object that specifies the search parameters. + * @return A new [Pipeline] object with this stage appended to the stage list. + */ + fun unnest(unnestStage: UnnestStage): Pipeline = append(unnestStage) private inner class ObserverSnapshotTask : PipelineResultObserver { private val userDataWriter = @@ -466,7 +613,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * @throws [IllegalArgumentException] Thrown if the [query] provided targets a different project * or database than the pipeline. */ - fun createFrom(query: Query): Pipeline { + fun convertFrom(query: Query): Pipeline { if (query.firestore.databaseId != firestore.databaseId) { throw IllegalArgumentException("Provided query is from a different Firestore instance.") } @@ -481,9 +628,9 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * @throws [IllegalArgumentException] Thrown if the [aggregateQuery] provided targets a different * project or database than the pipeline. */ - fun createFrom(aggregateQuery: AggregateQuery): Pipeline { + fun convertFrom(aggregateQuery: AggregateQuery): Pipeline { val aggregateFields = aggregateQuery.aggregateFields - return createFrom(aggregateQuery.query) + return convertFrom(aggregateQuery.query) .aggregate( aggregateFields.first().toPipeline(), *aggregateFields.drop(1).map(AggregateField::toPipeline).toTypedArray() diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt index 0edf481cf16..1e5c47da1f9 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt @@ -25,6 +25,11 @@ import com.google.firebase.firestore.model.Values.encodeValue import com.google.firestore.v1.Value import java.util.Date +/** + * Represents a constant value that can be used in a Firestore pipeline expression. + * + * You can create a [Constant] instance using the static [of] method: + */ abstract class Constant internal constructor() : Expr() { private class ValueConstant(val value: Value) : Constant() { @@ -38,41 +43,89 @@ abstract class Constant internal constructor() : Expr() { return ValueConstant(value) } + /** + * Create a [Constant] instance for a [String] value. + * + * @param value The [String] value. + * @return A new [Constant] instance. + */ @JvmStatic fun of(value: String): Constant { return ValueConstant(encodeValue(value)) } + /** + * Create a [Constant] instance for a [Number] value. + * + * @param value The [Number] value. + * @return A new [Constant] instance. + */ @JvmStatic fun of(value: Number): Constant { return ValueConstant(encodeValue(value)) } + /** + * Create a [Constant] instance for a [Date] value. + * + * @param value The [Date] value. + * @return A new [Constant] instance. + */ @JvmStatic fun of(value: Date): Constant { return ValueConstant(encodeValue(value)) } + /** + * Create a [Constant] instance for a [Timestamp] value. + * + * @param value The [Timestamp] value. + * @return A new [Constant] instance. + */ @JvmStatic fun of(value: Timestamp): Constant { return ValueConstant(encodeValue(value)) } + /** + * Create a [Constant] instance for a [Boolean] value. + * + * @param value The [Boolean] value. + * @return A new [Constant] instance. + */ @JvmStatic fun of(value: Boolean): Constant { return ValueConstant(encodeValue(value)) } + /** + * Create a [Constant] instance for a [GeoPoint] value. + * + * @param value The [GeoPoint] value. + * @return A new [Constant] instance. + */ @JvmStatic fun of(value: GeoPoint): Constant { return ValueConstant(encodeValue(value)) } + /** + * Create a [Constant] instance for a [Blob] value. + * + * @param value The [Blob] value. + * @return A new [Constant] instance. + */ @JvmStatic fun of(value: Blob): Constant { return ValueConstant(encodeValue(value)) } + /** + * Create a [Constant] instance for a [DocumentReference] value. + * + * @param ref The [DocumentReference] value. + * @return A new [Constant] instance. + */ @JvmStatic fun of(ref: DocumentReference): Constant { return object : Constant() { @@ -83,24 +136,47 @@ abstract class Constant internal constructor() : Expr() { } } + /** + * Create a [Constant] instance for a [VectorValue] value. + * + * @param value The [VectorValue] value. + * @return A new [Constant] instance. + */ @JvmStatic fun of(value: VectorValue): Constant { return ValueConstant(encodeValue(value)) } + /** + * [Constant] instance for a null value. + * + * @return A [Constant] instance. + */ @JvmStatic fun nullValue(): Constant { return NULL } + /** + * Create a vector [Constant] instance for a [DoubleArray] value. + * + * @param vector The [VectorValue] value. + * @return A new [Constant] instance. + */ @JvmStatic - fun vector(value: DoubleArray): Constant { - return ValueConstant(Values.encodeVectorValue(value)) + fun vector(vector: DoubleArray): Constant { + return ValueConstant(Values.encodeVectorValue(vector)) } + /** + * Create a vector [Constant] instance for a [VectorValue] value. + * + * @param vector The [VectorValue] value. + * @return A new [Constant] instance. + */ @JvmStatic - fun vector(value: VectorValue): Constant { - return ValueConstant(encodeValue(value)) + fun vector(vector: VectorValue): Constant { + return ValueConstant(encodeValue(vector)) } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt index 0461718d7fa..afcb2797df2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt @@ -20,6 +20,7 @@ import com.google.firestore.v1.Value class AggregateWithAlias internal constructor(internal val alias: String, internal val expr: AggregateFunction) +/** A class that represents an aggregate function. */ class AggregateFunction private constructor(private val name: String, private val params: Array) { private constructor(name: String) : this(name, emptyArray()) @@ -55,6 +56,13 @@ private constructor(private val name: String, private val params: ArrayExample: * - *
{@code // Calculate the total price and assign it the alias "totalPrice" and add it to the
+   * 
 // Calculate the total price and assign it the alias "totalPrice" and add it to the
+   *
    * output. firestore.pipeline().collection("items")
-   * .addFields(Field.of("price").multiply(Field.of("quantity")).as("totalPrice")); }
+ * .addFields(Field.of("price").multiply(Field.of("quantity")).as("totalPrice"));
* * @param alias The alias to assign to this expression. - * @return A new {@code Selectable} (typically an {@link ExprWithAlias}) that wraps this - * ``` - * expression and associates it with the provided alias. - * ``` + * @return A new [Selectable] (typically an [ExprWithAlias]) that wraps this expression and + * associates it with the provided alias. */ open fun alias(alias: String) = ExprWithAlias(alias, this) @@ -340,6 +354,7 @@ abstract class Expr internal constructor() { internal abstract fun toProto(userDataReader: UserDataReader): Value } +/** Expressions that have an alias are [Selectable] */ abstract class Selectable : Expr() { internal abstract fun getAlias(): String internal abstract fun getExpr(): Expr @@ -356,6 +371,7 @@ abstract class Selectable : Expr() { } } +/** Represents an expression that will be given the alias in the output document. */ class ExprWithAlias internal constructor(private val alias: String, private val expr: Expr) : Selectable() { override fun getAlias() = alias @@ -363,10 +379,33 @@ class ExprWithAlias internal constructor(private val alias: String, private val override fun toProto(userDataReader: UserDataReader): Value = expr.toProto(userDataReader) } +/** + * Represents a reference to a field in a Firestore document. + * + * [Field] references are used to access document field values in expressions and to specify fields + * for sorting, filtering, and projecting data in Firestore pipelines. + * + * You can create a [Field] instance using the static [of] method: + */ class Field internal constructor(private val fieldPath: ModelFieldPath) : Selectable() { companion object { + + /** + * An expression that returns the document ID. + * + * @return An [Field] representing the document ID. + */ @JvmField val DOCUMENT_ID: Field = of(FieldPath.documentId()) + /** + * Creates a [Field] instance representing the field at the given path. + * + * The path can be a simple field name (e.g., "name") or a dot-separated path to a nested field + * (e.g., "address.city"). + * + * @param name The path to the field. + * @return A new [Field] instance representing the specified path. + */ @JvmStatic fun of(name: String): Field { if (name == DocumentKey.KEY_FIELD_NAME) { @@ -375,6 +414,15 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select return Field(FieldPath.fromDotSeparatedPath(name).internalPath) } + /** + * Creates a [Field] instance representing the field at the given path. + * + * The path can be a simple field name (e.g., "name") or a dot-separated path to a nested field + * (e.g., "address.city"). + * + * @param fieldPath The [FieldPath] to the field. + * @return A new [Field] instance representing the specified path. + */ @JvmStatic fun of(fieldPath: FieldPath): Field { return Field(fieldPath.internalPath) @@ -382,6 +430,7 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select } override fun getAlias(): String = fieldPath.canonicalString() + override fun getExpr(): Expr = this override fun toProto(userDataReader: UserDataReader) = toProto() @@ -395,6 +444,14 @@ internal class ListOfExprs(private val expressions: Array) : Expr() { encodeValue(expressions.map { it.toProto(userDataReader) }) } +/** + * This class defines the base class for Firestore [Pipeline] functions, which can be evaluated + * within pipeline execution. + * + * Typically, you would not use this class or its children directly. Use either the functions like + * [and], [eq], or the methods on [Expr] ([Expr.eq]), [Expr.lt], etc) to construct new + * [FunctionExpr] instances. + */ open class FunctionExpr protected constructor(private val name: String, private val params: Array) : Expr() { private constructor( @@ -1023,6 +1080,7 @@ protected constructor(private val name: String, private val params: Array) : FunctionExpr(name, params) { internal constructor( @@ -1054,19 +1112,50 @@ class BooleanExpr internal constructor(name: String, params: Array) : fun cond(then: Any, otherwise: Any) = cond(this, then, otherwise) } +/** + * Represents an ordering criterion for sorting documents in a Firestore pipeline. + * + * You create [Ordering] instances using the [ascending] and [descending] helper methods. + */ class Ordering private constructor(val expr: Expr, private val dir: Direction) { companion object { + + /** + * Create an [Ordering] that sorts documents in ascending order based on value of [expr]. + * + * @param expr The order is based on the evaluation of the [Expr]. + * @return A new [Ordering] object with ascending sort by [expr]. + */ @JvmStatic fun ascending(expr: Expr): Ordering = Ordering(expr, Direction.ASCENDING) + /** + * Creates an [Ordering] that sorts documents in ascending order based on field. + * + * @param fieldName The name of field to sort documents. + * @return A new [Ordering] object with ascending sort by field. + */ @JvmStatic fun ascending(fieldName: String): Ordering = Ordering(Field.of(fieldName), Direction.ASCENDING) + /** + * Create an [Ordering] that sorts documents in descending order based on value of [expr]. + * + * @param expr The order is based on the evaluation of the [Expr]. + * @return A new [Ordering] object with descending sort by [expr]. + */ @JvmStatic fun descending(expr: Expr): Ordering = Ordering(expr, Direction.DESCENDING) + /** + * Creates an [Ordering] that sorts documents in descending order based on field. + * + * @param fieldName The name of field to sort documents. + * @return A new [Ordering] object with descending sort by field. + */ @JvmStatic fun descending(fieldName: String): Ordering = Ordering(Field.of(fieldName), Direction.DESCENDING) } + private class Direction private constructor(val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) companion object { @@ -1074,6 +1163,7 @@ class Ordering private constructor(val expr: Expr, private val dir: Direction) { val DESCENDING = Direction("descending") } } + internal fun toProto(userDataReader: UserDataReader): Value = Value.newBuilder() .setMapValue( @@ -1083,6 +1173,14 @@ class Ordering private constructor(val expr: Expr, private val dir: Direction) { ) .build() + /** + * Create an order that is in reverse. + * + * If the previous [Ordering] was ascending, then the new [Ordering] will be descending. Likewise, + * if the previous [Ordering] was descending, then the new [Ordering] will be ascending. + * + * @return New [Ordering] object that is has order reversed. + */ fun reverse(): Ordering = Ordering(expr, if (dir == Direction.ASCENDING) Direction.DESCENDING else Direction.ASCENDING) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index 2083ecc0837..57293c47bad 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -37,17 +37,60 @@ internal constructor(protected val name: String, internal val options: InternalO protected fun with(key: String, value: Value): T = self(options.with(key, value)) + /** + * Specify named [String] parameter + * + * @param key The name of parameter + * @param value The [String] value of parameter + * @return New stage with named parameter. + */ fun with(key: String, value: String): T = with(key, Values.encodeValue(value)) + /** + * Specify named [Boolean] parameter + * + * @param key The name of parameter + * @param value The [Boolean] value of parameter + * @return New stage with named parameter. + */ fun with(key: String, value: Boolean): T = with(key, Values.encodeValue(value)) + /** + * Specify named [Long] parameter + * + * @param key The name of parameter + * @param value The [Long] value of parameter + * @return New stage with named parameter. + */ fun with(key: String, value: Long): T = with(key, Values.encodeValue(value)) + /** + * Specify named [Double] parameter + * + * @param key The name of parameter + * @param value The [Double] value of parameter + * @return New stage with named parameter. + */ fun with(key: String, value: Double): T = with(key, Values.encodeValue(value)) + /** + * Specify named [Field] parameter + * + * @param key The name of parameter + * @param value The [Field] value of parameter + * @return New stage with named parameter. + */ fun with(key: String, value: Field): T = with(key, value.toProto()) } +/** + * Adds a stage to the pipeline by specifying the stage name as an argument. This does not offer any + * type safety on the stage params and requires the caller to know the order (and optionally names) + * of parameters accepted by the stage. + * + * This class provides a way to call stages that are supported by the Firestore backend but that are + * not implemented in the SDK version being used. + */ class GenericStage internal constructor( name: String, @@ -55,11 +98,23 @@ internal constructor( options: InternalOptions = InternalOptions.EMPTY ) : Stage(name, options) { companion object { + /** + * Specify name of stage + * + * @param name The unique name of the stage to add. + * @return [GenericStage] with specified parameters. + */ @JvmStatic fun ofName(name: String) = GenericStage(name, emptyList(), InternalOptions.EMPTY) } override fun self(options: InternalOptions) = GenericStage(name, arguments, options) + /** + * Specify arguments to stage. + * + * @param arguments A list of ordered parameters to configure the stage's behavior. + * @return [GenericStage] with specified parameters. + */ fun withArguments(vararg arguments: Any): GenericStage = GenericStage(name, arguments.map(GenericArg::from), options) @@ -186,7 +241,7 @@ internal constructor( * AggregateFunction} with an alias for the accumulated results. * @param additionalAccumulators The [AggregateWithAlias] expressions, each wrapping an * [AggregateFunction] with an alias for the accumulated results. - * @return Aggregate Stage with specified accumulators. + * @return [AggregateStage] with specified accumulators. */ @JvmStatic fun withAccumulators( @@ -208,7 +263,7 @@ internal constructor( * @param groupField The [String] representing field name. * @param additionalGroups The [Selectable] expressions to consider when determining group value * combinations or [String]s representing field names. - * @return Aggregate Stage with specified groups. + * @return [AggregateStage] with specified groups. */ fun withGroups(groupField: String, vararg additionalGroups: Any) = withGroups(Field.of(groupField), additionalGroups) @@ -220,7 +275,7 @@ internal constructor( * combinations. * @param additionalGroups The [Selectable] expressions to consider when determining group value * combinations or [String]s representing field names. - * @return Aggregate Stage with specified groups. + * @return [AggregateStage] with specified groups. */ fun withGroups(group: Selectable, vararg additionalGroups: Any) = AggregateStage( @@ -424,7 +479,7 @@ internal constructor( internal class ReplaceStage internal constructor( - private val field: Selectable, + private val mapValue: Expr, private val mode: Mode, options: InternalOptions = InternalOptions.EMPTY ) : Stage("replace", options) { @@ -436,11 +491,19 @@ internal constructor( val MERGE_PREFER_PARENT = Mode("merge_prefer_parent") } } - override fun self(options: InternalOptions) = ReplaceStage(field, mode, options) + override fun self(options: InternalOptions) = ReplaceStage(mapValue, mode, options) override fun args(userDataReader: UserDataReader): Sequence = - sequenceOf(field.toProto(userDataReader), mode.proto) + sequenceOf(mapValue.toProto(userDataReader), mode.proto) } +/** + * Performs a pseudo-random sampling of the input documents. + * + * The documents produced from this stage are non-deterministic, running the same query over the + * same dataset multiple times will produce different results. There are two different ways to + * dictate how the sample is calculated either by specifying a target output size, or by specifying + * a target percentage of the input size. + */ class SampleStage private constructor( private val size: Number, @@ -456,8 +519,29 @@ private constructor( } } companion object { + /** + * Creates [SampleStage] with size limited to percentage of prior stages results. + * + * The [percentage] parameter is the target percentage (between 0.0 & 1.0) of the number of + * input documents to produce. Each input document is independently selected against the given + * percentage. As a result the output size will be approximately documents * [percentage]. + * + * @param percentage The percentage of the prior stages documents to emit. + * @return [SampleStage] with specified [percentage]. + */ @JvmStatic fun withPercentage(percentage: Double) = SampleStage(percentage, Mode.PERCENT) + /** + * Creates [SampleStage] with size limited to number of documents. + * + * The [documents] parameter represents the target number of documents to produce and must be a + * non-negative integer value. If the previous stage produces less than size documents, the + * entire previous results are returned. If the previous stage produces more than size, this + * outputs a sample of exactly size entries where any sample is equally likely. + * + * @param documents The number of documents to emit. + * @return [SampleStage] with specified [documents]. + */ @JvmStatic fun withDocLimit(documents: Int) = SampleStage(documents, Mode.DOCUMENTS) } override fun args(userDataReader: UserDataReader): Sequence = @@ -474,20 +558,60 @@ internal constructor( sequenceOf(Value.newBuilder().setPipelineValue(other.toPipelineProto()).build()) } +/** + * Takes a specified array from the input documents and outputs a document for each element with the + * element stored in a field with name specified by the alias. + */ class UnnestStage internal constructor( private val selectable: Selectable, options: InternalOptions = InternalOptions.EMPTY ) : Stage("unnest", options) { companion object { - @JvmStatic fun withField(selectable: Selectable) = UnnestStage(selectable) + + /** + * Creates [UnnestStage] with input array and alias specified. + * + * For each document emitted by the prior stage, this stage will emit zero or more augmented + * documents. The input array is found in parameter [arrayWithAlias], which can be an [Expr] + * with an alias specified via [Expr.alias], or a [Field] that can also have alias specified. + * For each element of the input array, an augmented document will be produced. The element of + * input array will be stored in a field with name specified by the alias of the + * [arrayWithAlias] parameter. If the [arrayWithAlias] is a [Field] with no alias, then the + * original array field will be replaced with the individual element. + * + * @param arrayWithAlias The input array with field alias to store output element of array. + * @return [SampleStage] with input array and alias specified. + */ + @JvmStatic fun withField(arrayWithAlias: Selectable) = UnnestStage(arrayWithAlias) + + /** + * Creates [UnnestStage] with input array and alias specified. + * + * For each document emitted by the prior stage, this stage will emit zero or more augmented + * documents. The input array found in the previous stage document field specified by the + * [arrayField] parameter, will for each element of the input array produce an augmented + * document. The element of the input array will be stored in a field with name specified by + * [alias] parameter on the augmented document. + * + * @return [SampleStage] with input array and alias specified. + */ @JvmStatic - fun withField(field: String, alias: String): UnnestStage = - UnnestStage(Field.of(field).alias(alias)) + fun withField(arrayField: String, alias: String): UnnestStage = + UnnestStage(Field.of(arrayField).alias(alias)) } override fun self(options: InternalOptions) = UnnestStage(selectable, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(selectable.getAlias()), selectable.toProto(userDataReader)) + /** + * Adds index field to emitted documents + * + * A field with name specified in [indexField] will be added to emitted document. The index is a + * numeric value that corresponds to array index of the element from input array. + * + * @param indexField The field name of index field. + * @return [SampleStage] that includes specified index field. + */ fun withIndexField(indexField: String): UnnestStage = with("index_field", indexField) } From df1e7194900e83ab5be10e99f030faba93a0bbf1 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 10 Apr 2025 19:05:26 -0400 Subject: [PATCH 039/152] api.txt --- firebase-firestore/api.txt | 899 +++++++++++++++++++------------------ 1 file changed, 455 insertions(+), 444 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index c3ef4cac64a..78ce19cd2aa 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -420,49 +420,55 @@ package com.google.firebase.firestore { } public final class Pipeline { - method public com.google.firebase.firestore.Pipeline addFields(com.google.firebase.firestore.pipeline.Selectable... fields); + method public com.google.firebase.firestore.Pipeline addFields(com.google.firebase.firestore.pipeline.Selectable field, com.google.firebase.firestore.pipeline.Selectable... additionalFields); method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateStage aggregateStage); - method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateWithAlias... accumulators); - method public com.google.firebase.firestore.Pipeline distinct(com.google.firebase.firestore.pipeline.Selectable... groups); - method public com.google.firebase.firestore.Pipeline distinct(java.lang.Object... groups); - method public com.google.firebase.firestore.Pipeline distinct(java.lang.String... groups); + method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateWithAlias accumulator, com.google.firebase.firestore.pipeline.AggregateWithAlias... additionalAccumulators); + method public com.google.firebase.firestore.Pipeline distinct(com.google.firebase.firestore.pipeline.Selectable group, java.lang.Object... additionalGroups); + method public com.google.firebase.firestore.Pipeline distinct(String groupField, java.lang.Object... additionalGroups); method public com.google.android.gms.tasks.Task execute(); - method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Expr property, double[] vector, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); - method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Expr property, double[] vector, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure, com.google.firebase.firestore.pipeline.FindNearestOptions options); + method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.FindNearestStage stage); + method public com.google.firebase.firestore.Pipeline findNearest(String vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.Pipeline findNearest(String vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline genericStage(com.google.firebase.firestore.pipeline.GenericStage stage); method public com.google.firebase.firestore.Pipeline genericStage(String name, java.lang.Object... arguments); method public com.google.firebase.firestore.Pipeline limit(int limit); method public com.google.firebase.firestore.Pipeline offset(int offset); - method public com.google.firebase.firestore.Pipeline removeFields(com.google.firebase.firestore.pipeline.Field... fields); - method public com.google.firebase.firestore.Pipeline removeFields(java.lang.String... fields); - method public com.google.firebase.firestore.Pipeline replace(com.google.firebase.firestore.pipeline.Selectable field); + method public com.google.firebase.firestore.Pipeline removeFields(com.google.firebase.firestore.pipeline.Field field, com.google.firebase.firestore.pipeline.Field... additionalFields); + method public com.google.firebase.firestore.Pipeline removeFields(String field, java.lang.String... additionalFields); + method public com.google.firebase.firestore.Pipeline replace(com.google.firebase.firestore.pipeline.Expr mapValue); method public com.google.firebase.firestore.Pipeline replace(String field); method public com.google.firebase.firestore.Pipeline sample(com.google.firebase.firestore.pipeline.SampleStage sample); method public com.google.firebase.firestore.Pipeline sample(int documents); - method public com.google.firebase.firestore.Pipeline select(com.google.firebase.firestore.pipeline.Selectable... fields); - method public com.google.firebase.firestore.Pipeline select(java.lang.Object... fields); - method public com.google.firebase.firestore.Pipeline select(java.lang.String... fields); - method public com.google.firebase.firestore.Pipeline sort(com.google.firebase.firestore.pipeline.Ordering... orders); + method public com.google.firebase.firestore.Pipeline select(com.google.firebase.firestore.pipeline.Selectable selection, java.lang.Object... additionalSelections); + method public com.google.firebase.firestore.Pipeline select(String fieldName, java.lang.Object... additionalSelections); + method public com.google.firebase.firestore.Pipeline sort(com.google.firebase.firestore.pipeline.Ordering order, com.google.firebase.firestore.pipeline.Ordering... additionalOrders); method public com.google.firebase.firestore.Pipeline union(com.google.firebase.firestore.Pipeline other); - method public com.google.firebase.firestore.Pipeline unnest(com.google.firebase.firestore.pipeline.Selectable selectable); - method public com.google.firebase.firestore.Pipeline unnest(com.google.firebase.firestore.pipeline.Selectable selectable, com.google.firebase.firestore.pipeline.UnnestOptions options); - method public com.google.firebase.firestore.Pipeline unnest(String field, String alias); - method public com.google.firebase.firestore.Pipeline unnest(String field, String alias, com.google.firebase.firestore.pipeline.UnnestOptions options); + method public com.google.firebase.firestore.Pipeline unnest(com.google.firebase.firestore.pipeline.Selectable arrayWithAlias); + method public com.google.firebase.firestore.Pipeline unnest(com.google.firebase.firestore.pipeline.UnnestStage unnestStage); + method public com.google.firebase.firestore.Pipeline unnest(String arrayField, String alias); method public com.google.firebase.firestore.Pipeline where(com.google.firebase.firestore.pipeline.BooleanExpr condition); } public final class PipelineResult { method public Object? get(com.google.firebase.firestore.FieldPath fieldPath); method public Object? get(String field); + method public com.google.firebase.Timestamp? getCreateTime(); method public java.util.Map getData(); method public String? getId(); method public com.google.firebase.firestore.DocumentReference? getRef(); + method public com.google.firebase.Timestamp? getUpdateTime(); + property public final com.google.firebase.Timestamp? createTime; property public final com.google.firebase.firestore.DocumentReference? ref; + property public final com.google.firebase.Timestamp? updateTime; } public final class PipelineSnapshot implements java.lang.Iterable kotlin.jvm.internal.markers.KMappedMarker { + method public com.google.firebase.Timestamp getExecutionTime(); method public java.util.List getResults(); method public java.util.Iterator iterator(); + property public final com.google.firebase.Timestamp executionTime; property public final java.util.List results; } @@ -470,6 +476,8 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Pipeline collection(com.google.firebase.firestore.CollectionReference ref); method public com.google.firebase.firestore.Pipeline collection(String path); method public com.google.firebase.firestore.Pipeline collectionGroup(String collectionId); + method public com.google.firebase.firestore.Pipeline convertFrom(com.google.firebase.firestore.AggregateQuery aggregateQuery); + method public com.google.firebase.firestore.Pipeline convertFrom(com.google.firebase.firestore.Query query); method public com.google.firebase.firestore.Pipeline database(); method public com.google.firebase.firestore.Pipeline documents(com.google.firebase.firestore.DocumentReference... documents); method public com.google.firebase.firestore.Pipeline documents(java.lang.String... documents); @@ -502,7 +510,6 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Query orderBy(com.google.firebase.firestore.FieldPath, com.google.firebase.firestore.Query.Direction); method public com.google.firebase.firestore.Query orderBy(String); method public com.google.firebase.firestore.Query orderBy(String, com.google.firebase.firestore.Query.Direction); - method public com.google.firebase.firestore.Pipeline pipeline(); method public com.google.firebase.firestore.Query startAfter(com.google.firebase.firestore.DocumentSnapshot); method public com.google.firebase.firestore.Query startAfter(java.lang.Object!...!); method public com.google.firebase.firestore.Query startAt(com.google.firebase.firestore.DocumentSnapshot); @@ -668,69 +675,59 @@ package com.google.firebase.firestore.ktx { package com.google.firebase.firestore.pipeline { - public abstract class AbstractOptions> { - method public final T with(String key, boolean value); - method public final T with(String key, com.google.firebase.firestore.pipeline.Field value); - method public final T with(String key, double value); - method protected final T with(String key, error.NonExistentClass value); - method public final T with(String key, String value); - method public final T with(String key, long value); - } - - public final class AggregateExpr { - method public com.google.firebase.firestore.pipeline.AggregateWithAlias as(String alias); - method public static com.google.firebase.firestore.pipeline.AggregateExpr avg(com.google.firebase.firestore.pipeline.Expr expr); - method public static com.google.firebase.firestore.pipeline.AggregateExpr avg(String fieldName); - method public static com.google.firebase.firestore.pipeline.AggregateExpr count(com.google.firebase.firestore.pipeline.Expr expr); - method public static com.google.firebase.firestore.pipeline.AggregateExpr count(String fieldName); - method public static com.google.firebase.firestore.pipeline.AggregateExpr countAll(); - method public static com.google.firebase.firestore.pipeline.AggregateExpr countIf(com.google.firebase.firestore.pipeline.BooleanExpr condition); - method public static com.google.firebase.firestore.pipeline.AggregateExpr max(com.google.firebase.firestore.pipeline.Expr expr); - method public static com.google.firebase.firestore.pipeline.AggregateExpr max(String fieldName); - method public static com.google.firebase.firestore.pipeline.AggregateExpr min(com.google.firebase.firestore.pipeline.Expr expr); - method public static com.google.firebase.firestore.pipeline.AggregateExpr min(String fieldName); - method public static com.google.firebase.firestore.pipeline.AggregateExpr sum(com.google.firebase.firestore.pipeline.Expr expr); - method public static com.google.firebase.firestore.pipeline.AggregateExpr sum(String fieldName); - field public static final com.google.firebase.firestore.pipeline.AggregateExpr.Companion Companion; - } - - public static final class AggregateExpr.Companion { - method public com.google.firebase.firestore.pipeline.AggregateExpr avg(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.AggregateExpr avg(String fieldName); - method public com.google.firebase.firestore.pipeline.AggregateExpr count(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.AggregateExpr count(String fieldName); - method public com.google.firebase.firestore.pipeline.AggregateExpr countAll(); - method public com.google.firebase.firestore.pipeline.AggregateExpr countIf(com.google.firebase.firestore.pipeline.BooleanExpr condition); - method public com.google.firebase.firestore.pipeline.AggregateExpr max(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.AggregateExpr max(String fieldName); - method public com.google.firebase.firestore.pipeline.AggregateExpr min(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.AggregateExpr min(String fieldName); - method public com.google.firebase.firestore.pipeline.AggregateExpr sum(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.AggregateExpr sum(String fieldName); - } - - public final class AggregateStage extends com.google.firebase.firestore.pipeline.Stage { - method public static com.google.firebase.firestore.pipeline.AggregateStage withAccumulators(com.google.firebase.firestore.pipeline.AggregateWithAlias... accumulators); - method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(com.google.firebase.firestore.pipeline.Selectable... groups); - method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(java.lang.Object... selectable); - method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(java.lang.String... fields); + public final class AggregateFunction { + method public com.google.firebase.firestore.pipeline.AggregateWithAlias alias(String alias); + method public static com.google.firebase.firestore.pipeline.AggregateFunction avg(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateFunction avg(String fieldName); + method public static com.google.firebase.firestore.pipeline.AggregateFunction count(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateFunction count(String fieldName); + method public static com.google.firebase.firestore.pipeline.AggregateFunction countAll(); + method public static com.google.firebase.firestore.pipeline.AggregateFunction countIf(com.google.firebase.firestore.pipeline.BooleanExpr condition); + method public static com.google.firebase.firestore.pipeline.AggregateFunction generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public static com.google.firebase.firestore.pipeline.AggregateFunction max(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateFunction max(String fieldName); + method public static com.google.firebase.firestore.pipeline.AggregateFunction min(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateFunction min(String fieldName); + method public static com.google.firebase.firestore.pipeline.AggregateFunction sum(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateFunction sum(String fieldName); + field public static final com.google.firebase.firestore.pipeline.AggregateFunction.Companion Companion; + } + + public static final class AggregateFunction.Companion { + method public com.google.firebase.firestore.pipeline.AggregateFunction avg(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateFunction avg(String fieldName); + method public com.google.firebase.firestore.pipeline.AggregateFunction count(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateFunction count(String fieldName); + method public com.google.firebase.firestore.pipeline.AggregateFunction countAll(); + method public com.google.firebase.firestore.pipeline.AggregateFunction countIf(com.google.firebase.firestore.pipeline.BooleanExpr condition); + method public com.google.firebase.firestore.pipeline.AggregateFunction generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public com.google.firebase.firestore.pipeline.AggregateFunction max(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateFunction max(String fieldName); + method public com.google.firebase.firestore.pipeline.AggregateFunction min(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateFunction min(String fieldName); + method public com.google.firebase.firestore.pipeline.AggregateFunction sum(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateFunction sum(String fieldName); + } + + public final class AggregateStage extends com.google.firebase.firestore.pipeline.Stage { + method public static com.google.firebase.firestore.pipeline.AggregateStage withAccumulators(com.google.firebase.firestore.pipeline.AggregateWithAlias accumulator, com.google.firebase.firestore.pipeline.AggregateWithAlias... additionalAccumulators); + method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(com.google.firebase.firestore.pipeline.Selectable group, java.lang.Object... additionalGroups); + method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(String groupField, java.lang.Object... additionalGroups); field public static final com.google.firebase.firestore.pipeline.AggregateStage.Companion Companion; } public static final class AggregateStage.Companion { - method public com.google.firebase.firestore.pipeline.AggregateStage withAccumulators(com.google.firebase.firestore.pipeline.AggregateWithAlias... accumulators); + method public com.google.firebase.firestore.pipeline.AggregateStage withAccumulators(com.google.firebase.firestore.pipeline.AggregateWithAlias accumulator, com.google.firebase.firestore.pipeline.AggregateWithAlias... additionalAccumulators); } public final class AggregateWithAlias { } - public final class BooleanExpr extends com.google.firebase.firestore.pipeline.Function { - method public com.google.firebase.firestore.pipeline.AggregateExpr countIf(); + public final class BooleanExpr extends com.google.firebase.firestore.pipeline.FunctionExpr { + method public com.google.firebase.firestore.pipeline.FunctionExpr cond(com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); + method public com.google.firebase.firestore.pipeline.FunctionExpr cond(Object then, Object otherwise); + method public com.google.firebase.firestore.pipeline.AggregateFunction countIf(); method public static com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); - method public com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.Expr then); - method public com.google.firebase.firestore.pipeline.Function ifThen(Object then); - method public com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr else); - method public com.google.firebase.firestore.pipeline.Function ifThenElse(Object then, Object else); method public com.google.firebase.firestore.pipeline.BooleanExpr not(); field public static final com.google.firebase.firestore.pipeline.BooleanExpr.Companion Companion; } @@ -750,8 +747,8 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.Constant of(Number value); method public static final com.google.firebase.firestore.pipeline.Constant of(String value); method public static final com.google.firebase.firestore.pipeline.Constant of(java.util.Date value); - method public static final com.google.firebase.firestore.pipeline.Constant vector(com.google.firebase.firestore.VectorValue value); - method public static final com.google.firebase.firestore.pipeline.Constant vector(double[] value); + method public static final com.google.firebase.firestore.pipeline.Constant vector(com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.Constant vector(double[] vector); field public static final com.google.firebase.firestore.pipeline.Constant.Companion Companion; } @@ -766,54 +763,54 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Constant of(Number value); method public com.google.firebase.firestore.pipeline.Constant of(String value); method public com.google.firebase.firestore.pipeline.Constant of(java.util.Date value); - method public com.google.firebase.firestore.pipeline.Constant vector(com.google.firebase.firestore.VectorValue value); - method public com.google.firebase.firestore.pipeline.Constant vector(double[] value); + method public com.google.firebase.firestore.pipeline.Constant vector(com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.Constant vector(double[] vector); } public abstract class Expr { - method public final com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.Function add(Object other); - method public final com.google.firebase.firestore.pipeline.Function arrayConcat(com.google.firebase.firestore.pipeline.Expr... arrays); - method public final com.google.firebase.firestore.pipeline.Function arrayConcat(java.util.List arrays); + method public final com.google.firebase.firestore.pipeline.FunctionExpr add(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr add(Object other); + method public com.google.firebase.firestore.pipeline.ExprWithAlias alias(String alias); + method public final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(com.google.firebase.firestore.pipeline.Expr... arrays); + method public final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(java.util.List arrays); method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr value); method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(Object value); method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(java.util.List values); method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(java.util.List values); - method public final com.google.firebase.firestore.pipeline.Function arrayLength(); - method public final com.google.firebase.firestore.pipeline.Function arrayReverse(); - method public com.google.firebase.firestore.pipeline.ExprWithAlias as(String alias); + method public final com.google.firebase.firestore.pipeline.FunctionExpr arrayLength(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr arrayReverse(); method public final com.google.firebase.firestore.pipeline.Ordering ascending(); - method public final com.google.firebase.firestore.pipeline.AggregateExpr avg(); - method public final com.google.firebase.firestore.pipeline.Function bitAnd(com.google.firebase.firestore.pipeline.Expr right); - method public final com.google.firebase.firestore.pipeline.Function bitAnd(Object right); - method public final com.google.firebase.firestore.pipeline.Function bitLeftShift(com.google.firebase.firestore.pipeline.Expr numberExpr); - method public final com.google.firebase.firestore.pipeline.Function bitLeftShift(int number); - method public final com.google.firebase.firestore.pipeline.Function bitNot(); - method public final com.google.firebase.firestore.pipeline.Function bitOr(com.google.firebase.firestore.pipeline.Expr right); - method public final com.google.firebase.firestore.pipeline.Function bitOr(Object right); - method public final com.google.firebase.firestore.pipeline.Function bitRightShift(com.google.firebase.firestore.pipeline.Expr numberExpr); - method public final com.google.firebase.firestore.pipeline.Function bitRightShift(int number); - method public final com.google.firebase.firestore.pipeline.Function bitXor(com.google.firebase.firestore.pipeline.Expr right); - method public final com.google.firebase.firestore.pipeline.Function bitXor(Object right); - method public final com.google.firebase.firestore.pipeline.Function byteLength(); - method public final com.google.firebase.firestore.pipeline.Function charLength(); - method public final com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector); - method public final com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.VectorValue vector); - method public final com.google.firebase.firestore.pipeline.Function cosineDistance(double[] vector); + method public final com.google.firebase.firestore.pipeline.AggregateFunction avg(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(com.google.firebase.firestore.pipeline.Expr right); + method public final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(Object right); + method public final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(com.google.firebase.firestore.pipeline.Expr numberExpr); + method public final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(int number); + method public final com.google.firebase.firestore.pipeline.FunctionExpr bitNot(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(com.google.firebase.firestore.pipeline.Expr right); + method public final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(Object right); + method public final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(com.google.firebase.firestore.pipeline.Expr numberExpr); + method public final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(int number); + method public final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(com.google.firebase.firestore.pipeline.Expr right); + method public final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(Object right); + method public final com.google.firebase.firestore.pipeline.FunctionExpr byteLength(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr charLength(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector); + method public final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.VectorValue vector); + method public final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(double[] vector); method public final com.google.firebase.firestore.pipeline.Ordering descending(); - method public final com.google.firebase.firestore.pipeline.Function divide(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.Function divide(Object other); - method public final com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector); - method public final com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.VectorValue vector); - method public final com.google.firebase.firestore.pipeline.Function dotProduct(double[] vector); + method public final com.google.firebase.firestore.pipeline.FunctionExpr divide(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr divide(Object other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector); + method public final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.VectorValue vector); + method public final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(double[] vector); method public final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr suffix); method public final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String suffix); method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr other); method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(Object other); method public final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(java.util.List values); - method public final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector); - method public final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.VectorValue vector); - method public final com.google.firebase.firestore.pipeline.Function euclideanDistance(double[] vector); + method public final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector); + method public final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.VectorValue vector); + method public final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(double[] vector); method public final com.google.firebase.firestore.pipeline.BooleanExpr exists(); method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr other); method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(Object other); @@ -825,22 +822,25 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.BooleanExpr isNull(); method public final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr pattern); method public final com.google.firebase.firestore.pipeline.BooleanExpr like(String pattern); - method public final com.google.firebase.firestore.pipeline.Function logicalMax(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.Function logicalMax(Object other); - method public final com.google.firebase.firestore.pipeline.Function logicalMin(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.Function logicalMin(Object other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(Object other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(Object other); method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr other); method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(Object other); method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr other); method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(Object other); - method public final com.google.firebase.firestore.pipeline.Function mapGet(com.google.firebase.firestore.pipeline.Expr key); - method public final com.google.firebase.firestore.pipeline.Function mapGet(String key); - method public final com.google.firebase.firestore.pipeline.AggregateExpr max(); - method public final com.google.firebase.firestore.pipeline.AggregateExpr min(); - method public final com.google.firebase.firestore.pipeline.Function mod(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.Function mod(Object other); - method public final com.google.firebase.firestore.pipeline.Function multiply(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.Function multiply(Object other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(com.google.firebase.firestore.pipeline.Expr key); + method public final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(String key); + method public final com.google.firebase.firestore.pipeline.FunctionExpr mapMerge(com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(com.google.firebase.firestore.pipeline.Expr key); + method public final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(String key); + method public final com.google.firebase.firestore.pipeline.AggregateFunction max(); + method public final com.google.firebase.firestore.pipeline.AggregateFunction min(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr mod(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr mod(Object other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr multiply(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr multiply(Object other); method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr other); method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(Object other); method public final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(java.util.List values); @@ -848,35 +848,35 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String pattern); method public final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr pattern); method public final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String pattern); - method public final com.google.firebase.firestore.pipeline.Function replaceAll(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public final com.google.firebase.firestore.pipeline.Function replaceAll(String find, String replace); - method public final com.google.firebase.firestore.pipeline.Function replaceFirst(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public final com.google.firebase.firestore.pipeline.Function replaceFirst(String find, String replace); - method public final com.google.firebase.firestore.pipeline.Function reverse(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public final com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(String find, String replace); + method public final com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public final com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(String find, String replace); + method public final com.google.firebase.firestore.pipeline.FunctionExpr reverse(); method public final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr prefix); method public final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String prefix); - method public final com.google.firebase.firestore.pipeline.Function strConcat(com.google.firebase.firestore.pipeline.Expr... expr); - method public final com.google.firebase.firestore.pipeline.Function strConcat(java.lang.Object... string); - method public final com.google.firebase.firestore.pipeline.Function strConcat(java.lang.String... string); + method public final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(com.google.firebase.firestore.pipeline.Expr... expr); + method public final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(java.lang.Object... string); + method public final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(java.lang.String... string); method public final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr substring); method public final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String substring); - method public final com.google.firebase.firestore.pipeline.Function subtract(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.Function subtract(Object other); - method public final com.google.firebase.firestore.pipeline.AggregateExpr sum(); - method public final com.google.firebase.firestore.pipeline.Function timestampAdd(com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public final com.google.firebase.firestore.pipeline.Function timestampAdd(String unit, double amount); - method public final com.google.firebase.firestore.pipeline.Function timestampSub(com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public final com.google.firebase.firestore.pipeline.Function timestampSub(String unit, double amount); - method public final com.google.firebase.firestore.pipeline.Function timestampToUnixMicros(); - method public final com.google.firebase.firestore.pipeline.Function timestampToUnixMillis(); - method public final com.google.firebase.firestore.pipeline.Function timestampToUnixSeconds(); - method public final com.google.firebase.firestore.pipeline.Function toLower(); - method public final com.google.firebase.firestore.pipeline.Function toUpper(); - method public final com.google.firebase.firestore.pipeline.Function trim(); - method public final com.google.firebase.firestore.pipeline.Function unixMicrosToTimestamp(); - method public final com.google.firebase.firestore.pipeline.Function unixMillisToTimestamp(); - method public final com.google.firebase.firestore.pipeline.Function unixSecondsToTimestamp(); - method public final com.google.firebase.firestore.pipeline.Function vectorLength(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr subtract(com.google.firebase.firestore.pipeline.Expr other); + method public final com.google.firebase.firestore.pipeline.FunctionExpr subtract(Object other); + method public final com.google.firebase.firestore.pipeline.AggregateFunction sum(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(String unit, double amount); + method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(String unit, double amount); + method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMicros(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMillis(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixSeconds(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr toLower(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr toUpper(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr trim(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr unixMicrosToTimestamp(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr unixMillisToTimestamp(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr unixSecondsToTimestamp(); + method public final com.google.firebase.firestore.pipeline.FunctionExpr vectorLength(); } public final class ExprWithAlias extends com.google.firebase.firestore.pipeline.Selectable { @@ -894,44 +894,45 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Field of(String name); } - public final class FindNearestOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { - method public com.google.firebase.firestore.pipeline.FindNearestOptions withDistanceField(com.google.firebase.firestore.pipeline.Field distanceField); - method public com.google.firebase.firestore.pipeline.FindNearestOptions withDistanceField(String distanceField); - method public com.google.firebase.firestore.pipeline.FindNearestOptions withLimit(long limit); - field public static final com.google.firebase.firestore.pipeline.FindNearestOptions.Companion Companion; - field public static final com.google.firebase.firestore.pipeline.FindNearestOptions DEFAULT; + public final class FindNearestStage extends com.google.firebase.firestore.pipeline.Stage { + method public static com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public static com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public static com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public static com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.pipeline.FindNearestStage withDistanceField(com.google.firebase.firestore.pipeline.Field distanceField); + method public com.google.firebase.firestore.pipeline.FindNearestStage withDistanceField(String distanceField); + method public com.google.firebase.firestore.pipeline.FindNearestStage withLimit(long limit); + field public static final com.google.firebase.firestore.pipeline.FindNearestStage.Companion Companion; } - public static final class FindNearestOptions.Companion { - } - - public final class FindNearestStage extends com.google.firebase.firestore.pipeline.Stage { + public static final class FindNearestStage.Companion { + method public com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); } public static final class FindNearestStage.DistanceMeasure { + field public static final error.NonExistentClass COSINE; field public static final com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure.Companion Companion; + field public static final error.NonExistentClass DOT_PRODUCT; + field public static final error.NonExistentClass EUCLIDEAN; } public static final class FindNearestStage.DistanceMeasure.Companion { - method public error.NonExistentClass getCOSINE(); - method public error.NonExistentClass getDOT_PRODUCT(); - method public error.NonExistentClass getEUCLIDEAN(); - property public final error.NonExistentClass COSINE; - property public final error.NonExistentClass DOT_PRODUCT; - property public final error.NonExistentClass EUCLIDEAN; - } - - public class Function extends com.google.firebase.firestore.pipeline.Expr { - ctor protected Function(String name, com.google.firebase.firestore.pipeline.Expr[] params); - method public static final com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.Function add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Function add(String fieldName, Object other); + } + + public class FunctionExpr extends com.google.firebase.firestore.pipeline.Expr { + ctor protected FunctionExpr(String name, com.google.firebase.firestore.pipeline.Expr[] params); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr add(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr add(String fieldName, Object other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); - method public static final com.google.firebase.firestore.pipeline.Function arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); - method public static final com.google.firebase.firestore.pipeline.Function arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); - method public static final com.google.firebase.firestore.pipeline.Function arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); - method public static final com.google.firebase.firestore.pipeline.Function arrayConcat(String fieldName, java.util.List arrays); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(String fieldName, java.util.List arrays); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, Object value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, com.google.firebase.firestore.pipeline.Expr value); @@ -940,52 +941,54 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String fieldName, java.util.List values); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String fieldName, java.util.List values); - method public static final com.google.firebase.firestore.pipeline.Function arrayLength(com.google.firebase.firestore.pipeline.Expr array); - method public static final com.google.firebase.firestore.pipeline.Function arrayLength(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function arrayReverse(com.google.firebase.firestore.pipeline.Expr array); - method public static final com.google.firebase.firestore.pipeline.Function arrayReverse(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.Function bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function bitAnd(String fieldName, Object right); - method public static final com.google.firebase.firestore.pipeline.Function bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.Function bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); - method public static final com.google.firebase.firestore.pipeline.Function bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.Function bitLeftShift(String fieldName, int number); - method public static final com.google.firebase.firestore.pipeline.Function bitNot(com.google.firebase.firestore.pipeline.Expr left); - method public static final com.google.firebase.firestore.pipeline.Function bitNot(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.Function bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function bitOr(String fieldName, Object right); - method public static final com.google.firebase.firestore.pipeline.Function bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.Function bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); - method public static final com.google.firebase.firestore.pipeline.Function bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.Function bitRightShift(String fieldName, int number); - method public static final com.google.firebase.firestore.pipeline.Function bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.Function bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function bitXor(String fieldName, Object right); - method public static final com.google.firebase.firestore.pipeline.Function byteLength(com.google.firebase.firestore.pipeline.Expr value); - method public static final com.google.firebase.firestore.pipeline.Function byteLength(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function charLength(com.google.firebase.firestore.pipeline.Expr value); - method public static final com.google.firebase.firestore.pipeline.Function charLength(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public static final com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, double[] vector); - method public static final com.google.firebase.firestore.pipeline.Function divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function divide(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.Function divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Function divide(String fieldName, Object other); - method public static final com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public static final com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public static final com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public static final com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public static final com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public static final com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, double[] vector); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayLength(com.google.firebase.firestore.pipeline.Expr array); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayLength(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayReverse(com.google.firebase.firestore.pipeline.Expr array); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayReverse(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(String fieldName, int number); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitNot(com.google.firebase.firestore.pipeline.Expr left); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitNot(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(String fieldName, int number); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr byteLength(com.google.firebase.firestore.pipeline.Expr value); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr byteLength(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr charLength(com.google.firebase.firestore.pipeline.Expr value); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr charLength(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object otherwise); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, double[] vector); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr divide(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr divide(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, double[] vector); method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr suffix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, String suffix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr suffix); @@ -996,14 +999,14 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(String fieldName, java.util.List values); - method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public static final com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, double[] vector); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, double[] vector); method public static final com.google.firebase.firestore.pipeline.BooleanExpr exists(com.google.firebase.firestore.pipeline.Expr expr); - method public static final com.google.firebase.firestore.pipeline.Function generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); @@ -1012,10 +1015,6 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, Object right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, Object right); - method public static final com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then); - method public static final com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then); - method public static final com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr else); - method public static final com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object else); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(String fieldName); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(com.google.firebase.firestore.pipeline.Expr expr); @@ -1028,14 +1027,14 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, String pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, String pattern); - method public static final com.google.firebase.firestore.pipeline.Function logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.Function logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Function logicalMax(String fieldName, Object other); - method public static final com.google.firebase.firestore.pipeline.Function logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.Function logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Function logicalMin(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(String fieldName, Object other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); @@ -1044,18 +1043,25 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object right); - method public static final com.google.firebase.firestore.pipeline.Function mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); - method public static final com.google.firebase.firestore.pipeline.Function mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); - method public static final com.google.firebase.firestore.pipeline.Function mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); - method public static final com.google.firebase.firestore.pipeline.Function mapGet(String fieldName, String key); - method public static final com.google.firebase.firestore.pipeline.Function mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function mod(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.Function mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Function mod(String fieldName, Object other); - method public static final com.google.firebase.firestore.pipeline.Function multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.Function multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Function multiply(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr map(java.util.Map elements); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(String fieldName, String key); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapMerge(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapMerge(String mapField, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, String key); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(String mapField, com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(String mapField, String key); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mod(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr mod(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr multiply(String fieldName, Object other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); @@ -1072,72 +1078,72 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, String pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, String pattern); - method public static final com.google.firebase.firestore.pipeline.Function replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public static final com.google.firebase.firestore.pipeline.Function replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); - method public static final com.google.firebase.firestore.pipeline.Function replaceAll(String fieldName, String find, String replace); - method public static final com.google.firebase.firestore.pipeline.Function replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public static final com.google.firebase.firestore.pipeline.Function replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); - method public static final com.google.firebase.firestore.pipeline.Function replaceFirst(String fieldName, String find, String replace); - method public static final com.google.firebase.firestore.pipeline.Function reverse(com.google.firebase.firestore.pipeline.Expr expr); - method public static final com.google.firebase.firestore.pipeline.Function reverse(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(String fieldName, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(String fieldName, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr reverse(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr reverse(String fieldName); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr prefix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, String prefix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr prefix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, String prefix); - method public static final com.google.firebase.firestore.pipeline.Function strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); - method public static final com.google.firebase.firestore.pipeline.Function strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); - method public static final com.google.firebase.firestore.pipeline.Function strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); - method public static final com.google.firebase.firestore.pipeline.Function strConcat(String fieldName, java.lang.Object... rest); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(String fieldName, java.lang.Object... rest); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr substring); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, String substring); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, com.google.firebase.firestore.pipeline.Expr substring); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, String substring); - method public static final com.google.firebase.firestore.pipeline.Function subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Function subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.Function subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Function subtract(String fieldName, Object other); - method public static final com.google.firebase.firestore.pipeline.Function timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public static final com.google.firebase.firestore.pipeline.Function timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); - method public static final com.google.firebase.firestore.pipeline.Function timestampAdd(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public static final com.google.firebase.firestore.pipeline.Function timestampAdd(String fieldName, String unit, double amount); - method public static final com.google.firebase.firestore.pipeline.Function timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public static final com.google.firebase.firestore.pipeline.Function timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); - method public static final com.google.firebase.firestore.pipeline.Function timestampSub(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public static final com.google.firebase.firestore.pipeline.Function timestampSub(String fieldName, String unit, double amount); - method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixMicros(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixMillis(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.Function timestampToUnixSeconds(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function toLower(com.google.firebase.firestore.pipeline.Expr expr); - method public static final com.google.firebase.firestore.pipeline.Function toLower(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function toUpper(com.google.firebase.firestore.pipeline.Expr expr); - method public static final com.google.firebase.firestore.pipeline.Function toUpper(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function trim(com.google.firebase.firestore.pipeline.Expr expr); - method public static final com.google.firebase.firestore.pipeline.Function trim(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.Function unixMicrosToTimestamp(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.Function unixMillisToTimestamp(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.Function unixSecondsToTimestamp(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Function vectorLength(com.google.firebase.firestore.pipeline.Expr vector); - method public static final com.google.firebase.firestore.pipeline.Function vectorLength(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr subtract(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(String fieldName, String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(String fieldName, String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMicros(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMillis(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixSeconds(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr toLower(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr toLower(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr toUpper(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr toUpper(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr trim(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr trim(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixMicrosToTimestamp(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixMillisToTimestamp(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixSecondsToTimestamp(String fieldName); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr vectorLength(com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.FunctionExpr vectorLength(String fieldName); method public static final com.google.firebase.firestore.pipeline.BooleanExpr xor(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); - field public static final com.google.firebase.firestore.pipeline.Function.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.FunctionExpr.Companion Companion; } - public static final class Function.Companion { - method public com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function add(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Function add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Function add(String fieldName, Object other); + public static final class FunctionExpr.Companion { + method public com.google.firebase.firestore.pipeline.FunctionExpr add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr add(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.FunctionExpr add(String fieldName, Object other); method public com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); - method public com.google.firebase.firestore.pipeline.Function arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); - method public com.google.firebase.firestore.pipeline.Function arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); - method public com.google.firebase.firestore.pipeline.Function arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); - method public com.google.firebase.firestore.pipeline.Function arrayConcat(String fieldName, java.util.List arrays); + method public com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); + method public com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); + method public com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); + method public com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(String fieldName, java.util.List arrays); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr value); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, Object value); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, com.google.firebase.firestore.pipeline.Expr value); @@ -1146,52 +1152,54 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String fieldName, java.util.List values); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String fieldName, java.util.List values); - method public com.google.firebase.firestore.pipeline.Function arrayLength(com.google.firebase.firestore.pipeline.Expr array); - method public com.google.firebase.firestore.pipeline.Function arrayLength(String fieldName); - method public com.google.firebase.firestore.pipeline.Function arrayReverse(com.google.firebase.firestore.pipeline.Expr array); - method public com.google.firebase.firestore.pipeline.Function arrayReverse(String fieldName); - method public com.google.firebase.firestore.pipeline.Function bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Function bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function bitAnd(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.Function bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.Function bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); - method public com.google.firebase.firestore.pipeline.Function bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.Function bitLeftShift(String fieldName, int number); - method public com.google.firebase.firestore.pipeline.Function bitNot(com.google.firebase.firestore.pipeline.Expr left); - method public com.google.firebase.firestore.pipeline.Function bitNot(String fieldName); - method public com.google.firebase.firestore.pipeline.Function bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Function bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function bitOr(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.Function bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.Function bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); - method public com.google.firebase.firestore.pipeline.Function bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.Function bitRightShift(String fieldName, int number); - method public com.google.firebase.firestore.pipeline.Function bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Function bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function bitXor(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.Function byteLength(com.google.firebase.firestore.pipeline.Expr value); - method public com.google.firebase.firestore.pipeline.Function byteLength(String fieldName); - method public com.google.firebase.firestore.pipeline.Function charLength(com.google.firebase.firestore.pipeline.Expr value); - method public com.google.firebase.firestore.pipeline.Function charLength(String fieldName); - method public com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public com.google.firebase.firestore.pipeline.Function cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public com.google.firebase.firestore.pipeline.Function cosineDistance(String fieldName, double[] vector); - method public com.google.firebase.firestore.pipeline.Function divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function divide(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Function divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Function divide(String fieldName, Object other); - method public com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public com.google.firebase.firestore.pipeline.Function dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public com.google.firebase.firestore.pipeline.Function dotProduct(String fieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.FunctionExpr arrayLength(com.google.firebase.firestore.pipeline.Expr array); + method public com.google.firebase.firestore.pipeline.FunctionExpr arrayLength(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr arrayReverse(com.google.firebase.firestore.pipeline.Expr array); + method public com.google.firebase.firestore.pipeline.FunctionExpr arrayReverse(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(String fieldName, int number); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitNot(com.google.firebase.firestore.pipeline.Expr left); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitNot(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitOr(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(String fieldName, int number); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr bitXor(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr byteLength(com.google.firebase.firestore.pipeline.Expr value); + method public com.google.firebase.firestore.pipeline.FunctionExpr byteLength(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr charLength(com.google.firebase.firestore.pipeline.Expr value); + method public com.google.firebase.firestore.pipeline.FunctionExpr charLength(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); + method public com.google.firebase.firestore.pipeline.FunctionExpr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object otherwise); + method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.FunctionExpr divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr divide(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.FunctionExpr divide(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, double[] vector); method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr suffix); method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, String suffix); method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr suffix); @@ -1202,14 +1210,14 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); method public com.google.firebase.firestore.pipeline.BooleanExpr eqAny(String fieldName, java.util.List values); - method public com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public com.google.firebase.firestore.pipeline.Function euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public com.google.firebase.firestore.pipeline.Function euclideanDistance(String fieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, double[] vector); method public com.google.firebase.firestore.pipeline.BooleanExpr exists(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.Function generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public com.google.firebase.firestore.pipeline.FunctionExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); @@ -1218,10 +1226,6 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then); - method public com.google.firebase.firestore.pipeline.Function ifThen(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then); - method public com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr else); - method public com.google.firebase.firestore.pipeline.Function ifThenElse(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object else); method public com.google.firebase.firestore.pipeline.BooleanExpr isNan(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.BooleanExpr isNan(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(com.google.firebase.firestore.pipeline.Expr expr); @@ -1234,14 +1238,14 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, String pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, String pattern); - method public com.google.firebase.firestore.pipeline.Function logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Function logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Function logicalMax(String fieldName, Object other); - method public com.google.firebase.firestore.pipeline.Function logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Function logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Function logicalMin(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(String fieldName, Object other); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); @@ -1250,18 +1254,25 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.Function mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); - method public com.google.firebase.firestore.pipeline.Function mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); - method public com.google.firebase.firestore.pipeline.Function mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); - method public com.google.firebase.firestore.pipeline.Function mapGet(String fieldName, String key); - method public com.google.firebase.firestore.pipeline.Function mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function mod(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Function mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Function mod(String fieldName, Object other); - method public com.google.firebase.firestore.pipeline.Function multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Function multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Function multiply(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.FunctionExpr map(java.util.Map elements); + method public com.google.firebase.firestore.pipeline.FunctionExpr mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.FunctionExpr mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); + method public com.google.firebase.firestore.pipeline.FunctionExpr mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.FunctionExpr mapGet(String fieldName, String key); + method public com.google.firebase.firestore.pipeline.FunctionExpr mapMerge(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public com.google.firebase.firestore.pipeline.FunctionExpr mapMerge(String mapField, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, String key); + method public com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(String mapField, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(String mapField, String key); + method public com.google.firebase.firestore.pipeline.FunctionExpr mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr mod(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.FunctionExpr mod(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.FunctionExpr multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.FunctionExpr multiply(String fieldName, Object other); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); @@ -1278,78 +1289,69 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, String pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, String pattern); - method public com.google.firebase.firestore.pipeline.Function replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public com.google.firebase.firestore.pipeline.Function replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); - method public com.google.firebase.firestore.pipeline.Function replaceAll(String fieldName, String find, String replace); - method public com.google.firebase.firestore.pipeline.Function replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public com.google.firebase.firestore.pipeline.Function replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); - method public com.google.firebase.firestore.pipeline.Function replaceFirst(String fieldName, String find, String replace); - method public com.google.firebase.firestore.pipeline.Function reverse(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.Function reverse(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(String fieldName, String find, String replace); + method public com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(String fieldName, String find, String replace); + method public com.google.firebase.firestore.pipeline.FunctionExpr reverse(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.FunctionExpr reverse(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr prefix); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, String prefix); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr prefix); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, String prefix); - method public com.google.firebase.firestore.pipeline.Function strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); - method public com.google.firebase.firestore.pipeline.Function strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); - method public com.google.firebase.firestore.pipeline.Function strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); - method public com.google.firebase.firestore.pipeline.Function strConcat(String fieldName, java.lang.Object... rest); + method public com.google.firebase.firestore.pipeline.FunctionExpr strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); + method public com.google.firebase.firestore.pipeline.FunctionExpr strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); + method public com.google.firebase.firestore.pipeline.FunctionExpr strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); + method public com.google.firebase.firestore.pipeline.FunctionExpr strConcat(String fieldName, java.lang.Object... rest); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr substring); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, String substring); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, com.google.firebase.firestore.pipeline.Expr substring); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, String substring); - method public com.google.firebase.firestore.pipeline.Function subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Function subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Function subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Function subtract(String fieldName, Object other); - method public com.google.firebase.firestore.pipeline.Function timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public com.google.firebase.firestore.pipeline.Function timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); - method public com.google.firebase.firestore.pipeline.Function timestampAdd(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public com.google.firebase.firestore.pipeline.Function timestampAdd(String fieldName, String unit, double amount); - method public com.google.firebase.firestore.pipeline.Function timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public com.google.firebase.firestore.pipeline.Function timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); - method public com.google.firebase.firestore.pipeline.Function timestampSub(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public com.google.firebase.firestore.pipeline.Function timestampSub(String fieldName, String unit, double amount); - method public com.google.firebase.firestore.pipeline.Function timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.Function timestampToUnixMicros(String fieldName); - method public com.google.firebase.firestore.pipeline.Function timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.Function timestampToUnixMillis(String fieldName); - method public com.google.firebase.firestore.pipeline.Function timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.Function timestampToUnixSeconds(String fieldName); - method public com.google.firebase.firestore.pipeline.Function toLower(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.Function toLower(String fieldName); - method public com.google.firebase.firestore.pipeline.Function toUpper(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.Function toUpper(String fieldName); - method public com.google.firebase.firestore.pipeline.Function trim(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.Function trim(String fieldName); - method public com.google.firebase.firestore.pipeline.Function unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.Function unixMicrosToTimestamp(String fieldName); - method public com.google.firebase.firestore.pipeline.Function unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.Function unixMillisToTimestamp(String fieldName); - method public com.google.firebase.firestore.pipeline.Function unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.Function unixSecondsToTimestamp(String fieldName); - method public com.google.firebase.firestore.pipeline.Function vectorLength(com.google.firebase.firestore.pipeline.Expr vector); - method public com.google.firebase.firestore.pipeline.Function vectorLength(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.FunctionExpr subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.FunctionExpr subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.FunctionExpr subtract(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(String fieldName, String unit, double amount); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(String fieldName, String unit, double amount); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMicros(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMillis(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixSeconds(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr toLower(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.FunctionExpr toLower(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr toUpper(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.FunctionExpr toUpper(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr trim(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.FunctionExpr trim(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.FunctionExpr unixMicrosToTimestamp(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.FunctionExpr unixMillisToTimestamp(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.FunctionExpr unixSecondsToTimestamp(String fieldName); + method public com.google.firebase.firestore.pipeline.FunctionExpr vectorLength(com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.FunctionExpr vectorLength(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr xor(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); } - public final class GenericOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { - field public static final com.google.firebase.firestore.pipeline.GenericOptions.Companion Companion; - field public static final com.google.firebase.firestore.pipeline.GenericOptions DEFAULT; - } - - public static final class GenericOptions.Companion { - } - - public final class GenericStage extends com.google.firebase.firestore.pipeline.Stage { - method public static com.google.firebase.firestore.pipeline.GenericStage of(String name); + public final class GenericStage extends com.google.firebase.firestore.pipeline.Stage { + method public static com.google.firebase.firestore.pipeline.GenericStage ofName(String name); method public com.google.firebase.firestore.pipeline.GenericStage withArguments(java.lang.Object... arguments); - method public com.google.firebase.firestore.pipeline.GenericStage withOptions(com.google.firebase.firestore.pipeline.GenericOptions options); field public static final com.google.firebase.firestore.pipeline.GenericStage.Companion Companion; } public static final class GenericStage.Companion { - method public com.google.firebase.firestore.pipeline.GenericStage of(String name); + method public com.google.firebase.firestore.pipeline.GenericStage ofName(String name); } public final class Ordering { @@ -1370,7 +1372,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Ordering descending(String fieldName); } - public final class SampleStage extends com.google.firebase.firestore.pipeline.Stage { + public final class SampleStage extends com.google.firebase.firestore.pipeline.Stage { method public static com.google.firebase.firestore.pipeline.SampleStage withDocLimit(int documents); method public static com.google.firebase.firestore.pipeline.SampleStage withPercentage(double percentage); field public static final com.google.firebase.firestore.pipeline.SampleStage.Companion Companion; @@ -1396,18 +1398,27 @@ package com.google.firebase.firestore.pipeline { ctor public Selectable(); } - public abstract class Stage { + public abstract class Stage> { method protected final String getName(); + method public final T with(String key, boolean value); + method public final T with(String key, com.google.firebase.firestore.pipeline.Field value); + method public final T with(String key, double value); + method protected final T with(String key, error.NonExistentClass value); + method public final T with(String key, String value); + method public final T with(String key, long value); property protected final String name; } - public final class UnnestOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { - method public com.google.firebase.firestore.pipeline.UnnestOptions withIndexField(String indexField); - field public static final com.google.firebase.firestore.pipeline.UnnestOptions.Companion Companion; - field public static final com.google.firebase.firestore.pipeline.UnnestOptions DEFAULT; + public final class UnnestStage extends com.google.firebase.firestore.pipeline.Stage { + method public static com.google.firebase.firestore.pipeline.UnnestStage withField(com.google.firebase.firestore.pipeline.Selectable arrayWithAlias); + method public static com.google.firebase.firestore.pipeline.UnnestStage withField(String arrayField, String alias); + method public com.google.firebase.firestore.pipeline.UnnestStage withIndexField(String indexField); + field public static final com.google.firebase.firestore.pipeline.UnnestStage.Companion Companion; } - public static final class UnnestOptions.Companion { + public static final class UnnestStage.Companion { + method public com.google.firebase.firestore.pipeline.UnnestStage withField(com.google.firebase.firestore.pipeline.Selectable arrayWithAlias); + method public com.google.firebase.firestore.pipeline.UnnestStage withField(String arrayField, String alias); } } From 39c9bd7a698070ef5093da5d90b1085f1368cd0a Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 10 Apr 2025 19:23:58 -0400 Subject: [PATCH 040/152] fix --- .../java/com/google/firebase/firestore/core/Query.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index e3045ea2aec..ff832879af3 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -563,15 +563,17 @@ public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataR if (hasLimit()) { // TODO: Handle situation where user enters limit larger than integer. if (limitType == LimitType.LIMIT_TO_FIRST) { - p = p.sort(orderings.toArray(Ordering[]::new)); + p = p.sort(orderings.get(0), orderings.stream().skip(1).toArray(Ordering[]::new)); p = p.limit((int) limit); } else { - p = p.sort(orderings.stream().map(Ordering::reverse).toArray(Ordering[]::new)); + p = p.sort( + orderings.get(0).reverse(), + orderings.stream().skip(1).map(Ordering::reverse).toArray(Ordering[]::new)); p = p.limit((int) limit); - p = p.sort(orderings.toArray(Ordering[]::new)); + p = p.sort(orderings.get(0), orderings.stream().skip(1).toArray(Ordering[]::new)); } } else { - p = p.sort(orderings.toArray(Ordering[]::new)); + p = p.sort(orderings.get(0), orderings.stream().skip(1).toArray(Ordering[]::new)); } return p; From ea780f82116c929506dfa55964cb4ffecf295bab Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 15 Apr 2025 10:40:27 -0400 Subject: [PATCH 041/152] fixups --- .../firebase/firestore/PipelineTest.java | 136 +-- .../testutil/IntegrationTestUtil.java | 2 +- .../firebase/firestore/AggregateField.java | 7 +- .../com/google/firebase/firestore/Pipeline.kt | 27 +- .../firebase/firestore/core/FieldFilter.java | 2 +- .../google/firebase/firestore/core/Query.java | 8 +- .../firebase/firestore/pipeline/Constant.kt | 182 ---- .../firebase/firestore/pipeline/aggregates.kt | 2 +- .../firestore/pipeline/expressions.kt | 848 ++++++++++-------- .../firebase/firestore/pipeline/options.kt | 177 +++- .../firebase/firestore/pipeline/stage.kt | 17 +- 11 files changed, 771 insertions(+), 637 deletions(-) delete mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt 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 cd4fc945221..fbca6febc6b 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 @@ -15,25 +15,27 @@ package com.google.firebase.firestore; import static com.google.common.truth.Truth.assertThat; -import static com.google.firebase.firestore.pipeline.FunctionExpr.add; -import static com.google.firebase.firestore.pipeline.FunctionExpr.and; -import static com.google.firebase.firestore.pipeline.FunctionExpr.arrayContains; -import static com.google.firebase.firestore.pipeline.FunctionExpr.arrayContainsAny; -import static com.google.firebase.firestore.pipeline.FunctionExpr.cosineDistance; -import static com.google.firebase.firestore.pipeline.FunctionExpr.endsWith; -import static com.google.firebase.firestore.pipeline.FunctionExpr.eq; -import static com.google.firebase.firestore.pipeline.FunctionExpr.euclideanDistance; -import static com.google.firebase.firestore.pipeline.FunctionExpr.gt; -import static com.google.firebase.firestore.pipeline.FunctionExpr.logicalMax; -import static com.google.firebase.firestore.pipeline.FunctionExpr.lt; -import static com.google.firebase.firestore.pipeline.FunctionExpr.lte; -import static com.google.firebase.firestore.pipeline.FunctionExpr.mapGet; -import static com.google.firebase.firestore.pipeline.FunctionExpr.neq; -import static com.google.firebase.firestore.pipeline.FunctionExpr.not; -import static com.google.firebase.firestore.pipeline.FunctionExpr.or; -import static com.google.firebase.firestore.pipeline.FunctionExpr.startsWith; -import static com.google.firebase.firestore.pipeline.FunctionExpr.strConcat; -import static com.google.firebase.firestore.pipeline.FunctionExpr.subtract; +import static com.google.firebase.firestore.pipeline.Expr.add; +import static com.google.firebase.firestore.pipeline.Expr.and; +import static com.google.firebase.firestore.pipeline.Expr.arrayContains; +import static com.google.firebase.firestore.pipeline.Expr.arrayContainsAny; +import static com.google.firebase.firestore.pipeline.Expr.cosineDistance; +import static com.google.firebase.firestore.pipeline.Expr.endsWith; +import static com.google.firebase.firestore.pipeline.Expr.eq; +import static com.google.firebase.firestore.pipeline.Expr.euclideanDistance; +import static com.google.firebase.firestore.pipeline.Expr.field; +import static com.google.firebase.firestore.pipeline.Expr.gt; +import static com.google.firebase.firestore.pipeline.Expr.logicalMax; +import static com.google.firebase.firestore.pipeline.Expr.lt; +import static com.google.firebase.firestore.pipeline.Expr.lte; +import static com.google.firebase.firestore.pipeline.Expr.mapGet; +import static com.google.firebase.firestore.pipeline.Expr.neq; +import static com.google.firebase.firestore.pipeline.Expr.not; +import static com.google.firebase.firestore.pipeline.Expr.or; +import static com.google.firebase.firestore.pipeline.Expr.startsWith; +import static com.google.firebase.firestore.pipeline.Expr.strConcat; +import static com.google.firebase.firestore.pipeline.Expr.subtract; +import static com.google.firebase.firestore.pipeline.Expr.vector; import static com.google.firebase.firestore.pipeline.Ordering.ascending; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor; @@ -44,9 +46,7 @@ import com.google.common.truth.Correspondence; import com.google.firebase.firestore.pipeline.AggregateFunction; import com.google.firebase.firestore.pipeline.AggregateStage; -import com.google.firebase.firestore.pipeline.Constant; -import com.google.firebase.firestore.pipeline.Field; -import com.google.firebase.firestore.pipeline.FunctionExpr; +import com.google.firebase.firestore.pipeline.Expr; import com.google.firebase.firestore.pipeline.GenericStage; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import java.util.Collections; @@ -240,11 +240,11 @@ public void aggregateResultsMany() { firestore .pipeline() .collection(randomCol) - .where(FunctionExpr.eq("genre", "Science Fiction")) + .where(eq("genre", "Science Fiction")) .aggregate( AggregateFunction.countAll().alias("count"), AggregateFunction.avg("rating").alias("avgRating"), - Field.of("rating").max().alias("maxRating")) + field("rating").max().alias("maxRating")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -258,12 +258,12 @@ public void groupAndAccumulateResults() { firestore .pipeline() .collection(randomCol) - .where(lt(Field.of("published"), 1984)) + .where(lt(field("published"), 1984)) .aggregate( AggregateStage.withAccumulators(AggregateFunction.avg("rating").alias("avgRating")) .withGroups("genre")) .where(gt("avgRating", 4.3)) - .sort(Field.of("avgRating").descending()) + .sort(field("avgRating").descending()) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -279,13 +279,13 @@ public void groupAndAccumulateResultsGeneric() { firestore .pipeline() .collection(randomCol) - .genericStage("where", lt(Field.of("published"), 1984)) + .genericStage("where", lt(field("published"), 1984)) .genericStage( "aggregate", ImmutableMap.of("avgRating", AggregateFunction.avg("rating")), - ImmutableMap.of("genre", Field.of("genre"))) + ImmutableMap.of("genre", field("genre"))) .genericStage(GenericStage.ofName("where").withArguments(gt("avgRating", 4.3))) - .genericStage("sort", Field.of("avgRating").descending()) + .genericStage("sort", field("avgRating").descending()) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -304,8 +304,8 @@ public void minAndMaxAccumulations() { .collection(randomCol) .aggregate( AggregateFunction.countAll().alias("count"), - Field.of("rating").max().alias("maxRating"), - Field.of("published").min().alias("minPublished")) + field("rating").max().alias("maxRating"), + field("published").min().alias("minPublished")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -320,7 +320,7 @@ public void canSelectFields() { .pipeline() .collection(randomCol) .select("title", "author") - .sort(Field.of("author").ascending()) + .sort(field("author").ascending()) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -431,7 +431,7 @@ public void arrayContainsAllWorks() { firestore .pipeline() .collection(randomCol) - .where(Field.of("tags").arrayContainsAll(ImmutableList.of("adventure", "magic"))) + .where(field("tags").arrayContainsAll(ImmutableList.of("adventure", "magic"))) .select("title") .execute(); assertThat(waitFor(execute).getResults()) @@ -445,7 +445,7 @@ public void arrayLengthWorks() { firestore .pipeline() .collection(randomCol) - .select(Field.of("tags").arrayLength().alias("tagsCount")) + .select(field("tags").arrayLength().alias("tagsCount")) .where(eq("tagsCount", 3)) .execute(); assertThat(waitFor(execute).getResults()).hasSize(10); @@ -460,7 +460,7 @@ public void arrayConcatWorks() { .collection(randomCol) .where(eq("title", "The Hitchhiker's Guide to the Galaxy")) .select( - Field.of("tags") + field("tags") .arrayConcat(ImmutableList.of("newTag1", "newTag2")) .alias("modifiedTags")) .limit(1) @@ -479,7 +479,7 @@ public void testStrConcat() { firestore .pipeline() .collection(randomCol) - .select(Field.of("author").strConcat(" - ", Field.of("title")).alias("bookInfo")) + .select(field("author").strConcat(" - ", field("title")).alias("bookInfo")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -496,7 +496,7 @@ public void testStartsWith() { .collection(randomCol) .where(startsWith("title", "The")) .select("title") - .sort(Field.of("title").ascending()) + .sort(field("title").ascending()) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -515,7 +515,7 @@ public void testEndsWith() { .collection(randomCol) .where(endsWith("title", "y")) .select("title") - .sort(Field.of("title").descending()) + .sort(field("title").descending()) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -530,9 +530,9 @@ public void testLength() { firestore .pipeline() .collection(randomCol) - .select(Field.of("title").charLength().alias("titleLength"), Field.of("title")) + .select(field("title").charLength().alias("titleLength"), field("title")) .where(gt("titleLength", 20)) - .sort(Field.of("title").ascending()) + .sort(field("title").ascending()) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -550,7 +550,7 @@ public void testToLowercase() { firestore .pipeline() .collection(randomCol) - .select(Field.of("title").toLower().alias("lowercaseTitle")) + .select(field("title").toLower().alias("lowercaseTitle")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -565,7 +565,7 @@ public void testToUppercase() { firestore .pipeline() .collection(randomCol) - .select(Field.of("author").toLower().alias("uppercaseAuthor")) + .select(field("author").toLower().alias("uppercaseAuthor")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -580,8 +580,8 @@ public void testTrim() { firestore .pipeline() .collection(randomCol) - .addFields(strConcat(" ", Field.of("title"), " ").alias("spacedTitle")) - .select(Field.of("spacedTitle").trim().alias("trimmedTitle")) + .addFields(strConcat(" ", field("title"), " ").alias("spacedTitle")) + .select(field("spacedTitle").trim().alias("trimmedTitle")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -600,7 +600,7 @@ public void testLike() { firestore .pipeline() .collection(randomCol) - .where(FunctionExpr.like("title", "%Guide%")) + .where(Expr.like("title", "%Guide%")) .select("title") .execute(); assertThat(waitFor(execute).getResults()) @@ -614,7 +614,7 @@ public void testRegexContains() { firestore .pipeline() .collection(randomCol) - .where(FunctionExpr.regexContains("title", "(?i)(the|of)")) + .where(Expr.regexContains("title", "(?i)(the|of)")) .execute(); assertThat(waitFor(execute).getResults()).hasSize(5); } @@ -625,7 +625,7 @@ public void testRegexMatches() { firestore .pipeline() .collection(randomCol) - .where(FunctionExpr.regexContains("title", ".*(?i)(the|of).*")) + .where(Expr.regexContains("title", ".*(?i)(the|of).*")) .execute(); assertThat(waitFor(execute).getResults()).hasSize(5); } @@ -637,10 +637,10 @@ public void testArithmeticOperations() { .pipeline() .collection(randomCol) .select( - add(Field.of("rating"), 1).alias("ratingPlusOne"), - subtract(Field.of("published"), 1900).alias("yearsSince1900"), - Field.of("rating").multiply(10).alias("ratingTimesTen"), - Field.of("rating").divide(2).alias("ratingDividedByTwo")) + add(field("rating"), 1).alias("ratingPlusOne"), + subtract(field("published"), 1900).alias("yearsSince1900"), + field("rating").multiply(10).alias("ratingTimesTen"), + field("rating").divide(2).alias("ratingDividedByTwo")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -662,10 +662,10 @@ public void testComparisonOperators() { .where( and( gt("rating", 4.2), - lte(Field.of("rating"), 4.5), + lte(field("rating"), 4.5), neq("genre", "Science Function"))) .select("rating", "title") - .sort(Field.of("title").ascending()) + .sort(field("title").ascending()) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -684,9 +684,9 @@ public void testLogicalOperators() { .where( or( and(gt("rating", 4.5), eq("genre", "Science Fiction")), - lt(Field.of("published"), 1900))) + lt(field("published"), 1900))) .select("title") - .sort(Field.of("title").ascending()) + .sort(field("title").ascending()) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -702,11 +702,11 @@ public void testChecks() { firestore .pipeline() .collection(randomCol) - .where(not(Field.of("rating").isNan())) + .where(not(field("rating").isNan())) .select( - Field.of("rating").isNull().alias("ratingIsNull"), - Field.of("rating").eq(Constant.nullValue()).alias("ratingEqNull"), - not(Field.of("rating").isNan()).alias("ratingIsNotNan")) + field("rating").isNull().alias("ratingIsNull"), + field("rating").eq(Expr.nullValue()).alias("ratingEqNull"), + not(field("rating").isNan()).alias("ratingIsNotNan")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) @@ -725,10 +725,10 @@ public void testLogicalMax() { firestore .pipeline() .collection(randomCol) - .where(Field.of("author").eq("Douglas Adams")) + .where(field("author").eq("Douglas Adams")) .select( - Field.of("rating").logicalMax(4.5).alias("max_rating"), - logicalMax(Field.of("published"), 1900).alias("max_published")) + field("rating").logicalMax(4.5).alias("max_rating"), + logicalMax(field("published"), 1900).alias("max_published")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -739,7 +739,7 @@ public void testLogicalMax() { @Ignore("Not supported yet") public void testLogicalMin() { Task execute = - firestore.pipeline().collection(randomCol).sort(Field.of("rating").ascending()).execute(); + firestore.pipeline().collection(randomCol).sort(field("rating").ascending()).execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) .containsExactly(ImmutableMap.of("min_rating", 4.2, "min_published", 1900)); @@ -751,7 +751,7 @@ public void testMapGet() { firestore .pipeline() .collection(randomCol) - .select(Field.of("awards").mapGet("hugo").alias("hugoAward"), Field.of("title")) + .select(field("awards").mapGet("hugo").alias("hugoAward"), field("title")) .where(eq("hugoAward", true)) .execute(); assertThat(waitFor(execute).getResults()) @@ -770,10 +770,10 @@ public void testDistanceFunctions() { .pipeline() .collection(randomCol) .select( - cosineDistance(Constant.vector(sourceVector), targetVector).alias("cosineDistance"), - FunctionExpr.dotProduct(Constant.vector(sourceVector), targetVector) + cosineDistance(vector(sourceVector), targetVector).alias("cosineDistance"), + Expr.dotProduct(vector(sourceVector), targetVector) .alias("dotProductDistance"), - euclideanDistance(Constant.vector(sourceVector), targetVector) + euclideanDistance(vector(sourceVector), targetVector) .alias("euclideanDistance")) .limit(1) .execute(); @@ -811,7 +811,7 @@ public void testMapGetWithFieldNameIncludingNotation() { .where(eq("awards.hugo", true)) .select( "title", - Field.of("nestedField.level.1"), + field("nestedField.level.1"), mapGet("nestedField", "level.1").mapGet("level.2").alias("nested")) .execute(); assertThat(waitFor(execute).getResults()) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index e94fb3baf35..bea8743e0ba 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -569,7 +569,7 @@ public static void checkOnlineAndOfflineResultsMatch(Query query, String... expe public static void checkQueryAndPipelineResultsMatch(Query query, String... expectedDocs) { QuerySnapshot docsFromQuery = waitFor(query.get(Source.SERVER)); PipelineSnapshot docsFromPipeline = - waitFor(query.getFirestore().pipeline().createFrom(query).execute()); + waitFor(query.getFirestore().pipeline().convertFrom(query).execute()); assertEquals(querySnapshotToIds(docsFromQuery), pipelineSnapshotToIds(docsFromPipeline)); List expected = asList(expectedDocs); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java index d26f82ea7c2..a053a8d038a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java @@ -14,12 +14,13 @@ package com.google.firebase.firestore; +import static com.google.firebase.firestore.pipeline.Expr.field; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import com.google.firebase.firestore.pipeline.AggregateFunction; import com.google.firebase.firestore.pipeline.AggregateWithAlias; -import com.google.firebase.firestore.pipeline.Field; import java.util.Objects; /** Represents an aggregation that can be performed by Firestore. */ @@ -218,7 +219,7 @@ private SumAggregateField(@NonNull FieldPath fieldPath) { @NonNull @Override AggregateWithAlias toPipeline() { - return Field.of(getFieldPath()).sum().alias(getAlias()); + return field(getFieldPath()).sum().alias(getAlias()); } } @@ -231,7 +232,7 @@ private AverageAggregateField(@NonNull FieldPath fieldPath) { @NonNull @Override AggregateWithAlias toPipeline() { - return Field.of(getFieldPath()).avg().alias(getAlias()); + return field(getFieldPath()).avg().alias(getAlias()); } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 1cf45215a80..23ff5a5ba19 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList import com.google.firebase.Timestamp import com.google.firebase.firestore.model.DocumentKey import com.google.firebase.firestore.model.Values +import com.google.firebase.firestore.pipeline.AbstractOptions import com.google.firebase.firestore.pipeline.AddFieldsStage import com.google.firebase.firestore.pipeline.AggregateFunction import com.google.firebase.firestore.pipeline.AggregateStage @@ -38,9 +39,11 @@ import com.google.firebase.firestore.pipeline.FindNearestStage import com.google.firebase.firestore.pipeline.FunctionExpr import com.google.firebase.firestore.pipeline.GenericArg import com.google.firebase.firestore.pipeline.GenericStage +import com.google.firebase.firestore.pipeline.InternalOptions import com.google.firebase.firestore.pipeline.LimitStage import com.google.firebase.firestore.pipeline.OffsetStage import com.google.firebase.firestore.pipeline.Ordering +import com.google.firebase.firestore.pipeline.PipelineOptions import com.google.firebase.firestore.pipeline.RemoveFieldsStage import com.google.firebase.firestore.pipeline.ReplaceStage import com.google.firebase.firestore.pipeline.SampleStage @@ -72,9 +75,11 @@ internal constructor( return Pipeline(firestore, userDataReader, stages.append(stage)) } - fun execute(): Task { + fun execute(): Task = execute(PipelineOptions.DEFAULT) + + fun execute(options: PipelineOptions): Task { val observerTask = ObserverSnapshotTask() - firestore.callClient { call -> call!!.executePipeline(toProto(), observerTask) } + firestore.callClient { call -> call!!.executePipeline(toProto(options), observerTask) } return observerTask.task } @@ -82,7 +87,7 @@ internal constructor( return DocumentReference(key, firestore) } - private fun toProto(): ExecutePipelineRequest { + private fun toProto(options: PipelineOptions): ExecutePipelineRequest { val database = firestore.databaseId val builder = ExecutePipelineRequest.newBuilder() builder.database = "projects/${database.projectId}/databases/${database.databaseId}" @@ -169,7 +174,7 @@ internal constructor( */ fun removeFields(field: String, vararg additionalFields: String): Pipeline = append( - RemoveFieldsStage(arrayOf(Field.of(field), *additionalFields.map(Field::of).toTypedArray())) + RemoveFieldsStage(arrayOf(Expr.field(field), *additionalFields.map(Expr::field).toTypedArray())) ) /** @@ -221,7 +226,7 @@ internal constructor( append( SelectStage( arrayOf( - Field.of(fieldName), + Expr.field(fieldName), *additionalSelections.map(Selectable::toSelectable).toTypedArray() ) ) @@ -253,10 +258,10 @@ internal constructor( * You can filter documents based on their field values, using implementations of [BooleanExpr], * typically including but not limited to: * - * - field comparators: [FunctionExpr.eq], [FunctionExpr.lt] (less than), [FunctionExpr.gt] + * - field comparators: [Expr.eq], [Expr.lt] (less than), [Expr.gt] * (greater than), etc. - * - logical operators: [FunctionExpr.and], [FunctionExpr.or], [FunctionExpr.not], etc. - * - advanced functions: [FunctionExpr.regexMatch], [FunctionExpr.arrayContains[], etc. + * - logical operators: [Expr.and], [Expr.or], [Expr.not], etc. + * - advanced functions: [Expr.regexMatch], [Expr.arrayContains], etc. * * @param condition The [BooleanExpr] to apply. * @return A new [Pipeline] object with this stage appended to the stage list. @@ -336,7 +341,7 @@ internal constructor( append( DistinctStage( arrayOf( - Field.of(groupField), + Expr.field(groupField), *additionalGroups.map(Selectable::toSelectable).toTypedArray() ) ) @@ -468,7 +473,7 @@ internal constructor( * @param field The [String] specifying the field name containing the nested map. * @return A new [Pipeline] object with this stage appended to the stage list. */ - fun replace(field: String): Pipeline = replace(Field.of(field)) + fun replace(field: String): Pipeline = replace(Expr.field(field)) /** * Fully overwrites all fields in a document with those coming from a nested map. @@ -530,7 +535,7 @@ internal constructor( * @return A new [Pipeline] object with this stage appended to the stage list. */ fun unnest(arrayField: String, alias: String): Pipeline = - unnest(Field.of(arrayField).alias(alias)) + unnest(Expr.field(arrayField).alias(alias)) /** * Takes a specified array from the input documents and outputs a document for each element with diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java index cc3c5402eb6..20dd9ad585a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java @@ -14,7 +14,7 @@ package com.google.firebase.firestore.core; -import static com.google.firebase.firestore.pipeline.FunctionExpr.and; +import static com.google.firebase.firestore.pipeline.Expr.and; import static com.google.firebase.firestore.util.Assert.hardAssert; import static java.lang.Double.isNaN; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index ff832879af3..3607f584a2a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -14,8 +14,8 @@ package com.google.firebase.firestore.core; -import static com.google.firebase.firestore.pipeline.FunctionExpr.and; -import static com.google.firebase.firestore.pipeline.FunctionExpr.or; +import static com.google.firebase.firestore.pipeline.Expr.and; +import static com.google.firebase.firestore.pipeline.Expr.or; import static com.google.firebase.firestore.util.Assert.hardAssert; import androidx.annotation.NonNull; @@ -547,7 +547,7 @@ public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataR p = p.where(fields.get(0).exists()); } else { BooleanExpr[] conditions = - fields.stream().skip(1).map(Expr::exists).toArray(BooleanExpr[]::new); + fields.stream().skip(1).map(Expr.Companion::exists).toArray(BooleanExpr[]::new); p = p.where(and(fields.get(0).exists(), conditions)); } @@ -587,7 +587,7 @@ private static BooleanExpr whereConditionsFromCursor( int last = size - 1; BooleanExpr condition = cmp.apply(fields.get(last), boundPosition.get(last)); if (bound.isInclusive()) { - condition = or(condition, FunctionExpr.eq(fields.get(last), boundPosition.get(last))); + condition = or(condition, Expr.eq(fields.get(last), boundPosition.get(last))); } for (int i = size - 2; i >= 0; i--) { final Field field = fields.get(i); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt deleted file mode 100644 index 1e5c47da1f9..00000000000 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/Constant.kt +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright 2025 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.firebase.firestore.pipeline - -import com.google.firebase.Timestamp -import com.google.firebase.firestore.Blob -import com.google.firebase.firestore.DocumentReference -import com.google.firebase.firestore.GeoPoint -import com.google.firebase.firestore.UserDataReader -import com.google.firebase.firestore.VectorValue -import com.google.firebase.firestore.model.Values -import com.google.firebase.firestore.model.Values.encodeValue -import com.google.firestore.v1.Value -import java.util.Date - -/** - * Represents a constant value that can be used in a Firestore pipeline expression. - * - * You can create a [Constant] instance using the static [of] method: - */ -abstract class Constant internal constructor() : Expr() { - - private class ValueConstant(val value: Value) : Constant() { - override fun toProto(userDataReader: UserDataReader): Value = value - } - - companion object { - internal val NULL: Constant = ValueConstant(Values.NULL_VALUE) - - internal fun of(value: Value): Constant { - return ValueConstant(value) - } - - /** - * Create a [Constant] instance for a [String] value. - * - * @param value The [String] value. - * @return A new [Constant] instance. - */ - @JvmStatic - fun of(value: String): Constant { - return ValueConstant(encodeValue(value)) - } - - /** - * Create a [Constant] instance for a [Number] value. - * - * @param value The [Number] value. - * @return A new [Constant] instance. - */ - @JvmStatic - fun of(value: Number): Constant { - return ValueConstant(encodeValue(value)) - } - - /** - * Create a [Constant] instance for a [Date] value. - * - * @param value The [Date] value. - * @return A new [Constant] instance. - */ - @JvmStatic - fun of(value: Date): Constant { - return ValueConstant(encodeValue(value)) - } - - /** - * Create a [Constant] instance for a [Timestamp] value. - * - * @param value The [Timestamp] value. - * @return A new [Constant] instance. - */ - @JvmStatic - fun of(value: Timestamp): Constant { - return ValueConstant(encodeValue(value)) - } - - /** - * Create a [Constant] instance for a [Boolean] value. - * - * @param value The [Boolean] value. - * @return A new [Constant] instance. - */ - @JvmStatic - fun of(value: Boolean): Constant { - return ValueConstant(encodeValue(value)) - } - - /** - * Create a [Constant] instance for a [GeoPoint] value. - * - * @param value The [GeoPoint] value. - * @return A new [Constant] instance. - */ - @JvmStatic - fun of(value: GeoPoint): Constant { - return ValueConstant(encodeValue(value)) - } - - /** - * Create a [Constant] instance for a [Blob] value. - * - * @param value The [Blob] value. - * @return A new [Constant] instance. - */ - @JvmStatic - fun of(value: Blob): Constant { - return ValueConstant(encodeValue(value)) - } - - /** - * Create a [Constant] instance for a [DocumentReference] value. - * - * @param ref The [DocumentReference] value. - * @return A new [Constant] instance. - */ - @JvmStatic - fun of(ref: DocumentReference): Constant { - return object : Constant() { - override fun toProto(userDataReader: UserDataReader): Value { - userDataReader.validateDocumentReference(ref, ::IllegalArgumentException) - return encodeValue(ref) - } - } - } - - /** - * Create a [Constant] instance for a [VectorValue] value. - * - * @param value The [VectorValue] value. - * @return A new [Constant] instance. - */ - @JvmStatic - fun of(value: VectorValue): Constant { - return ValueConstant(encodeValue(value)) - } - - /** - * [Constant] instance for a null value. - * - * @return A [Constant] instance. - */ - @JvmStatic - fun nullValue(): Constant { - return NULL - } - - /** - * Create a vector [Constant] instance for a [DoubleArray] value. - * - * @param vector The [VectorValue] value. - * @return A new [Constant] instance. - */ - @JvmStatic - fun vector(vector: DoubleArray): Constant { - return ValueConstant(Values.encodeVectorValue(vector)) - } - - /** - * Create a vector [Constant] instance for a [VectorValue] value. - * - * @param vector The [VectorValue] value. - * @return A new [Constant] instance. - */ - @JvmStatic - fun vector(vector: VectorValue): Constant { - return ValueConstant(encodeValue(vector)) - } - } -} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt index afcb2797df2..bdc5707388b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt @@ -25,7 +25,7 @@ class AggregateFunction private constructor(private val name: String, private val params: Array) { private constructor(name: String) : this(name, emptyArray()) private constructor(name: String, expr: Expr) : this(name, arrayOf(expr)) - private constructor(name: String, fieldName: String) : this(name, Field.of(fieldName)) + private constructor(name: String, fieldName: String) : this(name, Expr.field(fieldName)) companion object { 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 87c7bca250c..eaa59645cb3 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 @@ -23,9 +23,9 @@ import com.google.firebase.firestore.Pipeline import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.VectorValue import com.google.firebase.firestore.model.DocumentKey +import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.FieldPath as ModelFieldPath import com.google.firebase.firestore.model.Values.encodeValue -import com.google.firebase.firestore.pipeline.Constant.Companion.of import com.google.firebase.firestore.util.CustomClassMapper import com.google.firestore.v1.MapValue import com.google.firestore.v1.Value @@ -41,14 +41,17 @@ import kotlin.reflect.KFunction1 * - **Field references:** Access values from document fields. * - **Literals:** Represent constant values (strings, numbers, booleans). * - **Function calls:** Apply functions to one or more expressions. - * - **Aggregations:** Calculate aggregate values (e.g., sum, average) over a set of documents. * * The [Expr] class provides a fluent API for building expressions. You can chain together method * calls to create complex expressions. */ abstract class Expr internal constructor() { - internal companion object { + private class ValueConstant(val value: Value) : Expr() { + override fun toProto(userDataReader: UserDataReader): Value = value + } + + companion object { internal fun toExprOrConstant(value: Any?): Expr = toExpr(value, ::toExprOrConstant) ?: pojoToExprOrConstant(CustomClassMapper.convertToPlainJavaTypes(value)) @@ -58,25 +61,25 @@ abstract class Expr internal constructor() { ?: throw IllegalArgumentException("Unknown type: $value") private fun toExpr(value: Any?, toExpr: KFunction1): Expr? { - if (value == null) return Constant.nullValue() + if (value == null) return NULL return when (value) { is Expr -> value - is String -> of(value) - is Number -> of(value) - is Date -> of(value) - is Timestamp -> of(value) - is Boolean -> of(value) - is GeoPoint -> of(value) - is Blob -> of(value) - is DocumentReference -> of(value) - is VectorValue -> of(value) - is Value -> of(value) + is String -> constant(value) + is Number -> constant(value) + is Date -> constant(value) + is Timestamp -> constant(value) + is Boolean -> constant(value) + is GeoPoint -> constant(value) + is Blob -> constant(value) + is DocumentReference -> constant(value) + is VectorValue -> constant(value) + is Value -> ValueConstant(value) is Map<*, *> -> - FunctionExpr.map( + map( value .flatMap { val key = it.key - if (key is String) listOf(of(key), toExpr(it.value)) + if (key is String) listOf(constant(key), toExpr(it.value)) else throw IllegalArgumentException("Maps with non-string keys are not supported") } .toTypedArray() @@ -91,311 +94,144 @@ abstract class Expr internal constructor() { internal fun toArrayOfExprOrConstant(others: Array): Array = others.map(::toExprOrConstant).toTypedArray() - } - - fun bitAnd(right: Expr) = FunctionExpr.bitAnd(this, right) - - fun bitAnd(right: Any) = FunctionExpr.bitAnd(this, right) - - fun bitOr(right: Expr) = FunctionExpr.bitOr(this, right) - - fun bitOr(right: Any) = FunctionExpr.bitOr(this, right) - - fun bitXor(right: Expr) = FunctionExpr.bitXor(this, right) - - fun bitXor(right: Any) = FunctionExpr.bitXor(this, right) - - fun bitNot() = FunctionExpr.bitNot(this) - - fun bitLeftShift(numberExpr: Expr) = FunctionExpr.bitLeftShift(this, numberExpr) - - fun bitLeftShift(number: Int) = FunctionExpr.bitLeftShift(this, number) - - fun bitRightShift(numberExpr: Expr) = FunctionExpr.bitRightShift(this, numberExpr) - - fun bitRightShift(number: Int) = FunctionExpr.bitRightShift(this, number) - - /** - * Assigns an alias to this expression. - * - *

Aliases are useful for renaming fields in the output of a stage or for giving meaningful - * names to calculated values. - * - *

Example: - * - *

 // Calculate the total price and assign it the alias "totalPrice" and add it to the
-   *
-   * output. firestore.pipeline().collection("items")
-   * .addFields(Field.of("price").multiply(Field.of("quantity")).as("totalPrice")); 
- * - * @param alias The alias to assign to this expression. - * @return A new [Selectable] (typically an [ExprWithAlias]) that wraps this expression and - * associates it with the provided alias. - */ - open fun alias(alias: String) = ExprWithAlias(alias, this) - - /** - * Creates an expression that this expression to another expression. - * - *

Example: - * - *

{@code // Add the value of the 'quantity' field and the 'reserve' field.
-   * Field.of("quantity").add(Field.of("reserve")); }
- * - * @param other The expression to add to this expression. - * @return A new {@code Expr} representing the addition operation. - */ - fun add(other: Expr) = FunctionExpr.add(this, other) - - /** - * Creates an expression that this expression to another expression. - * - *

Example: - * - *

{@code // Add the value of the 'quantity' field and the 'reserve' field.
-   * Field.of("quantity").add(Field.of("reserve")); }
- * - * @param other The constant value to add to this expression. - * @return A new {@code Expr} representing the addition operation. - */ - fun add(other: Any) = FunctionExpr.add(this, other) - - fun subtract(other: Expr) = FunctionExpr.subtract(this, other) - - fun subtract(other: Any) = FunctionExpr.subtract(this, other) - - fun multiply(other: Expr) = FunctionExpr.multiply(this, other) - - fun multiply(other: Any) = FunctionExpr.multiply(this, other) - - fun divide(other: Expr) = FunctionExpr.divide(this, other) - - fun divide(other: Any) = FunctionExpr.divide(this, other) - - fun mod(other: Expr) = FunctionExpr.mod(this, other) - - fun mod(other: Any) = FunctionExpr.mod(this, other) - - fun eqAny(values: List) = FunctionExpr.eqAny(this, values) - - fun notEqAny(values: List) = FunctionExpr.notEqAny(this, values) - - fun isNan() = FunctionExpr.isNan(this) - - fun isNotNan() = FunctionExpr.isNotNan(this) - - fun isNull() = FunctionExpr.isNull(this) - - fun isNotNull() = FunctionExpr.isNotNull(this) - - fun replaceFirst(find: Expr, replace: Expr) = FunctionExpr.replaceFirst(this, find, replace) - - fun replaceFirst(find: String, replace: String) = FunctionExpr.replaceFirst(this, find, replace) - - fun replaceAll(find: Expr, replace: Expr) = FunctionExpr.replaceAll(this, find, replace) - - fun replaceAll(find: String, replace: String) = FunctionExpr.replaceAll(this, find, replace) - - fun charLength() = FunctionExpr.charLength(this) - - fun byteLength() = FunctionExpr.byteLength(this) - - fun like(pattern: Expr) = FunctionExpr.like(this, pattern) - - fun like(pattern: String) = FunctionExpr.like(this, pattern) - - fun regexContains(pattern: Expr) = FunctionExpr.regexContains(this, pattern) - - fun regexContains(pattern: String) = FunctionExpr.regexContains(this, pattern) - - fun regexMatch(pattern: Expr) = FunctionExpr.regexMatch(this, pattern) - - fun regexMatch(pattern: String) = FunctionExpr.regexMatch(this, pattern) - - fun logicalMax(other: Expr) = FunctionExpr.logicalMax(this, other) - - fun logicalMax(other: Any) = FunctionExpr.logicalMax(this, other) - - fun logicalMin(other: Expr) = FunctionExpr.logicalMin(this, other) - - fun logicalMin(other: Any) = FunctionExpr.logicalMin(this, other) - - fun reverse() = FunctionExpr.reverse(this) - - fun strContains(substring: Expr) = FunctionExpr.strContains(this, substring) - - fun strContains(substring: String) = FunctionExpr.strContains(this, substring) - - fun startsWith(prefix: Expr) = FunctionExpr.startsWith(this, prefix) - - fun startsWith(prefix: String) = FunctionExpr.startsWith(this, prefix) - - fun endsWith(suffix: Expr) = FunctionExpr.endsWith(this, suffix) - - fun endsWith(suffix: String) = FunctionExpr.endsWith(this, suffix) - - fun toLower() = FunctionExpr.toLower(this) - - fun toUpper() = FunctionExpr.toUpper(this) - - fun trim() = FunctionExpr.trim(this) - - fun strConcat(vararg expr: Expr) = FunctionExpr.strConcat(this, *expr) - - fun strConcat(vararg string: String) = FunctionExpr.strConcat(this, *string) - - fun strConcat(vararg string: Any) = FunctionExpr.strConcat(this, *string) - - fun mapGet(key: Expr) = FunctionExpr.mapGet(this, key) - - fun mapGet(key: String) = FunctionExpr.mapGet(this, key) - - fun mapMerge(secondMap: Expr, vararg otherMaps: Expr) = - FunctionExpr.mapMerge(this, secondMap, *otherMaps) - - fun mapRemove(key: Expr) = FunctionExpr.mapRemove(this, key) - - fun mapRemove(key: String) = FunctionExpr.mapRemove(this, key) - - fun cosineDistance(vector: Expr) = FunctionExpr.cosineDistance(this, vector) - - fun cosineDistance(vector: DoubleArray) = FunctionExpr.cosineDistance(this, vector) - - fun cosineDistance(vector: VectorValue) = FunctionExpr.cosineDistance(this, vector) - - fun dotProduct(vector: Expr) = FunctionExpr.dotProduct(this, vector) - - fun dotProduct(vector: DoubleArray) = FunctionExpr.dotProduct(this, vector) - - fun dotProduct(vector: VectorValue) = FunctionExpr.dotProduct(this, vector) - - fun euclideanDistance(vector: Expr) = FunctionExpr.euclideanDistance(this, vector) - - fun euclideanDistance(vector: DoubleArray) = FunctionExpr.euclideanDistance(this, vector) - - fun euclideanDistance(vector: VectorValue) = FunctionExpr.euclideanDistance(this, vector) - - fun vectorLength() = FunctionExpr.vectorLength(this) - - fun unixMicrosToTimestamp() = FunctionExpr.unixMicrosToTimestamp(this) - - fun timestampToUnixMicros() = FunctionExpr.timestampToUnixMicros(this) - - fun unixMillisToTimestamp() = FunctionExpr.unixMillisToTimestamp(this) - - fun timestampToUnixMillis() = FunctionExpr.timestampToUnixMillis(this) - - fun unixSecondsToTimestamp() = FunctionExpr.unixSecondsToTimestamp(this) - - fun timestampToUnixSeconds() = FunctionExpr.timestampToUnixSeconds(this) - - fun timestampAdd(unit: Expr, amount: Expr) = FunctionExpr.timestampAdd(this, unit, amount) - - fun timestampAdd(unit: String, amount: Double) = FunctionExpr.timestampAdd(this, unit, amount) - - fun timestampSub(unit: Expr, amount: Expr) = FunctionExpr.timestampSub(this, unit, amount) - - fun timestampSub(unit: String, amount: Double) = FunctionExpr.timestampSub(this, unit, amount) - - fun arrayConcat(vararg arrays: Expr) = FunctionExpr.arrayConcat(this, *arrays) - - fun arrayConcat(arrays: List) = FunctionExpr.arrayConcat(this, arrays) - fun arrayReverse() = FunctionExpr.arrayReverse(this) + private val NULL: Expr = ValueConstant(Values.NULL_VALUE) - fun arrayContains(value: Expr) = FunctionExpr.arrayContains(this, value) - - fun arrayContains(value: Any) = FunctionExpr.arrayContains(this, value) - - fun arrayContainsAll(values: List) = FunctionExpr.arrayContainsAll(this, values) - - fun arrayContainsAny(values: List) = FunctionExpr.arrayContainsAny(this, values) - - fun arrayLength() = FunctionExpr.arrayLength(this) - - fun sum() = AggregateFunction.sum(this) - - fun avg() = AggregateFunction.avg(this) - - fun min() = AggregateFunction.min(this) - - fun max() = AggregateFunction.max(this) - - fun ascending() = Ordering.ascending(this) - - fun descending() = Ordering.descending(this) - - fun eq(other: Expr) = FunctionExpr.eq(this, other) - - fun eq(other: Any) = FunctionExpr.eq(this, other) - - fun neq(other: Expr) = FunctionExpr.neq(this, other) - - fun neq(other: Any) = FunctionExpr.neq(this, other) - - fun gt(other: Expr) = FunctionExpr.gt(this, other) - - fun gt(other: Any) = FunctionExpr.gt(this, other) - - fun gte(other: Expr) = FunctionExpr.gte(this, other) - - fun gte(other: Any) = FunctionExpr.gte(this, other) - - fun lt(other: Expr) = FunctionExpr.lt(this, other) + /** + * Create a constant for a [String] value. + * + * @param value The [String] value. + * @return A new [Expr] constant instance. + */ + @JvmStatic + fun constant(value: String): Expr { + return ValueConstant(encodeValue(value)) + } - fun lt(other: Any) = FunctionExpr.lt(this, other) + /** + * Create a constant for a [Number] value. + * + * @param value The [Number] value. + * @return A new [Expr] constant instance. + */ + @JvmStatic + fun constant(value: Number): Expr { + return ValueConstant(encodeValue(value)) + } - fun lte(other: Expr) = FunctionExpr.lte(this, other) + /** + * Create a constant for a [Date] value. + * + * @param value The [Date] value. + * @return A new [Expr] constant instance. + */ + @JvmStatic + fun constant(value: Date): Expr { + return ValueConstant(encodeValue(value)) + } - fun lte(other: Any) = FunctionExpr.lte(this, other) + /** + * Create a constant for a [Timestamp] value. + * + * @param value The [Timestamp] value. + * @return A new [Expr] constant instance. + */ + @JvmStatic + fun constant(value: Timestamp): Expr { + return ValueConstant(encodeValue(value)) + } - fun exists() = FunctionExpr.exists(this) + /** + * Create a constant for a [Boolean] value. + * + * @param value The [Boolean] value. + * @return A new [Expr] constant instance. + */ + @JvmStatic + fun constant(value: Boolean): Expr { + return ValueConstant(encodeValue(value)) + } - internal abstract fun toProto(userDataReader: UserDataReader): Value -} + /** + * Create a constant for a [GeoPoint] value. + * + * @param value The [GeoPoint] value. + * @return A new [Expr] constant instance. + */ + @JvmStatic + fun constant(value: GeoPoint): Expr { + return ValueConstant(encodeValue(value)) + } -/** Expressions that have an alias are [Selectable] */ -abstract class Selectable : Expr() { - internal abstract fun getAlias(): String - internal abstract fun getExpr(): Expr + /** + * Create a constant for a [Blob] value. + * + * @param value The [Blob] value. + * @return A new [Expr] constant instance. + */ + @JvmStatic + fun constant(value: Blob): Expr { + return ValueConstant(encodeValue(value)) + } - internal companion object { - fun toSelectable(o: Any): Selectable { - return when (o) { - is Selectable -> o - is String -> Field.of(o) - is FieldPath -> Field.of(o) - else -> throw IllegalArgumentException("Unknown Selectable type: $o") + /** + * Create a constant for a [DocumentReference] value. + * + * @param ref The [DocumentReference] value. + * @return A new [Expr] constant instance. + */ + @JvmStatic + fun constant(ref: DocumentReference): Expr { + return object : Expr() { + override fun toProto(userDataReader: UserDataReader): Value { + userDataReader.validateDocumentReference(ref, ::IllegalArgumentException) + return encodeValue(ref) + } } } - } -} -/** Represents an expression that will be given the alias in the output document. */ -class ExprWithAlias internal constructor(private val alias: String, private val expr: Expr) : - Selectable() { - override fun getAlias() = alias - override fun getExpr() = expr - override fun toProto(userDataReader: UserDataReader): Value = expr.toProto(userDataReader) -} + /** + * Create a constant for a [VectorValue] value. + * + * @param value The [VectorValue] value. + * @return A new [Expr] constant instance. + */ + @JvmStatic + fun constant(value: VectorValue): Expr { + return ValueConstant(encodeValue(value)) + } -/** - * Represents a reference to a field in a Firestore document. - * - * [Field] references are used to access document field values in expressions and to specify fields - * for sorting, filtering, and projecting data in Firestore pipelines. - * - * You can create a [Field] instance using the static [of] method: - */ -class Field internal constructor(private val fieldPath: ModelFieldPath) : Selectable() { - companion object { + /** + * Constant for a null value. + * + * @return A [Expr] constant instance. + */ + @JvmStatic + fun nullValue(): Expr { + return NULL + } /** - * An expression that returns the document ID. + * Create a vector constant for a [DoubleArray] value. * - * @return An [Field] representing the document ID. + * @param vector The [VectorValue] value. + * @return A [Expr] constant instance. */ - @JvmField val DOCUMENT_ID: Field = of(FieldPath.documentId()) + @JvmStatic + fun vector(vector: DoubleArray): Expr { + return ValueConstant(Values.encodeVectorValue(vector)) + } + + /** + * Create a vector constant for a [VectorValue] value. + * + * @param vector The [VectorValue] value. + * @return A [Expr] constant instance. + */ + @JvmStatic + fun vector(vector: VectorValue): Expr { + return ValueConstant(encodeValue(vector)) + } /** * Creates a [Field] instance representing the field at the given path. @@ -407,7 +243,7 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select * @return A new [Field] instance representing the specified path. */ @JvmStatic - fun of(name: String): Field { + fun field(name: String): Field { if (name == DocumentKey.KEY_FIELD_NAME) { return Field(ModelFieldPath.KEY_PATH) } @@ -424,47 +260,9 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select * @return A new [Field] instance representing the specified path. */ @JvmStatic - fun of(fieldPath: FieldPath): Field { + fun field(fieldPath: FieldPath): Field { return Field(fieldPath.internalPath) } - } - - override fun getAlias(): String = fieldPath.canonicalString() - - override fun getExpr(): Expr = this - - override fun toProto(userDataReader: UserDataReader) = toProto() - - internal fun toProto(): Value = - Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() -} - -internal class ListOfExprs(private val expressions: Array) : Expr() { - override fun toProto(userDataReader: UserDataReader): Value = - encodeValue(expressions.map { it.toProto(userDataReader) }) -} - -/** - * This class defines the base class for Firestore [Pipeline] functions, which can be evaluated - * within pipeline execution. - * - * Typically, you would not use this class or its children directly. Use either the functions like - * [and], [eq], or the methods on [Expr] ([Expr.eq]), [Expr.lt], etc) to construct new - * [FunctionExpr] instances. - */ -open class FunctionExpr -protected constructor(private val name: String, private val params: Array) : Expr() { - private constructor( - name: String, - param: Expr, - vararg params: Any - ) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) - private constructor( - name: String, - fieldName: String, - vararg params: Any - ) : this(name, arrayOf(Field.of(fieldName), *toArrayOfExprOrConstant(params))) - companion object { @JvmStatic fun generic(name: String, vararg expr: Expr) = FunctionExpr(name, expr) @@ -781,7 +579,7 @@ protected constructor(private val name: String, private val params: Array) = - map(elements.flatMap { listOf(of(it.key), toExprOrConstant(it.value)) }.toTypedArray()) + map(elements.flatMap { listOf(constant(it.key), toExprOrConstant(it.value)) }.toTypedArray()) @JvmStatic fun mapGet(map: Expr, key: Expr) = FunctionExpr("map_get", map, key) @@ -816,7 +614,7 @@ protected constructor(private val name: String, private val params: ArrayAliases are useful for renaming fields in the output of a stage or for giving meaningful + * names to calculated values. + * + *

Example: + * + *

 // Calculate the total price and assign it the alias "totalPrice" and add it to the
+   *
+   * output. firestore.pipeline().collection("items")
+   * .addFields(Expr.field("price").multiply(Expr.field("quantity")).as("totalPrice")); 
+ * + * @param alias The alias to assign to this expression. + * @return A new [Selectable] (typically an [ExprWithAlias]) that wraps this expression and + * associates it with the provided alias. + */ + open fun alias(alias: String) = ExprWithAlias(alias, this) + + /** + * Creates an expression that this expression to another expression. + * + *

Example: + * + *

{@code // Add the value of the 'quantity' field and the 'reserve' field.
+   * Expr.field("quantity").add(Expr.field("reserve")); }
+ * + * @param other The expression to add to this expression. + * @return A new {@code Expr} representing the addition operation. + */ + fun add(other: Expr) = add(this, other) + + /** + * Creates an expression that this expression to another expression. + * + *

Example: + * + *

{@code // Add the value of the 'quantity' field and the 'reserve' field.
+   * Expr.field("quantity").add(Expr.field("reserve")); }
+ * + * @param other The constant value to add to this expression. + * @return A new {@code Expr} representing the addition operation. + */ + fun add(other: Any) = add(this, other) + + fun subtract(other: Expr) = subtract(this, other) + + fun subtract(other: Any) = subtract(this, other) + + fun multiply(other: Expr) = multiply(this, other) + + fun multiply(other: Any) = multiply(this, other) + + fun divide(other: Expr) = divide(this, other) + + fun divide(other: Any) = divide(this, other) + + fun mod(other: Expr) = mod(this, other) + + fun mod(other: Any) = mod(this, other) + + fun eqAny(values: List) = eqAny(this, values) + + fun notEqAny(values: List) = notEqAny(this, values) + + fun isNan() = isNan(this) + + fun isNotNan() = isNotNan(this) + + fun isNull() = isNull(this) + + fun isNotNull() = isNotNull(this) + + fun replaceFirst(find: Expr, replace: Expr) = replaceFirst(this, find, replace) + + fun replaceFirst(find: String, replace: String) = replaceFirst(this, find, replace) + + fun replaceAll(find: Expr, replace: Expr) = replaceAll(this, find, replace) + + fun replaceAll(find: String, replace: String) = replaceAll(this, find, replace) + + fun charLength() = charLength(this) + + fun byteLength() = byteLength(this) + + fun like(pattern: Expr) = like(this, pattern) + + fun like(pattern: String) = like(this, pattern) + + fun regexContains(pattern: Expr) = regexContains(this, pattern) + + fun regexContains(pattern: String) = regexContains(this, pattern) + + fun regexMatch(pattern: Expr) = regexMatch(this, pattern) + + fun regexMatch(pattern: String) = regexMatch(this, pattern) + + fun logicalMax(other: Expr) = logicalMax(this, other) + + fun logicalMax(other: Any) = logicalMax(this, other) + + fun logicalMin(other: Expr) = logicalMin(this, other) + + fun logicalMin(other: Any) = logicalMin(this, other) + + fun reverse() = reverse(this) + + fun strContains(substring: Expr) = strContains(this, substring) + + fun strContains(substring: String) = strContains(this, substring) + + fun startsWith(prefix: Expr) = startsWith(this, prefix) + + fun startsWith(prefix: String) = startsWith(this, prefix) + + fun endsWith(suffix: Expr) = endsWith(this, suffix) + + fun endsWith(suffix: String) = endsWith(this, suffix) + + fun toLower() = toLower(this) + + fun toUpper() = toUpper(this) + + fun trim() = trim(this) + + fun strConcat(vararg expr: Expr) = Companion.strConcat(this, *expr) + + fun strConcat(vararg string: String) = strConcat(this, *string) + + fun strConcat(vararg string: Any) = Companion.strConcat(this, *string) + + fun mapGet(key: Expr) = mapGet(this, key) + + fun mapGet(key: String) = mapGet(this, key) + + fun mapMerge(secondMap: Expr, vararg otherMaps: Expr) = + Companion.mapMerge(this, secondMap, *otherMaps) + + fun mapRemove(key: Expr) = mapRemove(this, key) + + fun mapRemove(key: String) = mapRemove(this, key) + + fun cosineDistance(vector: Expr) = cosineDistance(this, vector) + + fun cosineDistance(vector: DoubleArray) = cosineDistance(this, vector) + + fun cosineDistance(vector: VectorValue) = cosineDistance(this, vector) + + fun dotProduct(vector: Expr) = dotProduct(this, vector) + + fun dotProduct(vector: DoubleArray) = dotProduct(this, vector) + + fun dotProduct(vector: VectorValue) = dotProduct(this, vector) + + fun euclideanDistance(vector: Expr) = euclideanDistance(this, vector) + + fun euclideanDistance(vector: DoubleArray) = euclideanDistance(this, vector) + + fun euclideanDistance(vector: VectorValue) = euclideanDistance(this, vector) + + fun vectorLength() = vectorLength(this) + + fun unixMicrosToTimestamp() = unixMicrosToTimestamp(this) + + fun timestampToUnixMicros() = timestampToUnixMicros(this) + + fun unixMillisToTimestamp() = unixMillisToTimestamp(this) + + fun timestampToUnixMillis() = timestampToUnixMillis(this) + + fun unixSecondsToTimestamp() = unixSecondsToTimestamp(this) + + fun timestampToUnixSeconds() = timestampToUnixSeconds(this) + + fun timestampAdd(unit: Expr, amount: Expr) = timestampAdd(this, unit, amount) + + fun timestampAdd(unit: String, amount: Double) = timestampAdd(this, unit, amount) + + fun timestampSub(unit: Expr, amount: Expr) = timestampSub(this, unit, amount) + + fun timestampSub(unit: String, amount: Double) = timestampSub(this, unit, amount) + + fun arrayConcat(vararg arrays: Expr) = Companion.arrayConcat(this, *arrays) + + fun arrayConcat(arrays: List) = arrayConcat(this, arrays) + + fun arrayReverse() = arrayReverse(this) + + fun arrayContains(value: Expr) = arrayContains(this, value) + + fun arrayContains(value: Any) = arrayContains(this, value) + + fun arrayContainsAll(values: List) = arrayContainsAll(this, values) + + fun arrayContainsAny(values: List) = arrayContainsAny(this, values) + + fun arrayLength() = arrayLength(this) + + fun sum() = AggregateFunction.sum(this) + + fun avg() = AggregateFunction.avg(this) + + fun min() = AggregateFunction.min(this) + + fun max() = AggregateFunction.max(this) + + fun ascending() = Ordering.ascending(this) + + fun descending() = Ordering.descending(this) + + fun eq(other: Expr) = eq(this, other) + + fun eq(other: Any) = eq(this, other) + + fun neq(other: Expr) = neq(this, other) + + fun neq(other: Any) = neq(this, other) + + fun gt(other: Expr) = gt(this, other) + + fun gt(other: Any) = gt(this, other) + + fun gte(other: Expr) = gte(this, other) + + fun gte(other: Any) = gte(this, other) + + fun lt(other: Expr) = lt(this, other) + + fun lt(other: Any) = lt(this, other) + + fun lte(other: Expr) = lte(this, other) + + fun lte(other: Any) = lte(this, other) + + fun exists() = exists(this) + + internal abstract fun toProto(userDataReader: UserDataReader): Value +} + +/** Expressions that have an alias are [Selectable] */ +abstract class Selectable : Expr() { + internal abstract fun getAlias(): String + internal abstract fun getExpr(): Expr + + internal companion object { + fun toSelectable(o: Any): Selectable { + return when (o) { + is Selectable -> o + is String -> Expr.field(o) + is FieldPath -> Expr.field(o) + else -> throw IllegalArgumentException("Unknown Selectable type: $o") + } + } + } +} + +/** Represents an expression that will be given the alias in the output document. */ +class ExprWithAlias internal constructor(private val alias: String, private val expr: Expr) : + Selectable() { + override fun getAlias() = alias + override fun getExpr() = expr + override fun toProto(userDataReader: UserDataReader): Value = expr.toProto(userDataReader) +} + +/** + * Represents a reference to a field in a Firestore document. + * + * [Field] references are used to access document field values in expressions and to specify fields + * for sorting, filtering, and projecting data in Firestore pipelines. + * + * You can create a [Field] instance using the static [Expr.field] method: + */ +class Field internal constructor(private val fieldPath: ModelFieldPath) : Selectable() { + companion object { + + /** + * An expression that returns the document ID. + * + * @return An [Field] representing the document ID. + */ + @JvmField val DOCUMENT_ID: Field = field(FieldPath.documentId()) + + } + + override fun getAlias(): String = fieldPath.canonicalString() + + override fun getExpr(): Expr = this + + override fun toProto(userDataReader: UserDataReader) = toProto() + + internal fun toProto(): Value = + Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() +} + +internal class ListOfExprs(private val expressions: Array) : Expr() { + override fun toProto(userDataReader: UserDataReader): Value = + encodeValue(expressions.map { it.toProto(userDataReader) }) +} + +/** + * This class defines the base class for Firestore [Pipeline] functions, which can be evaluated + * within pipeline execution. + * + * Typically, you would not use this class or its children directly. Use either the functions like + * [and], [eq], or the methods on [Expr] ([Expr.eq]), [Expr.lt], etc) to construct new + * [FunctionExpr] instances. + */ +open class FunctionExpr +internal constructor(private val name: String, private val params: Array) : Expr() { + internal constructor( + name: String, + param: Expr, + vararg params: Any + ) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) + internal constructor( + name: String, + fieldName: String, + vararg params: Any + ) : this(name, arrayOf(Expr.field(fieldName), *toArrayOfExprOrConstant(params))) + override fun toProto(userDataReader: UserDataReader): Value { val builder = com.google.firestore.v1.Function.newBuilder() builder.setName(name) @@ -1096,7 +1236,7 @@ class BooleanExpr internal constructor(name: String, params: Array) : name: String, fieldName: String, vararg params: Any - ) : this(name, arrayOf(Field.of(fieldName), *toArrayOfExprOrConstant(params))) + ) : this(name, arrayOf(Expr.field(fieldName), *toArrayOfExprOrConstant(params))) companion object { @@ -1135,7 +1275,7 @@ class Ordering private constructor(val expr: Expr, private val dir: Direction) { * @return A new [Ordering] object with ascending sort by field. */ @JvmStatic - fun ascending(fieldName: String): Ordering = Ordering(Field.of(fieldName), Direction.ASCENDING) + fun ascending(fieldName: String): Ordering = Ordering(Expr.field(fieldName), Direction.ASCENDING) /** * Create an [Ordering] that sorts documents in descending order based on value of [expr]. @@ -1153,7 +1293,7 @@ class Ordering private constructor(val expr: Expr, private val dir: Direction) { */ @JvmStatic fun descending(fieldName: String): Ordering = - Ordering(Field.of(fieldName), Direction.DESCENDING) + Ordering(Expr.field(fieldName), Direction.DESCENDING) } private class Direction private constructor(val proto: Value) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt index af3853f2f4a..300d770c337 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt @@ -15,6 +15,7 @@ package com.google.firebase.firestore.pipeline import com.google.common.collect.ImmutableMap +import com.google.firebase.firestore.model.Values import com.google.firestore.v1.ArrayValue import com.google.firestore.v1.MapValue import com.google.firestore.v1.Value @@ -27,7 +28,7 @@ import com.google.firestore.v1.Value * `ImmutableMap, Value>` is an implementation detail, not to be exposed, since * more efficient implementations are possible. */ -internal class InternalOptions +class InternalOptions internal constructor(private val options: ImmutableMap) { internal fun with(key: String, value: Value): InternalOptions { val builder = ImmutableMap.builderWithExpectedSize(options.size + 1) @@ -56,11 +57,179 @@ internal constructor(private val options: ImmutableMap) { return Value.newBuilder().setMapValue(mapValue).build() } - internal companion object { - internal val EMPTY: InternalOptions = InternalOptions(ImmutableMap.of()) + companion object { + @JvmField + val EMPTY: InternalOptions = InternalOptions(ImmutableMap.of()) - internal fun of(key: String, value: Value): InternalOptions { + fun of(key: String, value: Value): InternalOptions { return InternalOptions(ImmutableMap.of(key, value)) } } } + +abstract class AbstractOptions> +internal constructor(internal val options: InternalOptions) { + + internal abstract fun self(options: InternalOptions): T + + protected fun with(key: String, value: InternalOptions): T = self(options.with(key, value)) + + protected fun with(key: String, value: Value): T = self(options.with(key, value)) + + /** + * Specify generic [String] option + * + * @param key The option key + * @param value The [String] value of option + * @return A new options object. + */ + fun with(key: String, value: String): T = with(key, Values.encodeValue(value)) + + /** + * Specify generic [Boolean] option + * + * @param key The option key + * @param value The [Boolean] value of option + * @return A new options object. + */ + fun with(key: String, value: Boolean): T = with(key, Values.encodeValue(value)) + + /** + * Specify generic [Long] option + * + * @param key The option key + * @param value The [Long] value of option + * @return A new options object. + */ + fun with(key: String, value: Long): T = with(key, Values.encodeValue(value)) + + /** + * Specify generic [Double] option + * + * @param key The option key + * @param value The [Double] value of option + * @return A new options object. + */ + fun with(key: String, value: Double): T = with(key, Values.encodeValue(value)) + + /** + * Specify generic [Field] option + * + * @param key The option key + * @param value The [Field] value of option + * @return A new options object. + */ + fun with(key: String, value: Field): T = with(key, value.toProto()) + + /** + * Specify [GenericOptions] object + * + * @param key The option key + * @param value The [GenericOptions] object + * @return A new options object. + */ + fun with(key: String, value: GenericOptions): T = with(key, value.options) +} + +class GenericOptions private constructor(options: InternalOptions) : AbstractOptions(options) { + override fun self(options: InternalOptions) = GenericOptions(options) + + companion object { + @JvmField + val DEFAULT: GenericOptions = GenericOptions(InternalOptions.EMPTY) + } +} + +class PipelineOptions private constructor(options: InternalOptions) : AbstractOptions(options) { + + override fun self(options: InternalOptions) = PipelineOptions(options) + + companion object { + @JvmField + val DEFAULT: PipelineOptions = PipelineOptions(InternalOptions.EMPTY) + } + + class IndexMode private constructor(internal val value: String) { + companion object { + @JvmField + val RECOMMENDED = IndexMode("recommended") + } + } + + fun withIndexMode(indexMode: IndexMode): PipelineOptions = + with("index_mode", indexMode.value) + + fun withExplainOptions(options: ExplainOptions): PipelineOptions = + with("explain_options", options.options) +} + +class ExplainOptions private constructor(options: InternalOptions) : AbstractOptions(options) { + override fun self(options: InternalOptions) = ExplainOptions(options) + + companion object { + @JvmField + val DEFAULT = ExplainOptions(InternalOptions.EMPTY) + } + + fun withMode(value: ExplainMode) = with("mode", value.value) + + fun withOutputFormat(value: OutputFormat) = with("output_format", value.value) + + fun withVerbosity(value: Verbosity) = with("verbosity", value.value) + + fun withIndexRecommendation(value: Boolean) = with("index_recommendation", value) + + fun withProfiles(value: Profiles) = with("profiles", value.value) + + fun withRedact(value: Boolean) = with("redact", value) + + class ExplainMode private constructor(internal val value: String) { + companion object { + @JvmField + val EXECUTE = ExplainMode("execute") + + @JvmField + val EXPLAIN = ExplainMode("explain") + + @JvmField + val ANALYZE = ExplainMode("analyze") + } + } + + class OutputFormat private constructor(internal val value: String) { + companion object { + @JvmField + val TEXT = OutputFormat("text") + + @JvmField + val JSON = OutputFormat("json") + + @JvmField + val STRUCT = OutputFormat("struct") + } + } + + class Verbosity private constructor(internal val value: String) { + companion object { + @JvmField + val SUMMARY_ONLY = Verbosity("summary_only") + + @JvmField + val EXECUTION_TREE = Verbosity("execution_tree") + } + } + + class Profiles private constructor(internal val value: String) { + companion object { + @JvmField + val LATENCY = Profiles("latency") + + @JvmField + val RECORDS_COUNT = Profiles("records_count") + + @JvmField + val BYTES_THROUGHPUT = Profiles("bytes_throughput") + } + } +} + diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index 57293c47bad..6eedcb2fd4a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -18,7 +18,8 @@ import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.VectorValue import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue -import com.google.firebase.firestore.pipeline.Field.Companion.of +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.field import com.google.firestore.v1.Pipeline import com.google.firestore.v1.Value @@ -266,7 +267,7 @@ internal constructor( * @return [AggregateStage] with specified groups. */ fun withGroups(groupField: String, vararg additionalGroups: Any) = - withGroups(Field.of(groupField), additionalGroups) + withGroups(Expr.field(groupField), additionalGroups) /** * Add one or more groups to [AggregateStage] @@ -327,7 +328,7 @@ internal constructor( */ @JvmStatic fun of(vectorField: Field, vectorValue: VectorValue, distanceMeasure: DistanceMeasure) = - FindNearestStage(vectorField, Constant.of(vectorValue), distanceMeasure) + FindNearestStage(vectorField, constant(vectorValue), distanceMeasure) /** * Create [FindNearestStage]. @@ -341,7 +342,7 @@ internal constructor( */ @JvmStatic fun of(vectorField: Field, vectorValue: DoubleArray, distanceMeasure: DistanceMeasure) = - FindNearestStage(vectorField, Constant.vector(vectorValue), distanceMeasure) + FindNearestStage(vectorField, Expr.vector(vectorValue), distanceMeasure) /** * Create [FindNearestStage]. @@ -355,7 +356,7 @@ internal constructor( */ @JvmStatic fun of(vectorField: String, vectorValue: VectorValue, distanceMeasure: DistanceMeasure) = - FindNearestStage(Constant.of(vectorField), Constant.of(vectorValue), distanceMeasure) + FindNearestStage(constant(vectorField), constant(vectorValue), distanceMeasure) /** * Create [FindNearestStage]. @@ -369,7 +370,7 @@ internal constructor( */ @JvmStatic fun of(vectorField: String, vectorValue: DoubleArray, distanceMeasure: DistanceMeasure) = - FindNearestStage(Constant.of(vectorField), Constant.vector(vectorValue), distanceMeasure) + FindNearestStage(constant(vectorField), Expr.vector(vectorValue), distanceMeasure) } class DistanceMeasure private constructor(internal val proto: Value) { @@ -418,7 +419,7 @@ internal constructor( * @return [FindNearestStage] with specified [distanceField]. */ fun withDistanceField(distanceField: String): FindNearestStage = - withDistanceField(of(distanceField)) + withDistanceField(field(distanceField)) } internal class LimitStage @@ -598,7 +599,7 @@ internal constructor( */ @JvmStatic fun withField(arrayField: String, alias: String): UnnestStage = - UnnestStage(Field.of(arrayField).alias(alias)) + UnnestStage(Expr.field(arrayField).alias(alias)) } override fun self(options: InternalOptions) = UnnestStage(selectable, options) override fun args(userDataReader: UserDataReader): Sequence = From dd2e9bdb522729844bae04ba6e0c3b154c2e4fbd Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 15 Apr 2025 15:51:41 -0400 Subject: [PATCH 042/152] add named options to CollectionSource and CollectionGroupSource --- .../com/google/firebase/firestore/Pipeline.kt | 57 ++++++++-------- .../google/firebase/firestore/core/Query.java | 9 +-- .../firebase/firestore/pipeline/stage.kt | 68 ++++++++++++++++--- 3 files changed, 94 insertions(+), 40 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 23ff5a5ba19..cc61160f405 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -21,7 +21,6 @@ import com.google.common.collect.ImmutableList import com.google.firebase.Timestamp import com.google.firebase.firestore.model.DocumentKey import com.google.firebase.firestore.model.Values -import com.google.firebase.firestore.pipeline.AbstractOptions import com.google.firebase.firestore.pipeline.AddFieldsStage import com.google.firebase.firestore.pipeline.AggregateFunction import com.google.firebase.firestore.pipeline.AggregateStage @@ -39,7 +38,6 @@ import com.google.firebase.firestore.pipeline.FindNearestStage import com.google.firebase.firestore.pipeline.FunctionExpr import com.google.firebase.firestore.pipeline.GenericArg import com.google.firebase.firestore.pipeline.GenericStage -import com.google.firebase.firestore.pipeline.InternalOptions import com.google.firebase.firestore.pipeline.LimitStage import com.google.firebase.firestore.pipeline.OffsetStage import com.google.firebase.firestore.pipeline.Ordering @@ -54,7 +52,6 @@ import com.google.firebase.firestore.pipeline.Stage import com.google.firebase.firestore.pipeline.UnionStage import com.google.firebase.firestore.pipeline.UnnestStage import com.google.firebase.firestore.pipeline.WhereStage -import com.google.firebase.firestore.util.Preconditions import com.google.firestore.v1.ExecutePipelineRequest import com.google.firestore.v1.StructuredPipeline import com.google.firestore.v1.Value @@ -614,7 +611,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * Convert the given Query into an equivalent Pipeline. * * @param query A Query to be converted into a Pipeline. - * @return Pipeline that is equivalent to [query] + * @return A new [Pipeline] object that is equivalent to [query] * @throws [IllegalArgumentException] Thrown if the [query] provided targets a different project * or database than the pipeline. */ @@ -629,7 +626,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * Convert the given Aggregate Query into an equivalent Pipeline. * * @param aggregateQuery An Aggregate Query to be converted into a Pipeline. - * @return Pipeline that is equivalent to [aggregateQuery] + * @return A new [Pipeline] object that is equivalent to [aggregateQuery] * @throws [IllegalArgumentException] Thrown if the [aggregateQuery] provided targets a different * project or database than the pipeline. */ @@ -646,46 +643,50 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * Set the pipeline's source to the collection specified by the given path. * * @param path A path to a collection that will be the source of this pipeline. - * @return Pipeline with documents from target collection. + * @return A new [Pipeline] object with documents from target collection. */ - fun collection(path: String): Pipeline = - // Validate path by converting to CollectionReference - collection(firestore.collection(path)) + fun collection(path: String): Pipeline = collection(CollectionSource.of(path)) /** - * Set the pipeline's source to the collection specified by the given CollectionReference. + * Set the pipeline's source to the collection specified by the given [CollectionReference]. * - * @param ref A CollectionReference for a collection that will be the source of this pipeline. - * @return Pipeline with documents from target collection. + * @param ref A [CollectionReference] for a collection that will be the source of this pipeline. + * @return A new [Pipeline] object with documents from target collection. * @throws [IllegalArgumentException] Thrown if the [ref] provided targets a different project or * database than the pipeline. */ - fun collection(ref: CollectionReference): Pipeline { - if (ref.firestore.databaseId != firestore.databaseId) { - throw IllegalArgumentException( - "Provided collection reference is from a different Firestore instance." - ) + fun collection(ref: CollectionReference): Pipeline = collection(CollectionSource.of(ref)) + + /** + * Set the pipeline's source to the collection specified by CollectionSource. + * + * @param stage A [CollectionSource] that will be the source of this pipeline. + * @return Pipeline with documents from target collection. + * @throws [IllegalArgumentException] Thrown if the [stage] provided targets a different project + * or database than the pipeline. + */ + fun collection(stage: CollectionSource): Pipeline { + if (stage.firestore != null && stage.firestore.databaseId != firestore.databaseId) { + throw IllegalArgumentException("Provided collection is from a different Firestore instance.") } - return Pipeline(firestore, firestore.userDataReader, CollectionSource(ref.path)) + return Pipeline(firestore, firestore.userDataReader, stage) } /** * Set the pipeline's source to the collection group with the given id. * - * @param collectionid The id of a collection group that will be the source of this pipeline. + * @param collectionId The id of a collection group that will be the source of this pipeline. */ - fun collectionGroup(collectionId: String): Pipeline { - Preconditions.checkNotNull(collectionId, "Provided collection ID must not be null.") - require(!collectionId.contains("/")) { - "Invalid collectionId '$collectionId'. Collection IDs must not contain '/'." - } - return Pipeline(firestore, firestore.userDataReader, CollectionGroupSource(collectionId)) - } + fun collectionGroup(collectionId: String): Pipeline = + pipeline(CollectionGroupSource.of((collectionId))) + + fun pipeline(stage: CollectionGroupSource): Pipeline = + Pipeline(firestore, firestore.userDataReader, stage) /** * Set the pipeline's source to be all documents in this database. * - * @return Pipeline with all documents in this database. + * @return A new [Pipeline] object with all documents in this database. */ fun database(): Pipeline = Pipeline(firestore, firestore.userDataReader, DatabaseSource()) @@ -694,7 +695,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * * @param documents Paths specifying the individual documents that will be the source of this * pipeline. - * @return Pipeline with [documents]. + * @return A new [Pipeline] object with [documents]. */ fun documents(vararg documents: String): Pipeline = // Validate document path by converting to DocumentReference diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index 3607f584a2a..08c3918b901 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -35,6 +35,7 @@ import com.google.firebase.firestore.pipeline.Expr; import com.google.firebase.firestore.pipeline.Field; import com.google.firebase.firestore.pipeline.FunctionExpr; +import com.google.firebase.firestore.pipeline.InternalOptions; import com.google.firebase.firestore.pipeline.Ordering; import com.google.firebase.firestore.pipeline.Stage; import com.google.firestore.v1.Value; @@ -521,7 +522,7 @@ private synchronized Target toTarget(List orderBys) { @NonNull public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataReader) { - Pipeline p = new Pipeline(firestore, userDataReader, pipelineSource()); + Pipeline p = new Pipeline(firestore, userDataReader, pipelineSource(firestore)); // Filters for (Filter filter : filters) { @@ -598,13 +599,13 @@ private static BooleanExpr whereConditionsFromCursor( } @NonNull - private Stage pipelineSource() { + private Stage pipelineSource(FirebaseFirestore firestore) { if (isDocumentQuery()) { return new DocumentsSource(path.canonicalString()); } else if (isCollectionGroupQuery()) { - return new CollectionGroupSource(collectionGroup); + return CollectionGroupSource.of(collectionGroup); } else { - return new CollectionSource(path.canonicalString()); + return new CollectionSource(path.canonicalString(), firestore, InternalOptions.EMPTY); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index 6eedcb2fd4a..cc930495eb3 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -14,12 +14,16 @@ package com.google.firebase.firestore.pipeline +import com.google.firebase.firestore.CollectionReference +import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.VectorValue +import com.google.firebase.firestore.model.ResourcePath import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.pipeline.Expr.Companion.constant import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.util.Preconditions import com.google.firestore.v1.Pipeline import com.google.firestore.v1.Value @@ -93,7 +97,7 @@ internal constructor(protected val name: String, internal val options: InternalO * not implemented in the SDK version being used. */ class GenericStage -internal constructor( +private constructor( name: String, private val arguments: List, options: InternalOptions = InternalOptions.EMPTY @@ -168,24 +172,72 @@ internal constructor(options: InternalOptions = InternalOptions.EMPTY) : override fun args(userDataReader: UserDataReader): Sequence = emptySequence() } -internal class CollectionSource -@JvmOverloads -internal constructor(val path: String, options: InternalOptions = InternalOptions.EMPTY) : +class CollectionSource +internal constructor( + private val path: String, + // We validate [firestore.databaseId] when adding to pipeline. + internal val firestore: FirebaseFirestore?, + options: InternalOptions) : Stage("collection", options) { - override fun self(options: InternalOptions): CollectionSource = CollectionSource(path, options) + override fun self(options: InternalOptions): CollectionSource = CollectionSource(path, firestore, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf( Value.newBuilder().setReferenceValue(if (path.startsWith("/")) path else "/" + path).build() ) + companion object { + /** + * Set the pipeline's source to the collection specified by the given path. + * + * @param path A path to a collection that will be the source of this pipeline. + * @return Pipeline with documents from target collection. + */ + @JvmStatic + fun of(path: String): CollectionSource { + // Validate path by converting to ResourcePath + val resourcePath = ResourcePath.fromString(path) + return CollectionSource(resourcePath.canonicalString(), null, InternalOptions.EMPTY) + } + + /** + * Set the pipeline's source to the collection specified by the given CollectionReference. + * + * @param ref A CollectionReference for a collection that will be the source of this pipeline. + * @return Pipeline with documents from target collection. + */ + @JvmStatic + fun of(ref: CollectionReference): CollectionSource { + return CollectionSource(ref.path, ref.firestore, InternalOptions.EMPTY) + } + } + + fun withForceIndex(value: String) = with("force_index", value) } -internal class CollectionGroupSource -@JvmOverloads -internal constructor(val collectionId: String, options: InternalOptions = InternalOptions.EMPTY) : +class CollectionGroupSource +private constructor(private val collectionId: String, options: InternalOptions) : Stage("collection_group", options) { override fun self(options: InternalOptions) = CollectionGroupSource(collectionId, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setReferenceValue("").build(), encodeValue(collectionId)) + + companion object { + + /** + * Set the pipeline's source to the collection group with the given id. + * + * @param collectionId The id of a collection group that will be the source of this pipeline. + */ + @JvmStatic + fun of(collectionId: String): CollectionGroupSource { + Preconditions.checkNotNull(collectionId, "Provided collection ID must not be null.") + require(!collectionId.contains("/")) { + "Invalid collectionId '$collectionId'. Collection IDs must not contain '/'." + } + return CollectionGroupSource(collectionId, InternalOptions.EMPTY) + } + } + + fun withForceIndex(value: String) = with("force_index", value) } internal class DocumentsSource From 0231d720c221e00d4f2c8ea56daacdd62f33cca1 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 15 Apr 2025 16:08:07 -0400 Subject: [PATCH 043/152] spotless, apiTxt, use Expr types. --- firebase-firestore/api.txt | 996 ++++++++++-------- .../firebase/firestore/PipelineTest.java | 11 +- .../com/google/firebase/firestore/Pipeline.kt | 10 +- .../google/firebase/firestore/core/Query.java | 7 +- .../firestore/pipeline/expressions.kt | 292 ++--- .../firebase/firestore/pipeline/options.kt | 64 +- .../firebase/firestore/pipeline/stage.kt | 7 +- 7 files changed, 752 insertions(+), 635 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 78ce19cd2aa..592d2b24ba9 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -426,6 +426,7 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Pipeline distinct(com.google.firebase.firestore.pipeline.Selectable group, java.lang.Object... additionalGroups); method public com.google.firebase.firestore.Pipeline distinct(String groupField, java.lang.Object... additionalGroups); method public com.google.android.gms.tasks.Task execute(); + method public com.google.android.gms.tasks.Task execute(com.google.firebase.firestore.pipeline.PipelineOptions options); method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.FindNearestStage stage); @@ -474,6 +475,7 @@ package com.google.firebase.firestore { public final class PipelineSource { method public com.google.firebase.firestore.Pipeline collection(com.google.firebase.firestore.CollectionReference ref); + method public com.google.firebase.firestore.Pipeline collection(com.google.firebase.firestore.pipeline.CollectionSource stage); method public com.google.firebase.firestore.Pipeline collection(String path); method public com.google.firebase.firestore.Pipeline collectionGroup(String collectionId); method public com.google.firebase.firestore.Pipeline convertFrom(com.google.firebase.firestore.AggregateQuery aggregateQuery); @@ -481,6 +483,7 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Pipeline database(); method public com.google.firebase.firestore.Pipeline documents(com.google.firebase.firestore.DocumentReference... documents); method public com.google.firebase.firestore.Pipeline documents(java.lang.String... documents); + method public com.google.firebase.firestore.Pipeline pipeline(com.google.firebase.firestore.pipeline.CollectionGroupSource stage); } @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD}) public @interface PropertyName { @@ -675,6 +678,17 @@ package com.google.firebase.firestore.ktx { package com.google.firebase.firestore.pipeline { + public abstract class AbstractOptions> { + method public final T with(String key, boolean value); + method public final T with(String key, com.google.firebase.firestore.pipeline.Field value); + method public final T with(String key, com.google.firebase.firestore.pipeline.GenericOptions value); + method protected final T with(String key, com.google.firebase.firestore.pipeline.InternalOptions value); + method public final T with(String key, double value); + method protected final T with(String key, error.NonExistentClass value); + method public final T with(String key, String value); + method public final T with(String key, long value); + } + public final class AggregateFunction { method public com.google.firebase.firestore.pipeline.AggregateWithAlias alias(String alias); method public static com.google.firebase.firestore.pipeline.AggregateFunction avg(com.google.firebase.firestore.pipeline.Expr expr); @@ -724,8 +738,8 @@ package com.google.firebase.firestore.pipeline { } public final class BooleanExpr extends com.google.firebase.firestore.pipeline.FunctionExpr { - method public com.google.firebase.firestore.pipeline.FunctionExpr cond(com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); - method public com.google.firebase.firestore.pipeline.FunctionExpr cond(Object then, Object otherwise); + method public com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); + method public com.google.firebase.firestore.pipeline.Expr cond(Object then, Object otherwise); method public com.google.firebase.firestore.pipeline.AggregateFunction countIf(); method public static com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); method public com.google.firebase.firestore.pipeline.BooleanExpr not(); @@ -736,414 +750,425 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); } - public abstract class Constant extends com.google.firebase.firestore.pipeline.Expr { - method public static final com.google.firebase.firestore.pipeline.Constant nullValue(); - method public static final com.google.firebase.firestore.pipeline.Constant of(boolean value); - method public static final com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.Blob value); - method public static final com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.DocumentReference ref); - method public static final com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.GeoPoint value); - method public static final com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.VectorValue value); - method public static final com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.Timestamp value); - method public static final com.google.firebase.firestore.pipeline.Constant of(Number value); - method public static final com.google.firebase.firestore.pipeline.Constant of(String value); - method public static final com.google.firebase.firestore.pipeline.Constant of(java.util.Date value); - method public static final com.google.firebase.firestore.pipeline.Constant vector(com.google.firebase.firestore.VectorValue vector); - method public static final com.google.firebase.firestore.pipeline.Constant vector(double[] vector); - field public static final com.google.firebase.firestore.pipeline.Constant.Companion Companion; - } - - public static final class Constant.Companion { - method public com.google.firebase.firestore.pipeline.Constant nullValue(); - method public com.google.firebase.firestore.pipeline.Constant of(boolean value); - method public com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.Blob value); - method public com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.DocumentReference ref); - method public com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.GeoPoint value); - method public com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.firestore.VectorValue value); - method public com.google.firebase.firestore.pipeline.Constant of(com.google.firebase.Timestamp value); - method public com.google.firebase.firestore.pipeline.Constant of(Number value); - method public com.google.firebase.firestore.pipeline.Constant of(String value); - method public com.google.firebase.firestore.pipeline.Constant of(java.util.Date value); - method public com.google.firebase.firestore.pipeline.Constant vector(com.google.firebase.firestore.VectorValue vector); - method public com.google.firebase.firestore.pipeline.Constant vector(double[] vector); + public final class CollectionGroupSource extends com.google.firebase.firestore.pipeline.Stage { + method public static com.google.firebase.firestore.pipeline.CollectionGroupSource of(String collectionId); + method public error.NonExistentClass withForceIndex(String value); + field public static final com.google.firebase.firestore.pipeline.CollectionGroupSource.Companion Companion; } - public abstract class Expr { - method public final com.google.firebase.firestore.pipeline.FunctionExpr add(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.FunctionExpr add(Object other); - method public com.google.firebase.firestore.pipeline.ExprWithAlias alias(String alias); - method public final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(com.google.firebase.firestore.pipeline.Expr... arrays); - method public final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(java.util.List arrays); - method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr value); - method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(Object value); - method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(java.util.List values); - method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(java.util.List values); - method public final com.google.firebase.firestore.pipeline.FunctionExpr arrayLength(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr arrayReverse(); - method public final com.google.firebase.firestore.pipeline.Ordering ascending(); - method public final com.google.firebase.firestore.pipeline.AggregateFunction avg(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(com.google.firebase.firestore.pipeline.Expr right); - method public final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(Object right); - method public final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(com.google.firebase.firestore.pipeline.Expr numberExpr); - method public final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(int number); - method public final com.google.firebase.firestore.pipeline.FunctionExpr bitNot(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(com.google.firebase.firestore.pipeline.Expr right); - method public final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(Object right); - method public final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(com.google.firebase.firestore.pipeline.Expr numberExpr); - method public final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(int number); - method public final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(com.google.firebase.firestore.pipeline.Expr right); - method public final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(Object right); - method public final com.google.firebase.firestore.pipeline.FunctionExpr byteLength(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr charLength(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector); - method public final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.VectorValue vector); - method public final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(double[] vector); - method public final com.google.firebase.firestore.pipeline.Ordering descending(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr divide(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.FunctionExpr divide(Object other); - method public final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector); - method public final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.VectorValue vector); - method public final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(double[] vector); - method public final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr suffix); - method public final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String suffix); - method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(Object other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(java.util.List values); - method public final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector); - method public final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.VectorValue vector); - method public final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(double[] vector); - method public final com.google.firebase.firestore.pipeline.BooleanExpr exists(); - method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(Object other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(Object other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr isNan(); - method public final com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(); - method public final com.google.firebase.firestore.pipeline.BooleanExpr isNotNull(); - method public final com.google.firebase.firestore.pipeline.BooleanExpr isNull(); - method public final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr pattern); - method public final com.google.firebase.firestore.pipeline.BooleanExpr like(String pattern); - method public final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(Object other); - method public final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(Object other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(Object other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(Object other); - method public final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(com.google.firebase.firestore.pipeline.Expr key); - method public final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(String key); - method public final com.google.firebase.firestore.pipeline.FunctionExpr mapMerge(com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); - method public final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(com.google.firebase.firestore.pipeline.Expr key); - method public final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(String key); - method public final com.google.firebase.firestore.pipeline.AggregateFunction max(); - method public final com.google.firebase.firestore.pipeline.AggregateFunction min(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr mod(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.FunctionExpr mod(Object other); - method public final com.google.firebase.firestore.pipeline.FunctionExpr multiply(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.FunctionExpr multiply(Object other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(Object other); - method public final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(java.util.List values); - method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr pattern); - method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String pattern); - method public final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr pattern); - method public final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String pattern); - method public final com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public final com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(String find, String replace); - method public final com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public final com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(String find, String replace); - method public final com.google.firebase.firestore.pipeline.FunctionExpr reverse(); - method public final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr prefix); - method public final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String prefix); - method public final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(com.google.firebase.firestore.pipeline.Expr... expr); - method public final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(java.lang.Object... string); - method public final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(java.lang.String... string); - method public final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr substring); - method public final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String substring); - method public final com.google.firebase.firestore.pipeline.FunctionExpr subtract(com.google.firebase.firestore.pipeline.Expr other); - method public final com.google.firebase.firestore.pipeline.FunctionExpr subtract(Object other); - method public final com.google.firebase.firestore.pipeline.AggregateFunction sum(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(String unit, double amount); - method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(String unit, double amount); - method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMicros(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMillis(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixSeconds(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr toLower(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr toUpper(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr trim(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr unixMicrosToTimestamp(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr unixMillisToTimestamp(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr unixSecondsToTimestamp(); - method public final com.google.firebase.firestore.pipeline.FunctionExpr vectorLength(); + public static final class CollectionGroupSource.Companion { + method public com.google.firebase.firestore.pipeline.CollectionGroupSource of(String collectionId); } - public final class ExprWithAlias extends com.google.firebase.firestore.pipeline.Selectable { + public final class CollectionSource extends com.google.firebase.firestore.pipeline.Stage { + method public static com.google.firebase.firestore.pipeline.CollectionSource of(com.google.firebase.firestore.CollectionReference ref); + method public static com.google.firebase.firestore.pipeline.CollectionSource of(String path); + method public error.NonExistentClass withForceIndex(String value); + field public static final com.google.firebase.firestore.pipeline.CollectionSource.Companion Companion; } - public final class Field extends com.google.firebase.firestore.pipeline.Selectable { - method public static com.google.firebase.firestore.pipeline.Field of(com.google.firebase.firestore.FieldPath fieldPath); - method public static com.google.firebase.firestore.pipeline.Field of(String name); - field public static final com.google.firebase.firestore.pipeline.Field.Companion Companion; - field public static final com.google.firebase.firestore.pipeline.Field DOCUMENT_ID; + public static final class CollectionSource.Companion { + method public com.google.firebase.firestore.pipeline.CollectionSource of(com.google.firebase.firestore.CollectionReference ref); + method public com.google.firebase.firestore.pipeline.CollectionSource of(String path); } - public static final class Field.Companion { - method public com.google.firebase.firestore.pipeline.Field of(com.google.firebase.firestore.FieldPath fieldPath); - method public com.google.firebase.firestore.pipeline.Field of(String name); + public final class ExplainOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { + method public error.NonExistentClass withIndexRecommendation(boolean value); + method public error.NonExistentClass withMode(com.google.firebase.firestore.pipeline.ExplainOptions.ExplainMode value); + method public error.NonExistentClass withOutputFormat(com.google.firebase.firestore.pipeline.ExplainOptions.OutputFormat value); + method public error.NonExistentClass withProfiles(com.google.firebase.firestore.pipeline.ExplainOptions.Profiles value); + method public error.NonExistentClass withRedact(boolean value); + method public error.NonExistentClass withVerbosity(com.google.firebase.firestore.pipeline.ExplainOptions.Verbosity value); + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions DEFAULT; } - public final class FindNearestStage extends com.google.firebase.firestore.pipeline.Stage { - method public static com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); - method public static com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); - method public static com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); - method public static com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); - method public com.google.firebase.firestore.pipeline.FindNearestStage withDistanceField(com.google.firebase.firestore.pipeline.Field distanceField); - method public com.google.firebase.firestore.pipeline.FindNearestStage withDistanceField(String distanceField); - method public com.google.firebase.firestore.pipeline.FindNearestStage withLimit(long limit); - field public static final com.google.firebase.firestore.pipeline.FindNearestStage.Companion Companion; + public static final class ExplainOptions.Companion { } - public static final class FindNearestStage.Companion { - method public com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); - method public com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); - method public com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); - method public com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + public static final class ExplainOptions.ExplainMode { + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.ExplainMode ANALYZE; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.ExplainMode.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.ExplainMode EXECUTE; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.ExplainMode EXPLAIN; } - public static final class FindNearestStage.DistanceMeasure { - field public static final error.NonExistentClass COSINE; - field public static final com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure.Companion Companion; - field public static final error.NonExistentClass DOT_PRODUCT; - field public static final error.NonExistentClass EUCLIDEAN; + public static final class ExplainOptions.ExplainMode.Companion { } - public static final class FindNearestStage.DistanceMeasure.Companion { + public static final class ExplainOptions.OutputFormat { + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.OutputFormat.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.OutputFormat JSON; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.OutputFormat STRUCT; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.OutputFormat TEXT; } - public class FunctionExpr extends com.google.firebase.firestore.pipeline.Expr { - ctor protected FunctionExpr(String name, com.google.firebase.firestore.pipeline.Expr[] params); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr add(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr add(String fieldName, Object other); + public static final class ExplainOptions.OutputFormat.Companion { + } + + public static final class ExplainOptions.Profiles { + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.Profiles BYTES_THROUGHPUT; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.Profiles.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.Profiles LATENCY; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.Profiles RECORDS_COUNT; + } + + public static final class ExplainOptions.Profiles.Companion { + } + + public static final class ExplainOptions.Verbosity { + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.Verbosity.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.Verbosity EXECUTION_TREE; + field public static final com.google.firebase.firestore.pipeline.ExplainOptions.Verbosity SUMMARY_ONLY; + } + + public static final class ExplainOptions.Verbosity.Companion { + } + + public abstract class Expr { + method public final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.Expr add(Object other); + method public static final com.google.firebase.firestore.pipeline.Expr add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr add(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.ExprWithAlias alias(String alias); method public static final com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(String fieldName, java.util.List arrays); + method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); + method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); + method public final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr... arrays); + method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); + method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(String fieldName, java.util.List arrays); + method public final com.google.firebase.firestore.pipeline.Expr arrayConcat(java.util.List arrays); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, Object value); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(Object value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, com.google.firebase.firestore.pipeline.Expr value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, Object value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String fieldName, java.util.List values); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(java.util.List values); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String fieldName, java.util.List values); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayLength(com.google.firebase.firestore.pipeline.Expr array); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayLength(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayReverse(com.google.firebase.firestore.pipeline.Expr array); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr arrayReverse(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(String fieldName, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(String fieldName, int number); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitNot(com.google.firebase.firestore.pipeline.Expr left); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitNot(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitOr(String fieldName, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(String fieldName, int number); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr bitXor(String fieldName, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr byteLength(com.google.firebase.firestore.pipeline.Expr value); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr byteLength(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr charLength(com.google.firebase.firestore.pipeline.Expr value); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr charLength(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object otherwise); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, double[] vector); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr divide(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr divide(String fieldName, Object other); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, double[] vector); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(java.util.List values); + method public final com.google.firebase.firestore.pipeline.Expr arrayLength(); + method public static final com.google.firebase.firestore.pipeline.Expr arrayLength(com.google.firebase.firestore.pipeline.Expr array); + method public static final com.google.firebase.firestore.pipeline.Expr arrayLength(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr arrayReverse(); + method public static final com.google.firebase.firestore.pipeline.Expr arrayReverse(com.google.firebase.firestore.pipeline.Expr array); + method public static final com.google.firebase.firestore.pipeline.Expr arrayReverse(String fieldName); + method public final com.google.firebase.firestore.pipeline.Ordering ascending(); + method public final com.google.firebase.firestore.pipeline.AggregateFunction avg(); + method public final com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.Expr bitAnd(Object right); + method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(String fieldName, Object right); + method public final com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public final com.google.firebase.firestore.pipeline.Expr bitLeftShift(int number); + method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(String fieldName, int number); + method public final com.google.firebase.firestore.pipeline.Expr bitNot(); + method public static final com.google.firebase.firestore.pipeline.Expr bitNot(com.google.firebase.firestore.pipeline.Expr left); + method public static final com.google.firebase.firestore.pipeline.Expr bitNot(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.Expr bitOr(Object right); + method public static final com.google.firebase.firestore.pipeline.Expr bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr bitOr(String fieldName, Object right); + method public final com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public final com.google.firebase.firestore.pipeline.Expr bitRightShift(int number); + method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(String fieldName, int number); + method public final com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.Expr bitXor(Object right); + method public static final com.google.firebase.firestore.pipeline.Expr bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr bitXor(String fieldName, Object right); + method public final com.google.firebase.firestore.pipeline.Expr byteLength(); + method public static final com.google.firebase.firestore.pipeline.Expr byteLength(com.google.firebase.firestore.pipeline.Expr value); + method public static final com.google.firebase.firestore.pipeline.Expr byteLength(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr charLength(); + method public static final com.google.firebase.firestore.pipeline.Expr charLength(com.google.firebase.firestore.pipeline.Expr value); + method public static final com.google.firebase.firestore.pipeline.Expr charLength(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); + method public static final com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object otherwise); + method public static final com.google.firebase.firestore.pipeline.Expr constant(boolean value); + method public static final com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.Blob value); + method public static final com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.DocumentReference ref); + method public static final com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.GeoPoint value); + method public static final com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.VectorValue value); + method public static final com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.Timestamp value); + method public static final com.google.firebase.firestore.pipeline.Expr constant(Number value); + method public static final com.google.firebase.firestore.pipeline.Expr constant(String value); + method public static final com.google.firebase.firestore.pipeline.Expr constant(java.util.Date value); + method public final com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public final com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.VectorValue vector); + method public final com.google.firebase.firestore.pipeline.Expr cosineDistance(double[] vector); + method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, double[] vector); + method public final com.google.firebase.firestore.pipeline.Ordering descending(); + method public final com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.Expr divide(Object other); + method public static final com.google.firebase.firestore.pipeline.Expr divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr divide(String fieldName, Object other); + method public final com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public final com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.VectorValue vector); + method public final com.google.firebase.firestore.pipeline.Expr dotProduct(double[] vector); + method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, double[] vector); + method public final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr suffix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr suffix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, String suffix); + method public final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String suffix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr suffix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, String suffix); + method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(Object other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(String fieldName, java.util.List values); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, double[] vector); + method public final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(java.util.List values); + method public final com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public final com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.VectorValue vector); + method public final com.google.firebase.firestore.pipeline.Expr euclideanDistance(double[] vector); + method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, double[] vector); + method public final com.google.firebase.firestore.pipeline.BooleanExpr exists(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr exists(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Field field(com.google.firebase.firestore.FieldPath fieldPath); + method public static final com.google.firebase.firestore.pipeline.Field field(String name); method public static final com.google.firebase.firestore.pipeline.FunctionExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(Object other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(Object other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr isNan(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(String fieldName); + method public final com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(String fieldName); + method public final com.google.firebase.firestore.pipeline.BooleanExpr isNotNull(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNotNull(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNotNull(String fieldName); + method public final com.google.firebase.firestore.pipeline.BooleanExpr isNull(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNull(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNull(String fieldName); + method public final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public final com.google.firebase.firestore.pipeline.BooleanExpr like(String pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, String pattern); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(String fieldName, Object other); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(String fieldName, Object other); + method public final com.google.firebase.firestore.pipeline.Expr logicalMax(com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.Expr logicalMax(Object other); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMax(String fieldName, Object other); + method public final com.google.firebase.firestore.pipeline.Expr logicalMin(com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.Expr logicalMin(Object other); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMin(String fieldName, Object other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(Object other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(Object other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr map(java.util.Map elements); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapGet(String fieldName, String key); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapMerge(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapMerge(String mapField, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr key); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, String key); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(String mapField, com.google.firebase.firestore.pipeline.Expr key); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(String mapField, String key); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mod(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr mod(String fieldName, Object other); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr multiply(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.Expr map(java.util.Map elements); + method public final com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); + method public final com.google.firebase.firestore.pipeline.Expr mapGet(String key); + method public static final com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, String key); + method public static final com.google.firebase.firestore.pipeline.Expr mapMerge(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public final com.google.firebase.firestore.pipeline.Expr mapMerge(com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public static final com.google.firebase.firestore.pipeline.Expr mapMerge(String mapField, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public final com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, String key); + method public final com.google.firebase.firestore.pipeline.Expr mapRemove(String key); + method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, String key); + method public final com.google.firebase.firestore.pipeline.AggregateFunction max(); + method public final com.google.firebase.firestore.pipeline.AggregateFunction min(); + method public final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.Expr mod(Object other); + method public static final com.google.firebase.firestore.pipeline.Expr mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr mod(String fieldName, Object other); + method public final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.Expr multiply(Object other); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(String fieldName, Object other); + method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(Object other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, Object right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr not(com.google.firebase.firestore.pipeline.BooleanExpr condition); method public static final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); method public static final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(String fieldName, java.util.List values); + method public final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(java.util.List values); + method public static final com.google.firebase.firestore.pipeline.Expr nullValue(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr or(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); + method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String fieldName, String pattern); + method public final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, String pattern); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(String fieldName, String find, String replace); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(String fieldName, String find, String replace); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr reverse(com.google.firebase.firestore.pipeline.Expr expr); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr reverse(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public final com.google.firebase.firestore.pipeline.Expr replaceAll(String find, String replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceAll(String fieldName, String find, String replace); + method public final com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public final com.google.firebase.firestore.pipeline.Expr replaceFirst(String find, String replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceFirst(String fieldName, String find, String replace); + method public final com.google.firebase.firestore.pipeline.Expr reverse(); + method public static final com.google.firebase.firestore.pipeline.Expr reverse(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Expr reverse(String fieldName); + method public final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr prefix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr prefix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, String prefix); + method public final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String prefix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr prefix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, String prefix); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr strConcat(String fieldName, java.lang.Object... rest); + method public static final com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); + method public static final com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); + method public final com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr... expr); + method public final com.google.firebase.firestore.pipeline.Expr strConcat(java.lang.Object... string); + method public static final com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); + method public static final com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, java.lang.Object... rest); + method public final com.google.firebase.firestore.pipeline.Expr strConcat(java.lang.String... string); + method public final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr substring); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr substring); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, String substring); + method public final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String substring); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, com.google.firebase.firestore.pipeline.Expr substring); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, String substring); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr subtract(String fieldName, Object other); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(String fieldName, String unit, double amount); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(String fieldName, String unit, double amount); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMicros(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMillis(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixSeconds(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr toLower(com.google.firebase.firestore.pipeline.Expr expr); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr toLower(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr toUpper(com.google.firebase.firestore.pipeline.Expr expr); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr toUpper(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr trim(com.google.firebase.firestore.pipeline.Expr expr); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr trim(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixMicrosToTimestamp(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixMillisToTimestamp(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr unixSecondsToTimestamp(String fieldName); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr vectorLength(com.google.firebase.firestore.pipeline.Expr vector); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr vectorLength(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public static final com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public final com.google.firebase.firestore.pipeline.Expr subtract(Object other); + method public static final com.google.firebase.firestore.pipeline.Expr subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public static final com.google.firebase.firestore.pipeline.Expr subtract(String fieldName, Object other); + method public final com.google.firebase.firestore.pipeline.AggregateFunction sum(); + method public final com.google.firebase.firestore.pipeline.Expr timestampAdd(com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.Expr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.Expr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.Expr timestampAdd(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public final com.google.firebase.firestore.pipeline.Expr timestampAdd(String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.Expr timestampAdd(String fieldName, String unit, double amount); + method public final com.google.firebase.firestore.pipeline.Expr timestampSub(com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.Expr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public static final com.google.firebase.firestore.pipeline.Expr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.Expr timestampSub(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public final com.google.firebase.firestore.pipeline.Expr timestampSub(String unit, double amount); + method public static final com.google.firebase.firestore.pipeline.Expr timestampSub(String fieldName, String unit, double amount); + method public final com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(); + method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(); + method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(); + method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr toLower(); + method public static final com.google.firebase.firestore.pipeline.Expr toLower(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Expr toLower(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr toUpper(); + method public static final com.google.firebase.firestore.pipeline.Expr toUpper(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Expr toUpper(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr trim(); + method public static final com.google.firebase.firestore.pipeline.Expr trim(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Expr trim(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(); + method public static final com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(); + method public static final com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(); + method public static final com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Expr vector(com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.Expr vector(double[] vector); + method public final com.google.firebase.firestore.pipeline.Expr vectorLength(); + method public static final com.google.firebase.firestore.pipeline.Expr vectorLength(com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Expr vectorLength(String fieldName); method public static final com.google.firebase.firestore.pipeline.BooleanExpr xor(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); - field public static final com.google.firebase.firestore.pipeline.FunctionExpr.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.Expr.Companion Companion; } - public static final class FunctionExpr.Companion { - method public com.google.firebase.firestore.pipeline.FunctionExpr add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr add(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.FunctionExpr add(String fieldName, Object other); + public static final class Expr.Companion { + method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Expr add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Expr add(String fieldName, Object other); method public com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); - method public com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); - method public com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); - method public com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); - method public com.google.firebase.firestore.pipeline.FunctionExpr arrayConcat(String fieldName, java.util.List arrays); + method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); + method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); + method public com.google.firebase.firestore.pipeline.Expr arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); + method public com.google.firebase.firestore.pipeline.Expr arrayConcat(String fieldName, java.util.List arrays); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr value); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, Object value); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, com.google.firebase.firestore.pipeline.Expr value); @@ -1152,54 +1177,63 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String fieldName, java.util.List values); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String fieldName, java.util.List values); - method public com.google.firebase.firestore.pipeline.FunctionExpr arrayLength(com.google.firebase.firestore.pipeline.Expr array); - method public com.google.firebase.firestore.pipeline.FunctionExpr arrayLength(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr arrayReverse(com.google.firebase.firestore.pipeline.Expr array); - method public com.google.firebase.firestore.pipeline.FunctionExpr arrayReverse(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitAnd(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitLeftShift(String fieldName, int number); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitNot(com.google.firebase.firestore.pipeline.Expr left); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitNot(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitOr(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitRightShift(String fieldName, int number); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr bitXor(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr byteLength(com.google.firebase.firestore.pipeline.Expr value); - method public com.google.firebase.firestore.pipeline.FunctionExpr byteLength(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr charLength(com.google.firebase.firestore.pipeline.Expr value); - method public com.google.firebase.firestore.pipeline.FunctionExpr charLength(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); - method public com.google.firebase.firestore.pipeline.FunctionExpr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object otherwise); - method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public com.google.firebase.firestore.pipeline.FunctionExpr cosineDistance(String fieldName, double[] vector); - method public com.google.firebase.firestore.pipeline.FunctionExpr divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr divide(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.FunctionExpr divide(String fieldName, Object other); - method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public com.google.firebase.firestore.pipeline.FunctionExpr dotProduct(String fieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.Expr arrayLength(com.google.firebase.firestore.pipeline.Expr array); + method public com.google.firebase.firestore.pipeline.Expr arrayLength(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr arrayReverse(com.google.firebase.firestore.pipeline.Expr array); + method public com.google.firebase.firestore.pipeline.Expr arrayReverse(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Expr bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr bitAnd(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(String fieldName, int number); + method public com.google.firebase.firestore.pipeline.Expr bitNot(com.google.firebase.firestore.pipeline.Expr left); + method public com.google.firebase.firestore.pipeline.Expr bitNot(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Expr bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr bitOr(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public com.google.firebase.firestore.pipeline.Expr bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Expr bitRightShift(String fieldName, int number); + method public com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Expr bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr bitXor(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.Expr byteLength(com.google.firebase.firestore.pipeline.Expr value); + method public com.google.firebase.firestore.pipeline.Expr byteLength(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr charLength(com.google.firebase.firestore.pipeline.Expr value); + method public com.google.firebase.firestore.pipeline.Expr charLength(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); + method public com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object otherwise); + method public com.google.firebase.firestore.pipeline.Expr constant(boolean value); + method public com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.Blob value); + method public com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.DocumentReference ref); + method public com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.GeoPoint value); + method public com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.VectorValue value); + method public com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.Timestamp value); + method public com.google.firebase.firestore.pipeline.Expr constant(Number value); + method public com.google.firebase.firestore.pipeline.Expr constant(String value); + method public com.google.firebase.firestore.pipeline.Expr constant(java.util.Date value); + method public com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Expr divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Expr divide(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, double[] vector); method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr suffix); method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, String suffix); method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr suffix); @@ -1210,13 +1244,15 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); method public com.google.firebase.firestore.pipeline.BooleanExpr eqAny(String fieldName, java.util.List values); - method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); - method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); - method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public com.google.firebase.firestore.pipeline.FunctionExpr euclideanDistance(String fieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); + method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); + method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); + method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, double[] vector); method public com.google.firebase.firestore.pipeline.BooleanExpr exists(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Field field(com.google.firebase.firestore.FieldPath fieldPath); + method public com.google.firebase.firestore.pipeline.Field field(String name); method public com.google.firebase.firestore.pipeline.FunctionExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); @@ -1238,14 +1274,14 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, String pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, String pattern); - method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMax(String fieldName, Object other); - method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.FunctionExpr logicalMin(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Expr logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Expr logicalMax(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Expr logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Expr logicalMin(String fieldName, Object other); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); @@ -1254,25 +1290,25 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr map(java.util.Map elements); - method public com.google.firebase.firestore.pipeline.FunctionExpr mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); - method public com.google.firebase.firestore.pipeline.FunctionExpr mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); - method public com.google.firebase.firestore.pipeline.FunctionExpr mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); - method public com.google.firebase.firestore.pipeline.FunctionExpr mapGet(String fieldName, String key); - method public com.google.firebase.firestore.pipeline.FunctionExpr mapMerge(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); - method public com.google.firebase.firestore.pipeline.FunctionExpr mapMerge(String mapField, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); - method public com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr key); - method public com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, String key); - method public com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(String mapField, com.google.firebase.firestore.pipeline.Expr key); - method public com.google.firebase.firestore.pipeline.FunctionExpr mapRemove(String mapField, String key); - method public com.google.firebase.firestore.pipeline.FunctionExpr mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr mod(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.FunctionExpr mod(String fieldName, Object other); - method public com.google.firebase.firestore.pipeline.FunctionExpr multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.FunctionExpr multiply(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr map(java.util.Map elements); + method public com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); + method public com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, String key); + method public com.google.firebase.firestore.pipeline.Expr mapMerge(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public com.google.firebase.firestore.pipeline.Expr mapMerge(String mapField, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, String key); + method public com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, String key); + method public com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Expr mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Expr mod(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Expr multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Expr multiply(String fieldName, Object other); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); @@ -1280,6 +1316,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr not(com.google.firebase.firestore.pipeline.BooleanExpr condition); method public com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); method public com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(String fieldName, java.util.List values); + method public com.google.firebase.firestore.pipeline.Expr nullValue(); method public com.google.firebase.firestore.pipeline.BooleanExpr or(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, String pattern); @@ -1289,61 +1326,113 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, String pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, String pattern); - method public com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); - method public com.google.firebase.firestore.pipeline.FunctionExpr replaceAll(String fieldName, String find, String replace); - method public com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); - method public com.google.firebase.firestore.pipeline.FunctionExpr replaceFirst(String fieldName, String find, String replace); - method public com.google.firebase.firestore.pipeline.FunctionExpr reverse(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.FunctionExpr reverse(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public com.google.firebase.firestore.pipeline.Expr replaceAll(String fieldName, String find, String replace); + method public com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public com.google.firebase.firestore.pipeline.Expr replaceFirst(String fieldName, String find, String replace); + method public com.google.firebase.firestore.pipeline.Expr reverse(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Expr reverse(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr prefix); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, String prefix); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr prefix); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, String prefix); - method public com.google.firebase.firestore.pipeline.FunctionExpr strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); - method public com.google.firebase.firestore.pipeline.FunctionExpr strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); - method public com.google.firebase.firestore.pipeline.FunctionExpr strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); - method public com.google.firebase.firestore.pipeline.FunctionExpr strConcat(String fieldName, java.lang.Object... rest); + method public com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); + method public com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); + method public com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); + method public com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, java.lang.Object... rest); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr substring); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, String substring); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, com.google.firebase.firestore.pipeline.Expr substring); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, String substring); - method public com.google.firebase.firestore.pipeline.FunctionExpr subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.FunctionExpr subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.FunctionExpr subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.FunctionExpr subtract(String fieldName, Object other); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampAdd(String fieldName, String unit, double amount); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampSub(String fieldName, String unit, double amount); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMicros(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixMillis(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.FunctionExpr timestampToUnixSeconds(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr toLower(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.FunctionExpr toLower(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr toUpper(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.FunctionExpr toUpper(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr trim(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.FunctionExpr trim(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.FunctionExpr unixMicrosToTimestamp(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.FunctionExpr unixMillisToTimestamp(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); - method public com.google.firebase.firestore.pipeline.FunctionExpr unixSecondsToTimestamp(String fieldName); - method public com.google.firebase.firestore.pipeline.FunctionExpr vectorLength(com.google.firebase.firestore.pipeline.Expr vector); - method public com.google.firebase.firestore.pipeline.FunctionExpr vectorLength(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); + method public com.google.firebase.firestore.pipeline.Expr subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); + method public com.google.firebase.firestore.pipeline.Expr subtract(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.Expr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public com.google.firebase.firestore.pipeline.Expr timestampAdd(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.Expr timestampAdd(String fieldName, String unit, double amount); + method public com.google.firebase.firestore.pipeline.Expr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.Expr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); + method public com.google.firebase.firestore.pipeline.Expr timestampSub(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); + method public com.google.firebase.firestore.pipeline.Expr timestampSub(String fieldName, String unit, double amount); + method public com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr toLower(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Expr toLower(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr toUpper(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Expr toUpper(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr trim(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Expr trim(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr vector(com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.Expr vector(double[] vector); + method public com.google.firebase.firestore.pipeline.Expr vectorLength(com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Expr vectorLength(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr xor(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); } + public final class ExprWithAlias extends com.google.firebase.firestore.pipeline.Selectable { + } + + public final class Field extends com.google.firebase.firestore.pipeline.Selectable { + field public static final com.google.firebase.firestore.pipeline.Field.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.Field DOCUMENT_ID; + } + + public static final class Field.Companion { + } + + public final class FindNearestStage extends com.google.firebase.firestore.pipeline.Stage { + method public static com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public static com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public static com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public static com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.pipeline.FindNearestStage withDistanceField(com.google.firebase.firestore.pipeline.Field distanceField); + method public com.google.firebase.firestore.pipeline.FindNearestStage withDistanceField(String distanceField); + method public com.google.firebase.firestore.pipeline.FindNearestStage withLimit(long limit); + field public static final com.google.firebase.firestore.pipeline.FindNearestStage.Companion Companion; + } + + public static final class FindNearestStage.Companion { + method public com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + method public com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); + } + + public static final class FindNearestStage.DistanceMeasure { + field public static final error.NonExistentClass COSINE; + field public static final com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure.Companion Companion; + field public static final error.NonExistentClass DOT_PRODUCT; + field public static final error.NonExistentClass EUCLIDEAN; + } + + public static final class FindNearestStage.DistanceMeasure.Companion { + } + + public class FunctionExpr extends com.google.firebase.firestore.pipeline.Expr { + } + + public final class GenericOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { + field public static final com.google.firebase.firestore.pipeline.GenericOptions.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.GenericOptions DEFAULT; + } + + public static final class GenericOptions.Companion { + } + public final class GenericStage extends com.google.firebase.firestore.pipeline.Stage { method public static com.google.firebase.firestore.pipeline.GenericStage ofName(String name); method public com.google.firebase.firestore.pipeline.GenericStage withArguments(java.lang.Object... arguments); @@ -1354,6 +1443,15 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.GenericStage ofName(String name); } + public final class InternalOptions { + field public static final com.google.firebase.firestore.pipeline.InternalOptions.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.InternalOptions EMPTY; + } + + public static final class InternalOptions.Companion { + method public com.google.firebase.firestore.pipeline.InternalOptions of(String key, error.NonExistentClass value); + } + public final class Ordering { method public static com.google.firebase.firestore.pipeline.Ordering ascending(com.google.firebase.firestore.pipeline.Expr expr); method public static com.google.firebase.firestore.pipeline.Ordering ascending(String fieldName); @@ -1372,6 +1470,24 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Ordering descending(String fieldName); } + public final class PipelineOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { + method public com.google.firebase.firestore.pipeline.PipelineOptions withExplainOptions(com.google.firebase.firestore.pipeline.ExplainOptions options); + method public com.google.firebase.firestore.pipeline.PipelineOptions withIndexMode(com.google.firebase.firestore.pipeline.PipelineOptions.IndexMode indexMode); + field public static final com.google.firebase.firestore.pipeline.PipelineOptions.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.PipelineOptions DEFAULT; + } + + public static final class PipelineOptions.Companion { + } + + public static final class PipelineOptions.IndexMode { + field public static final com.google.firebase.firestore.pipeline.PipelineOptions.IndexMode.Companion Companion; + field public static final com.google.firebase.firestore.pipeline.PipelineOptions.IndexMode RECOMMENDED; + } + + public static final class PipelineOptions.IndexMode.Companion { + } + public final class SampleStage extends com.google.firebase.firestore.pipeline.Stage { method public static com.google.firebase.firestore.pipeline.SampleStage withDocLimit(int documents); method public static com.google.firebase.firestore.pipeline.SampleStage withPercentage(double percentage); 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 fbca6febc6b..a17dc79ab42 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 @@ -660,10 +660,7 @@ public void testComparisonOperators() { .pipeline() .collection(randomCol) .where( - and( - gt("rating", 4.2), - lte(field("rating"), 4.5), - neq("genre", "Science Function"))) + and(gt("rating", 4.2), lte(field("rating"), 4.5), neq("genre", "Science Function"))) .select("rating", "title") .sort(field("title").ascending()) .execute(); @@ -771,10 +768,8 @@ public void testDistanceFunctions() { .collection(randomCol) .select( cosineDistance(vector(sourceVector), targetVector).alias("cosineDistance"), - Expr.dotProduct(vector(sourceVector), targetVector) - .alias("dotProductDistance"), - euclideanDistance(vector(sourceVector), targetVector) - .alias("euclideanDistance")) + Expr.dotProduct(vector(sourceVector), targetVector).alias("dotProductDistance"), + euclideanDistance(vector(sourceVector), targetVector).alias("euclideanDistance")) .limit(1) .execute(); assertThat(waitFor(execute).getResults()) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index cc61160f405..06e4142d81b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -36,7 +36,6 @@ import com.google.firebase.firestore.pipeline.ExprWithAlias import com.google.firebase.firestore.pipeline.Field import com.google.firebase.firestore.pipeline.FindNearestStage import com.google.firebase.firestore.pipeline.FunctionExpr -import com.google.firebase.firestore.pipeline.GenericArg import com.google.firebase.firestore.pipeline.GenericStage import com.google.firebase.firestore.pipeline.LimitStage import com.google.firebase.firestore.pipeline.OffsetStage @@ -118,7 +117,7 @@ internal constructor( * @return A new [Pipeline] object with this stage appended to the stage list. */ fun genericStage(name: String, vararg arguments: Any): Pipeline = - append(GenericStage(name, arguments.map(GenericArg::from))) + append(GenericStage.ofName(name).withArguments(arguments)) /** * Adds a stage to the pipeline by specifying the stage name as an argument. This does not offer @@ -171,7 +170,9 @@ internal constructor( */ fun removeFields(field: String, vararg additionalFields: String): Pipeline = append( - RemoveFieldsStage(arrayOf(Expr.field(field), *additionalFields.map(Expr::field).toTypedArray())) + RemoveFieldsStage( + arrayOf(Expr.field(field), *additionalFields.map(Expr::field).toTypedArray()) + ) ) /** @@ -255,8 +256,7 @@ internal constructor( * You can filter documents based on their field values, using implementations of [BooleanExpr], * typically including but not limited to: * - * - field comparators: [Expr.eq], [Expr.lt] (less than), [Expr.gt] - * (greater than), etc. + * - field comparators: [Expr.eq], [Expr.lt] (less than), [Expr.gt] (greater than), etc. * - logical operators: [Expr.and], [Expr.or], [Expr.not], etc. * - advanced functions: [Expr.regexMatch], [Expr.arrayContains], etc. * diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index 08c3918b901..629135fe888 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -567,9 +567,10 @@ public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataR p = p.sort(orderings.get(0), orderings.stream().skip(1).toArray(Ordering[]::new)); p = p.limit((int) limit); } else { - p = p.sort( - orderings.get(0).reverse(), - orderings.stream().skip(1).map(Ordering::reverse).toArray(Ordering[]::new)); + p = + p.sort( + orderings.get(0).reverse(), + orderings.stream().skip(1).map(Ordering::reverse).toArray(Ordering[]::new)); p = p.limit((int) limit); p = p.sort(orderings.get(0), orderings.stream().skip(1).toArray(Ordering[]::new)); } 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 eaa59645cb3..c7b13f63aaa 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 @@ -23,8 +23,8 @@ import com.google.firebase.firestore.Pipeline import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.VectorValue import com.google.firebase.firestore.model.DocumentKey -import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.FieldPath as ModelFieldPath +import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.util.CustomClassMapper import com.google.firestore.v1.MapValue @@ -280,109 +280,115 @@ abstract class Expr internal constructor() { @JvmStatic fun not(condition: BooleanExpr) = BooleanExpr("not", condition) - @JvmStatic fun bitAnd(left: Expr, right: Expr) = FunctionExpr("bit_and", left, right) + @JvmStatic fun bitAnd(left: Expr, right: Expr): Expr = FunctionExpr("bit_and", left, right) - @JvmStatic fun bitAnd(left: Expr, right: Any) = FunctionExpr("bit_and", left, right) + @JvmStatic fun bitAnd(left: Expr, right: Any): Expr = FunctionExpr("bit_and", left, right) @JvmStatic - fun bitAnd(fieldName: String, right: Expr) = FunctionExpr("bit_and", fieldName, right) + fun bitAnd(fieldName: String, right: Expr): Expr = FunctionExpr("bit_and", fieldName, right) - @JvmStatic fun bitAnd(fieldName: String, right: Any) = FunctionExpr("bit_and", fieldName, right) + @JvmStatic + fun bitAnd(fieldName: String, right: Any): Expr = FunctionExpr("bit_and", fieldName, right) - @JvmStatic fun bitOr(left: Expr, right: Expr) = FunctionExpr("bit_or", left, right) + @JvmStatic fun bitOr(left: Expr, right: Expr): Expr = FunctionExpr("bit_or", left, right) - @JvmStatic fun bitOr(left: Expr, right: Any) = FunctionExpr("bit_or", left, right) + @JvmStatic fun bitOr(left: Expr, right: Any): Expr = FunctionExpr("bit_or", left, right) - @JvmStatic fun bitOr(fieldName: String, right: Expr) = FunctionExpr("bit_or", fieldName, right) + @JvmStatic + fun bitOr(fieldName: String, right: Expr): Expr = FunctionExpr("bit_or", fieldName, right) - @JvmStatic fun bitOr(fieldName: String, right: Any) = FunctionExpr("bit_or", fieldName, right) + @JvmStatic + fun bitOr(fieldName: String, right: Any): Expr = FunctionExpr("bit_or", fieldName, right) - @JvmStatic fun bitXor(left: Expr, right: Expr) = FunctionExpr("bit_xor", left, right) + @JvmStatic fun bitXor(left: Expr, right: Expr): Expr = FunctionExpr("bit_xor", left, right) - @JvmStatic fun bitXor(left: Expr, right: Any) = FunctionExpr("bit_xor", left, right) + @JvmStatic fun bitXor(left: Expr, right: Any): Expr = FunctionExpr("bit_xor", left, right) @JvmStatic - fun bitXor(fieldName: String, right: Expr) = FunctionExpr("bit_xor", fieldName, right) + fun bitXor(fieldName: String, right: Expr): Expr = FunctionExpr("bit_xor", fieldName, right) - @JvmStatic fun bitXor(fieldName: String, right: Any) = FunctionExpr("bit_xor", fieldName, right) + @JvmStatic + fun bitXor(fieldName: String, right: Any): Expr = FunctionExpr("bit_xor", fieldName, right) - @JvmStatic fun bitNot(left: Expr) = FunctionExpr("bit_not", left) + @JvmStatic fun bitNot(left: Expr): Expr = FunctionExpr("bit_not", left) - @JvmStatic fun bitNot(fieldName: String) = FunctionExpr("bit_not", fieldName) + @JvmStatic fun bitNot(fieldName: String): Expr = FunctionExpr("bit_not", fieldName) @JvmStatic - fun bitLeftShift(left: Expr, numberExpr: Expr) = + fun bitLeftShift(left: Expr, numberExpr: Expr): Expr = FunctionExpr("bit_left_shift", left, numberExpr) @JvmStatic - fun bitLeftShift(left: Expr, number: Int) = FunctionExpr("bit_left_shift", left, number) + fun bitLeftShift(left: Expr, number: Int): Expr = FunctionExpr("bit_left_shift", left, number) @JvmStatic - fun bitLeftShift(fieldName: String, numberExpr: Expr) = + fun bitLeftShift(fieldName: String, numberExpr: Expr): Expr = FunctionExpr("bit_left_shift", fieldName, numberExpr) @JvmStatic - fun bitLeftShift(fieldName: String, number: Int) = + fun bitLeftShift(fieldName: String, number: Int): Expr = FunctionExpr("bit_left_shift", fieldName, number) @JvmStatic - fun bitRightShift(left: Expr, numberExpr: Expr) = + fun bitRightShift(left: Expr, numberExpr: Expr): Expr = FunctionExpr("bit_right_shift", left, numberExpr) @JvmStatic - fun bitRightShift(left: Expr, number: Int) = FunctionExpr("bit_right_shift", left, number) + fun bitRightShift(left: Expr, number: Int): Expr = FunctionExpr("bit_right_shift", left, number) @JvmStatic - fun bitRightShift(fieldName: String, numberExpr: Expr) = + fun bitRightShift(fieldName: String, numberExpr: Expr): Expr = FunctionExpr("bit_right_shift", fieldName, numberExpr) @JvmStatic - fun bitRightShift(fieldName: String, number: Int) = + fun bitRightShift(fieldName: String, number: Int): Expr = FunctionExpr("bit_right_shift", fieldName, number) - @JvmStatic fun add(left: Expr, right: Expr) = FunctionExpr("add", left, right) + @JvmStatic fun add(left: Expr, right: Expr): Expr = FunctionExpr("add", left, right) - @JvmStatic fun add(left: Expr, right: Any) = FunctionExpr("add", left, right) + @JvmStatic fun add(left: Expr, right: Any): Expr = FunctionExpr("add", left, right) - @JvmStatic fun add(fieldName: String, other: Expr) = FunctionExpr("add", fieldName, other) + @JvmStatic fun add(fieldName: String, other: Expr): Expr = FunctionExpr("add", fieldName, other) - @JvmStatic fun add(fieldName: String, other: Any) = FunctionExpr("add", fieldName, other) + @JvmStatic fun add(fieldName: String, other: Any): Expr = FunctionExpr("add", fieldName, other) - @JvmStatic fun subtract(left: Expr, right: Expr) = FunctionExpr("subtract", left, right) + @JvmStatic fun subtract(left: Expr, right: Expr): Expr = FunctionExpr("subtract", left, right) - @JvmStatic fun subtract(left: Expr, right: Any) = FunctionExpr("subtract", left, right) + @JvmStatic fun subtract(left: Expr, right: Any): Expr = FunctionExpr("subtract", left, right) @JvmStatic - fun subtract(fieldName: String, other: Expr) = FunctionExpr("subtract", fieldName, other) + fun subtract(fieldName: String, other: Expr): Expr = FunctionExpr("subtract", fieldName, other) @JvmStatic - fun subtract(fieldName: String, other: Any) = FunctionExpr("subtract", fieldName, other) + fun subtract(fieldName: String, other: Any): Expr = FunctionExpr("subtract", fieldName, other) - @JvmStatic fun multiply(left: Expr, right: Expr) = FunctionExpr("multiply", left, right) + @JvmStatic fun multiply(left: Expr, right: Expr): Expr = FunctionExpr("multiply", left, right) - @JvmStatic fun multiply(left: Expr, right: Any) = FunctionExpr("multiply", left, right) + @JvmStatic fun multiply(left: Expr, right: Any): Expr = FunctionExpr("multiply", left, right) @JvmStatic - fun multiply(fieldName: String, other: Expr) = FunctionExpr("multiply", fieldName, other) + fun multiply(fieldName: String, other: Expr): Expr = FunctionExpr("multiply", fieldName, other) @JvmStatic - fun multiply(fieldName: String, other: Any) = FunctionExpr("multiply", fieldName, other) + fun multiply(fieldName: String, other: Any): Expr = FunctionExpr("multiply", fieldName, other) - @JvmStatic fun divide(left: Expr, right: Expr) = FunctionExpr("divide", left, right) + @JvmStatic fun divide(left: Expr, right: Expr): Expr = FunctionExpr("divide", left, right) - @JvmStatic fun divide(left: Expr, right: Any) = FunctionExpr("divide", left, right) + @JvmStatic fun divide(left: Expr, right: Any): Expr = FunctionExpr("divide", left, right) - @JvmStatic fun divide(fieldName: String, other: Expr) = FunctionExpr("divide", fieldName, other) + @JvmStatic + fun divide(fieldName: String, other: Expr): Expr = FunctionExpr("divide", fieldName, other) - @JvmStatic fun divide(fieldName: String, other: Any) = FunctionExpr("divide", fieldName, other) + @JvmStatic + fun divide(fieldName: String, other: Any): Expr = FunctionExpr("divide", fieldName, other) - @JvmStatic fun mod(left: Expr, right: Expr) = FunctionExpr("mod", left, right) + @JvmStatic fun mod(left: Expr, right: Expr): Expr = FunctionExpr("mod", left, right) - @JvmStatic fun mod(left: Expr, right: Any) = FunctionExpr("mod", left, right) + @JvmStatic fun mod(left: Expr, right: Any): Expr = FunctionExpr("mod", left, right) - @JvmStatic fun mod(fieldName: String, other: Expr) = FunctionExpr("mod", fieldName, other) + @JvmStatic fun mod(fieldName: String, other: Expr): Expr = FunctionExpr("mod", fieldName, other) - @JvmStatic fun mod(fieldName: String, other: Any) = FunctionExpr("mod", fieldName, other) + @JvmStatic fun mod(fieldName: String, other: Any): Expr = FunctionExpr("mod", fieldName, other) @JvmStatic fun eqAny(value: Expr, values: List) = @@ -417,36 +423,36 @@ abstract class Expr internal constructor() { @JvmStatic fun isNotNull(fieldName: String) = BooleanExpr("is_not_null", fieldName) @JvmStatic - fun replaceFirst(value: Expr, find: Expr, replace: Expr) = + fun replaceFirst(value: Expr, find: Expr, replace: Expr): Expr = FunctionExpr("replace_first", value, find, replace) @JvmStatic - fun replaceFirst(value: Expr, find: String, replace: String) = + fun replaceFirst(value: Expr, find: String, replace: String): Expr = FunctionExpr("replace_first", value, find, replace) @JvmStatic - fun replaceFirst(fieldName: String, find: String, replace: String) = + fun replaceFirst(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_first", fieldName, find, replace) @JvmStatic - fun replaceAll(value: Expr, find: Expr, replace: Expr) = + fun replaceAll(value: Expr, find: Expr, replace: Expr): Expr = FunctionExpr("replace_all", value, find, replace) @JvmStatic - fun replaceAll(value: Expr, find: String, replace: String) = + fun replaceAll(value: Expr, find: String, replace: String): Expr = FunctionExpr("replace_all", value, find, replace) @JvmStatic - fun replaceAll(fieldName: String, find: String, replace: String) = + fun replaceAll(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_all", fieldName, find, replace) - @JvmStatic fun charLength(value: Expr) = FunctionExpr("char_length", value) + @JvmStatic fun charLength(value: Expr): Expr = FunctionExpr("char_length", value) - @JvmStatic fun charLength(fieldName: String) = FunctionExpr("char_length", fieldName) + @JvmStatic fun charLength(fieldName: String): Expr = FunctionExpr("char_length", fieldName) - @JvmStatic fun byteLength(value: Expr) = FunctionExpr("byte_length", value) + @JvmStatic fun byteLength(value: Expr): Expr = FunctionExpr("byte_length", value) - @JvmStatic fun byteLength(fieldName: String) = FunctionExpr("byte_length", fieldName) + @JvmStatic fun byteLength(fieldName: String): Expr = FunctionExpr("byte_length", fieldName) @JvmStatic fun like(expr: Expr, pattern: Expr) = BooleanExpr("like", expr, pattern) @@ -484,29 +490,37 @@ abstract class Expr internal constructor() { fun regexMatch(fieldName: String, pattern: String) = BooleanExpr("regex_match", fieldName, pattern) - @JvmStatic fun logicalMax(left: Expr, right: Expr) = FunctionExpr("logical_max", left, right) + @JvmStatic + fun logicalMax(left: Expr, right: Expr): Expr = FunctionExpr("logical_max", left, right) - @JvmStatic fun logicalMax(left: Expr, right: Any) = FunctionExpr("logical_max", left, right) + @JvmStatic + fun logicalMax(left: Expr, right: Any): Expr = FunctionExpr("logical_max", left, right) @JvmStatic - fun logicalMax(fieldName: String, other: Expr) = FunctionExpr("logical_max", fieldName, other) + fun logicalMax(fieldName: String, other: Expr): Expr = + FunctionExpr("logical_max", fieldName, other) @JvmStatic - fun logicalMax(fieldName: String, other: Any) = FunctionExpr("logical_max", fieldName, other) + fun logicalMax(fieldName: String, other: Any): Expr = + FunctionExpr("logical_max", fieldName, other) - @JvmStatic fun logicalMin(left: Expr, right: Expr) = FunctionExpr("logical_min", left, right) + @JvmStatic + fun logicalMin(left: Expr, right: Expr): Expr = FunctionExpr("logical_min", left, right) - @JvmStatic fun logicalMin(left: Expr, right: Any) = FunctionExpr("logical_min", left, right) + @JvmStatic + fun logicalMin(left: Expr, right: Any): Expr = FunctionExpr("logical_min", left, right) @JvmStatic - fun logicalMin(fieldName: String, other: Expr) = FunctionExpr("logical_min", fieldName, other) + fun logicalMin(fieldName: String, other: Expr): Expr = + FunctionExpr("logical_min", fieldName, other) @JvmStatic - fun logicalMin(fieldName: String, other: Any) = FunctionExpr("logical_min", fieldName, other) + fun logicalMin(fieldName: String, other: Any): Expr = + FunctionExpr("logical_min", fieldName, other) - @JvmStatic fun reverse(expr: Expr) = FunctionExpr("reverse", expr) + @JvmStatic fun reverse(expr: Expr): Expr = FunctionExpr("reverse", expr) - @JvmStatic fun reverse(fieldName: String) = FunctionExpr("reverse", fieldName) + @JvmStatic fun reverse(fieldName: String): Expr = FunctionExpr("reverse", fieldName) @JvmStatic fun strContains(expr: Expr, substring: Expr) = BooleanExpr("str_contains", expr, substring) @@ -543,217 +557,222 @@ abstract class Expr internal constructor() { @JvmStatic fun endsWith(fieldName: String, suffix: String) = BooleanExpr("ends_with", fieldName, suffix) - @JvmStatic fun toLower(expr: Expr) = FunctionExpr("to_lower", expr) + @JvmStatic fun toLower(expr: Expr): Expr = FunctionExpr("to_lower", expr) @JvmStatic fun toLower( fieldName: String, - ) = FunctionExpr("to_lower", fieldName) + ): Expr = FunctionExpr("to_lower", fieldName) - @JvmStatic fun toUpper(expr: Expr) = FunctionExpr("to_upper", expr) + @JvmStatic fun toUpper(expr: Expr): Expr = FunctionExpr("to_upper", expr) @JvmStatic fun toUpper( fieldName: String, - ) = FunctionExpr("to_upper", fieldName) + ): Expr = FunctionExpr("to_upper", fieldName) - @JvmStatic fun trim(expr: Expr) = FunctionExpr("trim", expr) + @JvmStatic fun trim(expr: Expr): Expr = FunctionExpr("trim", expr) - @JvmStatic fun trim(fieldName: String) = FunctionExpr("trim", fieldName) + @JvmStatic fun trim(fieldName: String): Expr = FunctionExpr("trim", fieldName) @JvmStatic - fun strConcat(first: Expr, vararg rest: Expr) = FunctionExpr("str_concat", first, *rest) + fun strConcat(first: Expr, vararg rest: Expr): Expr = FunctionExpr("str_concat", first, *rest) @JvmStatic - fun strConcat(first: Expr, vararg rest: Any) = FunctionExpr("str_concat", first, *rest) + fun strConcat(first: Expr, vararg rest: Any): Expr = FunctionExpr("str_concat", first, *rest) @JvmStatic - fun strConcat(fieldName: String, vararg rest: Expr) = + fun strConcat(fieldName: String, vararg rest: Expr): Expr = FunctionExpr("str_concat", fieldName, *rest) @JvmStatic - fun strConcat(fieldName: String, vararg rest: Any) = + fun strConcat(fieldName: String, vararg rest: Any): Expr = FunctionExpr("str_concat", fieldName, *rest) - internal fun map(elements: Array) = FunctionExpr("map", elements) + internal fun map(elements: Array): Expr = FunctionExpr("map", elements) @JvmStatic fun map(elements: Map) = map(elements.flatMap { listOf(constant(it.key), toExprOrConstant(it.value)) }.toTypedArray()) - @JvmStatic fun mapGet(map: Expr, key: Expr) = FunctionExpr("map_get", map, key) + @JvmStatic fun mapGet(map: Expr, key: Expr): Expr = FunctionExpr("map_get", map, key) - @JvmStatic fun mapGet(map: Expr, key: String) = FunctionExpr("map_get", map, key) + @JvmStatic fun mapGet(map: Expr, key: String): Expr = FunctionExpr("map_get", map, key) - @JvmStatic fun mapGet(fieldName: String, key: Expr) = FunctionExpr("map_get", fieldName, key) + @JvmStatic + fun mapGet(fieldName: String, key: Expr): Expr = FunctionExpr("map_get", fieldName, key) - @JvmStatic fun mapGet(fieldName: String, key: String) = FunctionExpr("map_get", fieldName, key) + @JvmStatic + fun mapGet(fieldName: String, key: String): Expr = FunctionExpr("map_get", fieldName, key) @JvmStatic - fun mapMerge(firstMap: Expr, secondMap: Expr, vararg otherMaps: Expr) = + fun mapMerge(firstMap: Expr, secondMap: Expr, vararg otherMaps: Expr): Expr = FunctionExpr("map_merge", firstMap, secondMap, otherMaps) @JvmStatic - fun mapMerge(mapField: String, secondMap: Expr, vararg otherMaps: Expr) = + fun mapMerge(mapField: String, secondMap: Expr, vararg otherMaps: Expr): Expr = FunctionExpr("map_merge", mapField, secondMap, otherMaps) - @JvmStatic fun mapRemove(firstMap: Expr, key: Expr) = FunctionExpr("map_remove", firstMap, key) + @JvmStatic + fun mapRemove(firstMap: Expr, key: Expr): Expr = FunctionExpr("map_remove", firstMap, key) @JvmStatic - fun mapRemove(mapField: String, key: Expr) = FunctionExpr("map_remove", mapField, key) + fun mapRemove(mapField: String, key: Expr): Expr = FunctionExpr("map_remove", mapField, key) @JvmStatic - fun mapRemove(firstMap: Expr, key: String) = FunctionExpr("map_remove", firstMap, key) + fun mapRemove(firstMap: Expr, key: String): Expr = FunctionExpr("map_remove", firstMap, key) @JvmStatic - fun mapRemove(mapField: String, key: String) = FunctionExpr("map_remove", mapField, key) + fun mapRemove(mapField: String, key: String): Expr = FunctionExpr("map_remove", mapField, key) @JvmStatic - fun cosineDistance(vector1: Expr, vector2: Expr) = + fun cosineDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("cosine_distance", vector1, vector2) @JvmStatic - fun cosineDistance(vector1: Expr, vector2: DoubleArray) = + fun cosineDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("cosine_distance", vector1, vector(vector2)) @JvmStatic - fun cosineDistance(vector1: Expr, vector2: VectorValue) = + fun cosineDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("cosine_distance", vector1, vector2) @JvmStatic - fun cosineDistance(fieldName: String, vector: Expr) = + fun cosineDistance(fieldName: String, vector: Expr): Expr = FunctionExpr("cosine_distance", fieldName, vector) @JvmStatic - fun cosineDistance(fieldName: String, vector: DoubleArray) = + fun cosineDistance(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("cosine_distance", fieldName, vector(vector)) @JvmStatic - fun cosineDistance(fieldName: String, vector: VectorValue) = + fun cosineDistance(fieldName: String, vector: VectorValue): Expr = FunctionExpr("cosine_distance", fieldName, vector) @JvmStatic - fun dotProduct(vector1: Expr, vector2: Expr) = FunctionExpr("dot_product", vector1, vector2) + fun dotProduct(vector1: Expr, vector2: Expr): Expr = + FunctionExpr("dot_product", vector1, vector2) @JvmStatic - fun dotProduct(vector1: Expr, vector2: DoubleArray) = + fun dotProduct(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("dot_product", vector1, vector(vector2)) @JvmStatic - fun dotProduct(vector1: Expr, vector2: VectorValue) = + fun dotProduct(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("dot_product", vector1, vector2) @JvmStatic - fun dotProduct(fieldName: String, vector: Expr) = FunctionExpr("dot_product", fieldName, vector) + fun dotProduct(fieldName: String, vector: Expr): Expr = + FunctionExpr("dot_product", fieldName, vector) @JvmStatic - fun dotProduct(fieldName: String, vector: DoubleArray) = + fun dotProduct(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("dot_product", fieldName, vector(vector)) @JvmStatic - fun dotProduct(fieldName: String, vector: VectorValue) = + fun dotProduct(fieldName: String, vector: VectorValue): Expr = FunctionExpr("dot_product", fieldName, vector) @JvmStatic - fun euclideanDistance(vector1: Expr, vector2: Expr) = + fun euclideanDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("euclidean_distance", vector1, vector2) @JvmStatic - fun euclideanDistance(vector1: Expr, vector2: DoubleArray) = + fun euclideanDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("euclidean_distance", vector1, vector(vector2)) @JvmStatic - fun euclideanDistance(vector1: Expr, vector2: VectorValue) = + fun euclideanDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("euclidean_distance", vector1, vector2) @JvmStatic - fun euclideanDistance(fieldName: String, vector: Expr) = + fun euclideanDistance(fieldName: String, vector: Expr): Expr = FunctionExpr("euclidean_distance", fieldName, vector) @JvmStatic - fun euclideanDistance(fieldName: String, vector: DoubleArray) = + fun euclideanDistance(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("euclidean_distance", fieldName, vector(vector)) @JvmStatic - fun euclideanDistance(fieldName: String, vector: VectorValue) = + fun euclideanDistance(fieldName: String, vector: VectorValue): Expr = FunctionExpr("euclidean_distance", fieldName, vector) - @JvmStatic fun vectorLength(vector: Expr) = FunctionExpr("vector_length", vector) + @JvmStatic fun vectorLength(vector: Expr): Expr = FunctionExpr("vector_length", vector) - @JvmStatic fun vectorLength(fieldName: String) = FunctionExpr("vector_length", fieldName) + @JvmStatic fun vectorLength(fieldName: String): Expr = FunctionExpr("vector_length", fieldName) @JvmStatic - fun unixMicrosToTimestamp(input: Expr) = FunctionExpr("unix_micros_to_timestamp", input) + fun unixMicrosToTimestamp(input: Expr): Expr = FunctionExpr("unix_micros_to_timestamp", input) @JvmStatic - fun unixMicrosToTimestamp(fieldName: String) = + fun unixMicrosToTimestamp(fieldName: String): Expr = FunctionExpr("unix_micros_to_timestamp", fieldName) @JvmStatic - fun timestampToUnixMicros(input: Expr) = FunctionExpr("timestamp_to_unix_micros", input) + fun timestampToUnixMicros(input: Expr): Expr = FunctionExpr("timestamp_to_unix_micros", input) @JvmStatic - fun timestampToUnixMicros(fieldName: String) = + fun timestampToUnixMicros(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_micros", fieldName) @JvmStatic - fun unixMillisToTimestamp(input: Expr) = FunctionExpr("unix_millis_to_timestamp", input) + fun unixMillisToTimestamp(input: Expr): Expr = FunctionExpr("unix_millis_to_timestamp", input) @JvmStatic - fun unixMillisToTimestamp(fieldName: String) = + fun unixMillisToTimestamp(fieldName: String): Expr = FunctionExpr("unix_millis_to_timestamp", fieldName) @JvmStatic - fun timestampToUnixMillis(input: Expr) = FunctionExpr("timestamp_to_unix_millis", input) + fun timestampToUnixMillis(input: Expr): Expr = FunctionExpr("timestamp_to_unix_millis", input) @JvmStatic - fun timestampToUnixMillis(fieldName: String) = + fun timestampToUnixMillis(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_millis", fieldName) @JvmStatic - fun unixSecondsToTimestamp(input: Expr) = FunctionExpr("unix_seconds_to_timestamp", input) + fun unixSecondsToTimestamp(input: Expr): Expr = FunctionExpr("unix_seconds_to_timestamp", input) @JvmStatic - fun unixSecondsToTimestamp(fieldName: String) = + fun unixSecondsToTimestamp(fieldName: String): Expr = FunctionExpr("unix_seconds_to_timestamp", fieldName) @JvmStatic - fun timestampToUnixSeconds(input: Expr) = FunctionExpr("timestamp_to_unix_seconds", input) + fun timestampToUnixSeconds(input: Expr): Expr = FunctionExpr("timestamp_to_unix_seconds", input) @JvmStatic - fun timestampToUnixSeconds(fieldName: String) = + fun timestampToUnixSeconds(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_seconds", fieldName) @JvmStatic - fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr) = + fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) @JvmStatic - fun timestampAdd(timestamp: Expr, unit: String, amount: Double) = + fun timestampAdd(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) @JvmStatic - fun timestampAdd(fieldName: String, unit: Expr, amount: Expr) = + fun timestampAdd(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) @JvmStatic - fun timestampAdd(fieldName: String, unit: String, amount: Double) = + fun timestampAdd(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) @JvmStatic - fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr) = + fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) @JvmStatic - fun timestampSub(timestamp: Expr, unit: String, amount: Double) = + fun timestampSub(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) @JvmStatic - fun timestampSub(fieldName: String, unit: Expr, amount: Expr) = + fun timestampSub(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) @JvmStatic - fun timestampSub(fieldName: String, unit: String, amount: Double) = + fun timestampSub(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) @JvmStatic fun eq(left: Expr, right: Expr) = BooleanExpr("eq", left, right) @@ -805,23 +824,24 @@ abstract class Expr internal constructor() { @JvmStatic fun lte(fieldName: String, right: Any) = BooleanExpr("lte", fieldName, right) @JvmStatic - fun arrayConcat(array: Expr, vararg arrays: Expr) = FunctionExpr("array_concat", array, *arrays) + fun arrayConcat(array: Expr, vararg arrays: Expr): Expr = + FunctionExpr("array_concat", array, *arrays) @JvmStatic - fun arrayConcat(fieldName: String, vararg arrays: Expr) = + fun arrayConcat(fieldName: String, vararg arrays: Expr): Expr = FunctionExpr("array_concat", fieldName, *arrays) @JvmStatic - fun arrayConcat(array: Expr, arrays: List) = + fun arrayConcat(array: Expr, arrays: List): Expr = FunctionExpr("array_concat", array, ListOfExprs(toArrayOfExprOrConstant(arrays))) @JvmStatic - fun arrayConcat(fieldName: String, arrays: List) = + fun arrayConcat(fieldName: String, arrays: List): Expr = FunctionExpr("array_concat", fieldName, ListOfExprs(toArrayOfExprOrConstant(arrays))) - @JvmStatic fun arrayReverse(array: Expr) = FunctionExpr("array_reverse", array) + @JvmStatic fun arrayReverse(array: Expr): Expr = FunctionExpr("array_reverse", array) - @JvmStatic fun arrayReverse(fieldName: String) = FunctionExpr("array_reverse", fieldName) + @JvmStatic fun arrayReverse(fieldName: String): Expr = FunctionExpr("array_reverse", fieldName) @JvmStatic fun arrayContains(array: Expr, value: Expr) = BooleanExpr("array_contains", array, value) @@ -853,16 +873,16 @@ abstract class Expr internal constructor() { fun arrayContainsAny(fieldName: String, values: List) = BooleanExpr("array_contains_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) - @JvmStatic fun arrayLength(array: Expr) = FunctionExpr("array_length", array) + @JvmStatic fun arrayLength(array: Expr): Expr = FunctionExpr("array_length", array) - @JvmStatic fun arrayLength(fieldName: String) = FunctionExpr("array_length", fieldName) + @JvmStatic fun arrayLength(fieldName: String): Expr = FunctionExpr("array_length", fieldName) @JvmStatic - fun cond(condition: BooleanExpr, then: Expr, otherwise: Expr) = + fun cond(condition: BooleanExpr, then: Expr, otherwise: Expr): Expr = FunctionExpr("cond", condition, then, otherwise) @JvmStatic - fun cond(condition: BooleanExpr, then: Any, otherwise: Any) = + fun cond(condition: BooleanExpr, then: Any, otherwise: Any): Expr = FunctionExpr("cond", condition, then, otherwise) @JvmStatic fun exists(expr: Expr) = BooleanExpr("exists", expr) @@ -1171,7 +1191,6 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select * @return An [Field] representing the document ID. */ @JvmField val DOCUMENT_ID: Field = field(FieldPath.documentId()) - } override fun getAlias(): String = fieldPath.canonicalString() @@ -1275,7 +1294,8 @@ class Ordering private constructor(val expr: Expr, private val dir: Direction) { * @return A new [Ordering] object with ascending sort by field. */ @JvmStatic - fun ascending(fieldName: String): Ordering = Ordering(Expr.field(fieldName), Direction.ASCENDING) + fun ascending(fieldName: String): Ordering = + Ordering(Expr.field(fieldName), Direction.ASCENDING) /** * Create an [Ordering] that sorts documents in descending order based on value of [expr]. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt index 300d770c337..27d5375f3ab 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt @@ -28,8 +28,7 @@ import com.google.firestore.v1.Value * `ImmutableMap, Value>` is an implementation detail, not to be exposed, since * more efficient implementations are possible. */ -class InternalOptions -internal constructor(private val options: ImmutableMap) { +class InternalOptions internal constructor(private val options: ImmutableMap) { internal fun with(key: String, value: Value): InternalOptions { val builder = ImmutableMap.builderWithExpectedSize(options.size + 1) builder.putAll(options) @@ -58,8 +57,7 @@ internal constructor(private val options: ImmutableMap) { } companion object { - @JvmField - val EMPTY: InternalOptions = InternalOptions(ImmutableMap.of()) + @JvmField val EMPTY: InternalOptions = InternalOptions(ImmutableMap.of()) fun of(key: String, value: Value): InternalOptions { return InternalOptions(ImmutableMap.of(key, value)) @@ -131,44 +129,42 @@ internal constructor(internal val options: InternalOptions) { fun with(key: String, value: GenericOptions): T = with(key, value.options) } -class GenericOptions private constructor(options: InternalOptions) : AbstractOptions(options) { +class GenericOptions private constructor(options: InternalOptions) : + AbstractOptions(options) { override fun self(options: InternalOptions) = GenericOptions(options) companion object { - @JvmField - val DEFAULT: GenericOptions = GenericOptions(InternalOptions.EMPTY) + @JvmField val DEFAULT: GenericOptions = GenericOptions(InternalOptions.EMPTY) } } -class PipelineOptions private constructor(options: InternalOptions) : AbstractOptions(options) { +class PipelineOptions private constructor(options: InternalOptions) : + AbstractOptions(options) { override fun self(options: InternalOptions) = PipelineOptions(options) companion object { - @JvmField - val DEFAULT: PipelineOptions = PipelineOptions(InternalOptions.EMPTY) + @JvmField val DEFAULT: PipelineOptions = PipelineOptions(InternalOptions.EMPTY) } class IndexMode private constructor(internal val value: String) { companion object { - @JvmField - val RECOMMENDED = IndexMode("recommended") + @JvmField val RECOMMENDED = IndexMode("recommended") } } - fun withIndexMode(indexMode: IndexMode): PipelineOptions = - with("index_mode", indexMode.value) + fun withIndexMode(indexMode: IndexMode): PipelineOptions = with("index_mode", indexMode.value) fun withExplainOptions(options: ExplainOptions): PipelineOptions = with("explain_options", options.options) } -class ExplainOptions private constructor(options: InternalOptions) : AbstractOptions(options) { +class ExplainOptions private constructor(options: InternalOptions) : + AbstractOptions(options) { override fun self(options: InternalOptions) = ExplainOptions(options) companion object { - @JvmField - val DEFAULT = ExplainOptions(InternalOptions.EMPTY) + @JvmField val DEFAULT = ExplainOptions(InternalOptions.EMPTY) } fun withMode(value: ExplainMode) = with("mode", value.value) @@ -185,51 +181,39 @@ class ExplainOptions private constructor(options: InternalOptions) : AbstractOpt class ExplainMode private constructor(internal val value: String) { companion object { - @JvmField - val EXECUTE = ExplainMode("execute") + @JvmField val EXECUTE = ExplainMode("execute") - @JvmField - val EXPLAIN = ExplainMode("explain") + @JvmField val EXPLAIN = ExplainMode("explain") - @JvmField - val ANALYZE = ExplainMode("analyze") + @JvmField val ANALYZE = ExplainMode("analyze") } } class OutputFormat private constructor(internal val value: String) { companion object { - @JvmField - val TEXT = OutputFormat("text") + @JvmField val TEXT = OutputFormat("text") - @JvmField - val JSON = OutputFormat("json") + @JvmField val JSON = OutputFormat("json") - @JvmField - val STRUCT = OutputFormat("struct") + @JvmField val STRUCT = OutputFormat("struct") } } class Verbosity private constructor(internal val value: String) { companion object { - @JvmField - val SUMMARY_ONLY = Verbosity("summary_only") + @JvmField val SUMMARY_ONLY = Verbosity("summary_only") - @JvmField - val EXECUTION_TREE = Verbosity("execution_tree") + @JvmField val EXECUTION_TREE = Verbosity("execution_tree") } } class Profiles private constructor(internal val value: String) { companion object { - @JvmField - val LATENCY = Profiles("latency") + @JvmField val LATENCY = Profiles("latency") - @JvmField - val RECORDS_COUNT = Profiles("records_count") + @JvmField val RECORDS_COUNT = Profiles("records_count") - @JvmField - val BYTES_THROUGHPUT = Profiles("bytes_throughput") + @JvmField val BYTES_THROUGHPUT = Profiles("bytes_throughput") } } } - diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index cc930495eb3..e3d02d19652 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -177,9 +177,10 @@ internal constructor( private val path: String, // We validate [firestore.databaseId] when adding to pipeline. internal val firestore: FirebaseFirestore?, - options: InternalOptions) : - Stage("collection", options) { - override fun self(options: InternalOptions): CollectionSource = CollectionSource(path, firestore, options) + options: InternalOptions +) : Stage("collection", options) { + override fun self(options: InternalOptions): CollectionSource = + CollectionSource(path, firestore, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf( Value.newBuilder().setReferenceValue(if (path.startsWith("/")) path else "/" + path).build() From 356e8839d436e992d35adac17008a0317d4d379d Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 22 Apr 2025 15:48:57 -0400 Subject: [PATCH 044/152] Comments --- .../firestore/pipeline/expressions.kt | 917 +++++++++++++++++- 1 file changed, 912 insertions(+), 5 deletions(-) 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 c7b13f63aaa..aba07b76fcb 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 @@ -145,11 +145,24 @@ abstract class Expr internal constructor() { * Create a constant for a [Boolean] value. * * @param value The [Boolean] value. - * @return A new [Expr] constant instance. + * @return A new [BooleanExpr] constant instance. */ @JvmStatic - fun constant(value: Boolean): Expr { - return ValueConstant(encodeValue(value)) + fun constant(value: Boolean): BooleanExpr { + val encodedValue = encodeValue(value) + return object : BooleanExpr("N/A", emptyArray()) { + override fun toProto(userDataReader: UserDataReader): Value { + return encodedValue + } + + override fun hashCode(): Int { + return encodedValue.hashCode() + } + + override fun toString(): String { + return "constant($value)" + } + } } /** @@ -266,14 +279,35 @@ abstract class Expr internal constructor() { @JvmStatic fun generic(name: String, vararg expr: Expr) = FunctionExpr(name, expr) + /** + * Creates an expression that performs a logical 'AND' operation. + * + * @param condition The first [BooleanExpr]. + * @param conditions Addition [BooleanExpr]s. + * @return A new [BooleanExpr] representing the local 'AND' operation. + */ @JvmStatic fun and(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("and", condition, *conditions) + /** + * Creates an expression that performs a logical 'OR' operation. + * + * @param condition The first [BooleanExpr]. + * @param conditions Addition [BooleanExpr]s. + * @return A new [BooleanExpr] representing the local 'OR' operation. + */ @JvmStatic fun or(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("or", condition, *conditions) + /** + * Creates an expression that performs a logical 'XOR' operation. + * + * @param condition The first [BooleanExpr]. + * @param conditions Addition [BooleanExpr]s. + * @return A new [BooleanExpr] representing the local 'XOR' operation. + */ @JvmStatic fun xor(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("xor", condition, *conditions) @@ -344,570 +378,1143 @@ abstract class Expr internal constructor() { fun bitRightShift(fieldName: String, number: Int): Expr = FunctionExpr("bit_right_shift", fieldName, number) + /** + * + */ @JvmStatic fun add(left: Expr, right: Expr): Expr = FunctionExpr("add", left, right) + /** + * + */ @JvmStatic fun add(left: Expr, right: Any): Expr = FunctionExpr("add", left, right) + /** + * + */ @JvmStatic fun add(fieldName: String, other: Expr): Expr = FunctionExpr("add", fieldName, other) + /** + * + */ @JvmStatic fun add(fieldName: String, other: Any): Expr = FunctionExpr("add", fieldName, other) + /** + * + */ @JvmStatic fun subtract(left: Expr, right: Expr): Expr = FunctionExpr("subtract", left, right) + /** + * + */ @JvmStatic fun subtract(left: Expr, right: Any): Expr = FunctionExpr("subtract", left, right) + /** + * + */ @JvmStatic fun subtract(fieldName: String, other: Expr): Expr = FunctionExpr("subtract", fieldName, other) + /** + * + */ @JvmStatic fun subtract(fieldName: String, other: Any): Expr = FunctionExpr("subtract", fieldName, other) + /** + * + */ @JvmStatic fun multiply(left: Expr, right: Expr): Expr = FunctionExpr("multiply", left, right) + /** + * + */ @JvmStatic fun multiply(left: Expr, right: Any): Expr = FunctionExpr("multiply", left, right) + /** + * + */ @JvmStatic fun multiply(fieldName: String, other: Expr): Expr = FunctionExpr("multiply", fieldName, other) + /** + * + */ @JvmStatic fun multiply(fieldName: String, other: Any): Expr = FunctionExpr("multiply", fieldName, other) + /** + * + */ @JvmStatic fun divide(left: Expr, right: Expr): Expr = FunctionExpr("divide", left, right) + /** + * + */ @JvmStatic fun divide(left: Expr, right: Any): Expr = FunctionExpr("divide", left, right) + /** + * + */ @JvmStatic fun divide(fieldName: String, other: Expr): Expr = FunctionExpr("divide", fieldName, other) + /** + * + */ @JvmStatic fun divide(fieldName: String, other: Any): Expr = FunctionExpr("divide", fieldName, other) + /** + * + */ @JvmStatic fun mod(left: Expr, right: Expr): Expr = FunctionExpr("mod", left, right) + /** + * + */ @JvmStatic fun mod(left: Expr, right: Any): Expr = FunctionExpr("mod", left, right) + /** + * + */ @JvmStatic fun mod(fieldName: String, other: Expr): Expr = FunctionExpr("mod", fieldName, other) + /** + * + */ @JvmStatic fun mod(fieldName: String, other: Any): Expr = FunctionExpr("mod", fieldName, other) + /** + * + */ @JvmStatic fun eqAny(value: Expr, values: List) = BooleanExpr("eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) + /** + * + */ @JvmStatic fun eqAny(fieldName: String, values: List) = BooleanExpr("eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + /** + * + */ @JvmStatic fun notEqAny(value: Expr, values: List) = BooleanExpr("not_eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) + /** + * + */ @JvmStatic fun notEqAny(fieldName: String, values: List) = BooleanExpr("not_eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + /** + * + */ @JvmStatic fun isNan(expr: Expr) = BooleanExpr("is_nan", expr) + /** + * + */ @JvmStatic fun isNan(fieldName: String) = BooleanExpr("is_nan", fieldName) + /** + * + */ @JvmStatic fun isNotNan(expr: Expr) = BooleanExpr("is_not_nan", expr) + /** + * + */ @JvmStatic fun isNotNan(fieldName: String) = BooleanExpr("is_not_nan", fieldName) + /** + * + */ @JvmStatic fun isNull(expr: Expr) = BooleanExpr("is_null", expr) + /** + * + */ @JvmStatic fun isNull(fieldName: String) = BooleanExpr("is_null", fieldName) + /** + * + */ @JvmStatic fun isNotNull(expr: Expr) = BooleanExpr("is_not_null", expr) + /** + * + */ @JvmStatic fun isNotNull(fieldName: String) = BooleanExpr("is_not_null", fieldName) + /** + * + */ @JvmStatic fun replaceFirst(value: Expr, find: Expr, replace: Expr): Expr = FunctionExpr("replace_first", value, find, replace) + /** + * + */ @JvmStatic fun replaceFirst(value: Expr, find: String, replace: String): Expr = FunctionExpr("replace_first", value, find, replace) + /** + * + */ @JvmStatic fun replaceFirst(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_first", fieldName, find, replace) + /** + * + */ @JvmStatic fun replaceAll(value: Expr, find: Expr, replace: Expr): Expr = FunctionExpr("replace_all", value, find, replace) + /** + * + */ @JvmStatic fun replaceAll(value: Expr, find: String, replace: String): Expr = FunctionExpr("replace_all", value, find, replace) + /** + * + */ @JvmStatic fun replaceAll(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_all", fieldName, find, replace) + /** + * + */ @JvmStatic fun charLength(value: Expr): Expr = FunctionExpr("char_length", value) + /** + * + */ @JvmStatic fun charLength(fieldName: String): Expr = FunctionExpr("char_length", fieldName) + /** + * + */ @JvmStatic fun byteLength(value: Expr): Expr = FunctionExpr("byte_length", value) + /** + * + */ @JvmStatic fun byteLength(fieldName: String): Expr = FunctionExpr("byte_length", fieldName) + /** + * + */ @JvmStatic fun like(expr: Expr, pattern: Expr) = BooleanExpr("like", expr, pattern) + /** + * + */ @JvmStatic fun like(expr: Expr, pattern: String) = BooleanExpr("like", expr, pattern) + /** + * + */ @JvmStatic fun like(fieldName: String, pattern: Expr) = BooleanExpr("like", fieldName, pattern) + /** + * + */ @JvmStatic fun like(fieldName: String, pattern: String) = BooleanExpr("like", fieldName, pattern) + /** + * + */ @JvmStatic fun regexContains(expr: Expr, pattern: Expr) = BooleanExpr("regex_contains", expr, pattern) + /** + * + */ @JvmStatic fun regexContains(expr: Expr, pattern: String) = BooleanExpr("regex_contains", expr, pattern) + /** + * + */ @JvmStatic fun regexContains(fieldName: String, pattern: Expr) = BooleanExpr("regex_contains", fieldName, pattern) + /** + * + */ @JvmStatic fun regexContains(fieldName: String, pattern: String) = BooleanExpr("regex_contains", fieldName, pattern) + /** + * + */ @JvmStatic fun regexMatch(expr: Expr, pattern: Expr) = BooleanExpr("regex_match", expr, pattern) + /** + * + */ @JvmStatic fun regexMatch(expr: Expr, pattern: String) = BooleanExpr("regex_match", expr, pattern) + /** + * + */ @JvmStatic fun regexMatch(fieldName: String, pattern: Expr) = BooleanExpr("regex_match", fieldName, pattern) + /** + * + */ @JvmStatic fun regexMatch(fieldName: String, pattern: String) = BooleanExpr("regex_match", fieldName, pattern) + /** + * + */ @JvmStatic fun logicalMax(left: Expr, right: Expr): Expr = FunctionExpr("logical_max", left, right) + /** + * + */ @JvmStatic fun logicalMax(left: Expr, right: Any): Expr = FunctionExpr("logical_max", left, right) + /** + * + */ @JvmStatic fun logicalMax(fieldName: String, other: Expr): Expr = FunctionExpr("logical_max", fieldName, other) + /** + * + */ @JvmStatic fun logicalMax(fieldName: String, other: Any): Expr = FunctionExpr("logical_max", fieldName, other) + /** + * + */ @JvmStatic fun logicalMin(left: Expr, right: Expr): Expr = FunctionExpr("logical_min", left, right) + /** + * + */ @JvmStatic fun logicalMin(left: Expr, right: Any): Expr = FunctionExpr("logical_min", left, right) + /** + * + */ @JvmStatic fun logicalMin(fieldName: String, other: Expr): Expr = FunctionExpr("logical_min", fieldName, other) + /** + * + */ @JvmStatic fun logicalMin(fieldName: String, other: Any): Expr = FunctionExpr("logical_min", fieldName, other) + /** + * + */ @JvmStatic fun reverse(expr: Expr): Expr = FunctionExpr("reverse", expr) + /** + * + */ @JvmStatic fun reverse(fieldName: String): Expr = FunctionExpr("reverse", fieldName) + /** + * + */ @JvmStatic fun strContains(expr: Expr, substring: Expr) = BooleanExpr("str_contains", expr, substring) + /** + * + */ @JvmStatic fun strContains(expr: Expr, substring: String) = BooleanExpr("str_contains", expr, substring) + /** + * + */ @JvmStatic fun strContains(fieldName: String, substring: Expr) = BooleanExpr("str_contains", fieldName, substring) + /** + * + */ @JvmStatic fun strContains(fieldName: String, substring: String) = BooleanExpr("str_contains", fieldName, substring) + /** + * + */ @JvmStatic fun startsWith(expr: Expr, prefix: Expr) = BooleanExpr("starts_with", expr, prefix) + /** + * + */ @JvmStatic fun startsWith(expr: Expr, prefix: String) = BooleanExpr("starts_with", expr, prefix) + /** + * + */ @JvmStatic fun startsWith(fieldName: String, prefix: Expr) = BooleanExpr("starts_with", fieldName, prefix) + /** + * + */ @JvmStatic fun startsWith(fieldName: String, prefix: String) = BooleanExpr("starts_with", fieldName, prefix) + /** + * + */ @JvmStatic fun endsWith(expr: Expr, suffix: Expr) = BooleanExpr("ends_with", expr, suffix) + /** + * + */ @JvmStatic fun endsWith(expr: Expr, suffix: String) = BooleanExpr("ends_with", expr, suffix) + /** + * + */ @JvmStatic fun endsWith(fieldName: String, suffix: Expr) = BooleanExpr("ends_with", fieldName, suffix) + /** + * + */ @JvmStatic fun endsWith(fieldName: String, suffix: String) = BooleanExpr("ends_with", fieldName, suffix) + /** + * + */ @JvmStatic fun toLower(expr: Expr): Expr = FunctionExpr("to_lower", expr) + /** + * + */ @JvmStatic fun toLower( fieldName: String, ): Expr = FunctionExpr("to_lower", fieldName) + /** + * + */ @JvmStatic fun toUpper(expr: Expr): Expr = FunctionExpr("to_upper", expr) - @JvmStatic + /** + * + */ + @JvmStatic fun toUpper( fieldName: String, ): Expr = FunctionExpr("to_upper", fieldName) + /** + * + */ @JvmStatic fun trim(expr: Expr): Expr = FunctionExpr("trim", expr) + /** + * + */ @JvmStatic fun trim(fieldName: String): Expr = FunctionExpr("trim", fieldName) + /** + * + */ @JvmStatic fun strConcat(first: Expr, vararg rest: Expr): Expr = FunctionExpr("str_concat", first, *rest) + /** + * + */ @JvmStatic fun strConcat(first: Expr, vararg rest: Any): Expr = FunctionExpr("str_concat", first, *rest) + /** + * + */ @JvmStatic fun strConcat(fieldName: String, vararg rest: Expr): Expr = FunctionExpr("str_concat", fieldName, *rest) + /** + * + */ @JvmStatic fun strConcat(fieldName: String, vararg rest: Any): Expr = FunctionExpr("str_concat", fieldName, *rest) internal fun map(elements: Array): Expr = FunctionExpr("map", elements) + /** + * + */ @JvmStatic fun map(elements: Map) = map(elements.flatMap { listOf(constant(it.key), toExprOrConstant(it.value)) }.toTypedArray()) + /** + * + */ @JvmStatic fun mapGet(map: Expr, key: Expr): Expr = FunctionExpr("map_get", map, key) + /** + * + */ @JvmStatic fun mapGet(map: Expr, key: String): Expr = FunctionExpr("map_get", map, key) + /** + * + */ @JvmStatic fun mapGet(fieldName: String, key: Expr): Expr = FunctionExpr("map_get", fieldName, key) + /** + * + */ @JvmStatic fun mapGet(fieldName: String, key: String): Expr = FunctionExpr("map_get", fieldName, key) + /** + * + */ @JvmStatic fun mapMerge(firstMap: Expr, secondMap: Expr, vararg otherMaps: Expr): Expr = FunctionExpr("map_merge", firstMap, secondMap, otherMaps) + /** + * + */ @JvmStatic fun mapMerge(mapField: String, secondMap: Expr, vararg otherMaps: Expr): Expr = FunctionExpr("map_merge", mapField, secondMap, otherMaps) + /** + * + */ @JvmStatic fun mapRemove(firstMap: Expr, key: Expr): Expr = FunctionExpr("map_remove", firstMap, key) + /** + * + */ @JvmStatic fun mapRemove(mapField: String, key: Expr): Expr = FunctionExpr("map_remove", mapField, key) + /** + * + */ @JvmStatic fun mapRemove(firstMap: Expr, key: String): Expr = FunctionExpr("map_remove", firstMap, key) + /** + * + */ @JvmStatic fun mapRemove(mapField: String, key: String): Expr = FunctionExpr("map_remove", mapField, key) + /** + * + */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("cosine_distance", vector1, vector2) + /** + * + */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("cosine_distance", vector1, vector(vector2)) + /** + * + */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("cosine_distance", vector1, vector2) + /** + * + */ @JvmStatic fun cosineDistance(fieldName: String, vector: Expr): Expr = FunctionExpr("cosine_distance", fieldName, vector) + /** + * + */ @JvmStatic fun cosineDistance(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("cosine_distance", fieldName, vector(vector)) + /** + * + */ @JvmStatic fun cosineDistance(fieldName: String, vector: VectorValue): Expr = FunctionExpr("cosine_distance", fieldName, vector) + /** + * + */ @JvmStatic fun dotProduct(vector1: Expr, vector2: Expr): Expr = FunctionExpr("dot_product", vector1, vector2) + /** + * + */ @JvmStatic fun dotProduct(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("dot_product", vector1, vector(vector2)) + /** + * + */ @JvmStatic fun dotProduct(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("dot_product", vector1, vector2) + /** + * + */ @JvmStatic fun dotProduct(fieldName: String, vector: Expr): Expr = FunctionExpr("dot_product", fieldName, vector) + /** + * + */ @JvmStatic fun dotProduct(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("dot_product", fieldName, vector(vector)) + /** + * + */ @JvmStatic fun dotProduct(fieldName: String, vector: VectorValue): Expr = FunctionExpr("dot_product", fieldName, vector) + /** + * + */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("euclidean_distance", vector1, vector2) + /** + * + */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("euclidean_distance", vector1, vector(vector2)) + /** + * + */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("euclidean_distance", vector1, vector2) + /** + * + */ @JvmStatic fun euclideanDistance(fieldName: String, vector: Expr): Expr = FunctionExpr("euclidean_distance", fieldName, vector) + /** + * + */ @JvmStatic fun euclideanDistance(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("euclidean_distance", fieldName, vector(vector)) + /** + * + */ @JvmStatic fun euclideanDistance(fieldName: String, vector: VectorValue): Expr = FunctionExpr("euclidean_distance", fieldName, vector) + /** + * + */ @JvmStatic fun vectorLength(vector: Expr): Expr = FunctionExpr("vector_length", vector) + /** + * + */ @JvmStatic fun vectorLength(fieldName: String): Expr = FunctionExpr("vector_length", fieldName) + /** + * + */ @JvmStatic fun unixMicrosToTimestamp(input: Expr): Expr = FunctionExpr("unix_micros_to_timestamp", input) + /** + * + */ @JvmStatic fun unixMicrosToTimestamp(fieldName: String): Expr = FunctionExpr("unix_micros_to_timestamp", fieldName) + /** + * + */ @JvmStatic fun timestampToUnixMicros(input: Expr): Expr = FunctionExpr("timestamp_to_unix_micros", input) + /** + * + */ @JvmStatic fun timestampToUnixMicros(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_micros", fieldName) + /** + * + */ @JvmStatic fun unixMillisToTimestamp(input: Expr): Expr = FunctionExpr("unix_millis_to_timestamp", input) + /** + * + */ @JvmStatic fun unixMillisToTimestamp(fieldName: String): Expr = FunctionExpr("unix_millis_to_timestamp", fieldName) + /** + * + */ @JvmStatic fun timestampToUnixMillis(input: Expr): Expr = FunctionExpr("timestamp_to_unix_millis", input) + /** + * + */ @JvmStatic fun timestampToUnixMillis(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_millis", fieldName) + /** + * + */ @JvmStatic fun unixSecondsToTimestamp(input: Expr): Expr = FunctionExpr("unix_seconds_to_timestamp", input) + /** + * + */ @JvmStatic fun unixSecondsToTimestamp(fieldName: String): Expr = FunctionExpr("unix_seconds_to_timestamp", fieldName) + /** + * + */ @JvmStatic fun timestampToUnixSeconds(input: Expr): Expr = FunctionExpr("timestamp_to_unix_seconds", input) + /** + * + */ @JvmStatic fun timestampToUnixSeconds(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_seconds", fieldName) + /** + * + */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) + /** + * + */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) + /** + * + */ @JvmStatic fun timestampAdd(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) + /** + * + */ @JvmStatic fun timestampAdd(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) + /** + * + */ @JvmStatic fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) + /** + * + */ @JvmStatic fun timestampSub(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) + /** + * + */ @JvmStatic fun timestampSub(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) + /** + * + */ @JvmStatic fun timestampSub(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) + /** + * + */ @JvmStatic fun eq(left: Expr, right: Expr) = BooleanExpr("eq", left, right) + /** + * + */ @JvmStatic fun eq(left: Expr, right: Any) = BooleanExpr("eq", left, right) + /** + * + */ @JvmStatic fun eq(fieldName: String, right: Expr) = BooleanExpr("eq", fieldName, right) + /** + * + */ @JvmStatic fun eq(fieldName: String, right: Any) = BooleanExpr("eq", fieldName, right) + /** + * + */ @JvmStatic fun neq(left: Expr, right: Expr) = BooleanExpr("neq", left, right) + /** + * + */ @JvmStatic fun neq(left: Expr, right: Any) = BooleanExpr("neq", left, right) + /** + * + */ @JvmStatic fun neq(fieldName: String, right: Expr) = BooleanExpr("neq", fieldName, right) + /** + * + */ @JvmStatic fun neq(fieldName: String, right: Any) = BooleanExpr("neq", fieldName, right) + /** + * + */ @JvmStatic fun gt(left: Expr, right: Expr) = BooleanExpr("gt", left, right) + /** + * + */ @JvmStatic fun gt(left: Expr, right: Any) = BooleanExpr("gt", left, right) + /** + * + */ @JvmStatic fun gt(fieldName: String, right: Expr) = BooleanExpr("gt", fieldName, right) + /** + * + */ @JvmStatic fun gt(fieldName: String, right: Any) = BooleanExpr("gt", fieldName, right) + /** + * + */ @JvmStatic fun gte(left: Expr, right: Expr) = BooleanExpr("gte", left, right) + /** + * + */ @JvmStatic fun gte(left: Expr, right: Any) = BooleanExpr("gte", left, right) + /** + * + */ @JvmStatic fun gte(fieldName: String, right: Expr) = BooleanExpr("gte", fieldName, right) + /** + * + */ @JvmStatic fun gte(fieldName: String, right: Any) = BooleanExpr("gte", fieldName, right) + /** + * + */ @JvmStatic fun lt(left: Expr, right: Expr) = BooleanExpr("lt", left, right) + /** + * + */ @JvmStatic fun lt(left: Expr, right: Any) = BooleanExpr("lt", left, right) + /** + * + */ @JvmStatic fun lt(fieldName: String, right: Expr) = BooleanExpr("lt", fieldName, right) + /** + * + */ @JvmStatic fun lt(fieldName: String, right: Any) = BooleanExpr("lt", fieldName, right) + /** + * + */ @JvmStatic fun lte(left: Expr, right: Expr) = BooleanExpr("lte", left, right) + /** + * + */ @JvmStatic fun lte(left: Expr, right: Any) = BooleanExpr("lte", left, right) + /** + * + */ @JvmStatic fun lte(fieldName: String, right: Expr) = BooleanExpr("lte", fieldName, right) + /** + * + */ @JvmStatic fun lte(fieldName: String, right: Any) = BooleanExpr("lte", fieldName, right) + /** + * + */ @JvmStatic fun arrayConcat(array: Expr, vararg arrays: Expr): Expr = FunctionExpr("array_concat", array, *arrays) + /** + * + */ @JvmStatic fun arrayConcat(fieldName: String, vararg arrays: Expr): Expr = FunctionExpr("array_concat", fieldName, *arrays) + /** + * + */ @JvmStatic fun arrayConcat(array: Expr, arrays: List): Expr = FunctionExpr("array_concat", array, ListOfExprs(toArrayOfExprOrConstant(arrays))) + /** + * + */ @JvmStatic fun arrayConcat(fieldName: String, arrays: List): Expr = FunctionExpr("array_concat", fieldName, ListOfExprs(toArrayOfExprOrConstant(arrays))) + /** + * + */ @JvmStatic fun arrayReverse(array: Expr): Expr = FunctionExpr("array_reverse", array) + /** + * + */ @JvmStatic fun arrayReverse(fieldName: String): Expr = FunctionExpr("array_reverse", fieldName) + /** + * + */ @JvmStatic fun arrayContains(array: Expr, value: Expr) = BooleanExpr("array_contains", array, value) + /** + * + */ @JvmStatic fun arrayContains(fieldName: String, value: Expr) = BooleanExpr("array_contains", fieldName, value) + /** + * + */ @JvmStatic fun arrayContains(array: Expr, value: Any) = BooleanExpr("array_contains", array, value) + /** + * + */ @JvmStatic fun arrayContains(fieldName: String, value: Any) = BooleanExpr("array_contains", fieldName, value) + /** + * + */ @JvmStatic fun arrayContainsAll(array: Expr, values: List) = BooleanExpr("array_contains_all", array, ListOfExprs(toArrayOfExprOrConstant(values))) + /** + * + */ @JvmStatic fun arrayContainsAll(fieldName: String, values: List) = BooleanExpr("array_contains_all", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + /** + * + */ @JvmStatic fun arrayContainsAny(array: Expr, values: List) = BooleanExpr("array_contains_any", array, ListOfExprs(toArrayOfExprOrConstant(values))) + /** + * + */ @JvmStatic fun arrayContainsAny(fieldName: String, values: List) = BooleanExpr("array_contains_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + /** + * + */ @JvmStatic fun arrayLength(array: Expr): Expr = FunctionExpr("array_length", array) + /** + * + */ @JvmStatic fun arrayLength(fieldName: String): Expr = FunctionExpr("array_length", fieldName) + /** + * + */ @JvmStatic fun cond(condition: BooleanExpr, then: Expr, otherwise: Expr): Expr = FunctionExpr("cond", condition, then, otherwise) + /** + * + */ @JvmStatic fun cond(condition: BooleanExpr, then: Any, otherwise: Any): Expr = FunctionExpr("cond", condition, then, otherwise) + /** + * + */ @JvmStatic fun exists(expr: Expr) = BooleanExpr("exists", expr) } + /** + * + */ fun bitAnd(right: Expr) = bitAnd(this, right) + /** + * + */ fun bitAnd(right: Any) = bitAnd(this, right) + /** + * + */ fun bitOr(right: Expr) = bitOr(this, right) + /** + * + */ fun bitOr(right: Any) = bitOr(this, right) + /** + * + */ fun bitXor(right: Expr) = bitXor(this, right) + /** + * + */ fun bitXor(right: Any) = bitXor(this, right) + /** + * + */ fun bitNot() = bitNot(this) + /** + * + */ fun bitLeftShift(numberExpr: Expr) = bitLeftShift(this, numberExpr) + /** + * + */ fun bitLeftShift(number: Int) = bitLeftShift(this, number) + /** + * + */ fun bitRightShift(numberExpr: Expr) = bitRightShift(this, numberExpr) + /** + * + */ fun bitRightShift(number: Int) = bitRightShift(this, number) /** @@ -955,195 +1562,480 @@ abstract class Expr internal constructor() { */ fun add(other: Any) = add(this, other) + /** + * + */ fun subtract(other: Expr) = subtract(this, other) + /** + * + */ fun subtract(other: Any) = subtract(this, other) + /** + * + */ fun multiply(other: Expr) = multiply(this, other) + /** + * + */ fun multiply(other: Any) = multiply(this, other) + /** + * + */ fun divide(other: Expr) = divide(this, other) + /** + * + */ fun divide(other: Any) = divide(this, other) + /** + * + */ fun mod(other: Expr) = mod(this, other) + /** + * + */ fun mod(other: Any) = mod(this, other) + /** + * + */ fun eqAny(values: List) = eqAny(this, values) + /** + * + */ fun notEqAny(values: List) = notEqAny(this, values) + /** + * + */ fun isNan() = isNan(this) + /** + * + */ fun isNotNan() = isNotNan(this) + /** + * + */ fun isNull() = isNull(this) + /** + * + */ fun isNotNull() = isNotNull(this) + /** + * + */ fun replaceFirst(find: Expr, replace: Expr) = replaceFirst(this, find, replace) + /** + * + */ fun replaceFirst(find: String, replace: String) = replaceFirst(this, find, replace) + /** + * + */ fun replaceAll(find: Expr, replace: Expr) = replaceAll(this, find, replace) + /** + * + */ fun replaceAll(find: String, replace: String) = replaceAll(this, find, replace) + /** + * + */ fun charLength() = charLength(this) + /** + * + */ fun byteLength() = byteLength(this) + /** + * + */ fun like(pattern: Expr) = like(this, pattern) + /** + * + */ fun like(pattern: String) = like(this, pattern) + /** + * + */ fun regexContains(pattern: Expr) = regexContains(this, pattern) + /** + * + */ fun regexContains(pattern: String) = regexContains(this, pattern) + /** + * + */ fun regexMatch(pattern: Expr) = regexMatch(this, pattern) + /** + * + */ fun regexMatch(pattern: String) = regexMatch(this, pattern) + /** + * + */ fun logicalMax(other: Expr) = logicalMax(this, other) + /** + * + */ fun logicalMax(other: Any) = logicalMax(this, other) + /** + * + */ fun logicalMin(other: Expr) = logicalMin(this, other) + /** + * + */ fun logicalMin(other: Any) = logicalMin(this, other) + /** + * + */ fun reverse() = reverse(this) + /** + * + */ fun strContains(substring: Expr) = strContains(this, substring) + /** + * + */ fun strContains(substring: String) = strContains(this, substring) + /** + * + */ fun startsWith(prefix: Expr) = startsWith(this, prefix) + /** + * + */ fun startsWith(prefix: String) = startsWith(this, prefix) + /** + * + */ fun endsWith(suffix: Expr) = endsWith(this, suffix) + /** + * + */ fun endsWith(suffix: String) = endsWith(this, suffix) + /** + * + */ fun toLower() = toLower(this) + /** + * + */ fun toUpper() = toUpper(this) + /** + * + */ fun trim() = trim(this) + /** + * + */ fun strConcat(vararg expr: Expr) = Companion.strConcat(this, *expr) + /** + * + */ fun strConcat(vararg string: String) = strConcat(this, *string) + /** + * + */ fun strConcat(vararg string: Any) = Companion.strConcat(this, *string) + /** + * + */ fun mapGet(key: Expr) = mapGet(this, key) + /** + * + */ fun mapGet(key: String) = mapGet(this, key) + /** + * + */ fun mapMerge(secondMap: Expr, vararg otherMaps: Expr) = Companion.mapMerge(this, secondMap, *otherMaps) + /** + * + */ fun mapRemove(key: Expr) = mapRemove(this, key) + /** + * + */ fun mapRemove(key: String) = mapRemove(this, key) + /** + * + */ fun cosineDistance(vector: Expr) = cosineDistance(this, vector) + /** + * + */ fun cosineDistance(vector: DoubleArray) = cosineDistance(this, vector) + /** + * + */ fun cosineDistance(vector: VectorValue) = cosineDistance(this, vector) + /** + * + */ fun dotProduct(vector: Expr) = dotProduct(this, vector) + /** + * + */ fun dotProduct(vector: DoubleArray) = dotProduct(this, vector) + /** + * + */ fun dotProduct(vector: VectorValue) = dotProduct(this, vector) + /** + * + */ fun euclideanDistance(vector: Expr) = euclideanDistance(this, vector) + /** + * + */ fun euclideanDistance(vector: DoubleArray) = euclideanDistance(this, vector) + /** + * + */ fun euclideanDistance(vector: VectorValue) = euclideanDistance(this, vector) + /** + * + */ fun vectorLength() = vectorLength(this) + /** + * + */ fun unixMicrosToTimestamp() = unixMicrosToTimestamp(this) + /** + * + */ fun timestampToUnixMicros() = timestampToUnixMicros(this) + /** + * + */ fun unixMillisToTimestamp() = unixMillisToTimestamp(this) + /** + * + */ fun timestampToUnixMillis() = timestampToUnixMillis(this) + /** + * + */ fun unixSecondsToTimestamp() = unixSecondsToTimestamp(this) + /** + * + */ fun timestampToUnixSeconds() = timestampToUnixSeconds(this) + /** + * + */ fun timestampAdd(unit: Expr, amount: Expr) = timestampAdd(this, unit, amount) + /** + * + */ fun timestampAdd(unit: String, amount: Double) = timestampAdd(this, unit, amount) + /** + * + */ fun timestampSub(unit: Expr, amount: Expr) = timestampSub(this, unit, amount) + /** + * + */ fun timestampSub(unit: String, amount: Double) = timestampSub(this, unit, amount) + /** + * + */ fun arrayConcat(vararg arrays: Expr) = Companion.arrayConcat(this, *arrays) + /** + * + */ fun arrayConcat(arrays: List) = arrayConcat(this, arrays) + /** + * + */ fun arrayReverse() = arrayReverse(this) + /** + * + */ fun arrayContains(value: Expr) = arrayContains(this, value) + /** + * + */ fun arrayContains(value: Any) = arrayContains(this, value) + /** + * + */ fun arrayContainsAll(values: List) = arrayContainsAll(this, values) + /** + * + */ fun arrayContainsAny(values: List) = arrayContainsAny(this, values) + /** + * + */ fun arrayLength() = arrayLength(this) + /** + * + */ fun sum() = AggregateFunction.sum(this) + /** + * + */ fun avg() = AggregateFunction.avg(this) + /** + * + */ fun min() = AggregateFunction.min(this) + /** + * + */ fun max() = AggregateFunction.max(this) + /** + * + */ fun ascending() = Ordering.ascending(this) + /** + * + */ fun descending() = Ordering.descending(this) + /** + * + */ fun eq(other: Expr) = eq(this, other) + /** + * + */ fun eq(other: Any) = eq(this, other) + /** + * + */ fun neq(other: Expr) = neq(this, other) + /** + * + */ fun neq(other: Any) = neq(this, other) + /** + * + */ fun gt(other: Expr) = gt(this, other) + /** + * + */ fun gt(other: Any) = gt(this, other) + /** + * + */ fun gte(other: Expr) = gte(this, other) + /** + * + */ fun gte(other: Any) = gte(this, other) + /** + * + */ fun lt(other: Expr) = lt(this, other) + /** + * + */ fun lt(other: Any) = lt(this, other) + /** + * + */ fun lte(other: Expr) = lte(this, other) + /** + * + */ fun lte(other: Any) = lte(this, other) + /** + * + */ fun exists() = exists(this) internal abstract fun toProto(userDataReader: UserDataReader): Value @@ -1240,7 +2132,7 @@ internal constructor(private val name: String, private val params: Array) : +open class BooleanExpr internal constructor(name: String, params: Array) : FunctionExpr(name, params) { internal constructor( name: String, @@ -1259,15 +2151,30 @@ class BooleanExpr internal constructor(name: String, params: Array) : companion object { + /** + * + */ @JvmStatic fun generic(name: String, vararg expr: Expr) = BooleanExpr(name, expr) } + /** + * + */ fun not() = not(this) + /** + * + */ fun countIf(): AggregateFunction = AggregateFunction.countIf(this) + /** + * + */ fun cond(then: Expr, otherwise: Expr) = cond(this, then, otherwise) + /** + * + */ fun cond(then: Any, otherwise: Any) = cond(this, then, otherwise) } From ed24e71331c5b139218d2e1dd6623a4c0c5ecc80 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 23 Apr 2025 15:59:03 -0400 Subject: [PATCH 045/152] Comments --- .../firestore/pipeline/expressions.kt | 557 +++++------------- 1 file changed, 143 insertions(+), 414 deletions(-) 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 aba07b76fcb..0d6c041bb92 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 @@ -26,6 +26,7 @@ import com.google.firebase.firestore.model.DocumentKey import com.google.firebase.firestore.model.FieldPath as ModelFieldPath import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue +import com.google.firebase.firestore.pipeline.Expr.Companion.field import com.google.firebase.firestore.util.CustomClassMapper import com.google.firestore.v1.MapValue import com.google.firestore.v1.Value @@ -284,7 +285,7 @@ abstract class Expr internal constructor() { * * @param condition The first [BooleanExpr]. * @param conditions Addition [BooleanExpr]s. - * @return A new [BooleanExpr] representing the local 'AND' operation. + * @return A new [BooleanExpr] representing the logical 'AND' operation. */ @JvmStatic fun and(condition: BooleanExpr, vararg conditions: BooleanExpr) = @@ -295,7 +296,7 @@ abstract class Expr internal constructor() { * * @param condition The first [BooleanExpr]. * @param conditions Addition [BooleanExpr]s. - * @return A new [BooleanExpr] representing the local 'OR' operation. + * @return A new [BooleanExpr] representing the logical 'OR' operation. */ @JvmStatic fun or(condition: BooleanExpr, vararg conditions: BooleanExpr) = @@ -306,7 +307,7 @@ abstract class Expr internal constructor() { * * @param condition The first [BooleanExpr]. * @param conditions Addition [BooleanExpr]s. - * @return A new [BooleanExpr] representing the local 'XOR' operation. + * @return A new [BooleanExpr] representing the logical 'XOR' operation. */ @JvmStatic fun xor(condition: BooleanExpr, vararg conditions: BooleanExpr) = @@ -379,453 +380,405 @@ abstract class Expr internal constructor() { FunctionExpr("bit_right_shift", fieldName, number) /** + * Creates an expression that adds this expression to another expression. * + * @param first The first expression to add. + * @param second The second expression to add to first expression. + * @param others Additional expression or literal to add. + * @return A new [Expr] representing the addition operation. */ - @JvmStatic fun add(left: Expr, right: Expr): Expr = FunctionExpr("add", left, right) + @JvmStatic + fun add(first: Expr, second: Expr, vararg others: Any): Expr = + FunctionExpr("add", first, second, *others) /** + * Creates an expression that adds this expression to another expression. * + * @param first The first expression to add. + * @param second The second expression or literal to add to first expression. + * @param others Additional expression or literal to add. + * @return A new [Expr] representing the addition operation. */ - @JvmStatic fun add(left: Expr, right: Any): Expr = FunctionExpr("add", left, right) + @JvmStatic + fun add(first: Expr, second: Any, vararg others: Any): Expr = + FunctionExpr("add", first, second, *others) /** + * Creates an expression that adds a field's value to an expression. * + * @param fieldName The name of the field containing the value to add. + * @param second The second expression to add to field value. + * @param others Additional expression or literal to add. */ - @JvmStatic fun add(fieldName: String, other: Expr): Expr = FunctionExpr("add", fieldName, other) + @JvmStatic + fun add(fieldName: String, second: Expr, vararg others: Any): Expr = + FunctionExpr("add", fieldName, second, *others) /** + * Creates an expression that adds a field's value to an expression. * + * @param fieldName The name of the field containing the value to add. + * @param second The second expression or literal to add to field value. + * @param others Additional expression or literal to add. */ - @JvmStatic fun add(fieldName: String, other: Any): Expr = FunctionExpr("add", fieldName, other) + @JvmStatic + fun add(fieldName: String, second: Any, vararg others: Any): Expr = + FunctionExpr("add", fieldName, second, *others) /** - * */ @JvmStatic fun subtract(left: Expr, right: Expr): Expr = FunctionExpr("subtract", left, right) /** - * */ @JvmStatic fun subtract(left: Expr, right: Any): Expr = FunctionExpr("subtract", left, right) /** - * */ @JvmStatic fun subtract(fieldName: String, other: Expr): Expr = FunctionExpr("subtract", fieldName, other) /** - * */ @JvmStatic fun subtract(fieldName: String, other: Any): Expr = FunctionExpr("subtract", fieldName, other) /** - * */ @JvmStatic fun multiply(left: Expr, right: Expr): Expr = FunctionExpr("multiply", left, right) /** - * */ @JvmStatic fun multiply(left: Expr, right: Any): Expr = FunctionExpr("multiply", left, right) /** - * */ @JvmStatic fun multiply(fieldName: String, other: Expr): Expr = FunctionExpr("multiply", fieldName, other) /** - * */ @JvmStatic fun multiply(fieldName: String, other: Any): Expr = FunctionExpr("multiply", fieldName, other) /** - * */ @JvmStatic fun divide(left: Expr, right: Expr): Expr = FunctionExpr("divide", left, right) /** - * */ @JvmStatic fun divide(left: Expr, right: Any): Expr = FunctionExpr("divide", left, right) /** - * */ @JvmStatic fun divide(fieldName: String, other: Expr): Expr = FunctionExpr("divide", fieldName, other) /** - * */ @JvmStatic fun divide(fieldName: String, other: Any): Expr = FunctionExpr("divide", fieldName, other) /** - * */ @JvmStatic fun mod(left: Expr, right: Expr): Expr = FunctionExpr("mod", left, right) /** - * */ @JvmStatic fun mod(left: Expr, right: Any): Expr = FunctionExpr("mod", left, right) /** - * */ @JvmStatic fun mod(fieldName: String, other: Expr): Expr = FunctionExpr("mod", fieldName, other) /** - * */ @JvmStatic fun mod(fieldName: String, other: Any): Expr = FunctionExpr("mod", fieldName, other) /** - * */ @JvmStatic fun eqAny(value: Expr, values: List) = BooleanExpr("eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) /** - * */ @JvmStatic fun eqAny(fieldName: String, values: List) = BooleanExpr("eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) /** - * */ @JvmStatic fun notEqAny(value: Expr, values: List) = BooleanExpr("not_eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) /** - * */ @JvmStatic fun notEqAny(fieldName: String, values: List) = BooleanExpr("not_eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) /** - * */ @JvmStatic fun isNan(expr: Expr) = BooleanExpr("is_nan", expr) /** - * */ @JvmStatic fun isNan(fieldName: String) = BooleanExpr("is_nan", fieldName) /** - * */ @JvmStatic fun isNotNan(expr: Expr) = BooleanExpr("is_not_nan", expr) /** - * */ @JvmStatic fun isNotNan(fieldName: String) = BooleanExpr("is_not_nan", fieldName) /** - * */ @JvmStatic fun isNull(expr: Expr) = BooleanExpr("is_null", expr) /** - * */ @JvmStatic fun isNull(fieldName: String) = BooleanExpr("is_null", fieldName) /** - * */ @JvmStatic fun isNotNull(expr: Expr) = BooleanExpr("is_not_null", expr) /** - * */ @JvmStatic fun isNotNull(fieldName: String) = BooleanExpr("is_not_null", fieldName) /** - * */ @JvmStatic fun replaceFirst(value: Expr, find: Expr, replace: Expr): Expr = FunctionExpr("replace_first", value, find, replace) /** - * */ @JvmStatic fun replaceFirst(value: Expr, find: String, replace: String): Expr = FunctionExpr("replace_first", value, find, replace) /** - * */ @JvmStatic fun replaceFirst(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_first", fieldName, find, replace) /** - * */ @JvmStatic fun replaceAll(value: Expr, find: Expr, replace: Expr): Expr = FunctionExpr("replace_all", value, find, replace) /** - * */ @JvmStatic fun replaceAll(value: Expr, find: String, replace: String): Expr = FunctionExpr("replace_all", value, find, replace) /** - * */ @JvmStatic fun replaceAll(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_all", fieldName, find, replace) /** - * */ @JvmStatic fun charLength(value: Expr): Expr = FunctionExpr("char_length", value) /** - * */ @JvmStatic fun charLength(fieldName: String): Expr = FunctionExpr("char_length", fieldName) /** - * */ @JvmStatic fun byteLength(value: Expr): Expr = FunctionExpr("byte_length", value) /** - * */ @JvmStatic fun byteLength(fieldName: String): Expr = FunctionExpr("byte_length", fieldName) /** - * */ @JvmStatic fun like(expr: Expr, pattern: Expr) = BooleanExpr("like", expr, pattern) /** - * */ @JvmStatic fun like(expr: Expr, pattern: String) = BooleanExpr("like", expr, pattern) /** - * */ @JvmStatic fun like(fieldName: String, pattern: Expr) = BooleanExpr("like", fieldName, pattern) /** - * */ @JvmStatic fun like(fieldName: String, pattern: String) = BooleanExpr("like", fieldName, pattern) /** - * */ @JvmStatic fun regexContains(expr: Expr, pattern: Expr) = BooleanExpr("regex_contains", expr, pattern) /** - * */ @JvmStatic fun regexContains(expr: Expr, pattern: String) = BooleanExpr("regex_contains", expr, pattern) /** - * */ @JvmStatic fun regexContains(fieldName: String, pattern: Expr) = BooleanExpr("regex_contains", fieldName, pattern) /** - * */ @JvmStatic fun regexContains(fieldName: String, pattern: String) = BooleanExpr("regex_contains", fieldName, pattern) /** - * */ @JvmStatic fun regexMatch(expr: Expr, pattern: Expr) = BooleanExpr("regex_match", expr, pattern) /** - * */ @JvmStatic fun regexMatch(expr: Expr, pattern: String) = BooleanExpr("regex_match", expr, pattern) /** - * */ @JvmStatic fun regexMatch(fieldName: String, pattern: Expr) = BooleanExpr("regex_match", fieldName, pattern) /** - * */ @JvmStatic fun regexMatch(fieldName: String, pattern: String) = BooleanExpr("regex_match", fieldName, pattern) /** - * */ @JvmStatic fun logicalMax(left: Expr, right: Expr): Expr = FunctionExpr("logical_max", left, right) /** - * */ @JvmStatic fun logicalMax(left: Expr, right: Any): Expr = FunctionExpr("logical_max", left, right) /** - * */ @JvmStatic fun logicalMax(fieldName: String, other: Expr): Expr = FunctionExpr("logical_max", fieldName, other) /** - * */ @JvmStatic fun logicalMax(fieldName: String, other: Any): Expr = FunctionExpr("logical_max", fieldName, other) /** - * */ @JvmStatic fun logicalMin(left: Expr, right: Expr): Expr = FunctionExpr("logical_min", left, right) /** - * */ @JvmStatic fun logicalMin(left: Expr, right: Any): Expr = FunctionExpr("logical_min", left, right) /** - * */ @JvmStatic fun logicalMin(fieldName: String, other: Expr): Expr = FunctionExpr("logical_min", fieldName, other) /** - * */ @JvmStatic fun logicalMin(fieldName: String, other: Any): Expr = FunctionExpr("logical_min", fieldName, other) /** - * */ @JvmStatic fun reverse(expr: Expr): Expr = FunctionExpr("reverse", expr) /** - * */ @JvmStatic fun reverse(fieldName: String): Expr = FunctionExpr("reverse", fieldName) /** - * */ @JvmStatic fun strContains(expr: Expr, substring: Expr) = BooleanExpr("str_contains", expr, substring) /** - * */ @JvmStatic fun strContains(expr: Expr, substring: String) = BooleanExpr("str_contains", expr, substring) /** - * */ @JvmStatic fun strContains(fieldName: String, substring: Expr) = BooleanExpr("str_contains", fieldName, substring) /** - * */ @JvmStatic fun strContains(fieldName: String, substring: String) = BooleanExpr("str_contains", fieldName, substring) /** - * */ @JvmStatic fun startsWith(expr: Expr, prefix: Expr) = BooleanExpr("starts_with", expr, prefix) /** - * */ @JvmStatic fun startsWith(expr: Expr, prefix: String) = BooleanExpr("starts_with", expr, prefix) /** - * */ @JvmStatic fun startsWith(fieldName: String, prefix: Expr) = BooleanExpr("starts_with", fieldName, prefix) /** - * */ @JvmStatic fun startsWith(fieldName: String, prefix: String) = BooleanExpr("starts_with", fieldName, prefix) /** - * */ @JvmStatic fun endsWith(expr: Expr, suffix: Expr) = BooleanExpr("ends_with", expr, suffix) /** - * */ @JvmStatic fun endsWith(expr: Expr, suffix: String) = BooleanExpr("ends_with", expr, suffix) /** - * */ @JvmStatic fun endsWith(fieldName: String, suffix: Expr) = BooleanExpr("ends_with", fieldName, suffix) /** - * */ @JvmStatic fun endsWith(fieldName: String, suffix: String) = BooleanExpr("ends_with", fieldName, suffix) /** - * */ @JvmStatic fun toLower(expr: Expr): Expr = FunctionExpr("to_lower", expr) /** - * */ @JvmStatic fun toLower( @@ -833,12 +786,10 @@ abstract class Expr internal constructor() { ): Expr = FunctionExpr("to_lower", fieldName) /** - * */ @JvmStatic fun toUpper(expr: Expr): Expr = FunctionExpr("to_upper", expr) /** - * */ @JvmStatic fun toUpper( @@ -846,36 +797,30 @@ abstract class Expr internal constructor() { ): Expr = FunctionExpr("to_upper", fieldName) /** - * */ @JvmStatic fun trim(expr: Expr): Expr = FunctionExpr("trim", expr) /** - * */ @JvmStatic fun trim(fieldName: String): Expr = FunctionExpr("trim", fieldName) /** - * */ @JvmStatic fun strConcat(first: Expr, vararg rest: Expr): Expr = FunctionExpr("str_concat", first, *rest) /** - * */ @JvmStatic fun strConcat(first: Expr, vararg rest: Any): Expr = FunctionExpr("str_concat", first, *rest) /** - * */ @JvmStatic fun strConcat(fieldName: String, vararg rest: Expr): Expr = FunctionExpr("str_concat", fieldName, *rest) /** - * */ @JvmStatic fun strConcat(fieldName: String, vararg rest: Any): Expr = @@ -884,651 +829,539 @@ abstract class Expr internal constructor() { internal fun map(elements: Array): Expr = FunctionExpr("map", elements) /** - * */ @JvmStatic fun map(elements: Map) = map(elements.flatMap { listOf(constant(it.key), toExprOrConstant(it.value)) }.toTypedArray()) /** - * */ @JvmStatic fun mapGet(map: Expr, key: Expr): Expr = FunctionExpr("map_get", map, key) /** - * */ @JvmStatic fun mapGet(map: Expr, key: String): Expr = FunctionExpr("map_get", map, key) /** - * */ @JvmStatic fun mapGet(fieldName: String, key: Expr): Expr = FunctionExpr("map_get", fieldName, key) /** - * */ @JvmStatic fun mapGet(fieldName: String, key: String): Expr = FunctionExpr("map_get", fieldName, key) /** - * */ @JvmStatic fun mapMerge(firstMap: Expr, secondMap: Expr, vararg otherMaps: Expr): Expr = FunctionExpr("map_merge", firstMap, secondMap, otherMaps) /** - * */ @JvmStatic fun mapMerge(mapField: String, secondMap: Expr, vararg otherMaps: Expr): Expr = FunctionExpr("map_merge", mapField, secondMap, otherMaps) /** - * */ @JvmStatic fun mapRemove(firstMap: Expr, key: Expr): Expr = FunctionExpr("map_remove", firstMap, key) /** - * */ @JvmStatic fun mapRemove(mapField: String, key: Expr): Expr = FunctionExpr("map_remove", mapField, key) /** - * */ @JvmStatic fun mapRemove(firstMap: Expr, key: String): Expr = FunctionExpr("map_remove", firstMap, key) /** - * */ @JvmStatic fun mapRemove(mapField: String, key: String): Expr = FunctionExpr("map_remove", mapField, key) /** - * */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("cosine_distance", vector1, vector2) /** - * */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("cosine_distance", vector1, vector(vector2)) /** - * */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("cosine_distance", vector1, vector2) /** - * */ @JvmStatic fun cosineDistance(fieldName: String, vector: Expr): Expr = FunctionExpr("cosine_distance", fieldName, vector) /** - * */ @JvmStatic fun cosineDistance(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("cosine_distance", fieldName, vector(vector)) /** - * */ @JvmStatic fun cosineDistance(fieldName: String, vector: VectorValue): Expr = FunctionExpr("cosine_distance", fieldName, vector) /** - * */ @JvmStatic fun dotProduct(vector1: Expr, vector2: Expr): Expr = FunctionExpr("dot_product", vector1, vector2) /** - * */ @JvmStatic fun dotProduct(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("dot_product", vector1, vector(vector2)) /** - * */ @JvmStatic fun dotProduct(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("dot_product", vector1, vector2) /** - * */ @JvmStatic fun dotProduct(fieldName: String, vector: Expr): Expr = FunctionExpr("dot_product", fieldName, vector) /** - * */ @JvmStatic fun dotProduct(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("dot_product", fieldName, vector(vector)) /** - * */ @JvmStatic fun dotProduct(fieldName: String, vector: VectorValue): Expr = FunctionExpr("dot_product", fieldName, vector) /** - * */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("euclidean_distance", vector1, vector2) /** - * */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("euclidean_distance", vector1, vector(vector2)) /** - * */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("euclidean_distance", vector1, vector2) /** - * */ @JvmStatic fun euclideanDistance(fieldName: String, vector: Expr): Expr = FunctionExpr("euclidean_distance", fieldName, vector) /** - * */ @JvmStatic fun euclideanDistance(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("euclidean_distance", fieldName, vector(vector)) /** - * */ @JvmStatic fun euclideanDistance(fieldName: String, vector: VectorValue): Expr = FunctionExpr("euclidean_distance", fieldName, vector) /** - * */ @JvmStatic fun vectorLength(vector: Expr): Expr = FunctionExpr("vector_length", vector) /** - * */ @JvmStatic fun vectorLength(fieldName: String): Expr = FunctionExpr("vector_length", fieldName) /** - * */ @JvmStatic fun unixMicrosToTimestamp(input: Expr): Expr = FunctionExpr("unix_micros_to_timestamp", input) /** - * */ @JvmStatic fun unixMicrosToTimestamp(fieldName: String): Expr = FunctionExpr("unix_micros_to_timestamp", fieldName) /** - * */ @JvmStatic fun timestampToUnixMicros(input: Expr): Expr = FunctionExpr("timestamp_to_unix_micros", input) /** - * */ @JvmStatic fun timestampToUnixMicros(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_micros", fieldName) /** - * */ @JvmStatic fun unixMillisToTimestamp(input: Expr): Expr = FunctionExpr("unix_millis_to_timestamp", input) /** - * */ @JvmStatic fun unixMillisToTimestamp(fieldName: String): Expr = FunctionExpr("unix_millis_to_timestamp", fieldName) /** - * */ @JvmStatic fun timestampToUnixMillis(input: Expr): Expr = FunctionExpr("timestamp_to_unix_millis", input) /** - * */ @JvmStatic fun timestampToUnixMillis(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_millis", fieldName) /** - * */ @JvmStatic fun unixSecondsToTimestamp(input: Expr): Expr = FunctionExpr("unix_seconds_to_timestamp", input) /** - * */ @JvmStatic fun unixSecondsToTimestamp(fieldName: String): Expr = FunctionExpr("unix_seconds_to_timestamp", fieldName) /** - * */ @JvmStatic fun timestampToUnixSeconds(input: Expr): Expr = FunctionExpr("timestamp_to_unix_seconds", input) /** - * */ @JvmStatic fun timestampToUnixSeconds(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_seconds", fieldName) /** - * */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) /** - * */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) /** - * */ @JvmStatic fun timestampAdd(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) /** - * */ @JvmStatic fun timestampAdd(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) /** - * */ @JvmStatic fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) /** - * */ @JvmStatic fun timestampSub(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) /** - * */ @JvmStatic fun timestampSub(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) /** - * */ @JvmStatic fun timestampSub(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) /** - * */ @JvmStatic fun eq(left: Expr, right: Expr) = BooleanExpr("eq", left, right) /** - * */ @JvmStatic fun eq(left: Expr, right: Any) = BooleanExpr("eq", left, right) /** - * */ @JvmStatic fun eq(fieldName: String, right: Expr) = BooleanExpr("eq", fieldName, right) /** - * */ @JvmStatic fun eq(fieldName: String, right: Any) = BooleanExpr("eq", fieldName, right) /** - * */ @JvmStatic fun neq(left: Expr, right: Expr) = BooleanExpr("neq", left, right) /** - * */ @JvmStatic fun neq(left: Expr, right: Any) = BooleanExpr("neq", left, right) /** - * */ @JvmStatic fun neq(fieldName: String, right: Expr) = BooleanExpr("neq", fieldName, right) /** - * */ @JvmStatic fun neq(fieldName: String, right: Any) = BooleanExpr("neq", fieldName, right) /** - * */ @JvmStatic fun gt(left: Expr, right: Expr) = BooleanExpr("gt", left, right) /** - * */ @JvmStatic fun gt(left: Expr, right: Any) = BooleanExpr("gt", left, right) /** - * */ @JvmStatic fun gt(fieldName: String, right: Expr) = BooleanExpr("gt", fieldName, right) /** - * */ @JvmStatic fun gt(fieldName: String, right: Any) = BooleanExpr("gt", fieldName, right) /** - * */ @JvmStatic fun gte(left: Expr, right: Expr) = BooleanExpr("gte", left, right) /** - * */ @JvmStatic fun gte(left: Expr, right: Any) = BooleanExpr("gte", left, right) /** - * */ @JvmStatic fun gte(fieldName: String, right: Expr) = BooleanExpr("gte", fieldName, right) /** - * */ @JvmStatic fun gte(fieldName: String, right: Any) = BooleanExpr("gte", fieldName, right) /** - * */ @JvmStatic fun lt(left: Expr, right: Expr) = BooleanExpr("lt", left, right) /** - * */ @JvmStatic fun lt(left: Expr, right: Any) = BooleanExpr("lt", left, right) /** - * */ @JvmStatic fun lt(fieldName: String, right: Expr) = BooleanExpr("lt", fieldName, right) /** - * */ @JvmStatic fun lt(fieldName: String, right: Any) = BooleanExpr("lt", fieldName, right) /** - * */ @JvmStatic fun lte(left: Expr, right: Expr) = BooleanExpr("lte", left, right) /** - * */ @JvmStatic fun lte(left: Expr, right: Any) = BooleanExpr("lte", left, right) /** - * */ @JvmStatic fun lte(fieldName: String, right: Expr) = BooleanExpr("lte", fieldName, right) /** - * */ @JvmStatic fun lte(fieldName: String, right: Any) = BooleanExpr("lte", fieldName, right) /** - * */ @JvmStatic fun arrayConcat(array: Expr, vararg arrays: Expr): Expr = FunctionExpr("array_concat", array, *arrays) /** - * */ @JvmStatic fun arrayConcat(fieldName: String, vararg arrays: Expr): Expr = FunctionExpr("array_concat", fieldName, *arrays) /** - * */ @JvmStatic fun arrayConcat(array: Expr, arrays: List): Expr = FunctionExpr("array_concat", array, ListOfExprs(toArrayOfExprOrConstant(arrays))) /** - * */ @JvmStatic fun arrayConcat(fieldName: String, arrays: List): Expr = FunctionExpr("array_concat", fieldName, ListOfExprs(toArrayOfExprOrConstant(arrays))) /** - * */ @JvmStatic fun arrayReverse(array: Expr): Expr = FunctionExpr("array_reverse", array) /** - * */ @JvmStatic fun arrayReverse(fieldName: String): Expr = FunctionExpr("array_reverse", fieldName) /** - * */ @JvmStatic fun arrayContains(array: Expr, value: Expr) = BooleanExpr("array_contains", array, value) /** - * */ @JvmStatic fun arrayContains(fieldName: String, value: Expr) = BooleanExpr("array_contains", fieldName, value) /** - * */ @JvmStatic fun arrayContains(array: Expr, value: Any) = BooleanExpr("array_contains", array, value) /** - * */ @JvmStatic fun arrayContains(fieldName: String, value: Any) = BooleanExpr("array_contains", fieldName, value) /** - * */ @JvmStatic fun arrayContainsAll(array: Expr, values: List) = BooleanExpr("array_contains_all", array, ListOfExprs(toArrayOfExprOrConstant(values))) /** - * */ @JvmStatic fun arrayContainsAll(fieldName: String, values: List) = BooleanExpr("array_contains_all", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) /** - * */ @JvmStatic fun arrayContainsAny(array: Expr, values: List) = BooleanExpr("array_contains_any", array, ListOfExprs(toArrayOfExprOrConstant(values))) /** - * */ @JvmStatic fun arrayContainsAny(fieldName: String, values: List) = BooleanExpr("array_contains_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) /** - * */ @JvmStatic fun arrayLength(array: Expr): Expr = FunctionExpr("array_length", array) /** - * */ @JvmStatic fun arrayLength(fieldName: String): Expr = FunctionExpr("array_length", fieldName) /** - * */ @JvmStatic fun cond(condition: BooleanExpr, then: Expr, otherwise: Expr): Expr = FunctionExpr("cond", condition, then, otherwise) /** - * */ @JvmStatic fun cond(condition: BooleanExpr, then: Any, otherwise: Any): Expr = FunctionExpr("cond", condition, then, otherwise) /** - * */ @JvmStatic fun exists(expr: Expr) = BooleanExpr("exists", expr) } /** - * */ fun bitAnd(right: Expr) = bitAnd(this, right) /** - * */ fun bitAnd(right: Any) = bitAnd(this, right) /** - * */ fun bitOr(right: Expr) = bitOr(this, right) /** - * */ fun bitOr(right: Any) = bitOr(this, right) /** - * */ fun bitXor(right: Expr) = bitXor(this, right) /** - * */ fun bitXor(right: Any) = bitXor(this, right) /** - * */ fun bitNot() = bitNot(this) /** - * */ fun bitLeftShift(numberExpr: Expr) = bitLeftShift(this, numberExpr) /** - * */ fun bitLeftShift(number: Int) = bitLeftShift(this, number) /** - * */ fun bitRightShift(numberExpr: Expr) = bitRightShift(this, numberExpr) /** - * */ fun bitRightShift(number: Int) = bitRightShift(this, number) /** * Assigns an alias to this expression. * - *

Aliases are useful for renaming fields in the output of a stage or for giving meaningful - * names to calculated values. - * - *

Example: - * - *

 // Calculate the total price and assign it the alias "totalPrice" and add it to the
-   *
-   * output. firestore.pipeline().collection("items")
-   * .addFields(Expr.field("price").multiply(Expr.field("quantity")).as("totalPrice")); 
+ * Aliases are useful for renaming fields in the output of a stage or for giving meaningful names + * to calculated values. * * @param alias The alias to assign to this expression. * @return A new [Selectable] (typically an [ExprWithAlias]) that wraps this expression and @@ -1537,506 +1370,403 @@ abstract class Expr internal constructor() { open fun alias(alias: String) = ExprWithAlias(alias, this) /** - * Creates an expression that this expression to another expression. + * Creates an expression that adds this expression to another expression. * - *

Example: - * - *

{@code // Add the value of the 'quantity' field and the 'reserve' field.
-   * Expr.field("quantity").add(Expr.field("reserve")); }
- * - * @param other The expression to add to this expression. - * @return A new {@code Expr} representing the addition operation. + * @param second The second expression to add to this expression. + * @param others Additional expression or literal to add to this expression. + * @return A new [Expr] representing the addition operation. */ - fun add(other: Expr) = add(this, other) + fun add(second: Expr, vararg others: Any) = Companion.add(this, second, *others) /** - * Creates an expression that this expression to another expression. - * - *

Example: + * Creates an expression that adds this expression to another expression. * - *

{@code // Add the value of the 'quantity' field and the 'reserve' field.
-   * Expr.field("quantity").add(Expr.field("reserve")); }
- * - * @param other The constant value to add to this expression. - * @return A new {@code Expr} representing the addition operation. + * @param second The second expression or literal to add to this expression. + * @param others Additional expression or literal to add to this expression. + * @return A new [Expr] representing the addition operation. */ - fun add(other: Any) = add(this, other) + fun add(second: Any, vararg others: Any) = Companion.add(this, second, *others) /** - * */ - fun subtract(other: Expr) = subtract(this, other) + fun subtract(other: Expr) = Companion.subtract(this, other) /** - * */ - fun subtract(other: Any) = subtract(this, other) + fun subtract(other: Any) = Companion.subtract(this, other) /** - * */ - fun multiply(other: Expr) = multiply(this, other) + fun multiply(other: Expr) = Companion.multiply(this, other) /** - * */ - fun multiply(other: Any) = multiply(this, other) + fun multiply(other: Any) = Companion.multiply(this, other) /** - * */ - fun divide(other: Expr) = divide(this, other) + fun divide(other: Expr) = Companion.divide(this, other) /** - * */ - fun divide(other: Any) = divide(this, other) + fun divide(other: Any) = Companion.divide(this, other) /** - * */ - fun mod(other: Expr) = mod(this, other) + fun mod(other: Expr) = Companion.mod(this, other) /** - * */ - fun mod(other: Any) = mod(this, other) + fun mod(other: Any) = Companion.mod(this, other) /** - * */ - fun eqAny(values: List) = eqAny(this, values) + fun eqAny(values: List) = Companion.eqAny(this, values) /** - * */ - fun notEqAny(values: List) = notEqAny(this, values) + fun notEqAny(values: List) = Companion.notEqAny(this, values) /** - * */ - fun isNan() = isNan(this) + fun isNan() = Companion.isNan(this) /** - * */ - fun isNotNan() = isNotNan(this) + fun isNotNan() = Companion.isNotNan(this) /** - * */ - fun isNull() = isNull(this) + fun isNull() = Companion.isNull(this) /** - * */ - fun isNotNull() = isNotNull(this) + fun isNotNull() = Companion.isNotNull(this) /** - * */ - fun replaceFirst(find: Expr, replace: Expr) = replaceFirst(this, find, replace) + fun replaceFirst(find: Expr, replace: Expr) = Companion.replaceFirst(this, find, replace) /** - * */ - fun replaceFirst(find: String, replace: String) = replaceFirst(this, find, replace) + fun replaceFirst(find: String, replace: String) = Companion.replaceFirst(this, find, replace) /** - * */ - fun replaceAll(find: Expr, replace: Expr) = replaceAll(this, find, replace) + fun replaceAll(find: Expr, replace: Expr) = Companion.replaceAll(this, find, replace) /** - * */ - fun replaceAll(find: String, replace: String) = replaceAll(this, find, replace) + fun replaceAll(find: String, replace: String) = Companion.replaceAll(this, find, replace) /** - * */ - fun charLength() = charLength(this) + fun charLength() = Companion.charLength(this) /** - * */ - fun byteLength() = byteLength(this) + fun byteLength() = Companion.byteLength(this) /** - * */ - fun like(pattern: Expr) = like(this, pattern) + fun like(pattern: Expr) = Companion.like(this, pattern) /** - * */ - fun like(pattern: String) = like(this, pattern) + fun like(pattern: String) = Companion.like(this, pattern) /** - * */ - fun regexContains(pattern: Expr) = regexContains(this, pattern) + fun regexContains(pattern: Expr) = Companion.regexContains(this, pattern) /** - * */ - fun regexContains(pattern: String) = regexContains(this, pattern) + fun regexContains(pattern: String) = Companion.regexContains(this, pattern) /** - * */ - fun regexMatch(pattern: Expr) = regexMatch(this, pattern) + fun regexMatch(pattern: Expr) = Companion.regexMatch(this, pattern) /** - * */ - fun regexMatch(pattern: String) = regexMatch(this, pattern) + fun regexMatch(pattern: String) = Companion.regexMatch(this, pattern) /** - * */ - fun logicalMax(other: Expr) = logicalMax(this, other) + fun logicalMax(other: Expr) = Companion.logicalMax(this, other) /** - * */ - fun logicalMax(other: Any) = logicalMax(this, other) + fun logicalMax(other: Any) = Companion.logicalMax(this, other) /** - * */ - fun logicalMin(other: Expr) = logicalMin(this, other) + fun logicalMin(other: Expr) = Companion.logicalMin(this, other) /** - * */ - fun logicalMin(other: Any) = logicalMin(this, other) + fun logicalMin(other: Any) = Companion.logicalMin(this, other) /** - * */ - fun reverse() = reverse(this) + fun reverse() = Companion.reverse(this) /** - * */ - fun strContains(substring: Expr) = strContains(this, substring) + fun strContains(substring: Expr) = Companion.strContains(this, substring) /** - * */ - fun strContains(substring: String) = strContains(this, substring) + fun strContains(substring: String) = Companion.strContains(this, substring) /** - * */ - fun startsWith(prefix: Expr) = startsWith(this, prefix) + fun startsWith(prefix: Expr) = Companion.startsWith(this, prefix) /** - * */ - fun startsWith(prefix: String) = startsWith(this, prefix) + fun startsWith(prefix: String) = Companion.startsWith(this, prefix) /** - * */ - fun endsWith(suffix: Expr) = endsWith(this, suffix) + fun endsWith(suffix: Expr) = Companion.endsWith(this, suffix) /** - * */ - fun endsWith(suffix: String) = endsWith(this, suffix) + fun endsWith(suffix: String) = Companion.endsWith(this, suffix) /** - * */ - fun toLower() = toLower(this) + fun toLower() = Companion.toLower(this) /** - * */ - fun toUpper() = toUpper(this) + fun toUpper() = Companion.toUpper(this) /** - * */ - fun trim() = trim(this) + fun trim() = Companion.trim(this) /** - * */ fun strConcat(vararg expr: Expr) = Companion.strConcat(this, *expr) /** - * */ - fun strConcat(vararg string: String) = strConcat(this, *string) + fun strConcat(vararg string: String) = Companion.strConcat(this, *string) /** - * */ fun strConcat(vararg string: Any) = Companion.strConcat(this, *string) /** - * */ - fun mapGet(key: Expr) = mapGet(this, key) + fun mapGet(key: Expr) = Companion.mapGet(this, key) /** - * */ - fun mapGet(key: String) = mapGet(this, key) + fun mapGet(key: String) = Companion.mapGet(this, key) /** - * */ fun mapMerge(secondMap: Expr, vararg otherMaps: Expr) = Companion.mapMerge(this, secondMap, *otherMaps) /** - * */ - fun mapRemove(key: Expr) = mapRemove(this, key) + fun mapRemove(key: Expr) = Companion.mapRemove(this, key) /** - * */ - fun mapRemove(key: String) = mapRemove(this, key) + fun mapRemove(key: String) = Companion.mapRemove(this, key) /** - * */ - fun cosineDistance(vector: Expr) = cosineDistance(this, vector) + fun cosineDistance(vector: Expr) = Companion.cosineDistance(this, vector) /** - * */ - fun cosineDistance(vector: DoubleArray) = cosineDistance(this, vector) + fun cosineDistance(vector: DoubleArray) = Companion.cosineDistance(this, vector) /** - * */ - fun cosineDistance(vector: VectorValue) = cosineDistance(this, vector) + fun cosineDistance(vector: VectorValue) = Companion.cosineDistance(this, vector) /** - * */ - fun dotProduct(vector: Expr) = dotProduct(this, vector) + fun dotProduct(vector: Expr) = Companion.dotProduct(this, vector) /** - * */ - fun dotProduct(vector: DoubleArray) = dotProduct(this, vector) + fun dotProduct(vector: DoubleArray) = Companion.dotProduct(this, vector) /** - * */ - fun dotProduct(vector: VectorValue) = dotProduct(this, vector) + fun dotProduct(vector: VectorValue) = Companion.dotProduct(this, vector) /** - * */ - fun euclideanDistance(vector: Expr) = euclideanDistance(this, vector) + fun euclideanDistance(vector: Expr) = Companion.euclideanDistance(this, vector) /** - * */ - fun euclideanDistance(vector: DoubleArray) = euclideanDistance(this, vector) + fun euclideanDistance(vector: DoubleArray) = Companion.euclideanDistance(this, vector) /** - * */ - fun euclideanDistance(vector: VectorValue) = euclideanDistance(this, vector) + fun euclideanDistance(vector: VectorValue) = Companion.euclideanDistance(this, vector) /** - * */ - fun vectorLength() = vectorLength(this) + fun vectorLength() = Companion.vectorLength(this) /** - * */ - fun unixMicrosToTimestamp() = unixMicrosToTimestamp(this) + fun unixMicrosToTimestamp() = Companion.unixMicrosToTimestamp(this) /** - * */ - fun timestampToUnixMicros() = timestampToUnixMicros(this) + fun timestampToUnixMicros() = Companion.timestampToUnixMicros(this) /** - * */ - fun unixMillisToTimestamp() = unixMillisToTimestamp(this) + fun unixMillisToTimestamp() = Companion.unixMillisToTimestamp(this) /** - * */ - fun timestampToUnixMillis() = timestampToUnixMillis(this) + fun timestampToUnixMillis() = Companion.timestampToUnixMillis(this) /** - * */ - fun unixSecondsToTimestamp() = unixSecondsToTimestamp(this) + fun unixSecondsToTimestamp() = Companion.unixSecondsToTimestamp(this) /** - * */ - fun timestampToUnixSeconds() = timestampToUnixSeconds(this) + fun timestampToUnixSeconds() = Companion.timestampToUnixSeconds(this) /** - * */ - fun timestampAdd(unit: Expr, amount: Expr) = timestampAdd(this, unit, amount) + fun timestampAdd(unit: Expr, amount: Expr) = Companion.timestampAdd(this, unit, amount) /** - * */ - fun timestampAdd(unit: String, amount: Double) = timestampAdd(this, unit, amount) + fun timestampAdd(unit: String, amount: Double) = Companion.timestampAdd(this, unit, amount) /** - * */ - fun timestampSub(unit: Expr, amount: Expr) = timestampSub(this, unit, amount) + fun timestampSub(unit: Expr, amount: Expr) = Companion.timestampSub(this, unit, amount) /** - * */ - fun timestampSub(unit: String, amount: Double) = timestampSub(this, unit, amount) + fun timestampSub(unit: String, amount: Double) = Companion.timestampSub(this, unit, amount) /** - * */ fun arrayConcat(vararg arrays: Expr) = Companion.arrayConcat(this, *arrays) /** - * */ - fun arrayConcat(arrays: List) = arrayConcat(this, arrays) + fun arrayConcat(arrays: List) = Companion.arrayConcat(this, arrays) /** - * */ - fun arrayReverse() = arrayReverse(this) + fun arrayReverse() = Companion.arrayReverse(this) /** - * */ - fun arrayContains(value: Expr) = arrayContains(this, value) + fun arrayContains(value: Expr) = Companion.arrayContains(this, value) /** - * */ - fun arrayContains(value: Any) = arrayContains(this, value) + fun arrayContains(value: Any) = Companion.arrayContains(this, value) /** - * */ - fun arrayContainsAll(values: List) = arrayContainsAll(this, values) + fun arrayContainsAll(values: List) = Companion.arrayContainsAll(this, values) /** - * */ - fun arrayContainsAny(values: List) = arrayContainsAny(this, values) + fun arrayContainsAny(values: List) = Companion.arrayContainsAny(this, values) /** - * */ - fun arrayLength() = arrayLength(this) + fun arrayLength() = Companion.arrayLength(this) /** - * */ fun sum() = AggregateFunction.sum(this) /** - * */ fun avg() = AggregateFunction.avg(this) /** - * */ fun min() = AggregateFunction.min(this) /** - * */ fun max() = AggregateFunction.max(this) /** - * */ fun ascending() = Ordering.ascending(this) /** - * */ fun descending() = Ordering.descending(this) /** - * */ - fun eq(other: Expr) = eq(this, other) + fun eq(other: Expr) = Companion.eq(this, other) /** - * */ - fun eq(other: Any) = eq(this, other) + fun eq(other: Any) = Companion.eq(this, other) /** - * */ - fun neq(other: Expr) = neq(this, other) + fun neq(other: Expr) = Companion.neq(this, other) /** - * */ - fun neq(other: Any) = neq(this, other) + fun neq(other: Any) = Companion.neq(this, other) /** - * */ - fun gt(other: Expr) = gt(this, other) + fun gt(other: Expr) = Companion.gt(this, other) /** - * */ - fun gt(other: Any) = gt(this, other) + fun gt(other: Any) = Companion.gt(this, other) /** - * */ - fun gte(other: Expr) = gte(this, other) + fun gte(other: Expr) = Companion.gte(this, other) /** - * */ - fun gte(other: Any) = gte(this, other) + fun gte(other: Any) = Companion.gte(this, other) /** - * */ - fun lt(other: Expr) = lt(this, other) + fun lt(other: Expr) = Companion.lt(this, other) /** - * */ - fun lt(other: Any) = lt(this, other) + fun lt(other: Any) = Companion.lt(this, other) /** - * */ - fun lte(other: Expr) = lte(this, other) + fun lte(other: Expr) = Companion.lte(this, other) /** - * */ - fun lte(other: Any) = lte(this, other) + fun lte(other: Any) = Companion.lte(this, other) /** - * */ - fun exists() = exists(this) + fun exists() = Companion.exists(this) internal abstract fun toProto(userDataReader: UserDataReader): Value } @@ -2050,8 +1780,8 @@ abstract class Selectable : Expr() { fun toSelectable(o: Any): Selectable { return when (o) { is Selectable -> o - is String -> Expr.field(o) - is FieldPath -> Expr.field(o) + is String -> field(o) + is FieldPath -> field(o) else -> throw IllegalArgumentException("Unknown Selectable type: $o") } } @@ -2115,11 +1845,17 @@ internal constructor(private val name: String, private val params: Array Date: Fri, 25 Apr 2025 11:54:33 -0400 Subject: [PATCH 046/152] Comments --- .../firestore/pipeline/expressions.kt | 264 +++++++++++++++++- 1 file changed, 255 insertions(+), 9 deletions(-) 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 0d6c041bb92..f9bc0d193aa 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 @@ -313,68 +313,137 @@ abstract class Expr internal constructor() { fun xor(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("xor", condition, *conditions) + /** + * @return A new [Expr] representing the not operation. + */ @JvmStatic fun not(condition: BooleanExpr) = BooleanExpr("not", condition) + /** + * @return A new [Expr] representing the bitAnd operation. + */ @JvmStatic fun bitAnd(left: Expr, right: Expr): Expr = FunctionExpr("bit_and", left, right) + /** + * @return A new [Expr] representing the bitAnd operation. + */ @JvmStatic fun bitAnd(left: Expr, right: Any): Expr = FunctionExpr("bit_and", left, right) + /** + * @return A new [Expr] representing the bitAnd operation. + */ @JvmStatic fun bitAnd(fieldName: String, right: Expr): Expr = FunctionExpr("bit_and", fieldName, right) + /** + * @return A new [Expr] representing the bitAnd operation. + */ @JvmStatic fun bitAnd(fieldName: String, right: Any): Expr = FunctionExpr("bit_and", fieldName, right) + /** + * @return A new [Expr] representing the bitOr operation. + */ @JvmStatic fun bitOr(left: Expr, right: Expr): Expr = FunctionExpr("bit_or", left, right) + /** + * @return A new [Expr] representing the bitOr operation. + */ @JvmStatic fun bitOr(left: Expr, right: Any): Expr = FunctionExpr("bit_or", left, right) + /** + * @return A new [Expr] representing the bitOr operation. + */ @JvmStatic fun bitOr(fieldName: String, right: Expr): Expr = FunctionExpr("bit_or", fieldName, right) + /** + * @return A new [Expr] representing the bitOr operation. + */ @JvmStatic fun bitOr(fieldName: String, right: Any): Expr = FunctionExpr("bit_or", fieldName, right) + /** + * @return A new [Expr] representing the bitXor operation. + */ @JvmStatic fun bitXor(left: Expr, right: Expr): Expr = FunctionExpr("bit_xor", left, right) + /** + * @return A new [Expr] representing the bitXor operation. + */ @JvmStatic fun bitXor(left: Expr, right: Any): Expr = FunctionExpr("bit_xor", left, right) + /** + * @return A new [Expr] representing the bitXor operation. + */ @JvmStatic fun bitXor(fieldName: String, right: Expr): Expr = FunctionExpr("bit_xor", fieldName, right) + /** + * @return A new [Expr] representing the bitXor operation. + */ @JvmStatic fun bitXor(fieldName: String, right: Any): Expr = FunctionExpr("bit_xor", fieldName, right) + /** + * @return A new [Expr] representing the bitNot operation. + */ @JvmStatic fun bitNot(left: Expr): Expr = FunctionExpr("bit_not", left) + /** + * @return A new [Expr] representing the bitNot operation. + */ @JvmStatic fun bitNot(fieldName: String): Expr = FunctionExpr("bit_not", fieldName) + /** + * @return A new [Expr] representing the bitLeftShift operation. + */ @JvmStatic fun bitLeftShift(left: Expr, numberExpr: Expr): Expr = FunctionExpr("bit_left_shift", left, numberExpr) + /** + * @return A new [Expr] representing the bitLeftShift operation. + */ @JvmStatic fun bitLeftShift(left: Expr, number: Int): Expr = FunctionExpr("bit_left_shift", left, number) + /** + * @return A new [Expr] representing the bitLeftShift operation. + */ @JvmStatic fun bitLeftShift(fieldName: String, numberExpr: Expr): Expr = FunctionExpr("bit_left_shift", fieldName, numberExpr) + /** + * @return A new [Expr] representing the bitLeftShift operation. + */ @JvmStatic fun bitLeftShift(fieldName: String, number: Int): Expr = FunctionExpr("bit_left_shift", fieldName, number) + /** + * @return A new [Expr] representing the bitRightShift operation. + */ @JvmStatic fun bitRightShift(left: Expr, numberExpr: Expr): Expr = FunctionExpr("bit_right_shift", left, numberExpr) + /** + * @return A new [Expr] representing the bitRightShift operation. + */ @JvmStatic fun bitRightShift(left: Expr, number: Int): Expr = FunctionExpr("bit_right_shift", left, number) + /** + * @return A new [Expr] representing the bitRightShift operation. + */ @JvmStatic fun bitRightShift(fieldName: String, numberExpr: Expr): Expr = FunctionExpr("bit_right_shift", fieldName, numberExpr) + /** + * @return A new [Expr] representing the bitRightShift operation. + */ @JvmStatic fun bitRightShift(fieldName: String, number: Int): Expr = FunctionExpr("bit_right_shift", fieldName, number) @@ -426,359 +495,433 @@ abstract class Expr internal constructor() { FunctionExpr("add", fieldName, second, *others) /** + * @return A new [Expr] representing the subtract operation. */ @JvmStatic fun subtract(left: Expr, right: Expr): Expr = FunctionExpr("subtract", left, right) /** + * @return A new [Expr] representing the subtract operation. */ @JvmStatic fun subtract(left: Expr, right: Any): Expr = FunctionExpr("subtract", left, right) /** + * @return A new [Expr] representing the subtract operation. */ @JvmStatic fun subtract(fieldName: String, other: Expr): Expr = FunctionExpr("subtract", fieldName, other) /** + * @return A new [Expr] representing the subtract operation. */ @JvmStatic fun subtract(fieldName: String, other: Any): Expr = FunctionExpr("subtract", fieldName, other) /** + * @return A new [Expr] representing the multiply operation. */ @JvmStatic fun multiply(left: Expr, right: Expr): Expr = FunctionExpr("multiply", left, right) /** + * @return A new [Expr] representing the multiply operation. */ @JvmStatic fun multiply(left: Expr, right: Any): Expr = FunctionExpr("multiply", left, right) /** + * @return A new [Expr] representing the multiply operation. */ @JvmStatic fun multiply(fieldName: String, other: Expr): Expr = FunctionExpr("multiply", fieldName, other) /** + * @return A new [Expr] representing the multiply operation. */ @JvmStatic fun multiply(fieldName: String, other: Any): Expr = FunctionExpr("multiply", fieldName, other) /** + * @return A new [Expr] representing the divide operation. */ @JvmStatic fun divide(left: Expr, right: Expr): Expr = FunctionExpr("divide", left, right) /** + * @return A new [Expr] representing the divide operation. */ @JvmStatic fun divide(left: Expr, right: Any): Expr = FunctionExpr("divide", left, right) /** + * @return A new [Expr] representing the divide operation. */ @JvmStatic fun divide(fieldName: String, other: Expr): Expr = FunctionExpr("divide", fieldName, other) /** + * @return A new [Expr] representing the divide operation. */ @JvmStatic fun divide(fieldName: String, other: Any): Expr = FunctionExpr("divide", fieldName, other) /** + * @return A new [Expr] representing the mod operation. */ @JvmStatic fun mod(left: Expr, right: Expr): Expr = FunctionExpr("mod", left, right) /** + * @return A new [Expr] representing the mod operation. */ @JvmStatic fun mod(left: Expr, right: Any): Expr = FunctionExpr("mod", left, right) /** + * @return A new [Expr] representing the mod operation. */ @JvmStatic fun mod(fieldName: String, other: Expr): Expr = FunctionExpr("mod", fieldName, other) /** + * @return A new [Expr] representing the mod operation. */ @JvmStatic fun mod(fieldName: String, other: Any): Expr = FunctionExpr("mod", fieldName, other) /** + * @return A new [Expr] representing the eqAny operation. */ @JvmStatic fun eqAny(value: Expr, values: List) = BooleanExpr("eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) /** + * @return A new [Expr] representing the eqAny operation. */ @JvmStatic fun eqAny(fieldName: String, values: List) = BooleanExpr("eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) /** + * @return A new [Expr] representing the notEqAny operation. */ @JvmStatic fun notEqAny(value: Expr, values: List) = BooleanExpr("not_eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) /** + * @return A new [Expr] representing the notEqAny operation. */ @JvmStatic fun notEqAny(fieldName: String, values: List) = BooleanExpr("not_eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) /** + * @return A new [Expr] representing the isNan operation. */ @JvmStatic fun isNan(expr: Expr) = BooleanExpr("is_nan", expr) /** + * @return A new [Expr] representing the isNan operation. */ @JvmStatic fun isNan(fieldName: String) = BooleanExpr("is_nan", fieldName) /** + * @return A new [Expr] representing the isNotNan operation. */ @JvmStatic fun isNotNan(expr: Expr) = BooleanExpr("is_not_nan", expr) /** + * @return A new [Expr] representing the isNotNan operation. */ @JvmStatic fun isNotNan(fieldName: String) = BooleanExpr("is_not_nan", fieldName) /** + * @return A new [Expr] representing the isNull operation. */ @JvmStatic fun isNull(expr: Expr) = BooleanExpr("is_null", expr) /** + * @return A new [Expr] representing the isNull operation. */ @JvmStatic fun isNull(fieldName: String) = BooleanExpr("is_null", fieldName) /** + * @return A new [Expr] representing the isNotNull operation. */ @JvmStatic fun isNotNull(expr: Expr) = BooleanExpr("is_not_null", expr) /** + * @return A new [Expr] representing the isNotNull operation. */ @JvmStatic fun isNotNull(fieldName: String) = BooleanExpr("is_not_null", fieldName) /** + * @return A new [Expr] representing the replaceFirst operation. */ @JvmStatic fun replaceFirst(value: Expr, find: Expr, replace: Expr): Expr = FunctionExpr("replace_first", value, find, replace) /** + * @return A new [Expr] representing the replaceFirst operation. */ @JvmStatic fun replaceFirst(value: Expr, find: String, replace: String): Expr = FunctionExpr("replace_first", value, find, replace) /** + * @return A new [Expr] representing the replaceFirst operation. */ @JvmStatic fun replaceFirst(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_first", fieldName, find, replace) /** + * @return A new [Expr] representing the replaceAll operation. */ @JvmStatic fun replaceAll(value: Expr, find: Expr, replace: Expr): Expr = FunctionExpr("replace_all", value, find, replace) /** + * @return A new [Expr] representing the replaceAll operation. */ @JvmStatic fun replaceAll(value: Expr, find: String, replace: String): Expr = FunctionExpr("replace_all", value, find, replace) /** + * @return A new [Expr] representing the replaceAll operation. */ @JvmStatic fun replaceAll(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_all", fieldName, find, replace) /** + * @return A new [Expr] representing the charLength operation. */ @JvmStatic fun charLength(value: Expr): Expr = FunctionExpr("char_length", value) /** + * @return A new [Expr] representing the charLength operation. */ @JvmStatic fun charLength(fieldName: String): Expr = FunctionExpr("char_length", fieldName) /** + * @return A new [Expr] representing the byteLength operation. */ @JvmStatic fun byteLength(value: Expr): Expr = FunctionExpr("byte_length", value) /** + * @return A new [Expr] representing the byteLength operation. */ @JvmStatic fun byteLength(fieldName: String): Expr = FunctionExpr("byte_length", fieldName) /** + * @return A new [Expr] representing the like operation. */ @JvmStatic fun like(expr: Expr, pattern: Expr) = BooleanExpr("like", expr, pattern) /** + * @return A new [Expr] representing the like operation. */ @JvmStatic fun like(expr: Expr, pattern: String) = BooleanExpr("like", expr, pattern) /** + * @return A new [Expr] representing the like operation. */ @JvmStatic fun like(fieldName: String, pattern: Expr) = BooleanExpr("like", fieldName, pattern) /** + * @return A new [Expr] representing the like operation. */ @JvmStatic fun like(fieldName: String, pattern: String) = BooleanExpr("like", fieldName, pattern) /** + * @return A new [Expr] representing the regexContains operation. */ @JvmStatic fun regexContains(expr: Expr, pattern: Expr) = BooleanExpr("regex_contains", expr, pattern) /** + * @return A new [Expr] representing the regexContains operation. */ @JvmStatic fun regexContains(expr: Expr, pattern: String) = BooleanExpr("regex_contains", expr, pattern) /** + * @return A new [Expr] representing the regexContains operation. */ @JvmStatic fun regexContains(fieldName: String, pattern: Expr) = BooleanExpr("regex_contains", fieldName, pattern) /** + * @return A new [Expr] representing the regexContains operation. */ @JvmStatic fun regexContains(fieldName: String, pattern: String) = BooleanExpr("regex_contains", fieldName, pattern) /** + * @return A new [Expr] representing the regexMatch operation. */ @JvmStatic fun regexMatch(expr: Expr, pattern: Expr) = BooleanExpr("regex_match", expr, pattern) /** + * @return A new [Expr] representing the regexMatch operation. */ @JvmStatic fun regexMatch(expr: Expr, pattern: String) = BooleanExpr("regex_match", expr, pattern) /** + * @return A new [Expr] representing the regexMatch operation. */ @JvmStatic fun regexMatch(fieldName: String, pattern: Expr) = BooleanExpr("regex_match", fieldName, pattern) /** + * @return A new [Expr] representing the regexMatch operation. */ @JvmStatic fun regexMatch(fieldName: String, pattern: String) = BooleanExpr("regex_match", fieldName, pattern) /** + * @return A new [Expr] representing the logicalMax operation. */ @JvmStatic fun logicalMax(left: Expr, right: Expr): Expr = FunctionExpr("logical_max", left, right) /** + * @return A new [Expr] representing the logicalMax operation. */ @JvmStatic fun logicalMax(left: Expr, right: Any): Expr = FunctionExpr("logical_max", left, right) /** + * @return A new [Expr] representing the logicalMax operation. */ @JvmStatic fun logicalMax(fieldName: String, other: Expr): Expr = FunctionExpr("logical_max", fieldName, other) /** + * @return A new [Expr] representing the logicalMax operation. */ @JvmStatic fun logicalMax(fieldName: String, other: Any): Expr = FunctionExpr("logical_max", fieldName, other) /** + * @return A new [Expr] representing the logicalMin operation. */ @JvmStatic fun logicalMin(left: Expr, right: Expr): Expr = FunctionExpr("logical_min", left, right) /** + * @return A new [Expr] representing the logicalMin operation. */ @JvmStatic fun logicalMin(left: Expr, right: Any): Expr = FunctionExpr("logical_min", left, right) /** + * @return A new [Expr] representing the logicalMin operation. */ @JvmStatic fun logicalMin(fieldName: String, other: Expr): Expr = FunctionExpr("logical_min", fieldName, other) /** + * @return A new [Expr] representing the logicalMin operation. */ @JvmStatic fun logicalMin(fieldName: String, other: Any): Expr = FunctionExpr("logical_min", fieldName, other) /** + * @return A new [Expr] representing the reverse operation. */ @JvmStatic fun reverse(expr: Expr): Expr = FunctionExpr("reverse", expr) /** + * @return A new [Expr] representing the reverse operation. */ @JvmStatic fun reverse(fieldName: String): Expr = FunctionExpr("reverse", fieldName) /** + * @return A new [Expr] representing the strContains operation. */ @JvmStatic fun strContains(expr: Expr, substring: Expr) = BooleanExpr("str_contains", expr, substring) /** + * @return A new [Expr] representing the strContains operation. */ @JvmStatic fun strContains(expr: Expr, substring: String) = BooleanExpr("str_contains", expr, substring) /** + * @return A new [Expr] representing the strContains operation. */ @JvmStatic fun strContains(fieldName: String, substring: Expr) = BooleanExpr("str_contains", fieldName, substring) /** + * @return A new [Expr] representing the strContains operation. */ @JvmStatic fun strContains(fieldName: String, substring: String) = BooleanExpr("str_contains", fieldName, substring) /** + * @return A new [Expr] representing the startsWith operation. */ @JvmStatic fun startsWith(expr: Expr, prefix: Expr) = BooleanExpr("starts_with", expr, prefix) /** + * @return A new [Expr] representing the startsWith operation. */ @JvmStatic fun startsWith(expr: Expr, prefix: String) = BooleanExpr("starts_with", expr, prefix) /** + * @return A new [Expr] representing the startsWith operation. */ @JvmStatic fun startsWith(fieldName: String, prefix: Expr) = BooleanExpr("starts_with", fieldName, prefix) /** + * @return A new [Expr] representing the startsWith operation. */ @JvmStatic fun startsWith(fieldName: String, prefix: String) = BooleanExpr("starts_with", fieldName, prefix) /** + * @return A new [Expr] representing the endsWith operation. */ @JvmStatic fun endsWith(expr: Expr, suffix: Expr) = BooleanExpr("ends_with", expr, suffix) /** + * @return A new [Expr] representing the endsWith operation. */ @JvmStatic fun endsWith(expr: Expr, suffix: String) = BooleanExpr("ends_with", expr, suffix) /** + * @return A new [Expr] representing the endsWith operation. */ @JvmStatic fun endsWith(fieldName: String, suffix: Expr) = BooleanExpr("ends_with", fieldName, suffix) /** + * @return A new [Expr] representing the endsWith operation. */ @JvmStatic fun endsWith(fieldName: String, suffix: String) = BooleanExpr("ends_with", fieldName, suffix) /** + * @return A new [Expr] representing the toLower operation. */ @JvmStatic fun toLower(expr: Expr): Expr = FunctionExpr("to_lower", expr) /** + * @return A new [Expr] representing the toLower operation. */ @JvmStatic fun toLower( @@ -786,10 +929,12 @@ abstract class Expr internal constructor() { ): Expr = FunctionExpr("to_lower", fieldName) /** + * @return A new [Expr] representing the toUpper operation. */ @JvmStatic fun toUpper(expr: Expr): Expr = FunctionExpr("to_upper", expr) /** + * @return A new [Expr] representing the toUpper operation. */ @JvmStatic fun toUpper( @@ -797,30 +942,36 @@ abstract class Expr internal constructor() { ): Expr = FunctionExpr("to_upper", fieldName) /** + * @return A new [Expr] representing the trim operation. */ @JvmStatic fun trim(expr: Expr): Expr = FunctionExpr("trim", expr) /** + * @return A new [Expr] representing the trim operation. */ @JvmStatic fun trim(fieldName: String): Expr = FunctionExpr("trim", fieldName) /** + * @return A new [Expr] representing the strConcat operation. */ @JvmStatic fun strConcat(first: Expr, vararg rest: Expr): Expr = FunctionExpr("str_concat", first, *rest) /** + * @return A new [Expr] representing the strConcat operation. */ @JvmStatic fun strConcat(first: Expr, vararg rest: Any): Expr = FunctionExpr("str_concat", first, *rest) /** + * @return A new [Expr] representing the strConcat operation. */ @JvmStatic fun strConcat(fieldName: String, vararg rest: Expr): Expr = FunctionExpr("str_concat", fieldName, *rest) /** + * @return A new [Expr] representing the strConcat operation. */ @JvmStatic fun strConcat(fieldName: String, vararg rest: Any): Expr = @@ -829,410 +980,499 @@ abstract class Expr internal constructor() { internal fun map(elements: Array): Expr = FunctionExpr("map", elements) /** + * @return A new [Expr] representing the map operation. */ @JvmStatic fun map(elements: Map) = map(elements.flatMap { listOf(constant(it.key), toExprOrConstant(it.value)) }.toTypedArray()) /** + * @return A new [Expr] representing the mapGet operation. */ @JvmStatic fun mapGet(map: Expr, key: Expr): Expr = FunctionExpr("map_get", map, key) /** + * @return A new [Expr] representing the mapGet operation. */ @JvmStatic fun mapGet(map: Expr, key: String): Expr = FunctionExpr("map_get", map, key) /** + * @return A new [Expr] representing the mapGet operation. */ @JvmStatic fun mapGet(fieldName: String, key: Expr): Expr = FunctionExpr("map_get", fieldName, key) /** + * @return A new [Expr] representing the mapGet operation. */ @JvmStatic fun mapGet(fieldName: String, key: String): Expr = FunctionExpr("map_get", fieldName, key) /** + * @return A new [Expr] representing the mapMerge operation. */ @JvmStatic fun mapMerge(firstMap: Expr, secondMap: Expr, vararg otherMaps: Expr): Expr = FunctionExpr("map_merge", firstMap, secondMap, otherMaps) /** + * @return A new [Expr] representing the mapMerge operation. */ @JvmStatic fun mapMerge(mapField: String, secondMap: Expr, vararg otherMaps: Expr): Expr = FunctionExpr("map_merge", mapField, secondMap, otherMaps) /** + * @return A new [Expr] representing the mapRemove operation. */ @JvmStatic fun mapRemove(firstMap: Expr, key: Expr): Expr = FunctionExpr("map_remove", firstMap, key) /** + * @return A new [Expr] representing the mapRemove operation. */ @JvmStatic fun mapRemove(mapField: String, key: Expr): Expr = FunctionExpr("map_remove", mapField, key) /** + * @return A new [Expr] representing the mapRemove operation. */ @JvmStatic fun mapRemove(firstMap: Expr, key: String): Expr = FunctionExpr("map_remove", firstMap, key) /** + * @return A new [Expr] representing the mapRemove operation. */ @JvmStatic fun mapRemove(mapField: String, key: String): Expr = FunctionExpr("map_remove", mapField, key) /** + * @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("cosine_distance", vector1, vector2) /** + * @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("cosine_distance", vector1, vector(vector2)) /** + * @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("cosine_distance", vector1, vector2) /** + * @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(fieldName: String, vector: Expr): Expr = FunctionExpr("cosine_distance", fieldName, vector) /** + * @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("cosine_distance", fieldName, vector(vector)) /** + * @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(fieldName: String, vector: VectorValue): Expr = FunctionExpr("cosine_distance", fieldName, vector) /** + * @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(vector1: Expr, vector2: Expr): Expr = FunctionExpr("dot_product", vector1, vector2) /** + * @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("dot_product", vector1, vector(vector2)) /** + * @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("dot_product", vector1, vector2) /** + * @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(fieldName: String, vector: Expr): Expr = FunctionExpr("dot_product", fieldName, vector) /** + * @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("dot_product", fieldName, vector(vector)) /** + * @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(fieldName: String, vector: VectorValue): Expr = FunctionExpr("dot_product", fieldName, vector) /** + * @return A new [Expr] representing the euclideanDistance operation. */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("euclidean_distance", vector1, vector2) /** + * @return A new [Expr] representing the euclideanDistance operation. */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("euclidean_distance", vector1, vector(vector2)) /** + * @return A new [Expr] representing the not operation. */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("euclidean_distance", vector1, vector2) /** + * @return A new [Expr] representing the euclideanDistance operation. */ @JvmStatic fun euclideanDistance(fieldName: String, vector: Expr): Expr = FunctionExpr("euclidean_distance", fieldName, vector) /** + * @return A new [Expr] representing the euclideanDistance operation. */ @JvmStatic fun euclideanDistance(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("euclidean_distance", fieldName, vector(vector)) /** + * @return A new [Expr] representing the euclideanDistance operation. */ @JvmStatic fun euclideanDistance(fieldName: String, vector: VectorValue): Expr = FunctionExpr("euclidean_distance", fieldName, vector) /** + * @return A new [Expr] representing the vectorLength operation. */ @JvmStatic fun vectorLength(vector: Expr): Expr = FunctionExpr("vector_length", vector) /** + * @return A new [Expr] representing the vectorLength operation. */ @JvmStatic fun vectorLength(fieldName: String): Expr = FunctionExpr("vector_length", fieldName) /** + * @return A new [Expr] representing the unixMicrosToTimestamp operation. */ @JvmStatic fun unixMicrosToTimestamp(input: Expr): Expr = FunctionExpr("unix_micros_to_timestamp", input) /** + * @return A new [Expr] representing the unixMicrosToTimestamp operation. */ @JvmStatic fun unixMicrosToTimestamp(fieldName: String): Expr = FunctionExpr("unix_micros_to_timestamp", fieldName) /** + * @return A new [Expr] representing the timestampToUnixMicros operation. */ @JvmStatic fun timestampToUnixMicros(input: Expr): Expr = FunctionExpr("timestamp_to_unix_micros", input) /** + * @return A new [Expr] representing the timestampToUnixMicros operation. */ @JvmStatic fun timestampToUnixMicros(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_micros", fieldName) /** + * @return A new [Expr] representing the unixMillisToTimestamp operation. */ @JvmStatic fun unixMillisToTimestamp(input: Expr): Expr = FunctionExpr("unix_millis_to_timestamp", input) /** + * @return A new [Expr] representing the unixMillisToTimestamp operation. */ @JvmStatic fun unixMillisToTimestamp(fieldName: String): Expr = FunctionExpr("unix_millis_to_timestamp", fieldName) /** + * @return A new [Expr] representing the timestampToUnixMillis operation. */ @JvmStatic fun timestampToUnixMillis(input: Expr): Expr = FunctionExpr("timestamp_to_unix_millis", input) /** + * @return A new [Expr] representing the timestampToUnixMillis operation. */ @JvmStatic fun timestampToUnixMillis(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_millis", fieldName) /** + * @return A new [Expr] representing the unixSecondsToTimestamp operation. */ @JvmStatic fun unixSecondsToTimestamp(input: Expr): Expr = FunctionExpr("unix_seconds_to_timestamp", input) /** + * @return A new [Expr] representing the unixSecondsToTimestamp operation. */ @JvmStatic fun unixSecondsToTimestamp(fieldName: String): Expr = FunctionExpr("unix_seconds_to_timestamp", fieldName) /** + * @return A new [Expr] representing the timestampToUnixSeconds operation. */ @JvmStatic fun timestampToUnixSeconds(input: Expr): Expr = FunctionExpr("timestamp_to_unix_seconds", input) /** + * @return A new [Expr] representing the timestampToUnixSeconds operation. */ @JvmStatic fun timestampToUnixSeconds(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_seconds", fieldName) /** + * @return A new [Expr] representing the timestampAdd operation. */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) /** + * @return A new [Expr] representing the timestampAdd operation. */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) /** + * @return A new [Expr] representing the timestampAdd operation. */ @JvmStatic fun timestampAdd(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) /** + * @return A new [Expr] representing the timestampAdd operation. */ @JvmStatic fun timestampAdd(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) /** + * @return A new [Expr] representing the timestampSub operation. */ @JvmStatic fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) /** + * @return A new [Expr] representing the timestampSub operation. */ @JvmStatic fun timestampSub(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) /** + * @return A new [Expr] representing the timestampSub operation. */ @JvmStatic fun timestampSub(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) /** + * @return A new [Expr] representing the timestampSub operation. */ @JvmStatic fun timestampSub(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) /** + * @return A new [Expr] representing the eq operation. */ @JvmStatic fun eq(left: Expr, right: Expr) = BooleanExpr("eq", left, right) /** + * @return A new [Expr] representing the eq operation. */ @JvmStatic fun eq(left: Expr, right: Any) = BooleanExpr("eq", left, right) /** + * @return A new [Expr] representing the eq operation. */ @JvmStatic fun eq(fieldName: String, right: Expr) = BooleanExpr("eq", fieldName, right) /** + * @return A new [Expr] representing the eq operation. */ @JvmStatic fun eq(fieldName: String, right: Any) = BooleanExpr("eq", fieldName, right) /** + * @return A new [Expr] representing the neq operation. */ @JvmStatic fun neq(left: Expr, right: Expr) = BooleanExpr("neq", left, right) /** + * @return A new [Expr] representing the neq operation. */ @JvmStatic fun neq(left: Expr, right: Any) = BooleanExpr("neq", left, right) /** + * @return A new [Expr] representing the neq operation. */ @JvmStatic fun neq(fieldName: String, right: Expr) = BooleanExpr("neq", fieldName, right) /** + * @return A new [Expr] representing the neq operation. */ @JvmStatic fun neq(fieldName: String, right: Any) = BooleanExpr("neq", fieldName, right) /** + * @return A new [Expr] representing the gt operation. */ @JvmStatic fun gt(left: Expr, right: Expr) = BooleanExpr("gt", left, right) /** + * @return A new [Expr] representing the gt operation. */ @JvmStatic fun gt(left: Expr, right: Any) = BooleanExpr("gt", left, right) /** + * @return A new [Expr] representing the gt operation. */ @JvmStatic fun gt(fieldName: String, right: Expr) = BooleanExpr("gt", fieldName, right) /** + * @return A new [Expr] representing the gt operation. */ @JvmStatic fun gt(fieldName: String, right: Any) = BooleanExpr("gt", fieldName, right) /** + * @return A new [Expr] representing the gte operation. */ @JvmStatic fun gte(left: Expr, right: Expr) = BooleanExpr("gte", left, right) /** + * @return A new [Expr] representing the gte operation. */ @JvmStatic fun gte(left: Expr, right: Any) = BooleanExpr("gte", left, right) /** + * @return A new [Expr] representing the gte operation. */ @JvmStatic fun gte(fieldName: String, right: Expr) = BooleanExpr("gte", fieldName, right) /** + * @return A new [Expr] representing the gte operation. */ @JvmStatic fun gte(fieldName: String, right: Any) = BooleanExpr("gte", fieldName, right) /** + * @return A new [Expr] representing the lt operation. */ @JvmStatic fun lt(left: Expr, right: Expr) = BooleanExpr("lt", left, right) /** + * @return A new [Expr] representing the lt operation. */ @JvmStatic fun lt(left: Expr, right: Any) = BooleanExpr("lt", left, right) /** + * @return A new [Expr] representing the lt operation. */ @JvmStatic fun lt(fieldName: String, right: Expr) = BooleanExpr("lt", fieldName, right) /** + * @return A new [Expr] representing the lt operation. */ @JvmStatic fun lt(fieldName: String, right: Any) = BooleanExpr("lt", fieldName, right) /** + * @return A new [Expr] representing the lte operation. */ @JvmStatic fun lte(left: Expr, right: Expr) = BooleanExpr("lte", left, right) /** + * @return A new [Expr] representing the lte operation. */ @JvmStatic fun lte(left: Expr, right: Any) = BooleanExpr("lte", left, right) /** + * @return A new [Expr] representing the lte operation. */ @JvmStatic fun lte(fieldName: String, right: Expr) = BooleanExpr("lte", fieldName, right) /** + * @return A new [Expr] representing the lte operation. */ @JvmStatic fun lte(fieldName: String, right: Any) = BooleanExpr("lte", fieldName, right) /** + * Creates an expression that concatenates an array with other arrays. + * + * + * + * @return A new [Expr] representing the arrayConcat operation. */ @JvmStatic - fun arrayConcat(array: Expr, vararg arrays: Expr): Expr = - FunctionExpr("array_concat", array, *arrays) + fun arrayConcat(firstArray: Expr, secondArray: Expr, vararg otherArrays: Any): Expr = + FunctionExpr("array_concat", firstArray, secondArray, *otherArrays) /** + * Creates an expression that concatenates an array with other arrays. + * + * @return A new [Expr] representing the arrayConcat operation. */ @JvmStatic - fun arrayConcat(fieldName: String, vararg arrays: Expr): Expr = - FunctionExpr("array_concat", fieldName, *arrays) + fun arrayConcat(firstArray: Expr, vararg otherArrays: Any): Expr = + FunctionExpr("array_concat", firstArray, *otherArrays) /** + * Creates an expression that concatenates a field's array value with other arrays. + * + * @return A new [Expr] representing the arrayConcat operation. */ @JvmStatic - fun arrayConcat(array: Expr, arrays: List): Expr = - FunctionExpr("array_concat", array, ListOfExprs(toArrayOfExprOrConstant(arrays))) + fun arrayConcat(fieldName: String, secondArray: Expr, vararg otherArrays: Any): Expr = + FunctionExpr("array_concat", fieldName, secondArray, *otherArrays) /** + * Creates an expression that concatenates a field's array value with other arrays. + * + * @return A new [Expr] representing the arrayConcat operation. */ @JvmStatic - fun arrayConcat(fieldName: String, arrays: List): Expr = - FunctionExpr("array_concat", fieldName, ListOfExprs(toArrayOfExprOrConstant(arrays))) + fun arrayConcat(fieldName: String, vararg otherArrays: Any): Expr = + FunctionExpr("array_concat", fieldName, *otherArrays) /** */ @@ -1661,10 +1901,16 @@ abstract class Expr internal constructor() { fun timestampSub(unit: String, amount: Double) = Companion.timestampSub(this, unit, amount) /** + * Creates an expression that concatenates a field's array value with other arrays. + * + * @return A new [Expr] representing the arrayConcat operation. */ - fun arrayConcat(vararg arrays: Expr) = Companion.arrayConcat(this, *arrays) + fun arrayConcat(vararg otherArrays: Expr) = Companion.arrayConcat(this, *otherArrays) /** + * Creates an expression that concatenates a field's array value with other arrays. + * + * @return A new [Expr] representing the arrayConcat operation. */ fun arrayConcat(arrays: List) = Companion.arrayConcat(this, arrays) From 83d94e41b2c20701c82536d23082d99704ff97a6 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 25 Apr 2025 15:47:42 -0400 Subject: [PATCH 047/152] Comments --- .../firestore/pipeline/expressions.kt | 921 +++++++----------- 1 file changed, 354 insertions(+), 567 deletions(-) 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 f9bc0d193aa..19c2af56700 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 @@ -313,137 +313,91 @@ abstract class Expr internal constructor() { fun xor(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("xor", condition, *conditions) - /** - * @return A new [Expr] representing the not operation. - */ + /** @return A new [Expr] representing the not operation. */ @JvmStatic fun not(condition: BooleanExpr) = BooleanExpr("not", condition) - /** - * @return A new [Expr] representing the bitAnd operation. - */ + /** @return A new [Expr] representing the bitAnd operation. */ @JvmStatic fun bitAnd(left: Expr, right: Expr): Expr = FunctionExpr("bit_and", left, right) - /** - * @return A new [Expr] representing the bitAnd operation. - */ + /** @return A new [Expr] representing the bitAnd operation. */ @JvmStatic fun bitAnd(left: Expr, right: Any): Expr = FunctionExpr("bit_and", left, right) - /** - * @return A new [Expr] representing the bitAnd operation. - */ + /** @return A new [Expr] representing the bitAnd operation. */ @JvmStatic fun bitAnd(fieldName: String, right: Expr): Expr = FunctionExpr("bit_and", fieldName, right) - /** - * @return A new [Expr] representing the bitAnd operation. - */ + /** @return A new [Expr] representing the bitAnd operation. */ @JvmStatic fun bitAnd(fieldName: String, right: Any): Expr = FunctionExpr("bit_and", fieldName, right) - /** - * @return A new [Expr] representing the bitOr operation. - */ + /** @return A new [Expr] representing the bitOr operation. */ @JvmStatic fun bitOr(left: Expr, right: Expr): Expr = FunctionExpr("bit_or", left, right) - /** - * @return A new [Expr] representing the bitOr operation. - */ + /** @return A new [Expr] representing the bitOr operation. */ @JvmStatic fun bitOr(left: Expr, right: Any): Expr = FunctionExpr("bit_or", left, right) - /** - * @return A new [Expr] representing the bitOr operation. - */ + /** @return A new [Expr] representing the bitOr operation. */ @JvmStatic fun bitOr(fieldName: String, right: Expr): Expr = FunctionExpr("bit_or", fieldName, right) - /** - * @return A new [Expr] representing the bitOr operation. - */ + /** @return A new [Expr] representing the bitOr operation. */ @JvmStatic fun bitOr(fieldName: String, right: Any): Expr = FunctionExpr("bit_or", fieldName, right) - /** - * @return A new [Expr] representing the bitXor operation. - */ + /** @return A new [Expr] representing the bitXor operation. */ @JvmStatic fun bitXor(left: Expr, right: Expr): Expr = FunctionExpr("bit_xor", left, right) - /** - * @return A new [Expr] representing the bitXor operation. - */ + /** @return A new [Expr] representing the bitXor operation. */ @JvmStatic fun bitXor(left: Expr, right: Any): Expr = FunctionExpr("bit_xor", left, right) - /** - * @return A new [Expr] representing the bitXor operation. - */ + /** @return A new [Expr] representing the bitXor operation. */ @JvmStatic fun bitXor(fieldName: String, right: Expr): Expr = FunctionExpr("bit_xor", fieldName, right) - /** - * @return A new [Expr] representing the bitXor operation. - */ + /** @return A new [Expr] representing the bitXor operation. */ @JvmStatic fun bitXor(fieldName: String, right: Any): Expr = FunctionExpr("bit_xor", fieldName, right) - /** - * @return A new [Expr] representing the bitNot operation. - */ + /** @return A new [Expr] representing the bitNot operation. */ @JvmStatic fun bitNot(left: Expr): Expr = FunctionExpr("bit_not", left) - /** - * @return A new [Expr] representing the bitNot operation. - */ + /** @return A new [Expr] representing the bitNot operation. */ @JvmStatic fun bitNot(fieldName: String): Expr = FunctionExpr("bit_not", fieldName) - /** - * @return A new [Expr] representing the bitLeftShift operation. - */ + /** @return A new [Expr] representing the bitLeftShift operation. */ @JvmStatic fun bitLeftShift(left: Expr, numberExpr: Expr): Expr = FunctionExpr("bit_left_shift", left, numberExpr) - /** - * @return A new [Expr] representing the bitLeftShift operation. - */ + /** @return A new [Expr] representing the bitLeftShift operation. */ @JvmStatic fun bitLeftShift(left: Expr, number: Int): Expr = FunctionExpr("bit_left_shift", left, number) - /** - * @return A new [Expr] representing the bitLeftShift operation. - */ + /** @return A new [Expr] representing the bitLeftShift operation. */ @JvmStatic fun bitLeftShift(fieldName: String, numberExpr: Expr): Expr = FunctionExpr("bit_left_shift", fieldName, numberExpr) - /** - * @return A new [Expr] representing the bitLeftShift operation. - */ + /** @return A new [Expr] representing the bitLeftShift operation. */ @JvmStatic fun bitLeftShift(fieldName: String, number: Int): Expr = FunctionExpr("bit_left_shift", fieldName, number) - /** - * @return A new [Expr] representing the bitRightShift operation. - */ + /** @return A new [Expr] representing the bitRightShift operation. */ @JvmStatic fun bitRightShift(left: Expr, numberExpr: Expr): Expr = FunctionExpr("bit_right_shift", left, numberExpr) - /** - * @return A new [Expr] representing the bitRightShift operation. - */ + /** @return A new [Expr] representing the bitRightShift operation. */ @JvmStatic fun bitRightShift(left: Expr, number: Int): Expr = FunctionExpr("bit_right_shift", left, number) - /** - * @return A new [Expr] representing the bitRightShift operation. - */ + /** @return A new [Expr] representing the bitRightShift operation. */ @JvmStatic fun bitRightShift(fieldName: String, numberExpr: Expr): Expr = FunctionExpr("bit_right_shift", fieldName, numberExpr) - /** - * @return A new [Expr] representing the bitRightShift operation. - */ + /** @return A new [Expr] representing the bitRightShift operation. */ @JvmStatic fun bitRightShift(fieldName: String, number: Int): Expr = FunctionExpr("bit_right_shift", fieldName, number) @@ -494,953 +448,640 @@ abstract class Expr internal constructor() { fun add(fieldName: String, second: Any, vararg others: Any): Expr = FunctionExpr("add", fieldName, second, *others) - /** - * @return A new [Expr] representing the subtract operation. - */ + /** @return A new [Expr] representing the subtract operation. */ @JvmStatic fun subtract(left: Expr, right: Expr): Expr = FunctionExpr("subtract", left, right) - /** - * @return A new [Expr] representing the subtract operation. - */ + /** @return A new [Expr] representing the subtract operation. */ @JvmStatic fun subtract(left: Expr, right: Any): Expr = FunctionExpr("subtract", left, right) - /** - * @return A new [Expr] representing the subtract operation. - */ + /** @return A new [Expr] representing the subtract operation. */ @JvmStatic fun subtract(fieldName: String, other: Expr): Expr = FunctionExpr("subtract", fieldName, other) - /** - * @return A new [Expr] representing the subtract operation. - */ + /** @return A new [Expr] representing the subtract operation. */ @JvmStatic fun subtract(fieldName: String, other: Any): Expr = FunctionExpr("subtract", fieldName, other) - /** - * @return A new [Expr] representing the multiply operation. - */ + /** @return A new [Expr] representing the multiply operation. */ @JvmStatic fun multiply(left: Expr, right: Expr): Expr = FunctionExpr("multiply", left, right) - /** - * @return A new [Expr] representing the multiply operation. - */ + /** @return A new [Expr] representing the multiply operation. */ @JvmStatic fun multiply(left: Expr, right: Any): Expr = FunctionExpr("multiply", left, right) - /** - * @return A new [Expr] representing the multiply operation. - */ + /** @return A new [Expr] representing the multiply operation. */ @JvmStatic fun multiply(fieldName: String, other: Expr): Expr = FunctionExpr("multiply", fieldName, other) - /** - * @return A new [Expr] representing the multiply operation. - */ + /** @return A new [Expr] representing the multiply operation. */ @JvmStatic fun multiply(fieldName: String, other: Any): Expr = FunctionExpr("multiply", fieldName, other) - /** - * @return A new [Expr] representing the divide operation. - */ + /** @return A new [Expr] representing the divide operation. */ @JvmStatic fun divide(left: Expr, right: Expr): Expr = FunctionExpr("divide", left, right) - /** - * @return A new [Expr] representing the divide operation. - */ + /** @return A new [Expr] representing the divide operation. */ @JvmStatic fun divide(left: Expr, right: Any): Expr = FunctionExpr("divide", left, right) - /** - * @return A new [Expr] representing the divide operation. - */ + /** @return A new [Expr] representing the divide operation. */ @JvmStatic fun divide(fieldName: String, other: Expr): Expr = FunctionExpr("divide", fieldName, other) - /** - * @return A new [Expr] representing the divide operation. - */ + /** @return A new [Expr] representing the divide operation. */ @JvmStatic fun divide(fieldName: String, other: Any): Expr = FunctionExpr("divide", fieldName, other) - /** - * @return A new [Expr] representing the mod operation. - */ + /** @return A new [Expr] representing the mod operation. */ @JvmStatic fun mod(left: Expr, right: Expr): Expr = FunctionExpr("mod", left, right) - /** - * @return A new [Expr] representing the mod operation. - */ + /** @return A new [Expr] representing the mod operation. */ @JvmStatic fun mod(left: Expr, right: Any): Expr = FunctionExpr("mod", left, right) - /** - * @return A new [Expr] representing the mod operation. - */ + /** @return A new [Expr] representing the mod operation. */ @JvmStatic fun mod(fieldName: String, other: Expr): Expr = FunctionExpr("mod", fieldName, other) - /** - * @return A new [Expr] representing the mod operation. - */ + /** @return A new [Expr] representing the mod operation. */ @JvmStatic fun mod(fieldName: String, other: Any): Expr = FunctionExpr("mod", fieldName, other) - /** - * @return A new [Expr] representing the eqAny operation. - */ + /** @return A new [Expr] representing the eqAny operation. */ @JvmStatic fun eqAny(value: Expr, values: List) = BooleanExpr("eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) - /** - * @return A new [Expr] representing the eqAny operation. - */ + /** @return A new [Expr] representing the eqAny operation. */ @JvmStatic fun eqAny(fieldName: String, values: List) = BooleanExpr("eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) - /** - * @return A new [Expr] representing the notEqAny operation. - */ + /** @return A new [Expr] representing the notEqAny operation. */ @JvmStatic fun notEqAny(value: Expr, values: List) = BooleanExpr("not_eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) - /** - * @return A new [Expr] representing the notEqAny operation. - */ + /** @return A new [Expr] representing the notEqAny operation. */ @JvmStatic fun notEqAny(fieldName: String, values: List) = BooleanExpr("not_eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) - /** - * @return A new [Expr] representing the isNan operation. - */ + /** @return A new [Expr] representing the isNan operation. */ @JvmStatic fun isNan(expr: Expr) = BooleanExpr("is_nan", expr) - /** - * @return A new [Expr] representing the isNan operation. - */ + /** @return A new [Expr] representing the isNan operation. */ @JvmStatic fun isNan(fieldName: String) = BooleanExpr("is_nan", fieldName) - /** - * @return A new [Expr] representing the isNotNan operation. - */ + /** @return A new [Expr] representing the isNotNan operation. */ @JvmStatic fun isNotNan(expr: Expr) = BooleanExpr("is_not_nan", expr) - /** - * @return A new [Expr] representing the isNotNan operation. - */ + /** @return A new [Expr] representing the isNotNan operation. */ @JvmStatic fun isNotNan(fieldName: String) = BooleanExpr("is_not_nan", fieldName) - /** - * @return A new [Expr] representing the isNull operation. - */ + /** @return A new [Expr] representing the isNull operation. */ @JvmStatic fun isNull(expr: Expr) = BooleanExpr("is_null", expr) - /** - * @return A new [Expr] representing the isNull operation. - */ + /** @return A new [Expr] representing the isNull operation. */ @JvmStatic fun isNull(fieldName: String) = BooleanExpr("is_null", fieldName) - /** - * @return A new [Expr] representing the isNotNull operation. - */ + /** @return A new [Expr] representing the isNotNull operation. */ @JvmStatic fun isNotNull(expr: Expr) = BooleanExpr("is_not_null", expr) - /** - * @return A new [Expr] representing the isNotNull operation. - */ + /** @return A new [Expr] representing the isNotNull operation. */ @JvmStatic fun isNotNull(fieldName: String) = BooleanExpr("is_not_null", fieldName) - /** - * @return A new [Expr] representing the replaceFirst operation. - */ + /** @return A new [Expr] representing the replaceFirst operation. */ @JvmStatic fun replaceFirst(value: Expr, find: Expr, replace: Expr): Expr = FunctionExpr("replace_first", value, find, replace) - /** - * @return A new [Expr] representing the replaceFirst operation. - */ + /** @return A new [Expr] representing the replaceFirst operation. */ @JvmStatic fun replaceFirst(value: Expr, find: String, replace: String): Expr = FunctionExpr("replace_first", value, find, replace) - /** - * @return A new [Expr] representing the replaceFirst operation. - */ + /** @return A new [Expr] representing the replaceFirst operation. */ @JvmStatic fun replaceFirst(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_first", fieldName, find, replace) - /** - * @return A new [Expr] representing the replaceAll operation. - */ + /** @return A new [Expr] representing the replaceAll operation. */ @JvmStatic fun replaceAll(value: Expr, find: Expr, replace: Expr): Expr = FunctionExpr("replace_all", value, find, replace) - /** - * @return A new [Expr] representing the replaceAll operation. - */ + /** @return A new [Expr] representing the replaceAll operation. */ @JvmStatic fun replaceAll(value: Expr, find: String, replace: String): Expr = FunctionExpr("replace_all", value, find, replace) - /** - * @return A new [Expr] representing the replaceAll operation. - */ + /** @return A new [Expr] representing the replaceAll operation. */ @JvmStatic fun replaceAll(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_all", fieldName, find, replace) - /** - * @return A new [Expr] representing the charLength operation. - */ + /** @return A new [Expr] representing the charLength operation. */ @JvmStatic fun charLength(value: Expr): Expr = FunctionExpr("char_length", value) - /** - * @return A new [Expr] representing the charLength operation. - */ + /** @return A new [Expr] representing the charLength operation. */ @JvmStatic fun charLength(fieldName: String): Expr = FunctionExpr("char_length", fieldName) - /** - * @return A new [Expr] representing the byteLength operation. - */ + /** @return A new [Expr] representing the byteLength operation. */ @JvmStatic fun byteLength(value: Expr): Expr = FunctionExpr("byte_length", value) - /** - * @return A new [Expr] representing the byteLength operation. - */ + /** @return A new [Expr] representing the byteLength operation. */ @JvmStatic fun byteLength(fieldName: String): Expr = FunctionExpr("byte_length", fieldName) - /** - * @return A new [Expr] representing the like operation. - */ + /** @return A new [Expr] representing the like operation. */ @JvmStatic fun like(expr: Expr, pattern: Expr) = BooleanExpr("like", expr, pattern) - /** - * @return A new [Expr] representing the like operation. - */ + /** @return A new [Expr] representing the like operation. */ @JvmStatic fun like(expr: Expr, pattern: String) = BooleanExpr("like", expr, pattern) - /** - * @return A new [Expr] representing the like operation. - */ + /** @return A new [Expr] representing the like operation. */ @JvmStatic fun like(fieldName: String, pattern: Expr) = BooleanExpr("like", fieldName, pattern) - /** - * @return A new [Expr] representing the like operation. - */ + /** @return A new [Expr] representing the like operation. */ @JvmStatic fun like(fieldName: String, pattern: String) = BooleanExpr("like", fieldName, pattern) - /** - * @return A new [Expr] representing the regexContains operation. - */ + /** @return A new [Expr] representing the regexContains operation. */ @JvmStatic fun regexContains(expr: Expr, pattern: Expr) = BooleanExpr("regex_contains", expr, pattern) - /** - * @return A new [Expr] representing the regexContains operation. - */ + /** @return A new [Expr] representing the regexContains operation. */ @JvmStatic fun regexContains(expr: Expr, pattern: String) = BooleanExpr("regex_contains", expr, pattern) - /** - * @return A new [Expr] representing the regexContains operation. - */ + /** @return A new [Expr] representing the regexContains operation. */ @JvmStatic fun regexContains(fieldName: String, pattern: Expr) = BooleanExpr("regex_contains", fieldName, pattern) - /** - * @return A new [Expr] representing the regexContains operation. - */ + /** @return A new [Expr] representing the regexContains operation. */ @JvmStatic fun regexContains(fieldName: String, pattern: String) = BooleanExpr("regex_contains", fieldName, pattern) - /** - * @return A new [Expr] representing the regexMatch operation. - */ + /** @return A new [Expr] representing the regexMatch operation. */ @JvmStatic fun regexMatch(expr: Expr, pattern: Expr) = BooleanExpr("regex_match", expr, pattern) - /** - * @return A new [Expr] representing the regexMatch operation. - */ + /** @return A new [Expr] representing the regexMatch operation. */ @JvmStatic fun regexMatch(expr: Expr, pattern: String) = BooleanExpr("regex_match", expr, pattern) - /** - * @return A new [Expr] representing the regexMatch operation. - */ + /** @return A new [Expr] representing the regexMatch operation. */ @JvmStatic fun regexMatch(fieldName: String, pattern: Expr) = BooleanExpr("regex_match", fieldName, pattern) - /** - * @return A new [Expr] representing the regexMatch operation. - */ + /** @return A new [Expr] representing the regexMatch operation. */ @JvmStatic fun regexMatch(fieldName: String, pattern: String) = BooleanExpr("regex_match", fieldName, pattern) - /** - * @return A new [Expr] representing the logicalMax operation. - */ + /** @return A new [Expr] representing the logicalMax operation. */ @JvmStatic fun logicalMax(left: Expr, right: Expr): Expr = FunctionExpr("logical_max", left, right) - /** - * @return A new [Expr] representing the logicalMax operation. - */ + /** @return A new [Expr] representing the logicalMax operation. */ @JvmStatic fun logicalMax(left: Expr, right: Any): Expr = FunctionExpr("logical_max", left, right) - /** - * @return A new [Expr] representing the logicalMax operation. - */ + /** @return A new [Expr] representing the logicalMax operation. */ @JvmStatic fun logicalMax(fieldName: String, other: Expr): Expr = FunctionExpr("logical_max", fieldName, other) - /** - * @return A new [Expr] representing the logicalMax operation. - */ + /** @return A new [Expr] representing the logicalMax operation. */ @JvmStatic fun logicalMax(fieldName: String, other: Any): Expr = FunctionExpr("logical_max", fieldName, other) - /** - * @return A new [Expr] representing the logicalMin operation. - */ + /** @return A new [Expr] representing the logicalMin operation. */ @JvmStatic fun logicalMin(left: Expr, right: Expr): Expr = FunctionExpr("logical_min", left, right) - /** - * @return A new [Expr] representing the logicalMin operation. - */ + /** @return A new [Expr] representing the logicalMin operation. */ @JvmStatic fun logicalMin(left: Expr, right: Any): Expr = FunctionExpr("logical_min", left, right) - /** - * @return A new [Expr] representing the logicalMin operation. - */ + /** @return A new [Expr] representing the logicalMin operation. */ @JvmStatic fun logicalMin(fieldName: String, other: Expr): Expr = FunctionExpr("logical_min", fieldName, other) - /** - * @return A new [Expr] representing the logicalMin operation. - */ + /** @return A new [Expr] representing the logicalMin operation. */ @JvmStatic fun logicalMin(fieldName: String, other: Any): Expr = FunctionExpr("logical_min", fieldName, other) - /** - * @return A new [Expr] representing the reverse operation. - */ + /** @return A new [Expr] representing the reverse operation. */ @JvmStatic fun reverse(expr: Expr): Expr = FunctionExpr("reverse", expr) - /** - * @return A new [Expr] representing the reverse operation. - */ + /** @return A new [Expr] representing the reverse operation. */ @JvmStatic fun reverse(fieldName: String): Expr = FunctionExpr("reverse", fieldName) - /** - * @return A new [Expr] representing the strContains operation. - */ + /** @return A new [Expr] representing the strContains operation. */ @JvmStatic fun strContains(expr: Expr, substring: Expr) = BooleanExpr("str_contains", expr, substring) - /** - * @return A new [Expr] representing the strContains operation. - */ + /** @return A new [Expr] representing the strContains operation. */ @JvmStatic fun strContains(expr: Expr, substring: String) = BooleanExpr("str_contains", expr, substring) - /** - * @return A new [Expr] representing the strContains operation. - */ + /** @return A new [Expr] representing the strContains operation. */ @JvmStatic fun strContains(fieldName: String, substring: Expr) = BooleanExpr("str_contains", fieldName, substring) - /** - * @return A new [Expr] representing the strContains operation. - */ + /** @return A new [Expr] representing the strContains operation. */ @JvmStatic fun strContains(fieldName: String, substring: String) = BooleanExpr("str_contains", fieldName, substring) - /** - * @return A new [Expr] representing the startsWith operation. - */ + /** @return A new [Expr] representing the startsWith operation. */ @JvmStatic fun startsWith(expr: Expr, prefix: Expr) = BooleanExpr("starts_with", expr, prefix) - /** - * @return A new [Expr] representing the startsWith operation. - */ + /** @return A new [Expr] representing the startsWith operation. */ @JvmStatic fun startsWith(expr: Expr, prefix: String) = BooleanExpr("starts_with", expr, prefix) - /** - * @return A new [Expr] representing the startsWith operation. - */ + /** @return A new [Expr] representing the startsWith operation. */ @JvmStatic fun startsWith(fieldName: String, prefix: Expr) = BooleanExpr("starts_with", fieldName, prefix) - /** - * @return A new [Expr] representing the startsWith operation. - */ + /** @return A new [Expr] representing the startsWith operation. */ @JvmStatic fun startsWith(fieldName: String, prefix: String) = BooleanExpr("starts_with", fieldName, prefix) - /** - * @return A new [Expr] representing the endsWith operation. - */ + /** @return A new [Expr] representing the endsWith operation. */ @JvmStatic fun endsWith(expr: Expr, suffix: Expr) = BooleanExpr("ends_with", expr, suffix) - /** - * @return A new [Expr] representing the endsWith operation. - */ + /** @return A new [Expr] representing the endsWith operation. */ @JvmStatic fun endsWith(expr: Expr, suffix: String) = BooleanExpr("ends_with", expr, suffix) - /** - * @return A new [Expr] representing the endsWith operation. - */ + /** @return A new [Expr] representing the endsWith operation. */ @JvmStatic fun endsWith(fieldName: String, suffix: Expr) = BooleanExpr("ends_with", fieldName, suffix) - /** - * @return A new [Expr] representing the endsWith operation. - */ + /** @return A new [Expr] representing the endsWith operation. */ @JvmStatic fun endsWith(fieldName: String, suffix: String) = BooleanExpr("ends_with", fieldName, suffix) - /** - * @return A new [Expr] representing the toLower operation. - */ + /** @return A new [Expr] representing the toLower operation. */ @JvmStatic fun toLower(expr: Expr): Expr = FunctionExpr("to_lower", expr) - /** - * @return A new [Expr] representing the toLower operation. - */ + /** @return A new [Expr] representing the toLower operation. */ @JvmStatic fun toLower( fieldName: String, ): Expr = FunctionExpr("to_lower", fieldName) - /** - * @return A new [Expr] representing the toUpper operation. - */ + /** @return A new [Expr] representing the toUpper operation. */ @JvmStatic fun toUpper(expr: Expr): Expr = FunctionExpr("to_upper", expr) - /** - * @return A new [Expr] representing the toUpper operation. - */ + /** @return A new [Expr] representing the toUpper operation. */ @JvmStatic fun toUpper( fieldName: String, ): Expr = FunctionExpr("to_upper", fieldName) - /** - * @return A new [Expr] representing the trim operation. - */ + /** @return A new [Expr] representing the trim operation. */ @JvmStatic fun trim(expr: Expr): Expr = FunctionExpr("trim", expr) - /** - * @return A new [Expr] representing the trim operation. - */ + /** @return A new [Expr] representing the trim operation. */ @JvmStatic fun trim(fieldName: String): Expr = FunctionExpr("trim", fieldName) - /** - * @return A new [Expr] representing the strConcat operation. - */ + /** @return A new [Expr] representing the strConcat operation. */ @JvmStatic fun strConcat(first: Expr, vararg rest: Expr): Expr = FunctionExpr("str_concat", first, *rest) - /** - * @return A new [Expr] representing the strConcat operation. - */ + /** @return A new [Expr] representing the strConcat operation. */ @JvmStatic fun strConcat(first: Expr, vararg rest: Any): Expr = FunctionExpr("str_concat", first, *rest) - /** - * @return A new [Expr] representing the strConcat operation. - */ + /** @return A new [Expr] representing the strConcat operation. */ @JvmStatic fun strConcat(fieldName: String, vararg rest: Expr): Expr = FunctionExpr("str_concat", fieldName, *rest) - /** - * @return A new [Expr] representing the strConcat operation. - */ + /** @return A new [Expr] representing the strConcat operation. */ @JvmStatic fun strConcat(fieldName: String, vararg rest: Any): Expr = FunctionExpr("str_concat", fieldName, *rest) internal fun map(elements: Array): Expr = FunctionExpr("map", elements) - /** - * @return A new [Expr] representing the map operation. - */ + /** @return A new [Expr] representing the map operation. */ @JvmStatic fun map(elements: Map) = map(elements.flatMap { listOf(constant(it.key), toExprOrConstant(it.value)) }.toTypedArray()) - /** - * @return A new [Expr] representing the mapGet operation. - */ + /** @return A new [Expr] representing the mapGet operation. */ @JvmStatic fun mapGet(map: Expr, key: Expr): Expr = FunctionExpr("map_get", map, key) - /** - * @return A new [Expr] representing the mapGet operation. - */ + /** @return A new [Expr] representing the mapGet operation. */ @JvmStatic fun mapGet(map: Expr, key: String): Expr = FunctionExpr("map_get", map, key) - /** - * @return A new [Expr] representing the mapGet operation. - */ + /** @return A new [Expr] representing the mapGet operation. */ @JvmStatic fun mapGet(fieldName: String, key: Expr): Expr = FunctionExpr("map_get", fieldName, key) - /** - * @return A new [Expr] representing the mapGet operation. - */ + /** @return A new [Expr] representing the mapGet operation. */ @JvmStatic fun mapGet(fieldName: String, key: String): Expr = FunctionExpr("map_get", fieldName, key) - /** - * @return A new [Expr] representing the mapMerge operation. - */ + /** @return A new [Expr] representing the mapMerge operation. */ @JvmStatic fun mapMerge(firstMap: Expr, secondMap: Expr, vararg otherMaps: Expr): Expr = FunctionExpr("map_merge", firstMap, secondMap, otherMaps) - /** - * @return A new [Expr] representing the mapMerge operation. - */ + /** @return A new [Expr] representing the mapMerge operation. */ @JvmStatic fun mapMerge(mapField: String, secondMap: Expr, vararg otherMaps: Expr): Expr = FunctionExpr("map_merge", mapField, secondMap, otherMaps) - /** - * @return A new [Expr] representing the mapRemove operation. - */ + /** @return A new [Expr] representing the mapRemove operation. */ @JvmStatic fun mapRemove(firstMap: Expr, key: Expr): Expr = FunctionExpr("map_remove", firstMap, key) - /** - * @return A new [Expr] representing the mapRemove operation. - */ + /** @return A new [Expr] representing the mapRemove operation. */ @JvmStatic fun mapRemove(mapField: String, key: Expr): Expr = FunctionExpr("map_remove", mapField, key) - /** - * @return A new [Expr] representing the mapRemove operation. - */ + /** @return A new [Expr] representing the mapRemove operation. */ @JvmStatic fun mapRemove(firstMap: Expr, key: String): Expr = FunctionExpr("map_remove", firstMap, key) - /** - * @return A new [Expr] representing the mapRemove operation. - */ + /** @return A new [Expr] representing the mapRemove operation. */ @JvmStatic fun mapRemove(mapField: String, key: String): Expr = FunctionExpr("map_remove", mapField, key) - /** - * @return A new [Expr] representing the cosineDistance operation. - */ + /** @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("cosine_distance", vector1, vector2) - /** - * @return A new [Expr] representing the cosineDistance operation. - */ + /** @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("cosine_distance", vector1, vector(vector2)) - /** - * @return A new [Expr] representing the cosineDistance operation. - */ + /** @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("cosine_distance", vector1, vector2) - /** - * @return A new [Expr] representing the cosineDistance operation. - */ + /** @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(fieldName: String, vector: Expr): Expr = FunctionExpr("cosine_distance", fieldName, vector) - /** - * @return A new [Expr] representing the cosineDistance operation. - */ + /** @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("cosine_distance", fieldName, vector(vector)) - /** - * @return A new [Expr] representing the cosineDistance operation. - */ + /** @return A new [Expr] representing the cosineDistance operation. */ @JvmStatic fun cosineDistance(fieldName: String, vector: VectorValue): Expr = FunctionExpr("cosine_distance", fieldName, vector) - /** - * @return A new [Expr] representing the dotProduct operation. - */ + /** @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(vector1: Expr, vector2: Expr): Expr = FunctionExpr("dot_product", vector1, vector2) - /** - * @return A new [Expr] representing the dotProduct operation. - */ + /** @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("dot_product", vector1, vector(vector2)) - /** - * @return A new [Expr] representing the dotProduct operation. - */ + /** @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("dot_product", vector1, vector2) - /** - * @return A new [Expr] representing the dotProduct operation. - */ + /** @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(fieldName: String, vector: Expr): Expr = FunctionExpr("dot_product", fieldName, vector) - /** - * @return A new [Expr] representing the dotProduct operation. - */ + /** @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("dot_product", fieldName, vector(vector)) - /** - * @return A new [Expr] representing the dotProduct operation. - */ + /** @return A new [Expr] representing the dotProduct operation. */ @JvmStatic fun dotProduct(fieldName: String, vector: VectorValue): Expr = FunctionExpr("dot_product", fieldName, vector) - /** - * @return A new [Expr] representing the euclideanDistance operation. - */ + /** @return A new [Expr] representing the euclideanDistance operation. */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("euclidean_distance", vector1, vector2) - /** - * @return A new [Expr] representing the euclideanDistance operation. - */ + /** @return A new [Expr] representing the euclideanDistance operation. */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("euclidean_distance", vector1, vector(vector2)) - /** - * @return A new [Expr] representing the not operation. - */ + /** @return A new [Expr] representing the not operation. */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("euclidean_distance", vector1, vector2) - /** - * @return A new [Expr] representing the euclideanDistance operation. - */ + /** @return A new [Expr] representing the euclideanDistance operation. */ @JvmStatic fun euclideanDistance(fieldName: String, vector: Expr): Expr = FunctionExpr("euclidean_distance", fieldName, vector) - /** - * @return A new [Expr] representing the euclideanDistance operation. - */ + /** @return A new [Expr] representing the euclideanDistance operation. */ @JvmStatic fun euclideanDistance(fieldName: String, vector: DoubleArray): Expr = FunctionExpr("euclidean_distance", fieldName, vector(vector)) - /** - * @return A new [Expr] representing the euclideanDistance operation. - */ + /** @return A new [Expr] representing the euclideanDistance operation. */ @JvmStatic fun euclideanDistance(fieldName: String, vector: VectorValue): Expr = FunctionExpr("euclidean_distance", fieldName, vector) - /** - * @return A new [Expr] representing the vectorLength operation. - */ + /** @return A new [Expr] representing the vectorLength operation. */ @JvmStatic fun vectorLength(vector: Expr): Expr = FunctionExpr("vector_length", vector) - /** - * @return A new [Expr] representing the vectorLength operation. - */ + /** @return A new [Expr] representing the vectorLength operation. */ @JvmStatic fun vectorLength(fieldName: String): Expr = FunctionExpr("vector_length", fieldName) - /** - * @return A new [Expr] representing the unixMicrosToTimestamp operation. - */ + /** @return A new [Expr] representing the unixMicrosToTimestamp operation. */ @JvmStatic fun unixMicrosToTimestamp(input: Expr): Expr = FunctionExpr("unix_micros_to_timestamp", input) - /** - * @return A new [Expr] representing the unixMicrosToTimestamp operation. - */ + /** @return A new [Expr] representing the unixMicrosToTimestamp operation. */ @JvmStatic fun unixMicrosToTimestamp(fieldName: String): Expr = FunctionExpr("unix_micros_to_timestamp", fieldName) - /** - * @return A new [Expr] representing the timestampToUnixMicros operation. - */ + /** @return A new [Expr] representing the timestampToUnixMicros operation. */ @JvmStatic fun timestampToUnixMicros(input: Expr): Expr = FunctionExpr("timestamp_to_unix_micros", input) - /** - * @return A new [Expr] representing the timestampToUnixMicros operation. - */ + /** @return A new [Expr] representing the timestampToUnixMicros operation. */ @JvmStatic fun timestampToUnixMicros(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_micros", fieldName) - /** - * @return A new [Expr] representing the unixMillisToTimestamp operation. - */ + /** @return A new [Expr] representing the unixMillisToTimestamp operation. */ @JvmStatic fun unixMillisToTimestamp(input: Expr): Expr = FunctionExpr("unix_millis_to_timestamp", input) - /** - * @return A new [Expr] representing the unixMillisToTimestamp operation. - */ + /** @return A new [Expr] representing the unixMillisToTimestamp operation. */ @JvmStatic fun unixMillisToTimestamp(fieldName: String): Expr = FunctionExpr("unix_millis_to_timestamp", fieldName) - /** - * @return A new [Expr] representing the timestampToUnixMillis operation. - */ + /** @return A new [Expr] representing the timestampToUnixMillis operation. */ @JvmStatic fun timestampToUnixMillis(input: Expr): Expr = FunctionExpr("timestamp_to_unix_millis", input) - /** - * @return A new [Expr] representing the timestampToUnixMillis operation. - */ + /** @return A new [Expr] representing the timestampToUnixMillis operation. */ @JvmStatic fun timestampToUnixMillis(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_millis", fieldName) - /** - * @return A new [Expr] representing the unixSecondsToTimestamp operation. - */ + /** @return A new [Expr] representing the unixSecondsToTimestamp operation. */ @JvmStatic fun unixSecondsToTimestamp(input: Expr): Expr = FunctionExpr("unix_seconds_to_timestamp", input) - /** - * @return A new [Expr] representing the unixSecondsToTimestamp operation. - */ + /** @return A new [Expr] representing the unixSecondsToTimestamp operation. */ @JvmStatic fun unixSecondsToTimestamp(fieldName: String): Expr = FunctionExpr("unix_seconds_to_timestamp", fieldName) - /** - * @return A new [Expr] representing the timestampToUnixSeconds operation. - */ + /** @return A new [Expr] representing the timestampToUnixSeconds operation. */ @JvmStatic fun timestampToUnixSeconds(input: Expr): Expr = FunctionExpr("timestamp_to_unix_seconds", input) - /** - * @return A new [Expr] representing the timestampToUnixSeconds operation. - */ + /** @return A new [Expr] representing the timestampToUnixSeconds operation. */ @JvmStatic fun timestampToUnixSeconds(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_seconds", fieldName) - /** - * @return A new [Expr] representing the timestampAdd operation. - */ + /** @return A new [Expr] representing the timestampAdd operation. */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) - /** - * @return A new [Expr] representing the timestampAdd operation. - */ + /** @return A new [Expr] representing the timestampAdd operation. */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) - /** - * @return A new [Expr] representing the timestampAdd operation. - */ + /** @return A new [Expr] representing the timestampAdd operation. */ @JvmStatic fun timestampAdd(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) - /** - * @return A new [Expr] representing the timestampAdd operation. - */ + /** @return A new [Expr] representing the timestampAdd operation. */ @JvmStatic fun timestampAdd(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) - /** - * @return A new [Expr] representing the timestampSub operation. - */ + /** @return A new [Expr] representing the timestampSub operation. */ @JvmStatic fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) - /** - * @return A new [Expr] representing the timestampSub operation. - */ + /** @return A new [Expr] representing the timestampSub operation. */ @JvmStatic fun timestampSub(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) - /** - * @return A new [Expr] representing the timestampSub operation. - */ + /** @return A new [Expr] representing the timestampSub operation. */ @JvmStatic fun timestampSub(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) - /** - * @return A new [Expr] representing the timestampSub operation. - */ + /** @return A new [Expr] representing the timestampSub operation. */ @JvmStatic fun timestampSub(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) - /** - * @return A new [Expr] representing the eq operation. - */ + /** @return A new [Expr] representing the eq operation. */ @JvmStatic fun eq(left: Expr, right: Expr) = BooleanExpr("eq", left, right) - /** - * @return A new [Expr] representing the eq operation. - */ + /** @return A new [Expr] representing the eq operation. */ @JvmStatic fun eq(left: Expr, right: Any) = BooleanExpr("eq", left, right) - /** - * @return A new [Expr] representing the eq operation. - */ + /** @return A new [Expr] representing the eq operation. */ @JvmStatic fun eq(fieldName: String, right: Expr) = BooleanExpr("eq", fieldName, right) - /** - * @return A new [Expr] representing the eq operation. - */ + /** @return A new [Expr] representing the eq operation. */ @JvmStatic fun eq(fieldName: String, right: Any) = BooleanExpr("eq", fieldName, right) - /** - * @return A new [Expr] representing the neq operation. - */ + /** @return A new [Expr] representing the neq operation. */ @JvmStatic fun neq(left: Expr, right: Expr) = BooleanExpr("neq", left, right) - /** - * @return A new [Expr] representing the neq operation. - */ + /** @return A new [Expr] representing the neq operation. */ @JvmStatic fun neq(left: Expr, right: Any) = BooleanExpr("neq", left, right) - /** - * @return A new [Expr] representing the neq operation. - */ + /** @return A new [Expr] representing the neq operation. */ @JvmStatic fun neq(fieldName: String, right: Expr) = BooleanExpr("neq", fieldName, right) - /** - * @return A new [Expr] representing the neq operation. - */ + /** @return A new [Expr] representing the neq operation. */ @JvmStatic fun neq(fieldName: String, right: Any) = BooleanExpr("neq", fieldName, right) - /** - * @return A new [Expr] representing the gt operation. - */ + /** @return A new [Expr] representing the gt operation. */ @JvmStatic fun gt(left: Expr, right: Expr) = BooleanExpr("gt", left, right) - /** - * @return A new [Expr] representing the gt operation. - */ + /** @return A new [Expr] representing the gt operation. */ @JvmStatic fun gt(left: Expr, right: Any) = BooleanExpr("gt", left, right) - /** - * @return A new [Expr] representing the gt operation. - */ + /** @return A new [Expr] representing the gt operation. */ @JvmStatic fun gt(fieldName: String, right: Expr) = BooleanExpr("gt", fieldName, right) - /** - * @return A new [Expr] representing the gt operation. - */ + /** @return A new [Expr] representing the gt operation. */ @JvmStatic fun gt(fieldName: String, right: Any) = BooleanExpr("gt", fieldName, right) - /** - * @return A new [Expr] representing the gte operation. - */ + /** @return A new [Expr] representing the gte operation. */ @JvmStatic fun gte(left: Expr, right: Expr) = BooleanExpr("gte", left, right) - /** - * @return A new [Expr] representing the gte operation. - */ + /** @return A new [Expr] representing the gte operation. */ @JvmStatic fun gte(left: Expr, right: Any) = BooleanExpr("gte", left, right) - /** - * @return A new [Expr] representing the gte operation. - */ + /** @return A new [Expr] representing the gte operation. */ @JvmStatic fun gte(fieldName: String, right: Expr) = BooleanExpr("gte", fieldName, right) - /** - * @return A new [Expr] representing the gte operation. - */ + /** @return A new [Expr] representing the gte operation. */ @JvmStatic fun gte(fieldName: String, right: Any) = BooleanExpr("gte", fieldName, right) - /** - * @return A new [Expr] representing the lt operation. - */ + /** @return A new [Expr] representing the lt operation. */ @JvmStatic fun lt(left: Expr, right: Expr) = BooleanExpr("lt", left, right) - /** - * @return A new [Expr] representing the lt operation. - */ + /** @return A new [Expr] representing the lt operation. */ @JvmStatic fun lt(left: Expr, right: Any) = BooleanExpr("lt", left, right) - /** - * @return A new [Expr] representing the lt operation. - */ + /** @return A new [Expr] representing the lt operation. */ @JvmStatic fun lt(fieldName: String, right: Expr) = BooleanExpr("lt", fieldName, right) - /** - * @return A new [Expr] representing the lt operation. - */ + /** @return A new [Expr] representing the lt operation. */ @JvmStatic fun lt(fieldName: String, right: Any) = BooleanExpr("lt", fieldName, right) - /** - * @return A new [Expr] representing the lte operation. - */ + /** @return A new [Expr] representing the lte operation. */ @JvmStatic fun lte(left: Expr, right: Expr) = BooleanExpr("lte", left, right) - /** - * @return A new [Expr] representing the lte operation. - */ + /** @return A new [Expr] representing the lte operation. */ @JvmStatic fun lte(left: Expr, right: Any) = BooleanExpr("lte", left, right) - /** - * @return A new [Expr] representing the lte operation. - */ + /** @return A new [Expr] representing the lte operation. */ @JvmStatic fun lte(fieldName: String, right: Expr) = BooleanExpr("lte", fieldName, right) - /** - * @return A new [Expr] representing the lte operation. - */ + /** @return A new [Expr] representing the lte operation. */ @JvmStatic fun lte(fieldName: String, right: Any) = BooleanExpr("lte", fieldName, right) /** * Creates an expression that concatenates an array with other arrays. * - * - * + * @param firstArray The first array expression to concatenate to. + * @param secondArray An expression that evaluates to array to concatenate. + * @param otherArrays Optional additional array expressions or array literals to concatenate. * @return A new [Expr] representing the arrayConcat operation. */ @JvmStatic @@ -1450,105 +1091,210 @@ abstract class Expr internal constructor() { /** * Creates an expression that concatenates an array with other arrays. * + * @param firstArray The first array expression to concatenate to. + * @param secondArray An array expression or array literal to concatenate. + * @param otherArrays Optional additional array expressions or array literals to concatenate. * @return A new [Expr] representing the arrayConcat operation. */ @JvmStatic - fun arrayConcat(firstArray: Expr, vararg otherArrays: Any): Expr = - FunctionExpr("array_concat", firstArray, *otherArrays) + fun arrayConcat(firstArray: Expr, secondArray: Any, vararg otherArrays: Any): Expr = + FunctionExpr("array_concat", firstArray, secondArray, *otherArrays) /** * Creates an expression that concatenates a field's array value with other arrays. * + * @param firstArrayField The name of field that contains first array to concatenate to. + * @param secondArray An expression that evaluates to array to concatenate. + * @param otherArrays Optional additional array expressions or array literals to concatenate. * @return A new [Expr] representing the arrayConcat operation. */ @JvmStatic - fun arrayConcat(fieldName: String, secondArray: Expr, vararg otherArrays: Any): Expr = - FunctionExpr("array_concat", fieldName, secondArray, *otherArrays) + fun arrayConcat(firstArrayField: String, secondArray: Expr, vararg otherArrays: Any): Expr = + FunctionExpr("array_concat", firstArrayField, secondArray, *otherArrays) /** * Creates an expression that concatenates a field's array value with other arrays. * + * @param firstArrayField The name of field that contains first array to concatenate to. + * @param secondArray An array expression or array literal to concatenate. + * @param otherArrays Optional additional array expressions or array literals to concatenate. * @return A new [Expr] representing the arrayConcat operation. */ @JvmStatic - fun arrayConcat(fieldName: String, vararg otherArrays: Any): Expr = - FunctionExpr("array_concat", fieldName, *otherArrays) + fun arrayConcat(firstArrayField: String, secondArray: Any, vararg otherArrays: Any): Expr = + FunctionExpr("array_concat", firstArrayField, secondArray, *otherArrays) /** + * @return A new [Expr] representing the arrayReverse operation. */ @JvmStatic fun arrayReverse(array: Expr): Expr = FunctionExpr("array_reverse", array) /** + * @return A new [Expr] representing the arrayReverse operation. */ @JvmStatic fun arrayReverse(fieldName: String): Expr = FunctionExpr("array_reverse", fieldName) /** + * Creates an expression that checks if the array contains a specific [element]. + * + * @param array The array expression to check. + * @param element The element to search for in the array. + * @return A new [BooleanExpr] representing the arrayContains operation. */ @JvmStatic - fun arrayContains(array: Expr, value: Expr) = BooleanExpr("array_contains", array, value) + fun arrayContains(array: Expr, element: Expr) = BooleanExpr("array_contains", array, element) /** + * Creates an expression that checks if the array field contains a specific [element]. + * + * @param arrayFieldName The name of field that contains array to check. + * @param element The element to search for in the array. + * @return A new [BooleanExpr] representing the arrayContains operation. */ @JvmStatic - fun arrayContains(fieldName: String, value: Expr) = - BooleanExpr("array_contains", fieldName, value) + fun arrayContains(arrayFieldName: String, element: Expr) = + BooleanExpr("array_contains", arrayFieldName, element) /** + * Creates an expression that checks if the [array] contains a specific [element]. + * + * @param array The array expression to check. + * @param element The element to search for in the array. + * @return A new [BooleanExpr] representing the arrayContains operation. */ @JvmStatic - fun arrayContains(array: Expr, value: Any) = BooleanExpr("array_contains", array, value) + fun arrayContains(array: Expr, element: Any) = BooleanExpr("array_contains", array, element) /** + * Creates an expression that checks if the array field contains a specific [element]. + * + * @param arrayFieldName The name of field that contains array to check. + * @param element The element to search for in the array. + * @return A new [BooleanExpr] representing the arrayContains operation. */ @JvmStatic - fun arrayContains(fieldName: String, value: Any) = - BooleanExpr("array_contains", fieldName, value) + fun arrayContains(arrayFieldName: String, element: Any) = + BooleanExpr("array_contains", arrayFieldName, element) /** + * Creates an expression that checks if [array] contains all the specified [values]. + * + * @param array The array expression to check. + * @param values The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAll operation. */ @JvmStatic - fun arrayContainsAll(array: Expr, values: List) = - BooleanExpr("array_contains_all", array, ListOfExprs(toArrayOfExprOrConstant(values))) + fun arrayContainsAll(array: Expr, values: List) = arrayContainsAll(array, ListOfExprs(toArrayOfExprOrConstant(values))) /** + * Creates an expression that checks if [array] contains all elements of [arrayExpression]. + * + * @param array The array expression to check. + * @param arrayExpression The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAll operation. */ @JvmStatic - fun arrayContainsAll(fieldName: String, values: List) = - BooleanExpr("array_contains_all", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + fun arrayContainsAll(array: Expr, arrayExpression: Expr) = + BooleanExpr("array_contains_all", array, arrayExpression) /** + * Creates an expression that checks if array field contains all the specified [values]. + * + * @param arrayFieldName The name of field that contains array to check. + * @param values The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAll operation. + */ + @JvmStatic + fun arrayContainsAll(arrayFieldName: String, values: List) = + BooleanExpr("array_contains_all", arrayFieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + + /** + * Creates an expression that checks if array field contains all elements of [arrayExpression]. + * + * @param arrayFieldName The name of field that contains array to check. + * @param arrayExpression The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAll operation. + */ + @JvmStatic + fun arrayContainsAll(arrayFieldName: String, arrayExpression: Expr) = + BooleanExpr("array_contains_all", arrayFieldName, arrayExpression) + + /** + * Creates an expression that checks if [array] contains any of the specified [values]. + * + * @param array The array expression to check. + * @param values The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAny operation. */ @JvmStatic fun arrayContainsAny(array: Expr, values: List) = BooleanExpr("array_contains_any", array, ListOfExprs(toArrayOfExprOrConstant(values))) /** + * Creates an expression that checks if [array] contains any elements of [arrayExpression]. + * + * @param array The array expression to check. + * @param arrayExpression The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAny operation. + */ + @JvmStatic + fun arrayContainsAny(array: Expr, arrayExpression: Expr) = + BooleanExpr("array_contains_any", array, arrayExpression) + + /** + * Creates an expression that checks if array field contains any of the specified [values]. + * + * @param arrayFieldName The name of field that contains array to check. + * @param values The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAny operation. */ @JvmStatic - fun arrayContainsAny(fieldName: String, values: List) = - BooleanExpr("array_contains_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + fun arrayContainsAny(arrayFieldName: String, values: List) = + BooleanExpr("array_contains_any", arrayFieldName, ListOfExprs(toArrayOfExprOrConstant(values))) /** + * Creates an expression that checks if array field contains any elements of [arrayExpression]. + * + * @param arrayFieldName The name of field that contains array to check. + * @param arrayExpression The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAny operation. + */ + @JvmStatic + fun arrayContainsAny(arrayFieldName: String, arrayExpression: Expr) = + BooleanExpr("array_contains_any", arrayFieldName, arrayExpression) + + /** + * Creates an expression that calculates the length of an [array] expression. + * + * @param array The array expression to calculate the length of. + * @return A new [Expr] representing the the length of the array. */ @JvmStatic fun arrayLength(array: Expr): Expr = FunctionExpr("array_length", array) /** + * Creates an expression that calculates the length of an array field. + * + * @param arrayFieldName The name of the field containing an array to calculate the length of. + * @return A new [Expr] representing the the length of the array. */ - @JvmStatic fun arrayLength(fieldName: String): Expr = FunctionExpr("array_length", fieldName) + @JvmStatic fun arrayLength(arrayFieldName: String): Expr = FunctionExpr("array_length", arrayFieldName) /** + * @return A new [Expr] representing the cond operation. */ @JvmStatic fun cond(condition: BooleanExpr, then: Expr, otherwise: Expr): Expr = FunctionExpr("cond", condition, then, otherwise) /** + * @return A new [Expr] representing the cond operation. */ @JvmStatic fun cond(condition: BooleanExpr, then: Any, otherwise: Any): Expr = FunctionExpr("cond", condition, then, otherwise) /** + * @return A new [Expr] representing the exists operation. */ @JvmStatic fun exists(expr: Expr) = BooleanExpr("exists", expr) } @@ -1903,38 +1649,79 @@ abstract class Expr internal constructor() { /** * Creates an expression that concatenates a field's array value with other arrays. * + * @param secondArray An expression that evaluates to array to concatenate. + * @param otherArrays Optional additional array expressions or array literals to concatenate. * @return A new [Expr] representing the arrayConcat operation. */ - fun arrayConcat(vararg otherArrays: Expr) = Companion.arrayConcat(this, *otherArrays) + fun arrayConcat(secondArray: Expr, vararg otherArrays: Any) = + Companion.arrayConcat(this, secondArray, *otherArrays) /** * Creates an expression that concatenates a field's array value with other arrays. * + * @param secondArray An array expression or array literal to concatenate. + * @param otherArrays Optional additional array expressions or array literals to concatenate. * @return A new [Expr] representing the arrayConcat operation. */ - fun arrayConcat(arrays: List) = Companion.arrayConcat(this, arrays) + fun arrayConcat(secondArray: Any, vararg otherArrays: Any) = + Companion.arrayConcat(this, secondArray, *otherArrays) /** */ fun arrayReverse() = Companion.arrayReverse(this) /** + * Creates an expression that checks if array contains a specific [element]. + * + * @param element The element to search for in the array. + * @return A new [BooleanExpr] representing the arrayContains operation. */ - fun arrayContains(value: Expr) = Companion.arrayContains(this, value) + fun arrayContains(element: Expr): BooleanExpr = Companion.arrayContains(this, element) /** + * Creates an expression that checks if array contains a specific [element]. + * + * @param element The element to search for in the array. + * @return A new [BooleanExpr] representing the arrayContains operation. + */ + fun arrayContains(element: Any): BooleanExpr = Companion.arrayContains(this, element) + + /** + * Creates an expression that checks if array contains all the specified [values]. + * + * @param values The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAll operation. + */ + fun arrayContainsAll(values: List): BooleanExpr = Companion.arrayContainsAll(this, values) + + /** + * Creates an expression that checks if array contains all elements of [arrayExpression]. + * + * @param arrayExpression The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAll operation. */ - fun arrayContains(value: Any) = Companion.arrayContains(this, value) + fun arrayContainsAll(arrayExpression: Expr): BooleanExpr = Companion.arrayContainsAll(this, arrayExpression) /** + * Creates an expression that checks if array contains any of the specified [values]. + * + * @param values The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAny operation. */ - fun arrayContainsAll(values: List) = Companion.arrayContainsAll(this, values) + fun arrayContainsAny(values: List): BooleanExpr = Companion.arrayContainsAny(this, values) /** + * Creates an expression that checks if array contains any elements of [arrayExpression]. + * + * @param arrayExpression The elements to check for in the array. + * @return A new [BooleanExpr] representing the arrayContainsAny operation. */ - fun arrayContainsAny(values: List) = Companion.arrayContainsAny(this, values) + fun arrayContainsAny(arrayExpression: Expr): BooleanExpr = Companion.arrayContainsAny(this, arrayExpression) /** + * Creates an expression that calculates the length of an array expression. + * + * @return A new [Expr] representing the the length of the array. */ fun arrayLength() = Companion.arrayLength(this) From 309fa919cb4bb993b86e34ce32be3d62a8ec2d29 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 25 Apr 2025 16:51:24 -0400 Subject: [PATCH 048/152] WIP --- .../firebase/firestore/pipeline/aggregates.kt | 7 ++- .../firestore/pipeline/expressions.kt | 53 +++++++++++-------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt index bdc5707388b..60bdbdab8eb 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt @@ -22,7 +22,11 @@ internal constructor(internal val alias: String, internal val expr: AggregateFun /** A class that represents an aggregate function. */ class AggregateFunction -private constructor(private val name: String, private val params: Array) { +private constructor( + private val name: String, + private val params: Array, + private val options: InternalOptions = InternalOptions.EMPTY +) { private constructor(name: String) : this(name, emptyArray()) private constructor(name: String, expr: Expr) : this(name, arrayOf(expr)) private constructor(name: String, fieldName: String) : this(name, Expr.field(fieldName)) @@ -71,6 +75,7 @@ private constructor(private val name: String, private val params: Array) = arrayContainsAll(array, ListOfExprs(toArrayOfExprOrConstant(values))) + fun arrayContainsAll(array: Expr, values: List) = + arrayContainsAll(array, ListOfExprs(toArrayOfExprOrConstant(values))) /** * Creates an expression that checks if [array] contains all elements of [arrayExpression]. @@ -1206,7 +1203,11 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAll(arrayFieldName: String, values: List) = - BooleanExpr("array_contains_all", arrayFieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + BooleanExpr( + "array_contains_all", + arrayFieldName, + ListOfExprs(toArrayOfExprOrConstant(values)) + ) /** * Creates an expression that checks if array field contains all elements of [arrayExpression]. @@ -1250,7 +1251,11 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAny(arrayFieldName: String, values: List) = - BooleanExpr("array_contains_any", arrayFieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + BooleanExpr( + "array_contains_any", + arrayFieldName, + ListOfExprs(toArrayOfExprOrConstant(values)) + ) /** * Creates an expression that checks if array field contains any elements of [arrayExpression]. @@ -1277,25 +1282,20 @@ abstract class Expr internal constructor() { * @param arrayFieldName The name of the field containing an array to calculate the length of. * @return A new [Expr] representing the the length of the array. */ - @JvmStatic fun arrayLength(arrayFieldName: String): Expr = FunctionExpr("array_length", arrayFieldName) + @JvmStatic + fun arrayLength(arrayFieldName: String): Expr = FunctionExpr("array_length", arrayFieldName) - /** - * @return A new [Expr] representing the cond operation. - */ + /** @return A new [Expr] representing the cond operation. */ @JvmStatic fun cond(condition: BooleanExpr, then: Expr, otherwise: Expr): Expr = FunctionExpr("cond", condition, then, otherwise) - /** - * @return A new [Expr] representing the cond operation. - */ + /** @return A new [Expr] representing the cond operation. */ @JvmStatic fun cond(condition: BooleanExpr, then: Any, otherwise: Any): Expr = FunctionExpr("cond", condition, then, otherwise) - /** - * @return A new [Expr] representing the exists operation. - */ + /** @return A new [Expr] representing the exists operation. */ @JvmStatic fun exists(expr: Expr) = BooleanExpr("exists", expr) } @@ -1700,7 +1700,8 @@ abstract class Expr internal constructor() { * @param arrayExpression The elements to check for in the array. * @return A new [BooleanExpr] representing the arrayContainsAll operation. */ - fun arrayContainsAll(arrayExpression: Expr): BooleanExpr = Companion.arrayContainsAll(this, arrayExpression) + fun arrayContainsAll(arrayExpression: Expr): BooleanExpr = + Companion.arrayContainsAll(this, arrayExpression) /** * Creates an expression that checks if array contains any of the specified [values]. @@ -1716,7 +1717,8 @@ abstract class Expr internal constructor() { * @param arrayExpression The elements to check for in the array. * @return A new [BooleanExpr] representing the arrayContainsAny operation. */ - fun arrayContainsAny(arrayExpression: Expr): BooleanExpr = Companion.arrayContainsAny(this, arrayExpression) + fun arrayContainsAny(arrayExpression: Expr): BooleanExpr = + Companion.arrayContainsAny(this, arrayExpression) /** * Creates an expression that calculates the length of an array expression. @@ -1872,7 +1874,11 @@ internal class ListOfExprs(private val expressions: Array) : Expr() { * [FunctionExpr] instances. */ open class FunctionExpr -internal constructor(private val name: String, private val params: Array) : Expr() { +internal constructor( + private val name: String, + private val params: Array, + private val options: InternalOptions = InternalOptions.EMPTY +) : Expr() { internal constructor( name: String, param: Expr, @@ -1896,13 +1902,14 @@ internal constructor(private val name: String, private val params: Array) : - FunctionExpr(name, params) { + FunctionExpr(name, params, InternalOptions.EMPTY) { internal constructor( name: String, params: List From 0606a18a3914ed9ea67ae9c5d3cb63e4f3bd71dd Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 28 Apr 2025 22:21:26 -0400 Subject: [PATCH 049/152] Work on pipeline expressions. --- .../google/firebase/firestore/model/Values.kt | 72 +-- .../firestore/pipeline/expressions.kt | 440 +++++++++++++++--- 2 files changed, 396 insertions(+), 116 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index afda3ca32ae..3bcd2ed3c38 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -559,46 +559,31 @@ internal object Values { return VECTOR_VALUE_TYPE == value.mapValue.fieldsMap[TYPE_KEY] } - @JvmStatic - fun encodeValue(value: Long): Value { - return Value.newBuilder().setIntegerValue(value).build() - } + @JvmStatic fun encodeValue(value: Long): Value = Value.newBuilder().setIntegerValue(value).build() @JvmStatic - fun encodeValue(value: Int): Value { - return Value.newBuilder().setIntegerValue(value.toLong()).build() - } + fun encodeValue(value: Int): Value = Value.newBuilder().setIntegerValue(value.toLong()).build() @JvmStatic - fun encodeValue(value: Double): Value { - return Value.newBuilder().setDoubleValue(value).build() - } + fun encodeValue(value: Double): Value = Value.newBuilder().setDoubleValue(value).build() @JvmStatic - fun encodeValue(value: Float): Value { - return Value.newBuilder().setDoubleValue(value.toDouble()).build() - } + fun encodeValue(value: Float): Value = Value.newBuilder().setDoubleValue(value.toDouble()).build() @JvmStatic - fun encodeValue(value: Number): Value { - return when (value) { + fun encodeValue(value: Number): Value = + when (value) { is Long -> encodeValue(value) is Int -> encodeValue(value) is Double -> encodeValue(value) is Float -> encodeValue(value) else -> throw IllegalArgumentException("Unexpected number type: $value") } - } @JvmStatic - fun encodeValue(value: String): Value { - return Value.newBuilder().setStringValue(value).build() - } + fun encodeValue(value: String): Value = Value.newBuilder().setStringValue(value).build() - @JvmStatic - fun encodeValue(date: Date): Value { - return encodeValue(com.google.firebase.Timestamp((date))) - } + @JvmStatic fun encodeValue(date: Date): Value = encodeValue(com.google.firebase.Timestamp((date))) @JvmStatic fun encodeValue(timestamp: com.google.firebase.Timestamp): Value { @@ -615,33 +600,29 @@ internal object Values { } @JvmStatic - fun encodeValue(value: Boolean): Value { - return Value.newBuilder().setBooleanValue(value).build() - } + fun encodeValue(value: Boolean): Value = Value.newBuilder().setBooleanValue(value).build() @JvmStatic - fun encodeValue(geoPoint: GeoPoint): Value { - return Value.newBuilder() + fun encodeValue(geoPoint: GeoPoint): Value = + Value.newBuilder() .setGeoPointValue( LatLng.newBuilder().setLatitude(geoPoint.latitude).setLongitude(geoPoint.longitude) ) .build() - } @JvmStatic - fun encodeValue(value: Blob): Value { - return Value.newBuilder().setBytesValue(value.toByteString()).build() - } + fun encodeValue(value: ByteArray): Value = + Value.newBuilder().setBytesValue(ByteString.copyFrom(value)).build() @JvmStatic - fun encodeValue(docRef: DocumentReference): Value { - return Value.newBuilder().setReferenceValue(docRef.fullPath).build() - } + fun encodeValue(value: Blob): Value = + Value.newBuilder().setBytesValue(value.toByteString()).build() @JvmStatic - fun encodeValue(vector: VectorValue): Value { - return encodeVectorValue(vector.toArray()) - } + fun encodeValue(docRef: DocumentReference): Value = + Value.newBuilder().setReferenceValue(docRef.fullPath).build() + + @JvmStatic fun encodeValue(vector: VectorValue): Value = encodeVectorValue(vector.toArray()) @JvmStatic fun encodeVectorValue(vector: DoubleArray): Value { @@ -659,18 +640,16 @@ internal object Values { } @JvmStatic - fun encodeValue(map: Map): Value { - return Value.newBuilder().setMapValue(MapValue.newBuilder().putAllFields(map)).build() - } + fun encodeValue(map: Map): Value = + Value.newBuilder().setMapValue(MapValue.newBuilder().putAllFields(map)).build() @JvmStatic - fun encodeValue(values: Iterable): Value { - return Value.newBuilder().setArrayValue(ArrayValue.newBuilder().addAllValues(values)).build() - } + fun encodeValue(values: Iterable): Value = + Value.newBuilder().setArrayValue(ArrayValue.newBuilder().addAllValues(values)).build() @JvmStatic - fun encodeAnyValue(value: Any?): Value { - return when (value) { + fun encodeAnyValue(value: Any?): Value = + when (value) { null -> NULL_VALUE is String -> encodeValue(value) is Number -> encodeValue(value) @@ -682,5 +661,4 @@ internal object Values { is VectorValue -> encodeValue(value) else -> throw IllegalArgumentException("Unexpected type: $value") } - } } 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 07188c6a2ef..fbe5ad07a78 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 @@ -73,6 +73,7 @@ abstract class Expr internal constructor() { is GeoPoint -> constant(value) is Blob -> constant(value) is DocumentReference -> constant(value) + is ByteArray -> constant(value) is VectorValue -> constant(value) is Value -> ValueConstant(value) is Map<*, *> -> @@ -177,6 +178,17 @@ abstract class Expr internal constructor() { return ValueConstant(encodeValue(value)) } + /** + * Create a constant for a bytes value. + * + * @param value The bytes value. + * @return A new [Expr] constant instance. + */ + @JvmStatic + fun constant(value: ByteArray): Expr { + return ValueConstant(encodeValue(value)) + } + /** * Create a constant for a [Blob] value. * @@ -316,91 +328,247 @@ abstract class Expr internal constructor() { /** @return A new [Expr] representing the not operation. */ @JvmStatic fun not(condition: BooleanExpr) = BooleanExpr("not", condition) - /** @return A new [Expr] representing the bitAnd operation. */ - @JvmStatic fun bitAnd(left: Expr, right: Expr): Expr = FunctionExpr("bit_and", left, right) + /** + * Creates an expression that applies a bitwise AND operation between two expressions. + * + * @param bits An expression that returns bits when evaluated. + * @param bitsOther An expression that returns bits when evaluated. + * @return A new [Expr] representing the bitwise AND operation. + */ + @JvmStatic + fun bitAnd(bits: Expr, bitsOther: Expr): Expr = FunctionExpr("bit_and", bits, bitsOther) - /** @return A new [Expr] representing the bitAnd operation. */ - @JvmStatic fun bitAnd(left: Expr, right: Any): Expr = FunctionExpr("bit_and", left, right) + /** + * Creates an expression that applies a bitwise AND operation between an expression and a + * constant. + * + * @param bits An expression that returns bits when evaluated. + * @param bitsOther A constant byte array. + * @return A new [Expr] representing the bitwise AND operation. + */ + @JvmStatic + fun bitAnd(bits: Expr, bitsOther: ByteArray): Expr = + FunctionExpr("bit_and", bits, constant(bitsOther)) - /** @return A new [Expr] representing the bitAnd operation. */ + /** + * Creates an expression that applies a bitwise AND operation between an field and an + * expression. + * + * @param bitsFieldName Name of field that contains bits data. + * @param bitsOther An expression that returns bits when evaluated. + * @return A new [Expr] representing the bitwise AND operation. + */ @JvmStatic - fun bitAnd(fieldName: String, right: Expr): Expr = FunctionExpr("bit_and", fieldName, right) + fun bitAnd(bitsFieldName: String, bitsOther: Expr): Expr = + FunctionExpr("bit_and", bitsFieldName, bitsOther) - /** @return A new [Expr] representing the bitAnd operation. */ + /** + * Creates an expression that applies a bitwise AND operation between an field and constant. + * + * @param bitsFieldName Name of field that contains bits data. + * @param bitsOther A constant byte array. + * @return A new [Expr] representing the bitwise AND operation. + */ @JvmStatic - fun bitAnd(fieldName: String, right: Any): Expr = FunctionExpr("bit_and", fieldName, right) + fun bitAnd(bitsFieldName: String, bitsOther: ByteArray): Expr = + FunctionExpr("bit_and", bitsFieldName, constant(bitsOther)) - /** @return A new [Expr] representing the bitOr operation. */ - @JvmStatic fun bitOr(left: Expr, right: Expr): Expr = FunctionExpr("bit_or", left, right) + /** + * Creates an expression that applies a bitwise OR operation between two expressions. + * + * @param bits An expression that returns bits when evaluated. + * @param bitsOther An expression that returns bits when evaluated. + * @return A new [Expr] representing the bitwise OR operation. + */ + @JvmStatic + fun bitOr(bits: Expr, bitsOther: Expr): Expr = FunctionExpr("bit_or", bits, bitsOther) - /** @return A new [Expr] representing the bitOr operation. */ - @JvmStatic fun bitOr(left: Expr, right: Any): Expr = FunctionExpr("bit_or", left, right) + /** + * Creates an expression that applies a bitwise OR operation between an expression and a + * constant. + * + * @param bits An expression that returns bits when evaluated. + * @param bitsOther A constant byte array. + * @return A new [Expr] representing the bitwise OR operation. + */ + @JvmStatic + fun bitOr(bits: Expr, bitsOther: ByteArray): Expr = + FunctionExpr("bit_or", bits, constant(bitsOther)) - /** @return A new [Expr] representing the bitOr operation. */ + /** + * Creates an expression that applies a bitwise OR operation between an field and an expression. + * + * @param bitsFieldName Name of field that contains bits data. + * @param bitsOther An expression that returns bits when evaluated. + * @return A new [Expr] representing the bitwise OR operation. + */ @JvmStatic - fun bitOr(fieldName: String, right: Expr): Expr = FunctionExpr("bit_or", fieldName, right) + fun bitOr(bitsFieldName: String, bitsOther: Expr): Expr = + FunctionExpr("bit_or", bitsFieldName, bitsOther) - /** @return A new [Expr] representing the bitOr operation. */ + /** + * Creates an expression that applies a bitwise OR operation between an field and constant. + * + * @param bitsFieldName Name of field that contains bits data. + * @param bitsOther A constant byte array. + * @return A new [Expr] representing the bitwise OR operation. + */ @JvmStatic - fun bitOr(fieldName: String, right: Any): Expr = FunctionExpr("bit_or", fieldName, right) + fun bitOr(bitsFieldName: String, bitsOther: ByteArray): Expr = + FunctionExpr("bit_or", bitsFieldName, constant(bitsOther)) - /** @return A new [Expr] representing the bitXor operation. */ - @JvmStatic fun bitXor(left: Expr, right: Expr): Expr = FunctionExpr("bit_xor", left, right) + /** + * Creates an expression that applies a bitwise XOR operation between two expressions. + * + * @param bits An expression that returns bits when evaluated. + * @param bitsOther An expression that returns bits when evaluated. + * @return A new [Expr] representing the bitwise XOR operation. + */ + @JvmStatic + fun bitXor(bits: Expr, bitsOther: Expr): Expr = FunctionExpr("bit_xor", bits, bitsOther) - /** @return A new [Expr] representing the bitXor operation. */ - @JvmStatic fun bitXor(left: Expr, right: Any): Expr = FunctionExpr("bit_xor", left, right) + /** + * Creates an expression that applies a bitwise XOR operation between an expression and a + * constant. + * + * @param bits An expression that returns bits when evaluated. + * @param bitsOther A constant byte array. + * @return A new [Expr] representing the bitwise XOR operation. + */ + @JvmStatic + fun bitXor(bits: Expr, bitsOther: ByteArray): Expr = + FunctionExpr("bit_xor", bits, constant(bitsOther)) - /** @return A new [Expr] representing the bitXor operation. */ + /** + * Creates an expression that applies a bitwise XOR operation between an field and an + * expression. + * + * @param bitsFieldName Name of field that contains bits data. + * @param bitsOther An expression that returns bits when evaluated. + * @return A new [Expr] representing the bitwise XOR operation. + */ @JvmStatic - fun bitXor(fieldName: String, right: Expr): Expr = FunctionExpr("bit_xor", fieldName, right) + fun bitXor(bitsFieldName: String, bitsOther: Expr): Expr = + FunctionExpr("bit_xor", bitsFieldName, bitsOther) - /** @return A new [Expr] representing the bitXor operation. */ + /** + * Creates an expression that applies a bitwise XOR operation between an field and constant. + * + * @param bitsFieldName Name of field that contains bits data. + * @param bitsOther A constant byte array. + * @return A new [Expr] representing the bitwise XOR operation. + */ @JvmStatic - fun bitXor(fieldName: String, right: Any): Expr = FunctionExpr("bit_xor", fieldName, right) + fun bitXor(bitsFieldName: String, bitsOther: ByteArray): Expr = + FunctionExpr("bit_xor", bitsFieldName, constant(bitsOther)) - /** @return A new [Expr] representing the bitNot operation. */ - @JvmStatic fun bitNot(left: Expr): Expr = FunctionExpr("bit_not", left) + /** + * Creates an expression that applies a bitwise NOT operation to an expression. + * + * @param bits An expression that returns bits when evaluated. + * @return A new [Expr] representing the bitwise NOT operation. + */ + @JvmStatic fun bitNot(bits: Expr): Expr = FunctionExpr("bit_not", bits) - /** @return A new [Expr] representing the bitNot operation. */ - @JvmStatic fun bitNot(fieldName: String): Expr = FunctionExpr("bit_not", fieldName) + /** + * Creates an expression that applies a bitwise NOT operation to a field. + * + * @param bitsFieldName Name of field that contains bits data. + * @return A new [Expr] representing the bitwise NOT operation. + */ + @JvmStatic fun bitNot(bitsFieldName: String): Expr = FunctionExpr("bit_not", bitsFieldName) - /** @return A new [Expr] representing the bitLeftShift operation. */ + /** + * Creates an expression that applies a bitwise left shift operation between two expressions. + * + * @param bits An expression that returns bits when evaluated. + * @param numberExpr The number of bits to shift. + * @return A new [Expr] representing the bitwise left shift operation. + */ @JvmStatic - fun bitLeftShift(left: Expr, numberExpr: Expr): Expr = - FunctionExpr("bit_left_shift", left, numberExpr) + fun bitLeftShift(bits: Expr, numberExpr: Expr): Expr = + FunctionExpr("bit_left_shift", bits, numberExpr) - /** @return A new [Expr] representing the bitLeftShift operation. */ + /** + * Creates an expression that applies a bitwise left shift operation between an expression and a + * constant. + * + * @param bits An expression that returns bits when evaluated. + * @param number The number of bits to shift. + * @return A new [Expr] representing the bitwise left shift operation. + */ @JvmStatic - fun bitLeftShift(left: Expr, number: Int): Expr = FunctionExpr("bit_left_shift", left, number) + fun bitLeftShift(bits: Expr, number: Int): Expr = FunctionExpr("bit_left_shift", bits, number) - /** @return A new [Expr] representing the bitLeftShift operation. */ + /** + * Creates an expression that applies a bitwise left shift operation between a field and an + * expression. + * + * @param bitsFieldName Name of field that contains bits data. + * @param numberExpr The number of bits to shift. + * @return A new [Expr] representing the bitwise left shift operation. + */ @JvmStatic - fun bitLeftShift(fieldName: String, numberExpr: Expr): Expr = - FunctionExpr("bit_left_shift", fieldName, numberExpr) + fun bitLeftShift(bitsFieldName: String, numberExpr: Expr): Expr = + FunctionExpr("bit_left_shift", bitsFieldName, numberExpr) - /** @return A new [Expr] representing the bitLeftShift operation. */ + /** + * Creates an expression that applies a bitwise left shift operation between a field and a + * constant. + * + * @param bitsFieldName Name of field that contains bits data. + * @param number The number of bits to shift. + * @return A new [Expr] representing the bitwise left shift operation. + */ @JvmStatic - fun bitLeftShift(fieldName: String, number: Int): Expr = - FunctionExpr("bit_left_shift", fieldName, number) + fun bitLeftShift(bitsFieldName: String, number: Int): Expr = + FunctionExpr("bit_left_shift", bitsFieldName, number) - /** @return A new [Expr] representing the bitRightShift operation. */ + /** + * Creates an expression that applies a bitwise right shift operation between two expressions. + * + * @param bits An expression that returns bits when evaluated. + * @param numberExpr The number of bits to shift. + * @return A new [Expr] representing the bitwise right shift operation. + */ @JvmStatic - fun bitRightShift(left: Expr, numberExpr: Expr): Expr = - FunctionExpr("bit_right_shift", left, numberExpr) + fun bitRightShift(bits: Expr, numberExpr: Expr): Expr = + FunctionExpr("bit_right_shift", bits, numberExpr) - /** @return A new [Expr] representing the bitRightShift operation. */ + /** + * Creates an expression that applies a bitwise right shift operation between an expression and + * a constant. + * + * @param bits An expression that returns bits when evaluated. + * @param number The number of bits to shift. + * @return A new [Expr] representing the bitwise right shift operation. + */ @JvmStatic - fun bitRightShift(left: Expr, number: Int): Expr = FunctionExpr("bit_right_shift", left, number) + fun bitRightShift(bits: Expr, number: Int): Expr = FunctionExpr("bit_right_shift", bits, number) - /** @return A new [Expr] representing the bitRightShift operation. */ + /** + * Creates an expression that applies a bitwise right shift operation between a field and an + * expression. + * + * @param bitsFieldName Name of field that contains bits data. + * @param numberExpr The number of bits to shift. + * @return A new [Expr] representing the bitwise right shift operation. + */ @JvmStatic - fun bitRightShift(fieldName: String, numberExpr: Expr): Expr = - FunctionExpr("bit_right_shift", fieldName, numberExpr) + fun bitRightShift(bitsFieldName: String, numberExpr: Expr): Expr = + FunctionExpr("bit_right_shift", bitsFieldName, numberExpr) - /** @return A new [Expr] representing the bitRightShift operation. */ + /** + * Creates an expression that applies a bitwise right shift operation between a field and a + * constant. + * + * @param bitsFieldName Name of field that contains bits data. + * @param number The number of bits to shift. + * @return A new [Expr] representing the bitwise right shift operation. + */ @JvmStatic - fun bitRightShift(fieldName: String, number: Int): Expr = - FunctionExpr("bit_right_shift", fieldName, number) + fun bitRightShift(bitsFieldName: String, number: Int): Expr = + FunctionExpr("bit_right_shift", bitsFieldName, number) /** * Creates an expression that adds this expression to another expression. @@ -1124,11 +1292,22 @@ abstract class Expr internal constructor() { fun arrayConcat(firstArrayField: String, secondArray: Any, vararg otherArrays: Any): Expr = FunctionExpr("array_concat", firstArrayField, secondArray, *otherArrays) - /** @return A new [Expr] representing the arrayReverse operation. */ + /** + * Reverses the order of elements in the [array]. + * + * @param array The array expression to reverse. + * @return A new [Expr] representing the arrayReverse operation. + */ @JvmStatic fun arrayReverse(array: Expr): Expr = FunctionExpr("array_reverse", array) - /** @return A new [Expr] representing the arrayReverse operation. */ - @JvmStatic fun arrayReverse(fieldName: String): Expr = FunctionExpr("array_reverse", fieldName) + /** + * Reverses the order of elements in the array field. + * + * @param arrayFieldName The name of field that contains the array to reverse. + * @return A new [Expr] representing the arrayReverse operation. + */ + @JvmStatic + fun arrayReverse(arrayFieldName: String): Expr = FunctionExpr("array_reverse", arrayFieldName) /** * Creates an expression that checks if the array contains a specific [element]. @@ -1272,7 +1451,7 @@ abstract class Expr internal constructor() { * Creates an expression that calculates the length of an [array] expression. * * @param array The array expression to calculate the length of. - * @return A new [Expr] representing the the length of the array. + * @return A new [Expr] representing the length of the array. */ @JvmStatic fun arrayLength(array: Expr): Expr = FunctionExpr("array_length", array) @@ -1280,11 +1459,62 @@ abstract class Expr internal constructor() { * Creates an expression that calculates the length of an array field. * * @param arrayFieldName The name of the field containing an array to calculate the length of. - * @return A new [Expr] representing the the length of the array. + * @return A new [Expr] representing the length of the array. */ @JvmStatic fun arrayLength(arrayFieldName: String): Expr = FunctionExpr("array_length", arrayFieldName) + /** + * Creates an expression that indexes into an array from the beginning or end and return the + * element. If the offset exceeds the array length, an error is returned. A negative offset, + * starts from the end. + * + * @param array An [Expr] evaluating to an array. + * @param offset An Expr evaluating to the index of the element to return. + * @return A new [Expr] representing the arrayOffset operation. + */ + @JvmStatic + fun arrayOffset(array: Expr, offset: Expr): Expr = FunctionExpr("array_offset", array, offset) + + /** + * Creates an expression that indexes into an array from the beginning or end and return the + * element. If the offset exceeds the array length, an error is returned. A negative offset, + * starts from the end. + * + * @param array An [Expr] evaluating to an array. + * @param offset The index of the element to return. + * @return A new [Expr] representing the arrayOffset operation. + */ + @JvmStatic + fun arrayOffset(array: Expr, offset: Int): Expr = + FunctionExpr("array_offset", array, constant(offset)) + + /** + * Creates an expression that indexes into an array from the beginning or end and return the + * element. If the offset exceeds the array length, an error is returned. A negative offset, + * starts from the end. + * + * @param arrayFieldName The name of an array field. + * @param offset An Expr evaluating to the index of the element to return. + * @return A new [Expr] representing the arrayOffset operation. + */ + @JvmStatic + fun arrayOffset(arrayFieldName: String, offset: Expr): Expr = + FunctionExpr("array_offset", arrayFieldName, offset) + + /** + * Creates an expression that indexes into an array from the beginning or end and return the + * element. If the offset exceeds the array length, an error is returned. A negative offset, + * starts from the end. + * + * @param arrayFieldName The name of an array field. + * @param offset The index of the element to return. + * @return A new [Expr] representing the arrayOffset operation. + */ + @JvmStatic + fun arrayOffset(arrayFieldName: String, offset: Int): Expr = + FunctionExpr("array_offset", arrayFieldName, constant(offset)) + /** @return A new [Expr] representing the cond operation. */ @JvmStatic fun cond(condition: BooleanExpr, then: Expr, otherwise: Expr): Expr = @@ -1300,48 +1530,91 @@ abstract class Expr internal constructor() { } /** + * Creates an expression that applies a bitwise AND operation with other expression. + * + * @param bitsOther An expression that returns bits when evaluated. + * @return A new [Expr] representing the bitwise AND operation. */ - fun bitAnd(right: Expr) = bitAnd(this, right) + fun bitAnd(bitsOther: Expr): Expr = Companion.bitAnd(this, bitsOther) /** + * Creates an expression that applies a bitwise AND operation with a constant. + * + * @param bitsOther A constant byte array. + * @return A new [Expr] representing the bitwise AND operation. */ - fun bitAnd(right: Any) = bitAnd(this, right) + fun bitAnd(bitsOther: ByteArray): Expr = Companion.bitAnd(this, bitsOther) /** + * Creates an expression that applies a bitwise OR operation with other expression. + * + * @param bitsOther An expression that returns bits when evaluated. + * @return A new [Expr] representing the bitwise OR operation. */ - fun bitOr(right: Expr) = bitOr(this, right) + fun bitOr(bitsOther: Expr): Expr = Companion.bitOr(this, bitsOther) /** + * Creates an expression that applies a bitwise OR operation with a constant. + * + * @param bitsOther A constant byte array. + * @return A new [Expr] representing the bitwise OR operation. */ - fun bitOr(right: Any) = bitOr(this, right) + fun bitOr(bitsOther: ByteArray): Expr = Companion.bitOr(this, bitsOther) /** + * Creates an expression that applies a bitwise XOR operation with an expression. + * + * @param bitsOther An expression that returns bits when evaluated. + * @return A new [Expr] representing the bitwise XOR operation. */ - fun bitXor(right: Expr) = bitXor(this, right) + fun bitXor(bitsOther: Expr): Expr = Companion.bitXor(this, bitsOther) /** + * Creates an expression that applies a bitwise XOR operation with a constant. + * + * @param bitsOther A constant byte array. + * @return A new [Expr] representing the bitwise XOR operation. */ - fun bitXor(right: Any) = bitXor(this, right) + fun bitXor(bitsOther: ByteArray): Expr = Companion.bitXor(this, bitsOther) /** + * Creates an expression that applies a bitwise NOT operation to this expression. + * + * @return A new [Expr] representing the bitwise NOT operation. */ - fun bitNot() = bitNot(this) + fun bitNot(): Expr = Companion.bitNot(this) /** + * Creates an expression that applies a bitwise left shift operation with an expression. + * + * @param numberExpr The number of bits to shift. + * @return A new [Expr] representing the bitwise left shift operation. */ - fun bitLeftShift(numberExpr: Expr) = bitLeftShift(this, numberExpr) + fun bitLeftShift(numberExpr: Expr): Expr = Companion.bitLeftShift(this, numberExpr) /** + * Creates an expression that applies a bitwise left shift operation with a constant. + * + * @param number The number of bits to shift. + * @return A new [Expr] representing the bitwise left shift operation. */ - fun bitLeftShift(number: Int) = bitLeftShift(this, number) + fun bitLeftShift(number: Int): Expr = Companion.bitLeftShift(this, number) /** + * Creates an expression that applies a bitwise right shift operation with an expression. + * + * @param numberExpr The number of bits to shift. + * @return A new [Expr] representing the bitwise right shift operation. */ - fun bitRightShift(numberExpr: Expr) = bitRightShift(this, numberExpr) + fun bitRightShift(numberExpr: Expr): Expr = Companion.bitRightShift(this, numberExpr) /** + * Creates an expression that applies a bitwise right shift operation with a constant. + * + * @param number The number of bits to shift. + * @return A new [Expr] representing the bitwise right shift operation. */ - fun bitRightShift(number: Int) = bitRightShift(this, number) + fun bitRightShift(number: Int): Expr = Companion.bitRightShift(this, number) /** * Assigns an alias to this expression. @@ -1667,6 +1940,9 @@ abstract class Expr internal constructor() { Companion.arrayConcat(this, secondArray, *otherArrays) /** + * Reverses the order of elements in the array. + * + * @return A new [Expr] representing the arrayReverse operation. */ fun arrayReverse() = Companion.arrayReverse(this) @@ -1723,10 +1999,30 @@ abstract class Expr internal constructor() { /** * Creates an expression that calculates the length of an array expression. * - * @return A new [Expr] representing the the length of the array. + * @return A new [Expr] representing the length of the array. */ fun arrayLength() = Companion.arrayLength(this) + /** + * Creates an expression that indexes into an array from the beginning or end and return the + * element. If the offset exceeds the array length, an error is returned. A negative offset, + * starts from the end. + * + * @param offset An Expr evaluating to the index of the element to return. + * @return A new [Expr] representing the arrayOffset operation. + */ + fun arrayOffset(offset: Expr) = Companion.arrayOffset(this, offset) + + /** + * Creates an expression that indexes into an array from the beginning or end and return the + * element. If the offset exceeds the array length, an error is returned. A negative offset, + * starts from the end. + * + * @param offset An Expr evaluating to the index of the element to return. + * @return A new [Expr] representing the arrayOffset operation. + */ + fun arrayOffset(offset: Int) = Companion.arrayOffset(this, offset) + /** */ fun sum() = AggregateFunction.sum(this) @@ -1744,12 +2040,18 @@ abstract class Expr internal constructor() { fun max() = AggregateFunction.max(this) /** + * Create an [Ordering] that sorts documents in ascending order based on value of this expression + * + * @return A new [Ordering] object with ascending sort by this expression. */ - fun ascending() = Ordering.ascending(this) + fun ascending(): Ordering = Ordering.ascending(this) /** + * Create an [Ordering] that sorts documents in descending order based on value of this expression + * + * @return A new [Ordering] object with descending sort by this expression. */ - fun descending() = Ordering.descending(this) + fun descending(): Ordering = Ordering.descending(this) /** */ From 402a98d549426832c231fd2ea12b2e413b715c69 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 29 Apr 2025 11:30:55 -0400 Subject: [PATCH 050/152] Comments --- .../firestore/pipeline/expressions.kt | 355 +++++++++++++++--- 1 file changed, 298 insertions(+), 57 deletions(-) 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 fbe5ad07a78..eb32b56df5e 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 @@ -644,17 +644,41 @@ abstract class Expr internal constructor() { @JvmStatic fun multiply(fieldName: String, other: Any): Expr = FunctionExpr("multiply", fieldName, other) - /** @return A new [Expr] representing the divide operation. */ + /** + * Creates an expression that divides two expressions. + * + * @param left The expression to be divided. + * @param right The expression to divide by. + * @return A new [Expr] representing the division operation. + */ @JvmStatic fun divide(left: Expr, right: Expr): Expr = FunctionExpr("divide", left, right) - /** @return A new [Expr] representing the divide operation. */ + /** + * Creates an expression that divides an expression by an expression by a value. + * + * @param left The expression to be divided. + * @param right The value to divide by. + * @return A new [Expr] representing the division operation. + */ @JvmStatic fun divide(left: Expr, right: Any): Expr = FunctionExpr("divide", left, right) - /** @return A new [Expr] representing the divide operation. */ + /** + * Creates an expression that divides a field's value by an expression. + * + * @param fieldName The field name to be divided. + * @param other The expression to divide by. + * @return A new [Expr] representing the divide operation. + */ @JvmStatic fun divide(fieldName: String, other: Expr): Expr = FunctionExpr("divide", fieldName, other) - /** @return A new [Expr] representing the divide operation. */ + /** + * Creates an expression that divides a field's value by a value. + * + * @param fieldName The field name to be divided. + * @param other The value to divide by. + * @return A new [Expr] representing the divide operation. + */ @JvmStatic fun divide(fieldName: String, other: Any): Expr = FunctionExpr("divide", fieldName, other) @@ -744,16 +768,38 @@ abstract class Expr internal constructor() { fun replaceAll(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_all", fieldName, find, replace) - /** @return A new [Expr] representing the charLength operation. */ - @JvmStatic fun charLength(value: Expr): Expr = FunctionExpr("char_length", value) + /** + * Creates an expression that calculates the character length of a string expression in UTF8. + * + * @param expr The expression representing the string. + * @return A new [Expr] representing the charLength operation. + */ + @JvmStatic fun charLength(expr: Expr): Expr = FunctionExpr("char_length", expr) - /** @return A new [Expr] representing the charLength operation. */ + /** + * Creates an expression that calculates the character length of a string field in UTF8. + * + * @param fieldName The name of the field containing the string. + * @return A new [Expr] representing the charLength operation. + */ @JvmStatic fun charLength(fieldName: String): Expr = FunctionExpr("char_length", fieldName) - /** @return A new [Expr] representing the byteLength operation. */ + /** + * Creates an expression that calculates the length of a string in UTF-8 bytes, or just the + * length of a Blob. + * + * @param value The expression representing the string. + * @return A new [Expr] representing the length of the string in bytes. + */ @JvmStatic fun byteLength(value: Expr): Expr = FunctionExpr("byte_length", value) - /** @return A new [Expr] representing the byteLength operation. */ + /** + * Creates an expression that calculates the length of a string represented by a field in UTF-8 + * bytes, or just the length of a Blob. + * + * @param fieldName The name of the field containing the string. + * @return A new [Expr] representing the length of the string in bytes. + */ @JvmStatic fun byteLength(fieldName: String): Expr = FunctionExpr("byte_length", fieldName) /** @return A new [Expr] representing the like operation. */ @@ -982,95 +1028,203 @@ abstract class Expr internal constructor() { @JvmStatic fun mapRemove(mapField: String, key: String): Expr = FunctionExpr("map_remove", mapField, key) - /** @return A new [Expr] representing the cosineDistance operation. */ + /** + * Calculates the Cosine distance between two vector expressions. + * + * @param vector1 The first vector (represented as an Expr) to compare against. + * @param vector2 The other vector (represented as an Expr) to compare against. + * @return A new [Expr] representing the cosine distance between the two vectors. + */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("cosine_distance", vector1, vector2) - /** @return A new [Expr] representing the cosineDistance operation. */ + /** + * Calculates the Cosine distance between vector expression and a vector literal. + * + * @param vector1 The first vector (represented as an Expr) to compare against. + * @param vector2 The other vector (as an array of doubles) to compare against. + * @return A new [Expr] representing the cosine distance between the two vectors. + */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("cosine_distance", vector1, vector(vector2)) - /** @return A new [Expr] representing the cosineDistance operation. */ + /** + * Calculates the Cosine distance between vector expression and a vector literal. + * + * @param vector1 The first vector (represented as an [Expr]) to compare against. + * @param vector2 The other vector (represented as an [VectorValue]) to compare against. + * @return A new [Expr] representing the cosine distance between the two vectors. + */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("cosine_distance", vector1, vector2) - /** @return A new [Expr] representing the cosineDistance operation. */ + /** + * Calculates the Cosine distance between a vector field and a vector expression. + * + * @param vectorFieldName The name of the field containing the first vector. + * @param vector The other vector (represented as an Expr) to compare against. + * @return A new [Expr] representing the cosine distance between the two vectors. + */ @JvmStatic - fun cosineDistance(fieldName: String, vector: Expr): Expr = - FunctionExpr("cosine_distance", fieldName, vector) + fun cosineDistance(vectorFieldName: String, vector: Expr): Expr = + FunctionExpr("cosine_distance", vectorFieldName, vector) - /** @return A new [Expr] representing the cosineDistance operation. */ + /** + * Calculates the Cosine distance between a vector field and a vector literal. + * + * @param vectorFieldName The name of the field containing the first vector. + * @param vector The other vector (as an array of doubles) to compare against. + * @return A new [Expr] representing the cosine distance between the two vectors. + */ @JvmStatic - fun cosineDistance(fieldName: String, vector: DoubleArray): Expr = - FunctionExpr("cosine_distance", fieldName, vector(vector)) + fun cosineDistance(vectorFieldName: String, vector: DoubleArray): Expr = + FunctionExpr("cosine_distance", vectorFieldName, vector(vector)) - /** @return A new [Expr] representing the cosineDistance operation. */ + /** + * Calculates the Cosine distance between a vector field and a vector literal. + * + * @param vectorFieldName The name of the field containing the first vector. + * @param vector The other vector (represented as an [VectorValue]) to compare against. + * @return A new [Expr] representing the cosine distance between the two vectors. + */ @JvmStatic - fun cosineDistance(fieldName: String, vector: VectorValue): Expr = - FunctionExpr("cosine_distance", fieldName, vector) + fun cosineDistance(vectorFieldName: String, vector: VectorValue): Expr = + FunctionExpr("cosine_distance", vectorFieldName, vector) - /** @return A new [Expr] representing the dotProduct operation. */ + /** + * Calculates the dot product distance between two vector expressions. + * + * @param vector1 The first vector (represented as an Expr) to compare against. + * @param vector2 The other vector (represented as an Expr) to compare against. + * @return A new [Expr] representing the dot product distance between the two vectors. + */ @JvmStatic fun dotProduct(vector1: Expr, vector2: Expr): Expr = FunctionExpr("dot_product", vector1, vector2) - /** @return A new [Expr] representing the dotProduct operation. */ + /** + * Calculates the dot product distance between vector expression and a vector literal. + * + * @param vector1 The first vector (represented as an Expr) to compare against. + * @param vector2 The other vector (as an array of doubles) to compare against. + * @return A new [Expr] representing the dot product distance between the two vectors. + */ @JvmStatic fun dotProduct(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("dot_product", vector1, vector(vector2)) - /** @return A new [Expr] representing the dotProduct operation. */ + /** + * Calculates the dot product distance between vector expression and a vector literal. + * + * @param vector1 The first vector (represented as an [Expr]) to compare against. + * @param vector2 The other vector (represented as an [VectorValue]) to compare against. + * @return A new [Expr] representing the dot product distance between the two vectors. + */ @JvmStatic fun dotProduct(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("dot_product", vector1, vector2) - /** @return A new [Expr] representing the dotProduct operation. */ + /** + * Calculates the dot product distance between a vector field and a vector expression. + * + * @param vectorFieldName The name of the field containing the first vector. + * @param vector The other vector (represented as an Expr) to compare against. + * @return A new [Expr] representing the dot product distance between the two vectors. + */ @JvmStatic - fun dotProduct(fieldName: String, vector: Expr): Expr = - FunctionExpr("dot_product", fieldName, vector) + fun dotProduct(vectorFieldName: String, vector: Expr): Expr = + FunctionExpr("dot_product", vectorFieldName, vector) - /** @return A new [Expr] representing the dotProduct operation. */ + /** + * Calculates the dot product distance between vector field and a vector literal. + * + * @param vectorFieldName The name of the field containing the first vector. + * @param vector The other vector (as an array of doubles) to compare against. + * @return A new [Expr] representing the dot product distance between the two vectors. + */ @JvmStatic - fun dotProduct(fieldName: String, vector: DoubleArray): Expr = - FunctionExpr("dot_product", fieldName, vector(vector)) + fun dotProduct(vectorFieldName: String, vector: DoubleArray): Expr = + FunctionExpr("dot_product", vectorFieldName, vector(vector)) - /** @return A new [Expr] representing the dotProduct operation. */ + /** + * Calculates the dot product distance between a vector field and a vector literal. + * + * @param vectorFieldName The name of the field containing the first vector. + * @param vector The other vector (represented as an [VectorValue]) to compare against. + * @return A new [Expr] representing the dot product distance between the two vectors. + */ @JvmStatic - fun dotProduct(fieldName: String, vector: VectorValue): Expr = - FunctionExpr("dot_product", fieldName, vector) + fun dotProduct(vectorFieldName: String, vector: VectorValue): Expr = + FunctionExpr("dot_product", vectorFieldName, vector) - /** @return A new [Expr] representing the euclideanDistance operation. */ + /** + * Calculates the Euclidean distance between two vector expressions. + * + * @param vector1 The first vector (represented as an Expr) to compare against. + * @param vector2 The other vector (represented as an Expr) to compare against. + * @return A new [Expr] representing the Euclidean distance between the two vectors. + */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: Expr): Expr = FunctionExpr("euclidean_distance", vector1, vector2) - /** @return A new [Expr] representing the euclideanDistance operation. */ + /** + * Calculates the Euclidean distance between vector expression and a vector literal. + * + * @param vector1 The first vector (represented as an Expr) to compare against. + * @param vector2 The other vector (as an array of doubles) to compare against. + * @return A new [Expr] representing the Euclidean distance between the two vectors. + */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: DoubleArray): Expr = FunctionExpr("euclidean_distance", vector1, vector(vector2)) - /** @return A new [Expr] representing the not operation. */ + /** + * Calculates the Euclidean distance between vector expression and a vector literal. + * + * @param vector1 The first vector (represented as an [Expr]) to compare against. + * @param vector2 The other vector (represented as an [VectorValue]) to compare against. + * @return A new [Expr] representing the Euclidean distance between the two vectors. + */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: VectorValue): Expr = FunctionExpr("euclidean_distance", vector1, vector2) - /** @return A new [Expr] representing the euclideanDistance operation. */ + /** + * Calculates the Euclidean distance between a vector field and a vector expression. + * + * @param vectorFieldName The name of the field containing the first vector. + * @param vector The other vector (represented as an Expr) to compare against. + * @return A new [Expr] representing the Euclidean distance between the two vectors. + */ @JvmStatic - fun euclideanDistance(fieldName: String, vector: Expr): Expr = - FunctionExpr("euclidean_distance", fieldName, vector) + fun euclideanDistance(vectorFieldName: String, vector: Expr): Expr = + FunctionExpr("euclidean_distance", vectorFieldName, vector) - /** @return A new [Expr] representing the euclideanDistance operation. */ + /** + * Calculates the Euclidean distance between a vector field and a vector literal. + * + * @param vectorFieldName The name of the field containing the first vector. + * @param vector The other vector (as an array of doubles) to compare against. + * @return A new [Expr] representing the Euclidean distance between the two vectors. + */ @JvmStatic - fun euclideanDistance(fieldName: String, vector: DoubleArray): Expr = - FunctionExpr("euclidean_distance", fieldName, vector(vector)) + fun euclideanDistance(vectorFieldName: String, vector: DoubleArray): Expr = + FunctionExpr("euclidean_distance", vectorFieldName, vector(vector)) - /** @return A new [Expr] representing the euclideanDistance operation. */ + /** + * Calculates the Euclidean distance between a vector field and a vector literal. + * + * @param vectorFieldName The name of the field containing the first vector. + * @param vector The other vector (represented as an [VectorValue]) to compare against. + * @return A new [Expr] representing the Euclidean distance between the two vectors. + */ @JvmStatic - fun euclideanDistance(fieldName: String, vector: VectorValue): Expr = - FunctionExpr("euclidean_distance", fieldName, vector) + fun euclideanDistance(vectorFieldName: String, vector: VectorValue): Expr = + FunctionExpr("euclidean_distance", vectorFieldName, vector) /** @return A new [Expr] representing the vectorLength operation. */ @JvmStatic fun vectorLength(vector: Expr): Expr = FunctionExpr("vector_length", vector) @@ -1515,15 +1669,31 @@ abstract class Expr internal constructor() { fun arrayOffset(arrayFieldName: String, offset: Int): Expr = FunctionExpr("array_offset", arrayFieldName, constant(offset)) - /** @return A new [Expr] representing the cond operation. */ + /** + * Creates a conditional expression that evaluates to a [thenExpr] expression if a condition is + * true or an [elseExpr] expression if the condition is false. + * + * @param condition The condition to evaluate. + * @param thenExpr The expression to evaluate if the condition is true. + * @param elseExpr The expression to evaluate if the condition is false. + * @return A new [Expr] representing the conditional operation. + */ @JvmStatic - fun cond(condition: BooleanExpr, then: Expr, otherwise: Expr): Expr = - FunctionExpr("cond", condition, then, otherwise) + fun cond(condition: BooleanExpr, thenExpr: Expr, elseExpr: Expr): Expr = + FunctionExpr("cond", condition, thenExpr, elseExpr) - /** @return A new [Expr] representing the cond operation. */ + /** + * Creates a conditional expression that evaluates to a [thenValue] if a condition is true or an + * [elseValue] if the condition is false. + * + * @param condition The condition to evaluate. + * @param thenValue Value if the condition is true. + * @param elseValue Value if the condition is false. + * @return A new [Expr] representing the conditional operation. + */ @JvmStatic - fun cond(condition: BooleanExpr, then: Any, otherwise: Any): Expr = - FunctionExpr("cond", condition, then, otherwise) + fun cond(condition: BooleanExpr, thenValue: Any, elseValue: Any): Expr = + FunctionExpr("cond", condition, thenValue, elseValue) /** @return A new [Expr] representing the exists operation. */ @JvmStatic fun exists(expr: Expr) = BooleanExpr("exists", expr) @@ -1663,10 +1833,18 @@ abstract class Expr internal constructor() { fun multiply(other: Any) = Companion.multiply(this, other) /** + * Creates an expression that divides this expression by another expression. + * + * @param other The expression to divide by. + * @return A new [Expr] representing the division operation. */ fun divide(other: Expr) = Companion.divide(this, other) /** + * Creates an expression that divides this expression by a value. + * + * @param other The value to divide by. + * @return A new [Expr] representing the division operation. */ fun divide(other: Any) = Companion.divide(this, other) @@ -1719,12 +1897,19 @@ abstract class Expr internal constructor() { fun replaceAll(find: String, replace: String) = Companion.replaceAll(this, find, replace) /** + * Creates an expression that calculates the character length of this string expression in UTF8. + * + * @return A new [Expr] representing the charLength operation. */ - fun charLength() = Companion.charLength(this) + fun charLength(): Expr = Companion.charLength(this) /** + * Creates an expression that calculates the length of a string in UTF-8 bytes, or just the length + * of a Blob. + * + * @return A new [Expr] representing the length of the string in bytes. */ - fun byteLength() = Companion.byteLength(this) + fun byteLength(): Expr = Companion.byteLength(this) /** */ @@ -1840,34 +2025,66 @@ abstract class Expr internal constructor() { fun mapRemove(key: String) = Companion.mapRemove(this, key) /** + * Calculates the Cosine distance between this and another vector expressions. + * + * @param vector The other vector (represented as an Expr) to compare against. + * @return A new [Expr] representing the cosine distance between the two vectors. */ fun cosineDistance(vector: Expr) = Companion.cosineDistance(this, vector) /** + * Calculates the Cosine distance between this vector expression and a vector literal. + * + * @param vector The other vector (as an array of doubles) to compare against. + * @return A new [Expr] representing the cosine distance between the two vectors. */ fun cosineDistance(vector: DoubleArray) = Companion.cosineDistance(this, vector) /** + * Calculates the Cosine distance between this vector expression and a vector literal. + * + * @param vector The other vector (represented as an [VectorValue]) to compare against. + * @return A new [Expr] representing the cosine distance between the two vectors. */ fun cosineDistance(vector: VectorValue) = Companion.cosineDistance(this, vector) /** + * Calculates the dot product distance between this and another vector expression. + * + * @param vector The other vector (represented as an Expr) to compare against. + * @return A new [Expr] representing the dot product distance between the two vectors. */ fun dotProduct(vector: Expr) = Companion.dotProduct(this, vector) /** + * Calculates the dot product distance between this vector expression and a vector literal. + * + * @param vector The other vector (as an array of doubles) to compare against. + * @return A new [Expr] representing the dot product distance between the two vectors. */ fun dotProduct(vector: DoubleArray) = Companion.dotProduct(this, vector) /** + * Calculates the dot product distance between this vector expression and a vector literal. + * + * @param vector The other vector (represented as an [VectorValue]) to compare against. + * @return A new [Expr] representing the dot product distance between the two vectors. */ fun dotProduct(vector: VectorValue) = Companion.dotProduct(this, vector) /** + * Calculates the Euclidean distance between this and another vector expression. + * + * @param vector The other vector (represented as an Expr) to compare against. + * @return A new [Expr] representing the Euclidean distance between the two vectors. */ fun euclideanDistance(vector: Expr) = Companion.euclideanDistance(this, vector) /** + * Calculates the Euclidean distance between this vector expression and a vector literal. + * + * @param vector The other vector (as an array of doubles) to compare against. + * @return A new [Expr] representing the Euclidean distance between the two vectors. */ fun euclideanDistance(vector: DoubleArray) = Companion.euclideanDistance(this, vector) @@ -2024,20 +2241,44 @@ abstract class Expr internal constructor() { fun arrayOffset(offset: Int) = Companion.arrayOffset(this, offset) /** + * Creates an aggregation that counts the number of stage inputs with valid evaluations of the + * this expression. + * + * @return A new [AggregateFunction] representing the 'count' aggregation. */ - fun sum() = AggregateFunction.sum(this) + fun count(): AggregateFunction = AggregateFunction.count(this) /** + * Creates an aggregation that calculates the sum of this numeric expression across multiple + * stage inputs. + * + * @return A new [AggregateFunction] representing the 'sum' aggregation. */ - fun avg() = AggregateFunction.avg(this) + fun sum(): AggregateFunction = AggregateFunction.sum(this) /** + * Creates an aggregation that calculates the average (mean) of this numeric expression across + * multiple stage inputs. + * + * @return A new [AggregateFunction] representing the 'avg' aggregation. + */ + fun avg(): AggregateFunction = AggregateFunction.avg(this) + + /** + * Creates an aggregation that finds the minimum value of this expression across multiple stage + * inputs. + * + * @return A new [AggregateFunction] representing the 'min' aggregation. */ - fun min() = AggregateFunction.min(this) + fun min(): AggregateFunction = AggregateFunction.min(this) /** + * Creates an aggregation that finds the maximum value of this expression across multiple stage + * inputs. + * + * @return A new [AggregateFunction] representing the 'max' aggregation. */ - fun max() = AggregateFunction.max(this) + fun max(): AggregateFunction = AggregateFunction.max(this) /** * Create an [Ordering] that sorts documents in ascending order based on value of this expression From ef3a8f2745899ee654edb67835d30f7b6123f867 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 29 Apr 2025 17:25:27 -0400 Subject: [PATCH 051/152] Comments --- .../firestore/pipeline/expressions.kt | 660 ++++++++++++++---- 1 file changed, 538 insertions(+), 122 deletions(-) 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 eb32b56df5e..e3a97589292 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 @@ -694,49 +694,129 @@ abstract class Expr internal constructor() { /** @return A new [Expr] representing the mod operation. */ @JvmStatic fun mod(fieldName: String, other: Any): Expr = FunctionExpr("mod", fieldName, other) - /** @return A new [Expr] representing the eqAny operation. */ + /** + * Creates an expression that checks if an [expression], when evaluated, is equal to any of the + * provided [values]. + * + * @param expression The expression whose results to compare. + * @param values The values to check against. + * @return A new [BooleanExpr] representing the 'IN' comparison. + */ + @JvmStatic + fun eqAny(expression: Expr, values: List): BooleanExpr = + eqAny(expression, ListOfExprs(toArrayOfExprOrConstant(values))) + + /** + * Creates an expression that checks if an [expression], when evaluated, is equal to any of the + * elements of [arrayExpression]. + * + * @param expression The expression whose results to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for + * equality to the input. + * @return A new [BooleanExpr] representing the 'IN' comparison. + */ + @JvmStatic + fun eqAny(expression: Expr, arrayExpression: Expr): BooleanExpr = + BooleanExpr("eq_any", expression, arrayExpression) + + /** + * Creates an expression that checks if a field's value is equal to any of the provided [values] + * . + * + * @param fieldName The field to compare. + * @param values The values to check against. + * @return A new [BooleanExpr] representing the 'IN' comparison. + */ @JvmStatic - fun eqAny(value: Expr, values: List) = - BooleanExpr("eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) + fun eqAny(fieldName: String, values: List): BooleanExpr = + eqAny(fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) - /** @return A new [Expr] representing the eqAny operation. */ + /** + * Creates an expression that checks if a field's value is equal to any of the elements of + * [arrayExpression]. + * + * @param fieldName The field to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for + * equality to the input. + * @return A new [BooleanExpr] representing the 'IN' comparison. + */ @JvmStatic - fun eqAny(fieldName: String, values: List) = - BooleanExpr("eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + fun eqAny(fieldName: String, arrayExpression: Expr): BooleanExpr = + BooleanExpr("eq_any", fieldName, arrayExpression) - /** @return A new [Expr] representing the notEqAny operation. */ + /** + * Creates an expression that checks if an [expression], when evaluated, is not equal to all the + * provided [values]. + * + * @param expression The expression whose results to compare. + * @param values The values to check against. + * @return A new [BooleanExpr] representing the 'NOT IN' comparison. + */ @JvmStatic - fun notEqAny(value: Expr, values: List) = - BooleanExpr("not_eq_any", value, ListOfExprs(toArrayOfExprOrConstant(values))) + fun notEqAny(expression: Expr, values: List): BooleanExpr = + notEqAny(expression, ListOfExprs(toArrayOfExprOrConstant(values))) - /** @return A new [Expr] representing the notEqAny operation. */ + /** + * Creates an expression that checks if an [expression], when evaluated, is not equal to all the + * elements of [arrayExpression]. + * + * @param expression The expression whose results to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for + * equality to the input. + * @return A new [BooleanExpr] representing the 'NOT IN' comparison. + */ @JvmStatic - fun notEqAny(fieldName: String, values: List) = - BooleanExpr("not_eq_any", fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + fun notEqAny(expression: Expr, arrayExpression: Expr): BooleanExpr = + BooleanExpr("not_eq_any", expression, arrayExpression) + + /** + * Creates an expression that checks if a field's value is not equal to all of the provided + * [values]. + * + * @param fieldName The field to compare. + * @param values The values to check against. + * @return A new [BooleanExpr] representing the 'NOT IN' comparison. + */ + @JvmStatic + fun notEqAny(fieldName: String, values: List): BooleanExpr = + notEqAny(fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + + /** + * Creates an expression that checks if a field's value is not equal to all of the elements of + * [arrayExpression]. + * + * @param fieldName The field to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for + * equality to the input. + * @return A new [BooleanExpr] representing the 'NOT IN' comparison. + */ + @JvmStatic + fun notEqAny(fieldName: String, arrayExpression: Expr): BooleanExpr = + BooleanExpr("not_eq_any", fieldName, arrayExpression) /** @return A new [Expr] representing the isNan operation. */ - @JvmStatic fun isNan(expr: Expr) = BooleanExpr("is_nan", expr) + @JvmStatic fun isNan(expr: Expr): BooleanExpr = BooleanExpr("is_nan", expr) /** @return A new [Expr] representing the isNan operation. */ - @JvmStatic fun isNan(fieldName: String) = BooleanExpr("is_nan", fieldName) + @JvmStatic fun isNan(fieldName: String): BooleanExpr = BooleanExpr("is_nan", fieldName) /** @return A new [Expr] representing the isNotNan operation. */ - @JvmStatic fun isNotNan(expr: Expr) = BooleanExpr("is_not_nan", expr) + @JvmStatic fun isNotNan(expr: Expr): BooleanExpr = BooleanExpr("is_not_nan", expr) /** @return A new [Expr] representing the isNotNan operation. */ - @JvmStatic fun isNotNan(fieldName: String) = BooleanExpr("is_not_nan", fieldName) + @JvmStatic fun isNotNan(fieldName: String): BooleanExpr = BooleanExpr("is_not_nan", fieldName) /** @return A new [Expr] representing the isNull operation. */ - @JvmStatic fun isNull(expr: Expr) = BooleanExpr("is_null", expr) + @JvmStatic fun isNull(expr: Expr): BooleanExpr = BooleanExpr("is_null", expr) /** @return A new [Expr] representing the isNull operation. */ - @JvmStatic fun isNull(fieldName: String) = BooleanExpr("is_null", fieldName) + @JvmStatic fun isNull(fieldName: String): BooleanExpr = BooleanExpr("is_null", fieldName) /** @return A new [Expr] representing the isNotNull operation. */ - @JvmStatic fun isNotNull(expr: Expr) = BooleanExpr("is_not_null", expr) + @JvmStatic fun isNotNull(expr: Expr): BooleanExpr = BooleanExpr("is_not_null", expr) /** @return A new [Expr] representing the isNotNull operation. */ - @JvmStatic fun isNotNull(fieldName: String) = BooleanExpr("is_not_null", fieldName) + @JvmStatic fun isNotNull(fieldName: String): BooleanExpr = BooleanExpr("is_not_null", fieldName) /** @return A new [Expr] representing the replaceFirst operation. */ @JvmStatic @@ -803,25 +883,31 @@ abstract class Expr internal constructor() { @JvmStatic fun byteLength(fieldName: String): Expr = FunctionExpr("byte_length", fieldName) /** @return A new [Expr] representing the like operation. */ - @JvmStatic fun like(expr: Expr, pattern: Expr) = BooleanExpr("like", expr, pattern) + @JvmStatic fun like(expr: Expr, pattern: Expr): BooleanExpr = BooleanExpr("like", expr, pattern) /** @return A new [Expr] representing the like operation. */ - @JvmStatic fun like(expr: Expr, pattern: String) = BooleanExpr("like", expr, pattern) + @JvmStatic + fun like(expr: Expr, pattern: String): BooleanExpr = BooleanExpr("like", expr, pattern) /** @return A new [Expr] representing the like operation. */ - @JvmStatic fun like(fieldName: String, pattern: Expr) = BooleanExpr("like", fieldName, pattern) + @JvmStatic + fun like(fieldName: String, pattern: Expr): BooleanExpr = + BooleanExpr("like", fieldName, pattern) /** @return A new [Expr] representing the like operation. */ @JvmStatic - fun like(fieldName: String, pattern: String) = BooleanExpr("like", fieldName, pattern) + fun like(fieldName: String, pattern: String): BooleanExpr = + BooleanExpr("like", fieldName, pattern) /** @return A new [Expr] representing the regexContains operation. */ @JvmStatic - fun regexContains(expr: Expr, pattern: Expr) = BooleanExpr("regex_contains", expr, pattern) + fun regexContains(expr: Expr, pattern: Expr): BooleanExpr = + BooleanExpr("regex_contains", expr, pattern) /** @return A new [Expr] representing the regexContains operation. */ @JvmStatic - fun regexContains(expr: Expr, pattern: String) = BooleanExpr("regex_contains", expr, pattern) + fun regexContains(expr: Expr, pattern: String): BooleanExpr = + BooleanExpr("regex_contains", expr, pattern) /** @return A new [Expr] representing the regexContains operation. */ @JvmStatic @@ -834,11 +920,14 @@ abstract class Expr internal constructor() { BooleanExpr("regex_contains", fieldName, pattern) /** @return A new [Expr] representing the regexMatch operation. */ - @JvmStatic fun regexMatch(expr: Expr, pattern: Expr) = BooleanExpr("regex_match", expr, pattern) + @JvmStatic + fun regexMatch(expr: Expr, pattern: Expr): BooleanExpr = + BooleanExpr("regex_match", expr, pattern) /** @return A new [Expr] representing the regexMatch operation. */ @JvmStatic - fun regexMatch(expr: Expr, pattern: String) = BooleanExpr("regex_match", expr, pattern) + fun regexMatch(expr: Expr, pattern: String): BooleanExpr = + BooleanExpr("regex_match", expr, pattern) /** @return A new [Expr] representing the regexMatch operation. */ @JvmStatic @@ -894,50 +983,111 @@ abstract class Expr internal constructor() { /** @return A new [Expr] representing the strContains operation. */ @JvmStatic - fun strContains(expr: Expr, substring: Expr) = BooleanExpr("str_contains", expr, substring) + fun strContains(expr: Expr, substring: Expr): BooleanExpr = + BooleanExpr("str_contains", expr, substring) /** @return A new [Expr] representing the strContains operation. */ @JvmStatic - fun strContains(expr: Expr, substring: String) = BooleanExpr("str_contains", expr, substring) + fun strContains(expr: Expr, substring: String): BooleanExpr = + BooleanExpr("str_contains", expr, substring) - /** @return A new [Expr] representing the strContains operation. */ + /** @return A new [BooleanExpr] representing the strContains operation. */ @JvmStatic - fun strContains(fieldName: String, substring: Expr) = + fun strContains(fieldName: String, substring: Expr): BooleanExpr = BooleanExpr("str_contains", fieldName, substring) - /** @return A new [Expr] representing the strContains operation. */ + /** @return A new [BooleanExpr] representing the strContains operation. */ @JvmStatic - fun strContains(fieldName: String, substring: String) = + fun strContains(fieldName: String, substring: String): BooleanExpr = BooleanExpr("str_contains", fieldName, substring) - /** @return A new [Expr] representing the startsWith operation. */ - @JvmStatic fun startsWith(expr: Expr, prefix: Expr) = BooleanExpr("starts_with", expr, prefix) + /** + * Creates an expression that checks if a string expression starts with a given [prefix]. + * + * @param stringExpr The expression to check. + * @param prefix The prefix string expression to check for. + * @return A new [BooleanExpr] representing the 'starts with' comparison. + */ + @JvmStatic + fun startsWith(stringExpr: Expr, prefix: Expr): BooleanExpr = + BooleanExpr("starts_with", stringExpr, prefix) - /** @return A new [Expr] representing the startsWith operation. */ - @JvmStatic fun startsWith(expr: Expr, prefix: String) = BooleanExpr("starts_with", expr, prefix) + /** + * Creates an expression that checks if a string expression starts with a given [prefix]. + * + * @param stringExpr The expression to check. + * @param prefix The prefix string to check for. + * @return A new [BooleanExpr] representing the 'starts with' comparison. + */ + @JvmStatic + fun startsWith(stringExpr: Expr, prefix: String): BooleanExpr = + BooleanExpr("starts_with", stringExpr, prefix) - /** @return A new [Expr] representing the startsWith operation. */ + /** + * Creates an expression that checks if a string expression starts with a given [prefix]. + * + * @param fieldName The name of field that contains a string to check. + * @param prefix The prefix string expression to check for. + * @return A new [BooleanExpr] representing the 'starts with' comparison. + */ @JvmStatic - fun startsWith(fieldName: String, prefix: Expr) = BooleanExpr("starts_with", fieldName, prefix) + fun startsWith(fieldName: String, prefix: Expr): BooleanExpr = + BooleanExpr("starts_with", fieldName, prefix) - /** @return A new [Expr] representing the startsWith operation. */ + /** + * Creates an expression that checks if a string expression starts with a given [prefix]. + * + * @param fieldName The name of field that contains a string to check. + * @param prefix The prefix string to check for. + * @return A new [BooleanExpr] representing the 'starts with' comparison. + */ @JvmStatic - fun startsWith(fieldName: String, prefix: String) = + fun startsWith(fieldName: String, prefix: String): BooleanExpr = BooleanExpr("starts_with", fieldName, prefix) - /** @return A new [Expr] representing the endsWith operation. */ - @JvmStatic fun endsWith(expr: Expr, suffix: Expr) = BooleanExpr("ends_with", expr, suffix) + /** + * Creates an expression that checks if a string expression ends with a given [suffix]. + * + * @param stringExpr The expression to check. + * @param suffix The suffix string expression to check for. + * @return A new [BooleanExpr] representing the 'ends with' comparison. + */ + @JvmStatic + fun endsWith(stringExpr: Expr, suffix: Expr): BooleanExpr = + BooleanExpr("ends_with", stringExpr, suffix) - /** @return A new [Expr] representing the endsWith operation. */ - @JvmStatic fun endsWith(expr: Expr, suffix: String) = BooleanExpr("ends_with", expr, suffix) + /** + * Creates an expression that checks if a string expression ends with a given [suffix]. + * + * @param stringExpr The expression to check. + * @param suffix The suffix string to check for. + * @return A new [BooleanExpr] representing the 'ends with' comparison. + */ + @JvmStatic + fun endsWith(stringExpr: Expr, suffix: String): BooleanExpr = + BooleanExpr("ends_with", stringExpr, suffix) - /** @return A new [Expr] representing the endsWith operation. */ + /** + * Creates an expression that checks if a string expression ends with a given [suffix]. + * + * @param fieldName The name of field that contains a string to check. + * @param suffix The suffix string expression to check for. + * @return A new [BooleanExpr] representing the 'ends with' comparison. + */ @JvmStatic - fun endsWith(fieldName: String, suffix: Expr) = BooleanExpr("ends_with", fieldName, suffix) + fun endsWith(fieldName: String, suffix: Expr): BooleanExpr = + BooleanExpr("ends_with", fieldName, suffix) - /** @return A new [Expr] representing the endsWith operation. */ + /** + * Creates an expression that checks if a string expression ends with a given [suffix]. + * + * @param fieldName The name of field that contains a string to check. + * @param suffix The suffix string to check for. + * @return A new [BooleanExpr] representing the 'ends with' comparison. + */ @JvmStatic - fun endsWith(fieldName: String, suffix: String) = BooleanExpr("ends_with", fieldName, suffix) + fun endsWith(fieldName: String, suffix: String): BooleanExpr = + BooleanExpr("ends_with", fieldName, suffix) /** @return A new [Expr] representing the toLower operation. */ @JvmStatic fun toLower(expr: Expr): Expr = FunctionExpr("to_lower", expr) @@ -1326,77 +1476,244 @@ abstract class Expr internal constructor() { fun timestampSub(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) - /** @return A new [Expr] representing the eq operation. */ - @JvmStatic fun eq(left: Expr, right: Expr) = BooleanExpr("eq", left, right) + /** + * Creates an expression that checks if two expressions are equal. + * + * @param left The first expression to compare. + * @param right The second expression to compare to. + * @return A new [BooleanExpr] representing the equality comparison. + */ + @JvmStatic fun eq(left: Expr, right: Expr): BooleanExpr = BooleanExpr("eq", left, right) - /** @return A new [Expr] representing the eq operation. */ - @JvmStatic fun eq(left: Expr, right: Any) = BooleanExpr("eq", left, right) + /** + * Creates an expression that checks if an expression is equal to a value. + * + * @param left The first expression to compare. + * @param right The value to compare to. + * @return A new [BooleanExpr] representing the equality comparison. + */ + @JvmStatic fun eq(left: Expr, right: Any): BooleanExpr = BooleanExpr("eq", left, right) - /** @return A new [Expr] representing the eq operation. */ - @JvmStatic fun eq(fieldName: String, right: Expr) = BooleanExpr("eq", fieldName, right) + /** + * Creates an expression that checks if a field's value is equal to an expression. + * + * @param fieldName The field name to compare. + * @param expression The expression to compare to. + * @return A new [BooleanExpr] representing the equality comparison. + */ + @JvmStatic + fun eq(fieldName: String, expression: Expr): BooleanExpr = + BooleanExpr("eq", fieldName, expression) - /** @return A new [Expr] representing the eq operation. */ - @JvmStatic fun eq(fieldName: String, right: Any) = BooleanExpr("eq", fieldName, right) + /** + * Creates an expression that checks if a field's value is equal to another value. + * + * @param fieldName The field name to compare. + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the equality comparison. + */ + @JvmStatic + fun eq(fieldName: String, value: Any): BooleanExpr = BooleanExpr("eq", fieldName, value) - /** @return A new [Expr] representing the neq operation. */ - @JvmStatic fun neq(left: Expr, right: Expr) = BooleanExpr("neq", left, right) + /** + * Creates an expression that checks if two expressions are not equal. + * + * @param left The first expression to compare. + * @param right The second expression to compare to. + * @return A new [BooleanExpr] representing the inequality comparison. + */ + @JvmStatic fun neq(left: Expr, right: Expr): BooleanExpr = BooleanExpr("neq", left, right) - /** @return A new [Expr] representing the neq operation. */ - @JvmStatic fun neq(left: Expr, right: Any) = BooleanExpr("neq", left, right) + /** + * Creates an expression that checks if an expression is not equal to a value. + * + * @param left The first expression to compare. + * @param right The value to compare to. + * @return A new [BooleanExpr] representing the inequality comparison. + */ + @JvmStatic fun neq(left: Expr, right: Any): BooleanExpr = BooleanExpr("neq", left, right) - /** @return A new [Expr] representing the neq operation. */ - @JvmStatic fun neq(fieldName: String, right: Expr) = BooleanExpr("neq", fieldName, right) + /** + * Creates an expression that checks if a field's value is not equal to an expression. + * + * @param fieldName The field name to compare. + * @param expression The expression to compare to. + * @return A new [BooleanExpr] representing the inequality comparison. + */ + @JvmStatic + fun neq(fieldName: String, expression: Expr): BooleanExpr = + BooleanExpr("neq", fieldName, expression) - /** @return A new [Expr] representing the neq operation. */ - @JvmStatic fun neq(fieldName: String, right: Any) = BooleanExpr("neq", fieldName, right) + /** + * Creates an expression that checks if a field's value is not equal to another value. + * + * @param fieldName The field name to compare. + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the inequality comparison. + */ + @JvmStatic + fun neq(fieldName: String, value: Any): BooleanExpr = BooleanExpr("neq", fieldName, value) - /** @return A new [Expr] representing the gt operation. */ - @JvmStatic fun gt(left: Expr, right: Expr) = BooleanExpr("gt", left, right) + /** + * Creates an expression that checks if the first expression is greater than the second + * expression. + * + * @param left The first expression to compare. + * @param right The second expression to compare to. + * @return A new [BooleanExpr] representing the greater than comparison. + */ + @JvmStatic fun gt(left: Expr, right: Expr): BooleanExpr = BooleanExpr("gt", left, right) - /** @return A new [Expr] representing the gt operation. */ - @JvmStatic fun gt(left: Expr, right: Any) = BooleanExpr("gt", left, right) + /** + * Creates an expression that checks if an expression is greater than a value. + * + * @param left The first expression to compare. + * @param right The value to compare to. + * @return A new [BooleanExpr] representing the greater than comparison. + */ + @JvmStatic fun gt(left: Expr, right: Any): BooleanExpr = BooleanExpr("gt", left, right) - /** @return A new [Expr] representing the gt operation. */ - @JvmStatic fun gt(fieldName: String, right: Expr) = BooleanExpr("gt", fieldName, right) + /** + * Creates an expression that checks if a field's value is greater than an expression. + * + * @param fieldName The field name to compare. + * @param expression The expression to compare to. + * @return A new [BooleanExpr] representing the greater than comparison. + */ + @JvmStatic + fun gt(fieldName: String, expression: Expr): BooleanExpr = + BooleanExpr("gt", fieldName, expression) - /** @return A new [Expr] representing the gt operation. */ - @JvmStatic fun gt(fieldName: String, right: Any) = BooleanExpr("gt", fieldName, right) + /** + * Creates an expression that checks if a field's value is greater than another value. + * + * @param fieldName The field name to compare. + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the greater than comparison. + */ + @JvmStatic + fun gt(fieldName: String, value: Any): BooleanExpr = BooleanExpr("gt", fieldName, value) - /** @return A new [Expr] representing the gte operation. */ - @JvmStatic fun gte(left: Expr, right: Expr) = BooleanExpr("gte", left, right) + /** + * Creates an expression that checks if the first expression is greater than or equal to the + * second expression. + * + * @param left The first expression to compare. + * @param right The second expression to compare to. + * @return A new [BooleanExpr] representing the greater than or equal to comparison. + */ + @JvmStatic fun gte(left: Expr, right: Expr): BooleanExpr = BooleanExpr("gte", left, right) - /** @return A new [Expr] representing the gte operation. */ - @JvmStatic fun gte(left: Expr, right: Any) = BooleanExpr("gte", left, right) + /** + * Creates an expression that checks if an expression is greater than or equal to a value. + * + * @param left The first expression to compare. + * @param right The value to compare to. + * @return A new [BooleanExpr] representing the greater than or equal to comparison. + */ + @JvmStatic fun gte(left: Expr, right: Any): BooleanExpr = BooleanExpr("gte", left, right) - /** @return A new [Expr] representing the gte operation. */ - @JvmStatic fun gte(fieldName: String, right: Expr) = BooleanExpr("gte", fieldName, right) + /** + * Creates an expression that checks if a field's value is greater than or equal to an + * expression. + * + * @param fieldName The field name to compare. + * @param expression The expression to compare to. + * @return A new [BooleanExpr] representing the greater than or equal to comparison. + */ + @JvmStatic + fun gte(fieldName: String, expression: Expr): BooleanExpr = + BooleanExpr("gte", fieldName, expression) - /** @return A new [Expr] representing the gte operation. */ - @JvmStatic fun gte(fieldName: String, right: Any) = BooleanExpr("gte", fieldName, right) + /** + * Creates an expression that checks if a field's value is greater than or equal to another + * value. + * + * @param fieldName The field name to compare. + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the greater than or equal to comparison. + */ + @JvmStatic + fun gte(fieldName: String, value: Any): BooleanExpr = BooleanExpr("gte", fieldName, value) - /** @return A new [Expr] representing the lt operation. */ - @JvmStatic fun lt(left: Expr, right: Expr) = BooleanExpr("lt", left, right) + /** + * Creates an expression that checks if the first expression is less than the second expression. + * + * @param left The first expression to compare. + * @param right The second expression to compare to. + * @return A new [BooleanExpr] representing the less than comparison. + */ + @JvmStatic fun lt(left: Expr, right: Expr): BooleanExpr = BooleanExpr("lt", left, right) - /** @return A new [Expr] representing the lt operation. */ - @JvmStatic fun lt(left: Expr, right: Any) = BooleanExpr("lt", left, right) + /** + * Creates an expression that checks if an expression is less than a value. + * + * @param left The first expression to compare. + * @param right The value to compare to. + * @return A new [BooleanExpr] representing the less than comparison. + */ + @JvmStatic fun lt(left: Expr, right: Any): BooleanExpr = BooleanExpr("lt", left, right) - /** @return A new [Expr] representing the lt operation. */ - @JvmStatic fun lt(fieldName: String, right: Expr) = BooleanExpr("lt", fieldName, right) + /** + * Creates an expression that checks if a field's value is less than an expression. + * + * @param fieldName The field name to compare. + * @param expression The expression to compare to. + * @return A new [BooleanExpr] representing the less than comparison. + */ + @JvmStatic + fun lt(fieldName: String, expression: Expr): BooleanExpr = + BooleanExpr("lt", fieldName, expression) - /** @return A new [Expr] representing the lt operation. */ - @JvmStatic fun lt(fieldName: String, right: Any) = BooleanExpr("lt", fieldName, right) + /** + * Creates an expression that checks if a field's value is less than another value. + * + * @param fieldName The field name to compare. + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the less than comparison. + */ + @JvmStatic + fun lt(fieldName: String, right: Any): BooleanExpr = BooleanExpr("lt", fieldName, right) - /** @return A new [Expr] representing the lte operation. */ - @JvmStatic fun lte(left: Expr, right: Expr) = BooleanExpr("lte", left, right) + /** + * Creates an expression that checks if the first expression is less than or equal to the second + * expression. + * + * @param left The first expression to compare. + * @param right The second expression to compare to. + * @return A new [BooleanExpr] representing the less than or equal to comparison. + */ + @JvmStatic fun lte(left: Expr, right: Expr): BooleanExpr = BooleanExpr("lte", left, right) - /** @return A new [Expr] representing the lte operation. */ - @JvmStatic fun lte(left: Expr, right: Any) = BooleanExpr("lte", left, right) + /** + * Creates an expression that checks if an expression is less than or equal to a value. + * + * @param left The first expression to compare. + * @param right The value to compare to. + * @return A new [BooleanExpr] representing the less than or equal to comparison. + */ + @JvmStatic fun lte(left: Expr, right: Any): BooleanExpr = BooleanExpr("lte", left, right) - /** @return A new [Expr] representing the lte operation. */ - @JvmStatic fun lte(fieldName: String, right: Expr) = BooleanExpr("lte", fieldName, right) + /** + * Creates an expression that checks if a field's value is less than or equal to an expression. + * + * @param fieldName The field name to compare. + * @param expression The expression to compare to. + * @return A new [BooleanExpr] representing the less than or equal to comparison. + */ + @JvmStatic + fun lte(fieldName: String, expression: Expr): BooleanExpr = + BooleanExpr("lte", fieldName, expression) - /** @return A new [Expr] representing the lte operation. */ - @JvmStatic fun lte(fieldName: String, right: Any) = BooleanExpr("lte", fieldName, right) + /** + * Creates an expression that checks if a field's value is less than or equal to another value. + * + * @param fieldName The field name to compare. + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the less than or equal to comparison. + */ + @JvmStatic + fun lte(fieldName: String, value: Any): BooleanExpr = BooleanExpr("lte", fieldName, value) /** * Creates an expression that concatenates an array with other arrays. @@ -1471,7 +1788,8 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the arrayContains operation. */ @JvmStatic - fun arrayContains(array: Expr, element: Expr) = BooleanExpr("array_contains", array, element) + fun arrayContains(array: Expr, element: Expr): BooleanExpr = + BooleanExpr("array_contains", array, element) /** * Creates an expression that checks if the array field contains a specific [element]. @@ -1492,7 +1810,8 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the arrayContains operation. */ @JvmStatic - fun arrayContains(array: Expr, element: Any) = BooleanExpr("array_contains", array, element) + fun arrayContains(array: Expr, element: Any): BooleanExpr = + BooleanExpr("array_contains", array, element) /** * Creates an expression that checks if the array field contains a specific [element]. @@ -1696,7 +2015,31 @@ abstract class Expr internal constructor() { FunctionExpr("cond", condition, thenValue, elseValue) /** @return A new [Expr] representing the exists operation. */ - @JvmStatic fun exists(expr: Expr) = BooleanExpr("exists", expr) + @JvmStatic fun exists(expr: Expr): BooleanExpr = BooleanExpr("exists", expr) + + /** + * Creates an expression that returns the document ID from a path. + * + * @param documentPath An expression the evaluates to document path. + * @return A new [Expr] representing the documentId operation. + */ + @JvmStatic fun documentId(documentPath: Expr): Expr = FunctionExpr("document_id", documentPath) + + /** + * Creates an expression that returns the document ID from a path. + * + * @param documentPath The string representation of the document path. + * @return A new [Expr] representing the documentId operation. + */ + @JvmStatic fun documentId(documentPath: String): Expr = documentId(constant(documentPath)) + + /** + * Creates an expression that returns the document ID from a [DocumentReference]. + * + * @param docRef The [DocumentReference]. + * @return A new [Expr] representing the documentId operation. + */ + @JvmStatic fun documentId(docRef: DocumentReference): Expr = documentId(constant(docRef)) } /** @@ -1798,6 +2141,13 @@ abstract class Expr internal constructor() { */ open fun alias(alias: String) = ExprWithAlias(alias, this) + /** + * Creates an expression that returns the document ID from this path expression. + * + * @return A new [Expr] representing the documentId operation. + */ + fun documentId(): Expr = Companion.documentId(this) + /** * Creates an expression that adds this expression to another expression. * @@ -1957,25 +2307,41 @@ abstract class Expr internal constructor() { /** */ - fun strContains(substring: Expr) = Companion.strContains(this, substring) + fun strContains(substring: Expr): BooleanExpr = Companion.strContains(this, substring) /** */ - fun strContains(substring: String) = Companion.strContains(this, substring) + fun strContains(substring: String): BooleanExpr = Companion.strContains(this, substring) /** + * Creates an expression that checks if this string expression starts with a given [prefix]. + * + * @param prefix The prefix string expression to check for. + * @return A new [Expr] representing the the 'starts with' comparison. */ - fun startsWith(prefix: Expr) = Companion.startsWith(this, prefix) + fun startsWith(prefix: Expr): BooleanExpr = Companion.startsWith(this, prefix) /** + * Creates an expression that checks if this string expression starts with a given [prefix]. + * + * @param prefix The prefix string expression to check for. + * @return A new [Expr] representing the 'starts with' comparison. */ - fun startsWith(prefix: String) = Companion.startsWith(this, prefix) + fun startsWith(prefix: String): BooleanExpr = Companion.startsWith(this, prefix) /** + * Creates an expression that checks if this string expression ends with a given [suffix]. + * + * @param suffix The suffix string expression to check for. + * @return A new [Expr] representing the 'ends with' comparison. */ - fun endsWith(suffix: Expr) = Companion.endsWith(this, suffix) + fun endsWith(suffix: Expr): BooleanExpr = Companion.endsWith(this, suffix) /** + * Creates an expression that checks if this string expression ends with a given [suffix]. + * + * @param suffix The suffix string to check for. + * @return A new [Expr] representing the the 'ends with' comparison. */ fun endsWith(suffix: String) = Companion.endsWith(this, suffix) @@ -2249,8 +2615,8 @@ abstract class Expr internal constructor() { fun count(): AggregateFunction = AggregateFunction.count(this) /** - * Creates an aggregation that calculates the sum of this numeric expression across multiple - * stage inputs. + * Creates an aggregation that calculates the sum of this numeric expression across multiple stage + * inputs. * * @return A new [AggregateFunction] representing the 'sum' aggregation. */ @@ -2295,56 +2661,106 @@ abstract class Expr internal constructor() { fun descending(): Ordering = Ordering.descending(this) /** + * Creates an expression that checks if this and [other] expression are equal. + * + * @param other The expression to compare to. + * @return A new [BooleanExpr] representing the equality comparison. */ - fun eq(other: Expr) = Companion.eq(this, other) + fun eq(other: Expr): BooleanExpr = Companion.eq(this, other) /** + * Creates an expression that checks if this expression is equal to a [value]. + * + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the equality comparison. */ - fun eq(other: Any) = Companion.eq(this, other) + fun eq(value: Any): BooleanExpr = Companion.eq(this, value) /** + * Creates an expression that checks if this expressions is not equal to the [other] expression. + * + * @param other The expression to compare to. + * @return A new [BooleanExpr] representing the inequality comparison. */ - fun neq(other: Expr) = Companion.neq(this, other) + fun neq(other: Expr): BooleanExpr = Companion.neq(this, other) /** + * Creates an expression that checks if this expression is not equal to a [value]. + * + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the inequality comparison. */ - fun neq(other: Any) = Companion.neq(this, other) + fun neq(value: Any): BooleanExpr = Companion.neq(this, value) /** + * Creates an expression that checks if this expression is greater than the [other] expression. + * + * @param other The expression to compare to. + * @return A new [BooleanExpr] representing the greater than comparison. */ - fun gt(other: Expr) = Companion.gt(this, other) + fun gt(other: Expr): BooleanExpr = Companion.gt(this, other) /** + * Creates an expression that checks if this expression is greater than a [value]. + * + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the greater than comparison. */ - fun gt(other: Any) = Companion.gt(this, other) + fun gt(value: Any): BooleanExpr = Companion.gt(this, value) /** + * Creates an expression that checks if this expression is greater than or equal to the [other] + * expression. + * + * @param other The expression to compare to. + * @return A new [BooleanExpr] representing the greater than or equal to comparison. */ - fun gte(other: Expr) = Companion.gte(this, other) + fun gte(other: Expr): BooleanExpr = Companion.gte(this, other) /** + * Creates an expression that checks if this expression is greater than or equal to a [value]. + * + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the greater than or equal to comparison. */ - fun gte(other: Any) = Companion.gte(this, other) + fun gte(value: Any): BooleanExpr = Companion.gte(this, value) /** + * Creates an expression that checks if this expression is less than the [other] expression. + * + * @param other The expression to compare to. + * @return A new [BooleanExpr] representing the less than comparison. */ - fun lt(other: Expr) = Companion.lt(this, other) + fun lt(other: Expr): BooleanExpr = Companion.lt(this, other) /** + * Creates an expression that checks if this expression is less than a value. + * + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the less than comparison. */ - fun lt(other: Any) = Companion.lt(this, other) + fun lt(value: Any): BooleanExpr = Companion.lt(this, value) /** + * Creates an expression that checks if this expression is less than or equal to the [other] + * expression. + * + * @param other The expression to compare to. + * @return A new [BooleanExpr] representing the less than or equal to comparison. */ - fun lte(other: Expr) = Companion.lte(this, other) + fun lte(other: Expr): BooleanExpr = Companion.lte(this, other) /** + * Creates an expression that checks if this expression is less than or equal to a [value]. + * + * @param value The value to compare to. + * @return A new [BooleanExpr] representing the less than or equal to comparison. */ - fun lte(other: Any) = Companion.lte(this, other) + fun lte(value: Any): BooleanExpr = Companion.lte(this, value) /** */ - fun exists() = Companion.exists(this) + fun exists(): BooleanExpr = Companion.exists(this) internal abstract fun toProto(userDataReader: UserDataReader): Value } @@ -2472,7 +2888,7 @@ open class BooleanExpr internal constructor(name: String, params: Array Date: Wed, 30 Apr 2025 11:00:15 -0400 Subject: [PATCH 052/152] More expression work --- .../firestore/pipeline/expressions.kt | 116 +++++++++++++++++- 1 file changed, 111 insertions(+), 5 deletions(-) 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 e3a97589292..74f854a8926 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 @@ -2014,8 +2014,45 @@ abstract class Expr internal constructor() { fun cond(condition: BooleanExpr, thenValue: Any, elseValue: Any): Expr = FunctionExpr("cond", condition, thenValue, elseValue) - /** @return A new [Expr] representing the exists operation. */ - @JvmStatic fun exists(expr: Expr): BooleanExpr = BooleanExpr("exists", expr) + /** + * Creates an expression that checks if a field exists. + * + * @param value An expression evaluates to the name of the field to check. + * @return A new [Expr] representing the exists check. + */ + @JvmStatic fun exists(value: Expr): BooleanExpr = BooleanExpr("exists", value) + + /** + * Creates an expression that checks if a field exists. + * + * @param fieldName The field name to check. + * @return A new [Expr] representing the exists check. + */ + @JvmStatic fun exists(fieldName: String): BooleanExpr = BooleanExpr("exists", fieldName) + + /** + * Creates an expression that returns the [catchExpr] argument if there is an + * error, else return the result of the [tryExpr] argument evaluation. + * + * @param tryExpr The try expression. + * @param catchExpr The catch expression that will be evaluated and + * returned if the [tryExpr] produces an error. + * @return A new [Expr] representing the ifError operation. + */ + @JvmStatic + fun ifError(tryExpr: Expr, catchExpr: Expr): Expr = FunctionExpr("if_error", tryExpr, catchExpr) + + /** + * Creates an expression that returns the [catchValue] argument if there is an + * error, else return the result of the [tryExpr] argument evaluation. + * + * @param tryExpr The try expression. + * @param catchValue The value that will be returned if the [tryExpr] produces an error. + * @return A new [Expr] representing the ifError operation. + */ + @JvmStatic + fun ifError(tryExpr: Expr, catchValue: Any): Expr = + FunctionExpr("if_error", tryExpr, catchValue) /** * Creates an expression that returns the document ID from a path. @@ -2207,13 +2244,43 @@ abstract class Expr internal constructor() { fun mod(other: Any) = Companion.mod(this, other) /** + * Creates an expression that checks if this expression, when evaluated, is equal to any of the + * provided [values]. + * + * @param values The values to check against. + * @return A new [BooleanExpr] representing the 'IN' comparison. */ fun eqAny(values: List) = Companion.eqAny(this, values) /** + * Creates an expression that checks if this expression, when evaluated, is equal to any of the + * elements of [arrayExpression]. + * + * @param arrayExpression An expression that evaluates to an array, whose elements to check for + * equality to the input. + * @return A new [BooleanExpr] representing the 'IN' comparison. + */ + fun eqAny(arrayExpression: Expr) = Companion.eqAny(this, arrayExpression) + + /** + * Creates an expression that checks if this expression, when evaluated, is not equal to all the + * provided [values]. + * + * @param values The values to check against. + * @return A new [BooleanExpr] representing the 'NOT IN' comparison. */ fun notEqAny(values: List) = Companion.notEqAny(this, values) + /** + * Creates an expression that checks if this expression, when evaluated, is not equal to all the + * elements of [arrayExpression]. + * + * @param arrayExpression An expression that evaluates to an array, whose elements to check for + * equality to the input. + * @return A new [BooleanExpr] representing the 'NOT IN' comparison. + */ + fun notEqAny(arrayExpression: Expr) = Companion.notEqAny(this, arrayExpression) + /** */ fun isNan() = Companion.isNan(this) @@ -2759,9 +2826,32 @@ abstract class Expr internal constructor() { fun lte(value: Any): BooleanExpr = Companion.lte(this, value) /** + * Creates an expression that checks if this expression evaluates to a name of the field that + * exists. + * + * @return A new [Expr] representing the exists check. */ fun exists(): BooleanExpr = Companion.exists(this) + /** + * Creates an expression that returns the [catchExpr] argument if there is an + * error, else return the result of this expression. + * + * @param catchExpr The catch expression that will be evaluated and + * returned if the this expression produces an error. + * @return A new [Expr] representing the ifError operation. + */ + fun ifError(catchExpr: Expr): Expr = Companion.ifError(this, catchExpr) + + /** + * Creates an expression that returns the [catchValue] argument if there is an + * error, else return the result of this expression. + * + * @param catchValue The value that will be returned if this expression produces an error. + * @return A new [Expr] representing the ifError operation. + */ + fun ifError(catchValue: Any): Expr = Companion.ifError(this, catchValue) + internal abstract fun toProto(userDataReader: UserDataReader): Value } @@ -2838,17 +2928,24 @@ internal constructor( private val params: Array, private val options: InternalOptions = InternalOptions.EMPTY ) : Expr() { + internal constructor(name: String, param: Expr) : this(name, arrayOf(param)) internal constructor( name: String, param: Expr, vararg params: Any ) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) + internal constructor( + name: String, + param1: Expr, + param2: Expr + ) : this(name, arrayOf(param1, param2)) internal constructor( name: String, param1: Expr, param2: Expr, vararg params: Any ) : this(name, arrayOf(param1, param2, *toArrayOfExprOrConstant(params))) + internal constructor(name: String, fieldName: String) : this(name, arrayOf(field(fieldName))) internal constructor( name: String, fieldName: String, @@ -2866,18 +2963,27 @@ internal constructor( } } -/** An interface that represents a filter condition. */ +/** A class that represents a filter condition. */ open class BooleanExpr internal constructor(name: String, params: Array) : FunctionExpr(name, params, InternalOptions.EMPTY) { internal constructor( name: String, - params: List - ) : this(name, toArrayOfExprOrConstant(params)) + param: Expr + ) : this(name, arrayOf(param)) internal constructor( name: String, param: Expr, vararg params: Any ) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) + internal constructor( + name: String, + param1: Expr, + param2: Expr + ) : this(name, arrayOf(param1, param2)) + internal constructor( + name: String, + fieldName: String + ) : this(name, arrayOf(field(fieldName))) internal constructor( name: String, fieldName: String, From 1ae8be71113cffea2498633c041c54c5b342000e Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 1 May 2025 11:05:25 -0400 Subject: [PATCH 053/152] More expression work --- .../firestore/pipeline/expressions.kt | 161 +++++++++++++----- 1 file changed, 116 insertions(+), 45 deletions(-) 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 74f854a8926..ffb4148123f 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 @@ -794,28 +794,88 @@ abstract class Expr internal constructor() { fun notEqAny(fieldName: String, arrayExpression: Expr): BooleanExpr = BooleanExpr("not_eq_any", fieldName, arrayExpression) - /** @return A new [Expr] representing the isNan operation. */ + /** + * Creates an expression that returns true if a value is absent. Otherwise, returns false even + * if the value is null. + * + * @param value The expression to check. + * @return A new [BooleanExpr] representing the isAbsent operation. + */ + @JvmStatic fun isAbsent(value: Expr): BooleanExpr = BooleanExpr("is_absent", value) + + /** + * Creates an expression that returns true if a field is absent. Otherwise, returns false even + * if the field value is null. + * + * @param fieldName The field to check. + * @return A new [BooleanExpr] representing the isAbsent operation. + */ + @JvmStatic fun isAbsent(fieldName: String): BooleanExpr = BooleanExpr("is_absent", fieldName) + + /** + * Creates an expression that checks if an expression evaluates to 'NaN' (Not a Number). + * + * @param expr The expression to check. + * @return A new [BooleanExpr] representing the isNan operation. + */ @JvmStatic fun isNan(expr: Expr): BooleanExpr = BooleanExpr("is_nan", expr) - /** @return A new [Expr] representing the isNan operation. */ + /** + * Creates an expression that checks if [expr] evaluates to 'NaN' (Not a Number). + * + * @param fieldName The field to check. + * @return A new [BooleanExpr] representing the isNan operation. + */ @JvmStatic fun isNan(fieldName: String): BooleanExpr = BooleanExpr("is_nan", fieldName) - /** @return A new [Expr] representing the isNotNan operation. */ + /** + * Creates an expression that checks if the results of [expr] is NOT 'NaN' (Not a + * Number). + * + * @param expr The expression to check. + * @return A new [BooleanExpr] representing the isNotNan operation. + */ @JvmStatic fun isNotNan(expr: Expr): BooleanExpr = BooleanExpr("is_not_nan", expr) - /** @return A new [Expr] representing the isNotNan operation. */ + /** + * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a + * Number). + * + * @param fieldName The field to check. + * @return A new [BooleanExpr] representing the isNotNan operation. + */ @JvmStatic fun isNotNan(fieldName: String): BooleanExpr = BooleanExpr("is_not_nan", fieldName) - /** @return A new [Expr] representing the isNull operation. */ + /** + * Creates an expression that checks if tbe result of [expr] is null. + * + * @param expr The expression to check. + * @return A new [BooleanExpr] representing the isNull operation. + */ @JvmStatic fun isNull(expr: Expr): BooleanExpr = BooleanExpr("is_null", expr) - /** @return A new [Expr] representing the isNull operation. */ + /** + * Creates an expression that checks if tbe value of a field is null. + * + * @param fieldName The field to check. + * @return A new [BooleanExpr] representing the isNull operation. + */ @JvmStatic fun isNull(fieldName: String): BooleanExpr = BooleanExpr("is_null", fieldName) - /** @return A new [Expr] representing the isNotNull operation. */ + /** + * Creates an expression that checks if tbe result of [expr] is not null. + * + * @param expr The expression to check. + * @return A new [BooleanExpr] representing the isNotNull operation. + */ @JvmStatic fun isNotNull(expr: Expr): BooleanExpr = BooleanExpr("is_not_null", expr) - /** @return A new [Expr] representing the isNotNull operation. */ + /** + * Creates an expression that checks if tbe value of a field is not null. + * + * @param fieldName The field to check. + * @return A new [BooleanExpr] representing the isNotNull operation. + */ @JvmStatic fun isNotNull(fieldName: String): BooleanExpr = BooleanExpr("is_not_null", fieldName) /** @return A new [Expr] representing the replaceFirst operation. */ @@ -2031,20 +2091,20 @@ abstract class Expr internal constructor() { @JvmStatic fun exists(fieldName: String): BooleanExpr = BooleanExpr("exists", fieldName) /** - * Creates an expression that returns the [catchExpr] argument if there is an - * error, else return the result of the [tryExpr] argument evaluation. + * Creates an expression that returns the [catchExpr] argument if there is an error, else return + * the result of the [tryExpr] argument evaluation. * * @param tryExpr The try expression. - * @param catchExpr The catch expression that will be evaluated and - * returned if the [tryExpr] produces an error. + * @param catchExpr The catch expression that will be evaluated and returned if the [tryExpr] + * produces an error. * @return A new [Expr] representing the ifError operation. */ @JvmStatic fun ifError(tryExpr: Expr, catchExpr: Expr): Expr = FunctionExpr("if_error", tryExpr, catchExpr) /** - * Creates an expression that returns the [catchValue] argument if there is an - * error, else return the result of the [tryExpr] argument evaluation. + * Creates an expression that returns the [catchValue] argument if there is an error, else + * return the result of the [tryExpr] argument evaluation. * * @param tryExpr The try expression. * @param catchValue The value that will be returned if the [tryExpr] produces an error. @@ -2250,7 +2310,7 @@ abstract class Expr internal constructor() { * @param values The values to check against. * @return A new [BooleanExpr] representing the 'IN' comparison. */ - fun eqAny(values: List) = Companion.eqAny(this, values) + fun eqAny(values: List): BooleanExpr = Companion.eqAny(this, values) /** * Creates an expression that checks if this expression, when evaluated, is equal to any of the @@ -2260,7 +2320,7 @@ abstract class Expr internal constructor() { * equality to the input. * @return A new [BooleanExpr] representing the 'IN' comparison. */ - fun eqAny(arrayExpression: Expr) = Companion.eqAny(this, arrayExpression) + fun eqAny(arrayExpression: Expr): BooleanExpr = Companion.eqAny(this, arrayExpression) /** * Creates an expression that checks if this expression, when evaluated, is not equal to all the @@ -2269,7 +2329,7 @@ abstract class Expr internal constructor() { * @param values The values to check against. * @return A new [BooleanExpr] representing the 'NOT IN' comparison. */ - fun notEqAny(values: List) = Companion.notEqAny(this, values) + fun notEqAny(values: List): BooleanExpr = Companion.notEqAny(this, values) /** * Creates an expression that checks if this expression, when evaluated, is not equal to all the @@ -2279,23 +2339,36 @@ abstract class Expr internal constructor() { * equality to the input. * @return A new [BooleanExpr] representing the 'NOT IN' comparison. */ - fun notEqAny(arrayExpression: Expr) = Companion.notEqAny(this, arrayExpression) + fun notEqAny(arrayExpression: Expr): BooleanExpr = Companion.notEqAny(this, arrayExpression) /** + * Creates an expression that checks if this expression evaluates to 'NaN' (Not a Number). + * + * @return A new [BooleanExpr] representing the isNan operation. */ - fun isNan() = Companion.isNan(this) + fun isNan(): BooleanExpr = Companion.isNan(this) /** + * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a + * Number). + * + * @return A new [BooleanExpr] representing the isNotNan operation. */ - fun isNotNan() = Companion.isNotNan(this) + fun isNotNan(): BooleanExpr = Companion.isNotNan(this) /** + * Creates an expression that checks if tbe result of this expression is null. + * + * @return A new [BooleanExpr] representing the isNull operation. */ - fun isNull() = Companion.isNull(this) + fun isNull(): BooleanExpr = Companion.isNull(this) /** + * Creates an expression that checks if tbe result of this expression is not null. + * + * @return A new [BooleanExpr] representing the isNotNull operation. */ - fun isNotNull() = Companion.isNotNull(this) + fun isNotNull(): BooleanExpr = Companion.isNotNull(this) /** */ @@ -2463,7 +2536,7 @@ abstract class Expr internal constructor() { * @param vector The other vector (represented as an Expr) to compare against. * @return A new [Expr] representing the cosine distance between the two vectors. */ - fun cosineDistance(vector: Expr) = Companion.cosineDistance(this, vector) + fun cosineDistance(vector: Expr): Expr = Companion.cosineDistance(this, vector) /** * Calculates the Cosine distance between this vector expression and a vector literal. @@ -2471,7 +2544,7 @@ abstract class Expr internal constructor() { * @param vector The other vector (as an array of doubles) to compare against. * @return A new [Expr] representing the cosine distance between the two vectors. */ - fun cosineDistance(vector: DoubleArray) = Companion.cosineDistance(this, vector) + fun cosineDistance(vector: DoubleArray): Expr = Companion.cosineDistance(this, vector) /** * Calculates the Cosine distance between this vector expression and a vector literal. @@ -2479,7 +2552,7 @@ abstract class Expr internal constructor() { * @param vector The other vector (represented as an [VectorValue]) to compare against. * @return A new [Expr] representing the cosine distance between the two vectors. */ - fun cosineDistance(vector: VectorValue) = Companion.cosineDistance(this, vector) + fun cosineDistance(vector: VectorValue): Expr = Companion.cosineDistance(this, vector) /** * Calculates the dot product distance between this and another vector expression. @@ -2487,7 +2560,7 @@ abstract class Expr internal constructor() { * @param vector The other vector (represented as an Expr) to compare against. * @return A new [Expr] representing the dot product distance between the two vectors. */ - fun dotProduct(vector: Expr) = Companion.dotProduct(this, vector) + fun dotProduct(vector: Expr): Expr = Companion.dotProduct(this, vector) /** * Calculates the dot product distance between this vector expression and a vector literal. @@ -2495,7 +2568,7 @@ abstract class Expr internal constructor() { * @param vector The other vector (as an array of doubles) to compare against. * @return A new [Expr] representing the dot product distance between the two vectors. */ - fun dotProduct(vector: DoubleArray) = Companion.dotProduct(this, vector) + fun dotProduct(vector: DoubleArray): Expr = Companion.dotProduct(this, vector) /** * Calculates the dot product distance between this vector expression and a vector literal. @@ -2503,7 +2576,7 @@ abstract class Expr internal constructor() { * @param vector The other vector (represented as an [VectorValue]) to compare against. * @return A new [Expr] representing the dot product distance between the two vectors. */ - fun dotProduct(vector: VectorValue) = Companion.dotProduct(this, vector) + fun dotProduct(vector: VectorValue): Expr = Companion.dotProduct(this, vector) /** * Calculates the Euclidean distance between this and another vector expression. @@ -2511,7 +2584,7 @@ abstract class Expr internal constructor() { * @param vector The other vector (represented as an Expr) to compare against. * @return A new [Expr] representing the Euclidean distance between the two vectors. */ - fun euclideanDistance(vector: Expr) = Companion.euclideanDistance(this, vector) + fun euclideanDistance(vector: Expr): Expr = Companion.euclideanDistance(this, vector) /** * Calculates the Euclidean distance between this vector expression and a vector literal. @@ -2519,11 +2592,15 @@ abstract class Expr internal constructor() { * @param vector The other vector (as an array of doubles) to compare against. * @return A new [Expr] representing the Euclidean distance between the two vectors. */ - fun euclideanDistance(vector: DoubleArray) = Companion.euclideanDistance(this, vector) + fun euclideanDistance(vector: DoubleArray): Expr = Companion.euclideanDistance(this, vector) /** + * Calculates the Euclidean distance between this vector expression and a vector literal. + * + * @param vector The other vector (represented as an [VectorValue]) to compare against. + * @return A new [Expr] representing the Euclidean distance between the two vectors. */ - fun euclideanDistance(vector: VectorValue) = Companion.euclideanDistance(this, vector) + fun euclideanDistance(vector: VectorValue): Expr = Companion.euclideanDistance(this, vector) /** */ @@ -2834,18 +2911,18 @@ abstract class Expr internal constructor() { fun exists(): BooleanExpr = Companion.exists(this) /** - * Creates an expression that returns the [catchExpr] argument if there is an - * error, else return the result of this expression. + * Creates an expression that returns the [catchExpr] argument if there is an error, else return + * the result of this expression. * - * @param catchExpr The catch expression that will be evaluated and - * returned if the this expression produces an error. + * @param catchExpr The catch expression that will be evaluated and returned if the this + * expression produces an error. * @return A new [Expr] representing the ifError operation. */ fun ifError(catchExpr: Expr): Expr = Companion.ifError(this, catchExpr) /** - * Creates an expression that returns the [catchValue] argument if there is an - * error, else return the result of this expression. + * Creates an expression that returns the [catchValue] argument if there is an error, else return + * the result of this expression. * * @param catchValue The value that will be returned if this expression produces an error. * @return A new [Expr] representing the ifError operation. @@ -2966,10 +3043,7 @@ internal constructor( /** A class that represents a filter condition. */ open class BooleanExpr internal constructor(name: String, params: Array) : FunctionExpr(name, params, InternalOptions.EMPTY) { - internal constructor( - name: String, - param: Expr - ) : this(name, arrayOf(param)) + internal constructor(name: String, param: Expr) : this(name, arrayOf(param)) internal constructor( name: String, param: Expr, @@ -2980,10 +3054,7 @@ open class BooleanExpr internal constructor(name: String, params: Array Date: Thu, 1 May 2025 14:07:40 -0400 Subject: [PATCH 054/152] More expression work --- .../firebase/firestore/PipelineTest.java | 6 +- .../firestore/pipeline/expressions.kt | 183 +++++++++++++----- 2 files changed, 135 insertions(+), 54 deletions(-) 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 a17dc79ab42..12a4a5aa1d7 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 @@ -25,7 +25,7 @@ import static com.google.firebase.firestore.pipeline.Expr.euclideanDistance; import static com.google.firebase.firestore.pipeline.Expr.field; import static com.google.firebase.firestore.pipeline.Expr.gt; -import static com.google.firebase.firestore.pipeline.Expr.logicalMax; +import static com.google.firebase.firestore.pipeline.Expr.logicalMaximum; import static com.google.firebase.firestore.pipeline.Expr.lt; import static com.google.firebase.firestore.pipeline.Expr.lte; import static com.google.firebase.firestore.pipeline.Expr.mapGet; @@ -724,8 +724,8 @@ public void testLogicalMax() { .collection(randomCol) .where(field("author").eq("Douglas Adams")) .select( - field("rating").logicalMax(4.5).alias("max_rating"), - logicalMax(field("published"), 1900).alias("max_published")) + field("rating").logicalMaximum(4.5).alias("max_rating"), + logicalMaximum(field("published"), 1900).alias("max_published")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) 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 ffb4148123f..34b70aa546d 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 @@ -829,8 +829,7 @@ abstract class Expr internal constructor() { @JvmStatic fun isNan(fieldName: String): BooleanExpr = BooleanExpr("is_nan", fieldName) /** - * Creates an expression that checks if the results of [expr] is NOT 'NaN' (Not a - * Number). + * Creates an expression that checks if the results of [expr] is NOT 'NaN' (Not a Number). * * @param expr The expression to check. * @return A new [BooleanExpr] representing the isNotNan operation. @@ -942,19 +941,48 @@ abstract class Expr internal constructor() { */ @JvmStatic fun byteLength(fieldName: String): Expr = FunctionExpr("byte_length", fieldName) - /** @return A new [Expr] representing the like operation. */ - @JvmStatic fun like(expr: Expr, pattern: Expr): BooleanExpr = BooleanExpr("like", expr, pattern) + /** + * Creates an expression that performs a case-sensitive wildcard string comparison. + * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new [BooleanExpr] representing the like operation. + */ + @JvmStatic + fun like(stringExpression: Expr, pattern: Expr): BooleanExpr = + BooleanExpr("like", stringExpression, pattern) - /** @return A new [Expr] representing the like operation. */ + /** + * Creates an expression that performs a case-sensitive wildcard string comparison. + * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new [BooleanExpr] representing the like operation. + */ @JvmStatic - fun like(expr: Expr, pattern: String): BooleanExpr = BooleanExpr("like", expr, pattern) + fun like(stringExpression: Expr, pattern: String): BooleanExpr = + BooleanExpr("like", stringExpression, pattern) - /** @return A new [Expr] representing the like operation. */ + /** + * Creates an expression that performs a case-sensitive wildcard string comparison against a + * field. + * + * @param fieldName The name of the field containing the string. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new [BooleanExpr] representing the like comparison. + */ @JvmStatic fun like(fieldName: String, pattern: Expr): BooleanExpr = BooleanExpr("like", fieldName, pattern) - /** @return A new [Expr] representing the like operation. */ + /** + * Creates an expression that performs a case-sensitive wildcard string comparison against a + * field. + * + * @param fieldName The name of the field containing the string. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new [BooleanExpr] representing the like comparison. + */ @JvmStatic fun like(fieldName: String, pattern: String): BooleanExpr = BooleanExpr("like", fieldName, pattern) @@ -999,41 +1027,53 @@ abstract class Expr internal constructor() { fun regexMatch(fieldName: String, pattern: String) = BooleanExpr("regex_match", fieldName, pattern) - /** @return A new [Expr] representing the logicalMax operation. */ - @JvmStatic - fun logicalMax(left: Expr, right: Expr): Expr = FunctionExpr("logical_max", left, right) - - /** @return A new [Expr] representing the logicalMax operation. */ - @JvmStatic - fun logicalMax(left: Expr, right: Any): Expr = FunctionExpr("logical_max", left, right) - - /** @return A new [Expr] representing the logicalMax operation. */ - @JvmStatic - fun logicalMax(fieldName: String, other: Expr): Expr = - FunctionExpr("logical_max", fieldName, other) - - /** @return A new [Expr] representing the logicalMax operation. */ - @JvmStatic - fun logicalMax(fieldName: String, other: Any): Expr = - FunctionExpr("logical_max", fieldName, other) - - /** @return A new [Expr] representing the logicalMin operation. */ + /** + * Creates an expression that returns the largest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param expr The first operand expression. + * @param others Optional additional expressions or literals. + * @return A new [Expr] representing the logical maximum operation. + */ @JvmStatic - fun logicalMin(left: Expr, right: Expr): Expr = FunctionExpr("logical_min", left, right) + fun logicalMaximum(expr: Expr, vararg others: Any): Expr = + FunctionExpr("logical_max", expr, *others) - /** @return A new [Expr] representing the logicalMin operation. */ + /** + * Creates an expression that returns the largest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param fieldName The first operand field name. + * @param others Optional additional expressions or literals. + * @return A new [Expr] representing the logical maximum operation. + */ @JvmStatic - fun logicalMin(left: Expr, right: Any): Expr = FunctionExpr("logical_min", left, right) + fun logicalMaximum(fieldName: String, vararg others: Any): Expr = + FunctionExpr("logical_max", fieldName, *others) - /** @return A new [Expr] representing the logicalMin operation. */ + /** + * Creates an expression that returns the smallest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param expr The first operand expression. + * @param others Optional additional expressions or literals. + * @return A new [Expr] representing the logical minimum operation. + */ @JvmStatic - fun logicalMin(fieldName: String, other: Expr): Expr = - FunctionExpr("logical_min", fieldName, other) + fun logicalMinimum(expr: Expr, vararg others: Any): Expr = + FunctionExpr("logical_min", expr, *others) - /** @return A new [Expr] representing the logicalMin operation. */ + /** + * Creates an expression that returns the smallest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param fieldName The first operand field name. + * @param others Optional additional expressions or literals. + * @return A new [Expr] representing the logical minimum operation. + */ @JvmStatic - fun logicalMin(fieldName: String, other: Any): Expr = - FunctionExpr("logical_min", fieldName, other) + fun logicalMinimum(fieldName: String, vararg others: Any): Expr = + FunctionExpr("logical_min", fieldName, *others) /** @return A new [Expr] representing the reverse operation. */ @JvmStatic fun reverse(expr: Expr): Expr = FunctionExpr("reverse", expr) @@ -1193,9 +1233,14 @@ abstract class Expr internal constructor() { internal fun map(elements: Array): Expr = FunctionExpr("map", elements) - /** @return A new [Expr] representing the map operation. */ + /** + * Creates an expression that creates a Firestore map value from an input object. + * + * @param elements The input map to evaluate in the expression. + * @return A new [Expr] representing the map function. + */ @JvmStatic - fun map(elements: Map) = + fun map(elements: Map): Expr = map(elements.flatMap { listOf(constant(it.key), toExprOrConstant(it.value)) }.toTypedArray()) /** @return A new [Expr] representing the mapGet operation. */ @@ -1215,12 +1260,12 @@ abstract class Expr internal constructor() { /** @return A new [Expr] representing the mapMerge operation. */ @JvmStatic fun mapMerge(firstMap: Expr, secondMap: Expr, vararg otherMaps: Expr): Expr = - FunctionExpr("map_merge", firstMap, secondMap, otherMaps) + FunctionExpr("map_merge", firstMap, secondMap, *otherMaps) /** @return A new [Expr] representing the mapMerge operation. */ @JvmStatic fun mapMerge(mapField: String, secondMap: Expr, vararg otherMaps: Expr): Expr = - FunctionExpr("map_merge", mapField, secondMap, otherMaps) + FunctionExpr("map_merge", mapField, secondMap, *otherMaps) /** @return A new [Expr] representing the mapRemove operation. */ @JvmStatic @@ -2341,6 +2386,14 @@ abstract class Expr internal constructor() { */ fun notEqAny(arrayExpression: Expr): BooleanExpr = Companion.notEqAny(this, arrayExpression) + /** + * Creates an expression that returns true if yhe result of this expression is absent. Otherwise, + * returns false even if the value is null. + * + * @return A new [BooleanExpr] representing the isAbsent operation. + */ + fun isAbsent(): BooleanExpr = Companion.isAbsent(this) + /** * Creates an expression that checks if this expression evaluates to 'NaN' (Not a Number). * @@ -2402,48 +2455,76 @@ abstract class Expr internal constructor() { fun byteLength(): Expr = Companion.byteLength(this) /** + * Creates an expression that performs a case-sensitive wildcard string comparison. + * + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new [BooleanExpr] representing the like operation. */ - fun like(pattern: Expr) = Companion.like(this, pattern) + fun like(pattern: Expr): BooleanExpr = Companion.like(this, pattern) /** + * Creates an expression that performs a case-sensitive wildcard string comparison. + * + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new [BooleanExpr] representing the like operation. */ - fun like(pattern: String) = Companion.like(this, pattern) + fun like(pattern: String): BooleanExpr = Companion.like(this, pattern) /** */ - fun regexContains(pattern: Expr) = Companion.regexContains(this, pattern) + fun regexContains(pattern: Expr): BooleanExpr = Companion.regexContains(this, pattern) /** */ - fun regexContains(pattern: String) = Companion.regexContains(this, pattern) + fun regexContains(pattern: String): BooleanExpr = Companion.regexContains(this, pattern) /** */ - fun regexMatch(pattern: Expr) = Companion.regexMatch(this, pattern) + fun regexMatch(pattern: Expr): BooleanExpr = Companion.regexMatch(this, pattern) /** */ - fun regexMatch(pattern: String) = Companion.regexMatch(this, pattern) + fun regexMatch(pattern: String): BooleanExpr = Companion.regexMatch(this, pattern) /** + * Creates an expression that returns the largest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param others Expressions or literals. + * @return A new [Expr] representing the logical maximum operation. */ - fun logicalMax(other: Expr) = Companion.logicalMax(this, other) + fun logicalMaximum(vararg others: Expr): Expr = Companion.logicalMaximum(this, *others) /** + * Creates an expression that returns the largest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param others Expressions or literals. + * @return A new [Expr] representing the logical maximum operation. */ - fun logicalMax(other: Any) = Companion.logicalMax(this, other) + fun logicalMaximum(vararg others: Any): Expr = Companion.logicalMaximum(this, *others) /** + * Creates an expression that returns the smallest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param others Expressions or literals. + * @return A new [Expr] representing the logical minimum operation. */ - fun logicalMin(other: Expr) = Companion.logicalMin(this, other) + fun logicalMinimum(vararg others: Expr): Expr = Companion.logicalMinimum(this, *others) /** + * Creates an expression that returns the smallest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param others Expressions or literals. + * @return A new [Expr] representing the logical minimum operation. */ - fun logicalMin(other: Any) = Companion.logicalMin(this, other) + fun logicalMinimum(vararg others: Any): Expr = Companion.logicalMinimum(this, *others) /** */ - fun reverse() = Companion.reverse(this) + fun reverse(): Expr = Companion.reverse(this) /** */ From c32429818b8cbf144d8356092bc37cc4c8c10133 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 5 May 2025 11:28:02 -0400 Subject: [PATCH 055/152] More expression work --- .../firestore/pipeline/expressions.kt | 147 +++++++++++++++--- 1 file changed, 125 insertions(+), 22 deletions(-) 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 34b70aa546d..771e4c60c98 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 @@ -570,6 +570,53 @@ abstract class Expr internal constructor() { fun bitRightShift(bitsFieldName: String, number: Int): Expr = FunctionExpr("bit_right_shift", bitsFieldName, number) + @JvmStatic + fun round(numericExpr: Expr, decimalPlace: Int): Expr = + FunctionExpr("round", numericExpr, constant(decimalPlace)) + + @JvmStatic + fun round(numericField: String, decimalPlace: Int): Expr = + FunctionExpr("round", numericField, constant(decimalPlace)) + + @JvmStatic fun round(numericExpr: Expr): Expr = FunctionExpr("round", numericExpr) + + @JvmStatic fun round(numericField: String): Expr = FunctionExpr("round", numericField) + + @JvmStatic + fun round(numericExpr: Expr, decimalPlace: Expr): Expr = + FunctionExpr("round", numericExpr, decimalPlace) + + @JvmStatic + fun round(numericField: String, decimalPlace: Expr): Expr = + FunctionExpr("round", numericField, decimalPlace) + + @JvmStatic fun ceil(numericExpr: Expr): Expr = FunctionExpr("ceil", numericExpr) + + @JvmStatic fun ceil(numericField: String): Expr = FunctionExpr("ceil", numericField) + + @JvmStatic fun floor(numericExpr: Expr): Expr = FunctionExpr("floor", numericExpr) + + @JvmStatic fun floor(numericField: String): Expr = FunctionExpr("floor", numericField) + + @JvmStatic + fun pow(numericExpr: Expr, exponent: Number): Expr = + FunctionExpr("pow", numericExpr, constant(exponent)) + + @JvmStatic + fun pow(numericField: String, exponent: Number): Expr = + FunctionExpr("pow", numericField, constant(exponent)) + + @JvmStatic + fun pow(numericExpr: Expr, exponent: Expr): Expr = FunctionExpr("pow", numericExpr, exponent) + + @JvmStatic + fun pow(numericField: String, exponent: Expr): Expr = + FunctionExpr("pow", numericField, exponent) + + @JvmStatic fun sqrt(numericExpr: Expr): Expr = FunctionExpr("sqrt", numericExpr) + + @JvmStatic fun sqrt(numericField: String): Expr = FunctionExpr("sqrt", numericField) + /** * Creates an expression that adds this expression to another expression. * @@ -1243,35 +1290,69 @@ abstract class Expr internal constructor() { fun map(elements: Map): Expr = map(elements.flatMap { listOf(constant(it.key), toExprOrConstant(it.value)) }.toTypedArray()) - /** @return A new [Expr] representing the mapGet operation. */ - @JvmStatic fun mapGet(map: Expr, key: Expr): Expr = FunctionExpr("map_get", map, key) - - /** @return A new [Expr] representing the mapGet operation. */ - @JvmStatic fun mapGet(map: Expr, key: String): Expr = FunctionExpr("map_get", map, key) - - /** @return A new [Expr] representing the mapGet operation. */ + /** + * Accesses a value from a map (object) field using the provided [key]. + * + * @param mapExpression The expression representing the map. + * @param key The key to access in the map. + * @return A new [Expr] representing the value associated with the given key in the map. + */ @JvmStatic - fun mapGet(fieldName: String, key: Expr): Expr = FunctionExpr("map_get", fieldName, key) + fun mapGet(mapExpression: Expr, key: String): Expr = FunctionExpr("map_get", mapExpression, key) - /** @return A new [Expr] representing the mapGet operation. */ + /** + * Accesses a value from a map (object) field using the provided [key]. + * + * @param fieldName The field name of the map field. + * @param key The key to access in the map. + * @return A new [Expr] representing the value associated with the given key in the map. + */ @JvmStatic fun mapGet(fieldName: String, key: String): Expr = FunctionExpr("map_get", fieldName, key) - /** @return A new [Expr] representing the mapMerge operation. */ + /** + * Creates an expression that merges multiple maps into a single map. If multiple maps have the + * same key, the later value is used. + * + * @param firstMap First map expression that will be merged. + * @param secondMap Second map expression that will be merged. + * @param otherMaps Additional maps to merge. + * @return A new [Expr] representing the mapMerge operation. + */ @JvmStatic fun mapMerge(firstMap: Expr, secondMap: Expr, vararg otherMaps: Expr): Expr = FunctionExpr("map_merge", firstMap, secondMap, *otherMaps) - /** @return A new [Expr] representing the mapMerge operation. */ + /** + * Creates an expression that merges multiple maps into a single map. If multiple maps have the + * same key, the later value is used. + * + * @param firstMapFieldName First map field name that will be merged. + * @param secondMap Second map expression that will be merged. + * @param otherMaps Additional maps to merge. + * @return A new [Expr] representing the mapMerge operation. + */ @JvmStatic - fun mapMerge(mapField: String, secondMap: Expr, vararg otherMaps: Expr): Expr = - FunctionExpr("map_merge", mapField, secondMap, *otherMaps) + fun mapMerge(firstMapFieldName: String, secondMap: Expr, vararg otherMaps: Expr): Expr = + FunctionExpr("map_merge", firstMapFieldName, secondMap, *otherMaps) - /** @return A new [Expr] representing the mapRemove operation. */ + /** + * Creates an expression that removes a key from the map produced by evaluating an expression. + * + * @param mapExpr An expression return a map value. + * @param key The name of the key to remove from the input map. + * @return A new [Expr] that evaluates to a modified map. + */ @JvmStatic - fun mapRemove(firstMap: Expr, key: Expr): Expr = FunctionExpr("map_remove", firstMap, key) + fun mapRemove(mapExpr: Expr, key: Expr): Expr = FunctionExpr("map_remove", mapExpr, key) - /** @return A new [Expr] representing the mapRemove operation. */ + /** + * Creates an expression that removes a key from the map produced by evaluating an expression. + * + * @param mapField The name of a field containing a map value. + * @param key The name of the key to remove from the input map. + * @return A new [Expr] that evaluates to a modified map. + */ @JvmStatic fun mapRemove(mapField: String, key: Expr): Expr = FunctionExpr("map_remove", mapField, key) @@ -2348,6 +2429,22 @@ abstract class Expr internal constructor() { */ fun mod(other: Any) = Companion.mod(this, other) + fun round() = Companion.round(this) + + fun round(decimalPlace: Int) = Companion.round(this, decimalPlace) + + fun round(decimalPlace: Expr) = Companion.round(this, decimalPlace) + + fun ceil() = Companion.ceil(this) + + fun floor() = Companion.floor(this) + + fun pow(exponentExpr: Number) = Companion.pow(this, exponentExpr) + + fun pow(exponentExpr: Expr) = Companion.pow(this, exponentExpr) + + fun sqrt() = Companion.sqrt(this) + /** * Creates an expression that checks if this expression, when evaluated, is equal to any of the * provided [values]. @@ -2591,17 +2688,23 @@ abstract class Expr internal constructor() { fun strConcat(vararg string: Any) = Companion.strConcat(this, *string) /** - */ - fun mapGet(key: Expr) = Companion.mapGet(this, key) - - /** + * Accesses a map (object) value using the provided [key]. + * + * @param key The key to access in the map. + * @return A new [Expr] representing the value associated with the given key in the map. */ fun mapGet(key: String) = Companion.mapGet(this, key) /** + * Creates an expression that merges multiple maps into a single map. If multiple maps have the + * same key, the later value is used. + * + * @param mapExpr Map expression that will be merged. + * @param otherMaps Additional maps to merge. + * @return A new [Expr] representing the mapMerge operation. */ - fun mapMerge(secondMap: Expr, vararg otherMaps: Expr) = - Companion.mapMerge(this, secondMap, *otherMaps) + fun mapMerge(mapExpr: Expr, vararg otherMaps: Expr) = + Companion.mapMerge(this, mapExpr, *otherMaps) /** */ From 7e685f79c147da512d998c42f7a0ac856f1bbe88 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 5 May 2025 14:19:50 -0400 Subject: [PATCH 056/152] More expression work --- .../firestore/pipeline/expressions.kt | 192 +++++++++++++++++- 1 file changed, 186 insertions(+), 6 deletions(-) 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 771e4c60c98..9a1af812b31 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 @@ -570,51 +570,175 @@ abstract class Expr internal constructor() { fun bitRightShift(bitsFieldName: String, number: Int): Expr = FunctionExpr("bit_right_shift", bitsFieldName, number) + /** + * Creates an expression that rounds [numericExpr] to nearest integer. + * + * Rounds away from zero in halfway cases. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new [Expr] representing an integer result from the round operation. + */ + @JvmStatic fun round(numericExpr: Expr): Expr = FunctionExpr("round", numericExpr) + + /** + * Creates an expression that rounds [numericField] to nearest integer. + * + * Rounds away from zero in halfway cases. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new [Expr] representing an integer result from the round operation. + */ + @JvmStatic fun round(numericField: String): Expr = FunctionExpr("round", numericField) + + /** + * Creates an expression that rounds off [numericExpr] to [decimalPlace] decimal places if + * [decimalPlace] is positive, rounds off digits to the left of the decimal point if + * [decimalPlace] is negative. Rounds away from zero in halfway cases. + * + * @param numericExpr An expression that returns number when evaluated. + * @param decimalPlace The number of decimal places to round. + * @return A new [Expr] representing the round operation. + */ @JvmStatic fun round(numericExpr: Expr, decimalPlace: Int): Expr = FunctionExpr("round", numericExpr, constant(decimalPlace)) + /** + * Creates an expression that rounds off [numericField] to [decimalPlace] decimal places if + * [decimalPlace] is positive, rounds off digits to the left of the decimal point if + * [decimalPlace] is negative. Rounds away from zero in halfway cases. + * + * @param numericField Name of field that returns number when evaluated. + * @param decimalPlace The number of decimal places to round. + * @return A new [Expr] representing the round operation. + */ @JvmStatic fun round(numericField: String, decimalPlace: Int): Expr = FunctionExpr("round", numericField, constant(decimalPlace)) - @JvmStatic fun round(numericExpr: Expr): Expr = FunctionExpr("round", numericExpr) - - @JvmStatic fun round(numericField: String): Expr = FunctionExpr("round", numericField) - + /** + * Creates an expression that rounds off [numericExpr] to [decimalPlace] decimal places if + * [decimalPlace] is positive, rounds off digits to the left of the decimal point if + * [decimalPlace] is negative. Rounds away from zero in halfway cases. + * + * @param numericExpr An expression that returns number when evaluated. + * @param decimalPlace The number of decimal places to round. + * @return A new [Expr] representing the round operation. + */ @JvmStatic fun round(numericExpr: Expr, decimalPlace: Expr): Expr = FunctionExpr("round", numericExpr, decimalPlace) + /** + * Creates an expression that rounds off [numericField] to [decimalPlace] decimal places if + * [decimalPlace] is positive, rounds off digits to the left of the decimal point if + * [decimalPlace] is negative. Rounds away from zero in halfway cases. + * + * @param numericField Name of field that returns number when evaluated. + * @param decimalPlace The number of decimal places to round. + * @return A new [Expr] representing the round operation. + */ @JvmStatic fun round(numericField: String, decimalPlace: Expr): Expr = FunctionExpr("round", numericField, decimalPlace) + /** + * Creates an expression that returns the smalled integer that isn't less than [numericExpr]. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new [Expr] representing an integer result from the ceil operation. + */ @JvmStatic fun ceil(numericExpr: Expr): Expr = FunctionExpr("ceil", numericExpr) + /** + * Creates an expression that returns the smalled integer that isn't less than [numericField]. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new [Expr] representing an integer result from the ceil operation. + */ @JvmStatic fun ceil(numericField: String): Expr = FunctionExpr("ceil", numericField) + /** + * Creates an expression that returns the largest integer that isn't less than [numericExpr]. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new [Expr] representing an integer result from the floor operation. + */ @JvmStatic fun floor(numericExpr: Expr): Expr = FunctionExpr("floor", numericExpr) + /** + * Creates an expression that returns the largest integer that isn't less than [numericField]. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new [Expr] representing an integer result from the floor operation. + */ @JvmStatic fun floor(numericField: String): Expr = FunctionExpr("floor", numericField) + /** + * Creates an expression that returns the [numericExpr] raised to the power of the [exponent]. + * Returns infinity on overflow and zero on underflow. + * + * @param numericExpr An expression that returns number when evaluated. + * @param exponent The numeric power to raise the [numericExpr]. + * @return A new [Expr] representing a numeric result from raising [numericExpr] to the power of + * [exponent]. + */ @JvmStatic fun pow(numericExpr: Expr, exponent: Number): Expr = FunctionExpr("pow", numericExpr, constant(exponent)) + /** + * Creates an expression that returns the [numericField] raised to the power of the [exponent]. + * Returns infinity on overflow and zero on underflow. + * + * @param numericField Name of field that returns number when evaluated. + * @param exponent The numeric power to raise the [numericField]. + * @return A new [Expr] representing a numeric result from raising [numericField] to the power + * of [exponent]. + */ @JvmStatic fun pow(numericField: String, exponent: Number): Expr = FunctionExpr("pow", numericField, constant(exponent)) + /** + * Creates an expression that returns the [numericExpr] raised to the power of the [exponent]. + * Returns infinity on overflow and zero on underflow. + * + * @param numericExpr An expression that returns number when evaluated. + * @param exponent The numeric power to raise the [numericExpr]. + * @return A new [Expr] representing a numeric result from raising [numericExpr] to the power of + * [exponent]. + */ @JvmStatic fun pow(numericExpr: Expr, exponent: Expr): Expr = FunctionExpr("pow", numericExpr, exponent) + /** + * Creates an expression that returns the [numericField] raised to the power of the [exponent]. + * Returns infinity on overflow and zero on underflow. + * + * @param numericField Name of field that returns number when evaluated. + * @param exponent The numeric power to raise the [numericField]. + * @return A new [Expr] representing a numeric result from raising [numericField] to the power + * of [exponent]. + */ @JvmStatic fun pow(numericField: String, exponent: Expr): Expr = FunctionExpr("pow", numericField, exponent) + /** + * Creates an expression that returns the square root of [numericExpr]. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new [Expr] representing the numeric result of the square root operation. + */ @JvmStatic fun sqrt(numericExpr: Expr): Expr = FunctionExpr("sqrt", numericExpr) + /** + * Creates an expression that returns the square root of [numericField]. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new [Expr] representing the numeric result of the square root operation. + */ @JvmStatic fun sqrt(numericField: String): Expr = FunctionExpr("sqrt", numericField) /** @@ -2429,20 +2553,76 @@ abstract class Expr internal constructor() { */ fun mod(other: Any) = Companion.mod(this, other) + /** + * Creates an expression that rounds this numeric expression to nearest integer. + * + * Rounds away from zero in halfway cases. + * + * @return A new [Expr] representing an integer result from the round operation. + */ fun round() = Companion.round(this) + /** + * Creates an expression that rounds off this numeric expression to [decimalPlace] decimal places + * if [decimalPlace] is positive, rounds off digits to the left of the decimal point if + * [decimalPlace] is negative. Rounds away from zero in halfway cases. + * + * @param decimalPlace The number of decimal places to round. + * @return A new [Expr] representing the round operation. + */ fun round(decimalPlace: Int) = Companion.round(this, decimalPlace) + /** + * Creates an expression that rounds off this numeric expression to [decimalPlace] decimal places + * if [decimalPlace] is positive, rounds off digits to the left of the decimal point if + * [decimalPlace] is negative. Rounds away from zero in halfway cases. + * + * @param decimalPlace The number of decimal places to round. + * @return A new [Expr] representing the round operation. + */ fun round(decimalPlace: Expr) = Companion.round(this, decimalPlace) + /** + * Creates an expression that returns the smalled integer that isn't less than this numeric + * expression. + * + * @return A new [Expr] representing an integer result from the ceil operation. + */ fun ceil() = Companion.ceil(this) + /** + * Creates an expression that returns the largest integer that isn't less than this numeric + * expression. + * + * @return A new [Expr] representing an integer result from the floor operation. + */ fun floor() = Companion.floor(this) - fun pow(exponentExpr: Number) = Companion.pow(this, exponentExpr) + /** + * Creates an expression that returns this numeric expression raised to the power of the + * [exponent]. Returns infinity on overflow and zero on underflow. + * + * @param exponent The numeric power to raise this numeric expression. + * @return A new [Expr] representing a numeric result from raising this numeric expression to the + * power of [exponent]. + */ + fun pow(exponent: Number) = Companion.pow(this, exponent) - fun pow(exponentExpr: Expr) = Companion.pow(this, exponentExpr) + /** + * Creates an expression that returns this numeric expression raised to the power of the + * [exponent]. Returns infinity on overflow and zero on underflow. + * + * @param exponent The numeric power to raise this numeric expression. + * @return A new [Expr] representing a numeric result from raising this numeric expression to the + * power of [exponent]. + */ + fun pow(exponent: Expr) = Companion.pow(this, exponent) + /** + * Creates an expression that returns the square root of this numeric expression. + * + * @return A new [Expr] representing the numeric result of the square root operation. + */ fun sqrt() = Companion.sqrt(this) /** From fec44cf881fc2f4e76e13214427eee9af97135fd Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 5 May 2025 14:31:29 -0400 Subject: [PATCH 057/152] Generate API.txt --- firebase-firestore/api.txt | 476 ++++++++++-------- .../firestore/pipeline/expressions.kt | 12 +- 2 files changed, 284 insertions(+), 204 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 592d2b24ba9..5129580877f 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -737,12 +737,12 @@ package com.google.firebase.firestore.pipeline { public final class AggregateWithAlias { } - public final class BooleanExpr extends com.google.firebase.firestore.pipeline.FunctionExpr { - method public com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); - method public com.google.firebase.firestore.pipeline.Expr cond(Object then, Object otherwise); - method public com.google.firebase.firestore.pipeline.AggregateFunction countIf(); - method public static com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); - method public com.google.firebase.firestore.pipeline.BooleanExpr not(); + public class BooleanExpr extends com.google.firebase.firestore.pipeline.FunctionExpr { + method public final com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); + method public final com.google.firebase.firestore.pipeline.Expr cond(Object then, Object otherwise); + method public final com.google.firebase.firestore.pipeline.AggregateFunction countIf(); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public final com.google.firebase.firestore.pipeline.BooleanExpr not(); field public static final com.google.firebase.firestore.pipeline.BooleanExpr.Companion Companion; } @@ -826,82 +826,98 @@ package com.google.firebase.firestore.pipeline { } public abstract class Expr { - method public final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.Expr add(Object other); - method public static final com.google.firebase.firestore.pipeline.Expr add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr add(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, Object second, java.lang.Object... others); + method public final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public final com.google.firebase.firestore.pipeline.Expr add(Object second, java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr add(String fieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr add(String fieldName, Object second, java.lang.Object... others); method public com.google.firebase.firestore.pipeline.ExprWithAlias alias(String alias); method public static final com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); - method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); - method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); - method public final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr... arrays); - method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); - method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(String fieldName, java.util.List arrays); - method public final com.google.firebase.firestore.pipeline.Expr arrayConcat(java.util.List arrays); - method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr value); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr value); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, Object value); - method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(Object value); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, com.google.firebase.firestore.pipeline.Expr value); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, Object value); + method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); + method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, Object secondArray, java.lang.Object... otherArrays); + method public final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); + method public final com.google.firebase.firestore.pipeline.Expr arrayConcat(Object secondArray, java.lang.Object... otherArrays); + method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(String firstArrayField, com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); + method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(String firstArrayField, Object secondArray, java.lang.Object... otherArrays); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr element); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr element); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, Object element); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(Object element); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String arrayFieldName, com.google.firebase.firestore.pipeline.Expr element); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String arrayFieldName, Object element); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr arrayExpression); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String fieldName, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String arrayFieldName, com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String arrayFieldName, java.util.List values); method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(java.util.List values); + method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr arrayExpression); method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String fieldName, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String arrayFieldName, com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String arrayFieldName, java.util.List values); method public final com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(java.util.List values); method public final com.google.firebase.firestore.pipeline.Expr arrayLength(); method public static final com.google.firebase.firestore.pipeline.Expr arrayLength(com.google.firebase.firestore.pipeline.Expr array); - method public static final com.google.firebase.firestore.pipeline.Expr arrayLength(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Expr arrayLength(String arrayFieldName); + method public final com.google.firebase.firestore.pipeline.Expr arrayOffset(com.google.firebase.firestore.pipeline.Expr offset); + method public static final com.google.firebase.firestore.pipeline.Expr arrayOffset(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr offset); + method public static final com.google.firebase.firestore.pipeline.Expr arrayOffset(com.google.firebase.firestore.pipeline.Expr array, int offset); + method public final com.google.firebase.firestore.pipeline.Expr arrayOffset(int offset); + method public static final com.google.firebase.firestore.pipeline.Expr arrayOffset(String arrayFieldName, com.google.firebase.firestore.pipeline.Expr offset); + method public static final com.google.firebase.firestore.pipeline.Expr arrayOffset(String arrayFieldName, int offset); method public final com.google.firebase.firestore.pipeline.Expr arrayReverse(); method public static final com.google.firebase.firestore.pipeline.Expr arrayReverse(com.google.firebase.firestore.pipeline.Expr array); - method public static final com.google.firebase.firestore.pipeline.Expr arrayReverse(String fieldName); + method public static final com.google.firebase.firestore.pipeline.Expr arrayReverse(String arrayFieldName); method public final com.google.firebase.firestore.pipeline.Ordering ascending(); method public final com.google.firebase.firestore.pipeline.AggregateFunction avg(); - method public final com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.Expr bitAnd(Object right); - method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(String fieldName, Object right); + method public final com.google.firebase.firestore.pipeline.Expr bitAnd(byte[] bitsOther); + method public final com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr bits, byte[] bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(String bitsFieldName, byte[] bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitAnd(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr bitsOther); method public final com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr bits, int number); method public final com.google.firebase.firestore.pipeline.Expr bitLeftShift(int number); - method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(String fieldName, int number); + method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Expr bitLeftShift(String bitsFieldName, int number); method public final com.google.firebase.firestore.pipeline.Expr bitNot(); - method public static final com.google.firebase.firestore.pipeline.Expr bitNot(com.google.firebase.firestore.pipeline.Expr left); - method public static final com.google.firebase.firestore.pipeline.Expr bitNot(String fieldName); - method public final com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.Expr bitOr(Object right); - method public static final com.google.firebase.firestore.pipeline.Expr bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr bitOr(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.Expr bitNot(com.google.firebase.firestore.pipeline.Expr bits); + method public static final com.google.firebase.firestore.pipeline.Expr bitNot(String bitsFieldName); + method public final com.google.firebase.firestore.pipeline.Expr bitOr(byte[] bitsOther); + method public final com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr bits, byte[] bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitOr(String bitsFieldName, byte[] bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitOr(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr bitsOther); method public final com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); + method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr bits, int number); method public final com.google.firebase.firestore.pipeline.Expr bitRightShift(int number); - method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(String fieldName, int number); - method public final com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.Expr bitXor(Object right); - method public static final com.google.firebase.firestore.pipeline.Expr bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr bitXor(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public static final com.google.firebase.firestore.pipeline.Expr bitRightShift(String bitsFieldName, int number); + method public final com.google.firebase.firestore.pipeline.Expr bitXor(byte[] bitsOther); + method public final com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr bits, byte[] bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitXor(String bitsFieldName, byte[] bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr bitXor(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr bitsOther); method public final com.google.firebase.firestore.pipeline.Expr byteLength(); method public static final com.google.firebase.firestore.pipeline.Expr byteLength(com.google.firebase.firestore.pipeline.Expr value); method public static final com.google.firebase.firestore.pipeline.Expr byteLength(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr ceil(); + method public static final com.google.firebase.firestore.pipeline.Expr ceil(com.google.firebase.firestore.pipeline.Expr numericExpr); + method public static final com.google.firebase.firestore.pipeline.Expr ceil(String numericField); method public final com.google.firebase.firestore.pipeline.Expr charLength(); - method public static final com.google.firebase.firestore.pipeline.Expr charLength(com.google.firebase.firestore.pipeline.Expr value); + method public static final com.google.firebase.firestore.pipeline.Expr charLength(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.Expr charLength(String fieldName); - method public static final com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); - method public static final com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object otherwise); - method public static final com.google.firebase.firestore.pipeline.Expr constant(boolean value); + method public static final com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr thenExpr, com.google.firebase.firestore.pipeline.Expr elseExpr); + method public static final com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object thenValue, Object elseValue); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr constant(boolean value); + method public static final com.google.firebase.firestore.pipeline.Expr constant(byte[] value); method public static final com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.Blob value); method public static final com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.DocumentReference ref); method public static final com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.GeoPoint value); @@ -916,9 +932,10 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); method public final com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.VectorValue vector); method public final com.google.firebase.firestore.pipeline.Expr cosineDistance(double[] vector); - method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, double[] vector); + method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(String vectorFieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(String vectorFieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(String vectorFieldName, double[] vector); + method public final com.google.firebase.firestore.pipeline.AggregateFunction count(); method public final com.google.firebase.firestore.pipeline.Ordering descending(); method public final com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); @@ -926,28 +943,35 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.Expr divide(Object other); method public static final com.google.firebase.firestore.pipeline.Expr divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.Expr divide(String fieldName, Object other); + method public final com.google.firebase.firestore.pipeline.Expr documentId(); + method public static final com.google.firebase.firestore.pipeline.Expr documentId(com.google.firebase.firestore.DocumentReference docRef); + method public static final com.google.firebase.firestore.pipeline.Expr documentId(com.google.firebase.firestore.pipeline.Expr documentPath); + method public static final com.google.firebase.firestore.pipeline.Expr documentId(String documentPath); method public final com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector); method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); method public final com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.VectorValue vector); method public final com.google.firebase.firestore.pipeline.Expr dotProduct(double[] vector); - method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, double[] vector); + method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(String vectorFieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(String vectorFieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.Expr dotProduct(String vectorFieldName, double[] vector); method public final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr suffix); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr suffix); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, String suffix); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr stringExpr, com.google.firebase.firestore.pipeline.Expr suffix); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr stringExpr, String suffix); method public final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String suffix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr suffix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, String suffix); method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(Object other); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object right); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); + method public final com.google.firebase.firestore.pipeline.BooleanExpr eq(Object value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object value); + method public final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr expression, com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr expression, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(String fieldName, com.google.firebase.firestore.pipeline.Expr arrayExpression); method public static final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(String fieldName, java.util.List values); method public final com.google.firebase.firestore.pipeline.BooleanExpr eqAny(java.util.List values); method public final com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector); @@ -956,26 +980,37 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); method public final com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.VectorValue vector); method public final com.google.firebase.firestore.pipeline.Expr euclideanDistance(double[] vector); - method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, double[] vector); + method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(String vectorFieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(String vectorFieldName, com.google.firebase.firestore.VectorValue vector); + method public static final com.google.firebase.firestore.pipeline.Expr euclideanDistance(String vectorFieldName, double[] vector); method public final com.google.firebase.firestore.pipeline.BooleanExpr exists(); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr exists(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr exists(com.google.firebase.firestore.pipeline.Expr value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr exists(String fieldName); method public static final com.google.firebase.firestore.pipeline.Field field(com.google.firebase.firestore.FieldPath fieldPath); method public static final com.google.firebase.firestore.pipeline.Field field(String name); + method public final com.google.firebase.firestore.pipeline.Expr floor(); + method public static final com.google.firebase.firestore.pipeline.Expr floor(com.google.firebase.firestore.pipeline.Expr numericExpr); + method public static final com.google.firebase.firestore.pipeline.Expr floor(String numericField); method public static final com.google.firebase.firestore.pipeline.FunctionExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(Object other); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(Object value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, Object value); method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(Object other); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(Object value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, Object value); + method public final com.google.firebase.firestore.pipeline.Expr ifError(com.google.firebase.firestore.pipeline.Expr catchExpr); + method public static final com.google.firebase.firestore.pipeline.Expr ifError(com.google.firebase.firestore.pipeline.Expr tryExpr, com.google.firebase.firestore.pipeline.Expr catchExpr); + method public static final com.google.firebase.firestore.pipeline.Expr ifError(com.google.firebase.firestore.pipeline.Expr tryExpr, Object catchValue); + method public final com.google.firebase.firestore.pipeline.Expr ifError(Object catchValue); + method public final com.google.firebase.firestore.pipeline.BooleanExpr isAbsent(); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr isAbsent(com.google.firebase.firestore.pipeline.Expr value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr isAbsent(String fieldName); method public final com.google.firebase.firestore.pipeline.BooleanExpr isNan(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(String fieldName); @@ -989,47 +1024,40 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNull(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNull(String fieldName); method public final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr pattern); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr stringExpression, String pattern); method public final com.google.firebase.firestore.pipeline.BooleanExpr like(String pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, String pattern); - method public final com.google.firebase.firestore.pipeline.Expr logicalMax(com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.Expr logicalMax(Object other); - method public static final com.google.firebase.firestore.pipeline.Expr logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr logicalMax(String fieldName, Object other); - method public final com.google.firebase.firestore.pipeline.Expr logicalMin(com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.Expr logicalMin(Object other); - method public static final com.google.firebase.firestore.pipeline.Expr logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr logicalMin(String fieldName, Object other); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMaximum(com.google.firebase.firestore.pipeline.Expr expr, java.lang.Object... others); + method public final com.google.firebase.firestore.pipeline.Expr logicalMaximum(com.google.firebase.firestore.pipeline.Expr... others); + method public final com.google.firebase.firestore.pipeline.Expr logicalMaximum(java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMaximum(String fieldName, java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMinimum(com.google.firebase.firestore.pipeline.Expr expr, java.lang.Object... others); + method public final com.google.firebase.firestore.pipeline.Expr logicalMinimum(com.google.firebase.firestore.pipeline.Expr... others); + method public final com.google.firebase.firestore.pipeline.Expr logicalMinimum(java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr logicalMinimum(String fieldName, java.lang.Object... others); method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(Object other); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(Object value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object right); method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(Object other); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(Object value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object value); method public static final com.google.firebase.firestore.pipeline.Expr map(java.util.Map elements); - method public final com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr key); - method public static final com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); - method public static final com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); + method public static final com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr mapExpression, String key); method public final com.google.firebase.firestore.pipeline.Expr mapGet(String key); - method public static final com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); method public static final com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, String key); method public static final com.google.firebase.firestore.pipeline.Expr mapMerge(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); - method public final com.google.firebase.firestore.pipeline.Expr mapMerge(com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); - method public static final com.google.firebase.firestore.pipeline.Expr mapMerge(String mapField, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public final com.google.firebase.firestore.pipeline.Expr mapMerge(com.google.firebase.firestore.pipeline.Expr mapExpr, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public static final com.google.firebase.firestore.pipeline.Expr mapMerge(String firstMapFieldName, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); method public final com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr key); - method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr key); + method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr mapExpr, com.google.firebase.firestore.pipeline.Expr key); method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, String key); method public final com.google.firebase.firestore.pipeline.Expr mapRemove(String key); method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, com.google.firebase.firestore.pipeline.Expr key); @@ -1051,15 +1079,24 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(Object other); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, Object right); + method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(Object value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, Object value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr not(com.google.firebase.firestore.pipeline.BooleanExpr condition); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); + method public final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(com.google.firebase.firestore.pipeline.Expr expression, com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(com.google.firebase.firestore.pipeline.Expr expression, java.util.List values); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(String fieldName, com.google.firebase.firestore.pipeline.Expr arrayExpression); method public static final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(String fieldName, java.util.List values); method public final com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(java.util.List values); method public static final com.google.firebase.firestore.pipeline.Expr nullValue(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr or(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); + method public final com.google.firebase.firestore.pipeline.Expr pow(com.google.firebase.firestore.pipeline.Expr exponent); + method public static final com.google.firebase.firestore.pipeline.Expr pow(com.google.firebase.firestore.pipeline.Expr numericExpr, com.google.firebase.firestore.pipeline.Expr exponent); + method public static final com.google.firebase.firestore.pipeline.Expr pow(com.google.firebase.firestore.pipeline.Expr numericExpr, Number exponent); + method public final com.google.firebase.firestore.pipeline.Expr pow(Number exponent); + method public static final com.google.firebase.firestore.pipeline.Expr pow(String numericField, com.google.firebase.firestore.pipeline.Expr exponent); + method public static final com.google.firebase.firestore.pipeline.Expr pow(String numericField, Number exponent); method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, String pattern); @@ -1085,9 +1122,21 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.Expr reverse(); method public static final com.google.firebase.firestore.pipeline.Expr reverse(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.Expr reverse(String fieldName); + method public final com.google.firebase.firestore.pipeline.Expr round(); + method public static final com.google.firebase.firestore.pipeline.Expr round(com.google.firebase.firestore.pipeline.Expr numericExpr); + method public static final com.google.firebase.firestore.pipeline.Expr round(String numericField); + method public final com.google.firebase.firestore.pipeline.Expr roundToDecimal(com.google.firebase.firestore.pipeline.Expr decimalPlace); + method public static final com.google.firebase.firestore.pipeline.Expr roundToDecimal(com.google.firebase.firestore.pipeline.Expr numericExpr, com.google.firebase.firestore.pipeline.Expr decimalPlace); + method public static final com.google.firebase.firestore.pipeline.Expr roundToDecimal(com.google.firebase.firestore.pipeline.Expr numericExpr, int decimalPlace); + method public final com.google.firebase.firestore.pipeline.Expr roundToDecimal(int decimalPlace); + method public static final com.google.firebase.firestore.pipeline.Expr roundToDecimal(String numericField, com.google.firebase.firestore.pipeline.Expr decimalPlace); + method public static final com.google.firebase.firestore.pipeline.Expr roundToDecimal(String numericField, int decimalPlace); + method public final com.google.firebase.firestore.pipeline.Expr sqrt(); + method public static final com.google.firebase.firestore.pipeline.Expr sqrt(com.google.firebase.firestore.pipeline.Expr numericExpr); + method public static final com.google.firebase.firestore.pipeline.Expr sqrt(String numericField); method public final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr prefix); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr prefix); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, String prefix); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr stringExpr, com.google.firebase.firestore.pipeline.Expr prefix); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr stringExpr, String prefix); method public final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String prefix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr prefix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, String prefix); @@ -1160,56 +1209,67 @@ package com.google.firebase.firestore.pipeline { } public static final class Expr.Companion { - method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Expr add(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Expr add(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, Object second, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr add(String fieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr add(String fieldName, Object second, java.lang.Object... others); method public com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); - method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr... arrays); - method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr array, java.util.List arrays); - method public com.google.firebase.firestore.pipeline.Expr arrayConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... arrays); - method public com.google.firebase.firestore.pipeline.Expr arrayConcat(String fieldName, java.util.List arrays); - method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr value); - method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, Object value); - method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, com.google.firebase.firestore.pipeline.Expr value); - method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String fieldName, Object value); + method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); + method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, Object secondArray, java.lang.Object... otherArrays); + method public com.google.firebase.firestore.pipeline.Expr arrayConcat(String firstArrayField, com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); + method public com.google.firebase.firestore.pipeline.Expr arrayConcat(String firstArrayField, Object secondArray, java.lang.Object... otherArrays); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr element); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(com.google.firebase.firestore.pipeline.Expr array, Object element); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String arrayFieldName, com.google.firebase.firestore.pipeline.Expr element); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContains(String arrayFieldName, Object element); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr arrayExpression); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); - method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String fieldName, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String arrayFieldName, com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAll(String arrayFieldName, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr arrayExpression); method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(com.google.firebase.firestore.pipeline.Expr array, java.util.List values); - method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String fieldName, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String arrayFieldName, com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public com.google.firebase.firestore.pipeline.BooleanExpr arrayContainsAny(String arrayFieldName, java.util.List values); method public com.google.firebase.firestore.pipeline.Expr arrayLength(com.google.firebase.firestore.pipeline.Expr array); - method public com.google.firebase.firestore.pipeline.Expr arrayLength(String fieldName); + method public com.google.firebase.firestore.pipeline.Expr arrayLength(String arrayFieldName); + method public com.google.firebase.firestore.pipeline.Expr arrayOffset(com.google.firebase.firestore.pipeline.Expr array, com.google.firebase.firestore.pipeline.Expr offset); + method public com.google.firebase.firestore.pipeline.Expr arrayOffset(com.google.firebase.firestore.pipeline.Expr array, int offset); + method public com.google.firebase.firestore.pipeline.Expr arrayOffset(String arrayFieldName, com.google.firebase.firestore.pipeline.Expr offset); + method public com.google.firebase.firestore.pipeline.Expr arrayOffset(String arrayFieldName, int offset); method public com.google.firebase.firestore.pipeline.Expr arrayReverse(com.google.firebase.firestore.pipeline.Expr array); - method public com.google.firebase.firestore.pipeline.Expr arrayReverse(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Expr bitAnd(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr bitAnd(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr left, int number); - method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(String fieldName, int number); - method public com.google.firebase.firestore.pipeline.Expr bitNot(com.google.firebase.firestore.pipeline.Expr left); - method public com.google.firebase.firestore.pipeline.Expr bitNot(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Expr bitOr(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr bitOr(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr left, int number); - method public com.google.firebase.firestore.pipeline.Expr bitRightShift(String fieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); - method public com.google.firebase.firestore.pipeline.Expr bitRightShift(String fieldName, int number); - method public com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Expr bitXor(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr bitXor(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.Expr arrayReverse(String arrayFieldName); + method public com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr bits, byte[] bitsOther); + method public com.google.firebase.firestore.pipeline.Expr bitAnd(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr bitsOther); + method public com.google.firebase.firestore.pipeline.Expr bitAnd(String bitsFieldName, byte[] bitsOther); + method public com.google.firebase.firestore.pipeline.Expr bitAnd(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr bitsOther); + method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(com.google.firebase.firestore.pipeline.Expr bits, int number); + method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Expr bitLeftShift(String bitsFieldName, int number); + method public com.google.firebase.firestore.pipeline.Expr bitNot(com.google.firebase.firestore.pipeline.Expr bits); + method public com.google.firebase.firestore.pipeline.Expr bitNot(String bitsFieldName); + method public com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr bits, byte[] bitsOther); + method public com.google.firebase.firestore.pipeline.Expr bitOr(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr bitsOther); + method public com.google.firebase.firestore.pipeline.Expr bitOr(String bitsFieldName, byte[] bitsOther); + method public com.google.firebase.firestore.pipeline.Expr bitOr(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr bitsOther); + method public com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Expr bitRightShift(com.google.firebase.firestore.pipeline.Expr bits, int number); + method public com.google.firebase.firestore.pipeline.Expr bitRightShift(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr numberExpr); + method public com.google.firebase.firestore.pipeline.Expr bitRightShift(String bitsFieldName, int number); + method public com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr bits, byte[] bitsOther); + method public com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr bitsOther); + method public com.google.firebase.firestore.pipeline.Expr bitXor(String bitsFieldName, byte[] bitsOther); + method public com.google.firebase.firestore.pipeline.Expr bitXor(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr bitsOther); method public com.google.firebase.firestore.pipeline.Expr byteLength(com.google.firebase.firestore.pipeline.Expr value); method public com.google.firebase.firestore.pipeline.Expr byteLength(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr charLength(com.google.firebase.firestore.pipeline.Expr value); + method public com.google.firebase.firestore.pipeline.Expr ceil(com.google.firebase.firestore.pipeline.Expr numericExpr); + method public com.google.firebase.firestore.pipeline.Expr ceil(String numericField); + method public com.google.firebase.firestore.pipeline.Expr charLength(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.Expr charLength(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); - method public com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object then, Object otherwise); - method public com.google.firebase.firestore.pipeline.Expr constant(boolean value); + method public com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.Expr thenExpr, com.google.firebase.firestore.pipeline.Expr elseExpr); + method public com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.BooleanExpr condition, Object thenValue, Object elseValue); + method public com.google.firebase.firestore.pipeline.BooleanExpr constant(boolean value); + method public com.google.firebase.firestore.pipeline.Expr constant(byte[] value); method public com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.Blob value); method public com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.DocumentReference ref); method public com.google.firebase.firestore.pipeline.Expr constant(com.google.firebase.firestore.GeoPoint value); @@ -1221,47 +1281,59 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); method public com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); method public com.google.firebase.firestore.pipeline.Expr cosineDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String fieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String vectorFieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String vectorFieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String vectorFieldName, double[] vector); method public com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.Expr divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); method public com.google.firebase.firestore.pipeline.Expr divide(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr documentId(com.google.firebase.firestore.DocumentReference docRef); + method public com.google.firebase.firestore.pipeline.Expr documentId(com.google.firebase.firestore.pipeline.Expr documentPath); + method public com.google.firebase.firestore.pipeline.Expr documentId(String documentPath); method public com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); method public com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); method public com.google.firebase.firestore.pipeline.Expr dotProduct(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public com.google.firebase.firestore.pipeline.Expr dotProduct(String fieldName, double[] vector); - method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr suffix); - method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr expr, String suffix); + method public com.google.firebase.firestore.pipeline.Expr dotProduct(String vectorFieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Expr dotProduct(String vectorFieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.Expr dotProduct(String vectorFieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr stringExpr, com.google.firebase.firestore.pipeline.Expr suffix); + method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(com.google.firebase.firestore.pipeline.Expr stringExpr, String suffix); method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr suffix); method public com.google.firebase.firestore.pipeline.BooleanExpr endsWith(String fieldName, String suffix); method public com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr eq(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object right); - method public com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); + method public com.google.firebase.firestore.pipeline.BooleanExpr eq(String fieldName, Object value); + method public com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr expression, com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public com.google.firebase.firestore.pipeline.BooleanExpr eqAny(com.google.firebase.firestore.pipeline.Expr expression, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr eqAny(String fieldName, com.google.firebase.firestore.pipeline.Expr arrayExpression); method public com.google.firebase.firestore.pipeline.BooleanExpr eqAny(String fieldName, java.util.List values); method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.pipeline.Expr vector2); method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, com.google.firebase.firestore.VectorValue vector2); method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(com.google.firebase.firestore.pipeline.Expr vector1, double[] vector2); - method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, com.google.firebase.firestore.pipeline.Expr vector); - method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, com.google.firebase.firestore.VectorValue vector); - method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(String fieldName, double[] vector); - method public com.google.firebase.firestore.pipeline.BooleanExpr exists(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(String vectorFieldName, com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(String vectorFieldName, com.google.firebase.firestore.VectorValue vector); + method public com.google.firebase.firestore.pipeline.Expr euclideanDistance(String vectorFieldName, double[] vector); + method public com.google.firebase.firestore.pipeline.BooleanExpr exists(com.google.firebase.firestore.pipeline.Expr value); + method public com.google.firebase.firestore.pipeline.BooleanExpr exists(String fieldName); method public com.google.firebase.firestore.pipeline.Field field(com.google.firebase.firestore.FieldPath fieldPath); method public com.google.firebase.firestore.pipeline.Field field(String name); + method public com.google.firebase.firestore.pipeline.Expr floor(com.google.firebase.firestore.pipeline.Expr numericExpr); + method public com.google.firebase.firestore.pipeline.Expr floor(String numericField); method public com.google.firebase.firestore.pipeline.FunctionExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); + method public com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, Object value); method public com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); + method public com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, Object value); + method public com.google.firebase.firestore.pipeline.Expr ifError(com.google.firebase.firestore.pipeline.Expr tryExpr, com.google.firebase.firestore.pipeline.Expr catchExpr); + method public com.google.firebase.firestore.pipeline.Expr ifError(com.google.firebase.firestore.pipeline.Expr tryExpr, Object catchValue); + method public com.google.firebase.firestore.pipeline.BooleanExpr isAbsent(com.google.firebase.firestore.pipeline.Expr value); + method public com.google.firebase.firestore.pipeline.BooleanExpr isAbsent(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr isNan(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.BooleanExpr isNan(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(com.google.firebase.firestore.pipeline.Expr expr); @@ -1270,34 +1342,28 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr isNotNull(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr isNull(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.BooleanExpr isNull(String fieldName); - method public com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); - method public com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr like(com.google.firebase.firestore.pipeline.Expr stringExpression, String pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr like(String fieldName, String pattern); - method public com.google.firebase.firestore.pipeline.Expr logicalMax(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr logicalMax(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Expr logicalMax(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Expr logicalMax(String fieldName, Object other); - method public com.google.firebase.firestore.pipeline.Expr logicalMin(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr logicalMin(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Expr logicalMin(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Expr logicalMin(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr logicalMaximum(com.google.firebase.firestore.pipeline.Expr expr, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr logicalMaximum(String fieldName, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr logicalMinimum(com.google.firebase.firestore.pipeline.Expr expr, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr logicalMinimum(String fieldName, java.lang.Object... others); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr right); + method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); + method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object value); method public com.google.firebase.firestore.pipeline.Expr map(java.util.Map elements); - method public com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr map, com.google.firebase.firestore.pipeline.Expr key); - method public com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr map, String key); - method public com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr mapExpression, String key); method public com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, String key); method public com.google.firebase.firestore.pipeline.Expr mapMerge(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); - method public com.google.firebase.firestore.pipeline.Expr mapMerge(String mapField, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); - method public com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr key); + method public com.google.firebase.firestore.pipeline.Expr mapMerge(String firstMapFieldName, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); + method public com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr mapExpr, com.google.firebase.firestore.pipeline.Expr key); method public com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, String key); method public com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, com.google.firebase.firestore.pipeline.Expr key); method public com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, String key); @@ -1311,13 +1377,19 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr multiply(String fieldName, Object other); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); + method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, Object value); method public com.google.firebase.firestore.pipeline.BooleanExpr not(com.google.firebase.firestore.pipeline.BooleanExpr condition); - method public com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(com.google.firebase.firestore.pipeline.Expr value, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(com.google.firebase.firestore.pipeline.Expr expression, com.google.firebase.firestore.pipeline.Expr arrayExpression); + method public com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(com.google.firebase.firestore.pipeline.Expr expression, java.util.List values); + method public com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(String fieldName, com.google.firebase.firestore.pipeline.Expr arrayExpression); method public com.google.firebase.firestore.pipeline.BooleanExpr notEqAny(String fieldName, java.util.List values); method public com.google.firebase.firestore.pipeline.Expr nullValue(); method public com.google.firebase.firestore.pipeline.BooleanExpr or(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); + method public com.google.firebase.firestore.pipeline.Expr pow(com.google.firebase.firestore.pipeline.Expr numericExpr, com.google.firebase.firestore.pipeline.Expr exponent); + method public com.google.firebase.firestore.pipeline.Expr pow(com.google.firebase.firestore.pipeline.Expr numericExpr, Number exponent); + method public com.google.firebase.firestore.pipeline.Expr pow(String numericField, com.google.firebase.firestore.pipeline.Expr exponent); + method public com.google.firebase.firestore.pipeline.Expr pow(String numericField, Number exponent); method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, String pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); @@ -1334,8 +1406,16 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr replaceFirst(String fieldName, String find, String replace); method public com.google.firebase.firestore.pipeline.Expr reverse(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.Expr reverse(String fieldName); - method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr prefix); - method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr expr, String prefix); + method public com.google.firebase.firestore.pipeline.Expr round(com.google.firebase.firestore.pipeline.Expr numericExpr); + method public com.google.firebase.firestore.pipeline.Expr round(String numericField); + method public com.google.firebase.firestore.pipeline.Expr roundToDecimal(com.google.firebase.firestore.pipeline.Expr numericExpr, com.google.firebase.firestore.pipeline.Expr decimalPlace); + method public com.google.firebase.firestore.pipeline.Expr roundToDecimal(com.google.firebase.firestore.pipeline.Expr numericExpr, int decimalPlace); + method public com.google.firebase.firestore.pipeline.Expr roundToDecimal(String numericField, com.google.firebase.firestore.pipeline.Expr decimalPlace); + method public com.google.firebase.firestore.pipeline.Expr roundToDecimal(String numericField, int decimalPlace); + method public com.google.firebase.firestore.pipeline.Expr sqrt(com.google.firebase.firestore.pipeline.Expr numericExpr); + method public com.google.firebase.firestore.pipeline.Expr sqrt(String numericField); + method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr stringExpr, com.google.firebase.firestore.pipeline.Expr prefix); + method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr stringExpr, String prefix); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr prefix); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, String prefix); method public com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); 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 9a1af812b31..44e35d146a9 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 @@ -600,7 +600,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the round operation. */ @JvmStatic - fun round(numericExpr: Expr, decimalPlace: Int): Expr = + fun roundToDecimal(numericExpr: Expr, decimalPlace: Int): Expr = FunctionExpr("round", numericExpr, constant(decimalPlace)) /** @@ -613,7 +613,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the round operation. */ @JvmStatic - fun round(numericField: String, decimalPlace: Int): Expr = + fun roundToDecimal(numericField: String, decimalPlace: Int): Expr = FunctionExpr("round", numericField, constant(decimalPlace)) /** @@ -626,7 +626,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the round operation. */ @JvmStatic - fun round(numericExpr: Expr, decimalPlace: Expr): Expr = + fun roundToDecimal(numericExpr: Expr, decimalPlace: Expr): Expr = FunctionExpr("round", numericExpr, decimalPlace) /** @@ -639,7 +639,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the round operation. */ @JvmStatic - fun round(numericField: String, decimalPlace: Expr): Expr = + fun roundToDecimal(numericField: String, decimalPlace: Expr): Expr = FunctionExpr("round", numericField, decimalPlace) /** @@ -2570,7 +2570,7 @@ abstract class Expr internal constructor() { * @param decimalPlace The number of decimal places to round. * @return A new [Expr] representing the round operation. */ - fun round(decimalPlace: Int) = Companion.round(this, decimalPlace) + fun roundToDecimal(decimalPlace: Int) = Companion.roundToDecimal(this, decimalPlace) /** * Creates an expression that rounds off this numeric expression to [decimalPlace] decimal places @@ -2580,7 +2580,7 @@ abstract class Expr internal constructor() { * @param decimalPlace The number of decimal places to round. * @return A new [Expr] representing the round operation. */ - fun round(decimalPlace: Expr) = Companion.round(this, decimalPlace) + fun roundToDecimal(decimalPlace: Expr) = Companion.roundToDecimal(this, decimalPlace) /** * Creates an expression that returns the smalled integer that isn't less than this numeric From c320e1e1e024d861251efee90c8f666c9f19cc77 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 5 May 2025 22:45:48 -0400 Subject: [PATCH 058/152] More expression work --- firebase-firestore/api.txt | 98 +++--- .../firestore/pipeline/expressions.kt | 332 +++++++++++++----- 2 files changed, 295 insertions(+), 135 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 5129580877f..4de6a67fff5 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -827,11 +827,11 @@ package com.google.firebase.firestore.pipeline { public abstract class Expr { method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, Object second, java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, Number second, java.lang.Object... others); method public final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public final com.google.firebase.firestore.pipeline.Expr add(Object second, java.lang.Object... others); - method public static final com.google.firebase.firestore.pipeline.Expr add(String fieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public static final com.google.firebase.firestore.pipeline.Expr add(String fieldName, Object second, java.lang.Object... others); + method public final com.google.firebase.firestore.pipeline.Expr add(Number second, java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, Number second, java.lang.Object... others); method public com.google.firebase.firestore.pipeline.ExprWithAlias alias(String alias); method public static final com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); @@ -937,12 +937,12 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.Expr cosineDistance(String vectorFieldName, double[] vector); method public final com.google.firebase.firestore.pipeline.AggregateFunction count(); method public final com.google.firebase.firestore.pipeline.Ordering descending(); - method public final com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.Expr divide(Object other); - method public static final com.google.firebase.firestore.pipeline.Expr divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr divide(String fieldName, Object other); + method public final com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr divisor); + method public static final com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr dividend, com.google.firebase.firestore.pipeline.Expr divisor); + method public static final com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr dividend, Number divisor); + method public final com.google.firebase.firestore.pipeline.Expr divide(Number divisor); + method public static final com.google.firebase.firestore.pipeline.Expr divide(String dividendFieldName, com.google.firebase.firestore.pipeline.Expr divisor); + method public static final com.google.firebase.firestore.pipeline.Expr divide(String dividendFieldName, Number divisor); method public final com.google.firebase.firestore.pipeline.Expr documentId(); method public static final com.google.firebase.firestore.pipeline.Expr documentId(com.google.firebase.firestore.DocumentReference docRef); method public static final com.google.firebase.firestore.pipeline.Expr documentId(com.google.firebase.firestore.pipeline.Expr documentPath); @@ -1058,24 +1058,24 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.Expr mapMerge(String firstMapFieldName, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); method public final com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr key); method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr mapExpr, com.google.firebase.firestore.pipeline.Expr key); - method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, String key); + method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr mapExpr, String key); method public final com.google.firebase.firestore.pipeline.Expr mapRemove(String key); method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, com.google.firebase.firestore.pipeline.Expr key); method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, String key); method public final com.google.firebase.firestore.pipeline.AggregateFunction max(); method public final com.google.firebase.firestore.pipeline.AggregateFunction min(); - method public final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.Expr mod(Object other); - method public static final com.google.firebase.firestore.pipeline.Expr mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr mod(String fieldName, Object other); - method public final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.Expr multiply(Object other); - method public static final com.google.firebase.firestore.pipeline.Expr multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr multiply(String fieldName, Object other); + method public final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr divisor); + method public static final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr dividend, com.google.firebase.firestore.pipeline.Expr divisor); + method public static final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr dividend, Number divisor); + method public final com.google.firebase.firestore.pipeline.Expr mod(Number divisor); + method public static final com.google.firebase.firestore.pipeline.Expr mod(String dividendFieldName, com.google.firebase.firestore.pipeline.Expr divisor); + method public static final com.google.firebase.firestore.pipeline.Expr mod(String dividendFieldName, Number divisor); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, Number second, java.lang.Object... others); + method public final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public final com.google.firebase.firestore.pipeline.Expr multiply(Number second, java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, Number second, java.lang.Object... others); method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); @@ -1153,12 +1153,12 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String substring); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, com.google.firebase.firestore.pipeline.Expr substring); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, String substring); - method public final com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public static final com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public final com.google.firebase.firestore.pipeline.Expr subtract(Object other); - method public static final com.google.firebase.firestore.pipeline.Expr subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public static final com.google.firebase.firestore.pipeline.Expr subtract(String fieldName, Object other); + method public final com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr subtrahend); + method public static final com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr minuend, com.google.firebase.firestore.pipeline.Expr subtrahend); + method public static final com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr minuend, Number subtrahend); + method public final com.google.firebase.firestore.pipeline.Expr subtract(Number subtrahend); + method public static final com.google.firebase.firestore.pipeline.Expr subtract(String numericFieldName, com.google.firebase.firestore.pipeline.Expr subtrahend); + method public static final com.google.firebase.firestore.pipeline.Expr subtract(String numericFieldName, Number subtrahend); method public final com.google.firebase.firestore.pipeline.AggregateFunction sum(); method public final com.google.firebase.firestore.pipeline.Expr timestampAdd(com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); method public static final com.google.firebase.firestore.pipeline.Expr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); @@ -1210,9 +1210,9 @@ package com.google.firebase.firestore.pipeline { public static final class Expr.Companion { method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, Object second, java.lang.Object... others); - method public com.google.firebase.firestore.pipeline.Expr add(String fieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public com.google.firebase.firestore.pipeline.Expr add(String fieldName, Object second, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, Number second, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, Number second, java.lang.Object... others); method public com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, Object secondArray, java.lang.Object... otherArrays); @@ -1284,10 +1284,10 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String vectorFieldName, com.google.firebase.firestore.pipeline.Expr vector); method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String vectorFieldName, com.google.firebase.firestore.VectorValue vector); method public com.google.firebase.firestore.pipeline.Expr cosineDistance(String vectorFieldName, double[] vector); - method public com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Expr divide(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Expr divide(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr dividend, com.google.firebase.firestore.pipeline.Expr divisor); + method public com.google.firebase.firestore.pipeline.Expr divide(com.google.firebase.firestore.pipeline.Expr dividend, Number divisor); + method public com.google.firebase.firestore.pipeline.Expr divide(String dividendFieldName, com.google.firebase.firestore.pipeline.Expr divisor); + method public com.google.firebase.firestore.pipeline.Expr divide(String dividendFieldName, Number divisor); method public com.google.firebase.firestore.pipeline.Expr documentId(com.google.firebase.firestore.DocumentReference docRef); method public com.google.firebase.firestore.pipeline.Expr documentId(com.google.firebase.firestore.pipeline.Expr documentPath); method public com.google.firebase.firestore.pipeline.Expr documentId(String documentPath); @@ -1364,17 +1364,17 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr mapMerge(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); method public com.google.firebase.firestore.pipeline.Expr mapMerge(String firstMapFieldName, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); method public com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr mapExpr, com.google.firebase.firestore.pipeline.Expr key); - method public com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr firstMap, String key); + method public com.google.firebase.firestore.pipeline.Expr mapRemove(com.google.firebase.firestore.pipeline.Expr mapExpr, String key); method public com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, com.google.firebase.firestore.pipeline.Expr key); method public com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, String key); - method public com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Expr mod(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Expr mod(String fieldName, Object other); - method public com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Expr multiply(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Expr multiply(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr dividend, com.google.firebase.firestore.pipeline.Expr divisor); + method public com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr dividend, Number divisor); + method public com.google.firebase.firestore.pipeline.Expr mod(String dividendFieldName, com.google.firebase.firestore.pipeline.Expr divisor); + method public com.google.firebase.firestore.pipeline.Expr mod(String dividendFieldName, Number divisor); + method public com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, Number second, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, Number second, java.lang.Object... others); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); @@ -1426,10 +1426,10 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, String substring); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, com.google.firebase.firestore.pipeline.Expr substring); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, String substring); - method public com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); - method public com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr left, Object right); - method public com.google.firebase.firestore.pipeline.Expr subtract(String fieldName, com.google.firebase.firestore.pipeline.Expr other); - method public com.google.firebase.firestore.pipeline.Expr subtract(String fieldName, Object other); + method public com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr minuend, com.google.firebase.firestore.pipeline.Expr subtrahend); + method public com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr minuend, Number subtrahend); + method public com.google.firebase.firestore.pipeline.Expr subtract(String numericFieldName, com.google.firebase.firestore.pipeline.Expr subtrahend); + method public com.google.firebase.firestore.pipeline.Expr subtract(String numericFieldName, Number subtrahend); method public com.google.firebase.firestore.pipeline.Expr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); method public com.google.firebase.firestore.pipeline.Expr timestampAdd(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); method public com.google.firebase.firestore.pipeline.Expr timestampAdd(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); 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 44e35d146a9..d33f41dddda 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 @@ -325,8 +325,13 @@ abstract class Expr internal constructor() { fun xor(condition: BooleanExpr, vararg conditions: BooleanExpr) = BooleanExpr("xor", condition, *conditions) - /** @return A new [Expr] representing the not operation. */ - @JvmStatic fun not(condition: BooleanExpr) = BooleanExpr("not", condition) + /** + * Creates an expression that negates a boolean expression. + * + * @param condition The boolean expression to negate. + * @return A new [Expr] representing the not operation. + */ + @JvmStatic fun not(condition: BooleanExpr): BooleanExpr = BooleanExpr("not", condition) /** * Creates an expression that applies a bitwise AND operation between two expressions. @@ -742,11 +747,11 @@ abstract class Expr internal constructor() { @JvmStatic fun sqrt(numericField: String): Expr = FunctionExpr("sqrt", numericField) /** - * Creates an expression that adds this expression to another expression. + * Creates an expression that adds numeric expressions and constants. * - * @param first The first expression to add. - * @param second The second expression to add to first expression. - * @param others Additional expression or literal to add. + * @param first Numeric expression to add. + * @param second Numeric expression to add. + * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ @JvmStatic @@ -754,116 +759,219 @@ abstract class Expr internal constructor() { FunctionExpr("add", first, second, *others) /** - * Creates an expression that adds this expression to another expression. + * Creates an expression that adds numeric expressions and constants. * - * @param first The first expression to add. - * @param second The second expression or literal to add to first expression. - * @param others Additional expression or literal to add. + * @param first Numeric expression to add. + * @param second Constant to add. + * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ @JvmStatic - fun add(first: Expr, second: Any, vararg others: Any): Expr = + fun add(first: Expr, second: Number, vararg others: Any): Expr = FunctionExpr("add", first, second, *others) /** - * Creates an expression that adds a field's value to an expression. + * Creates an expression that adds a numeric field with numeric expressions and constants. * - * @param fieldName The name of the field containing the value to add. - * @param second The second expression to add to field value. - * @param others Additional expression or literal to add. + * @param numericFieldName Numeric field to add. + * @param second Numeric expression to add to field value. + * @param others Additional numeric expressions or constants to add. + * @return A new [Expr] representing the addition operation. */ @JvmStatic - fun add(fieldName: String, second: Expr, vararg others: Any): Expr = - FunctionExpr("add", fieldName, second, *others) + fun add(numericFieldName: String, second: Expr, vararg others: Any): Expr = + FunctionExpr("add", numericFieldName, second, *others) /** - * Creates an expression that adds a field's value to an expression. + * Creates an expression that adds a numeric field with numeric expressions and constants. * - * @param fieldName The name of the field containing the value to add. - * @param second The second expression or literal to add to field value. - * @param others Additional expression or literal to add. + * @param numericFieldName Numeric field to add. + * @param second Constant to add. + * @param others Additional numeric expressions or constants to add. + * @return A new [Expr] representing the addition operation. */ @JvmStatic - fun add(fieldName: String, second: Any, vararg others: Any): Expr = - FunctionExpr("add", fieldName, second, *others) + fun add(numericFieldName: String, second: Number, vararg others: Any): Expr = + FunctionExpr("add", numericFieldName, second, *others) - /** @return A new [Expr] representing the subtract operation. */ - @JvmStatic fun subtract(left: Expr, right: Expr): Expr = FunctionExpr("subtract", left, right) + /** + * Creates an expression that subtracts two expressions. + * + * @param minuend Numeric expression to subtract from. + * @param subtrahend Numeric expression to subtract. + * @return A new [Expr] representing the subtract operation. + */ + @JvmStatic + fun subtract(minuend: Expr, subtrahend: Expr): Expr = + FunctionExpr("subtract", minuend, subtrahend) - /** @return A new [Expr] representing the subtract operation. */ - @JvmStatic fun subtract(left: Expr, right: Any): Expr = FunctionExpr("subtract", left, right) + /** + * Creates an expression that subtracts a constant value from a numeric expression. + * + * @param minuend Numeric expression to subtract from. + * @param subtrahend Constant to subtract. + * @return A new [Expr] representing the subtract operation. + */ + @JvmStatic + fun subtract(minuend: Expr, subtrahend: Number): Expr = + FunctionExpr("subtract", minuend, subtrahend) - /** @return A new [Expr] representing the subtract operation. */ + /** + * Creates an expression that subtracts a numeric expressions from numeric field. + * + * @param numericFieldName Numeric field to subtract from. + * @param subtrahend Numeric expression to subtract. + * @return A new [Expr] representing the subtract operation. + */ @JvmStatic - fun subtract(fieldName: String, other: Expr): Expr = FunctionExpr("subtract", fieldName, other) + fun subtract(numericFieldName: String, subtrahend: Expr): Expr = + FunctionExpr("subtract", numericFieldName, subtrahend) - /** @return A new [Expr] representing the subtract operation. */ + /** + * Creates an expression that subtracts a constant from numeric field. + * + * @param numericFieldName Numeric field to subtract from. + * @param subtrahend Constant to subtract. + * @return A new [Expr] representing the subtract operation. + */ @JvmStatic - fun subtract(fieldName: String, other: Any): Expr = FunctionExpr("subtract", fieldName, other) + fun subtract(numericFieldName: String, subtrahend: Number): Expr = + FunctionExpr("subtract", numericFieldName, subtrahend) - /** @return A new [Expr] representing the multiply operation. */ - @JvmStatic fun multiply(left: Expr, right: Expr): Expr = FunctionExpr("multiply", left, right) + /** + * Creates an expression that multiplies numeric expressions and constants. + * + * @param first Numeric expression to multiply. + * @param second Numeric expression to multiply. + * @param others Additional numeric expressions or constants to multiply. + * @return A new [Expr] representing the multiplication operation. + */ + @JvmStatic + fun multiply(first: Expr, second: Expr, vararg others: Any): Expr = + FunctionExpr("multiply", first, second, *others) - /** @return A new [Expr] representing the multiply operation. */ - @JvmStatic fun multiply(left: Expr, right: Any): Expr = FunctionExpr("multiply", left, right) + /** + * Creates an expression that multiplies numeric expressions and constants. + * + * @param first Numeric expression to multiply. + * @param second Constant to multiply. + * @param others Additional numeric expressions or constants to multiply. + * @return A new [Expr] representing the multiplication operation. + */ + @JvmStatic + fun multiply(first: Expr, second: Number, vararg others: Any): Expr = + FunctionExpr("multiply", first, second, *others) - /** @return A new [Expr] representing the multiply operation. */ + /** + * Creates an expression that multiplies a numeric field with numeric expressions and constants. + * + * @param numericFieldName Numeric field to multiply. + * @param second Numeric expression to add to field multiply. + * @param others Additional numeric expressions or constants to multiply. + * @return A new [Expr] representing the multiplication operation. + */ @JvmStatic - fun multiply(fieldName: String, other: Expr): Expr = FunctionExpr("multiply", fieldName, other) + fun multiply(numericFieldName: String, second: Expr, vararg others: Any): Expr = + FunctionExpr("multiply", numericFieldName, second, *others) - /** @return A new [Expr] representing the multiply operation. */ + /** + * Creates an expression that multiplies a numeric field with numeric expressions and constants. + * + * @param numericFieldName Numeric field to multiply. + * @param second Constant to multiply. + * @param others Additional numeric expressions or constants to multiply. + * @return A new [Expr] representing the multiplication operation. + */ @JvmStatic - fun multiply(fieldName: String, other: Any): Expr = FunctionExpr("multiply", fieldName, other) + fun multiply(numericFieldName: String, second: Number, vararg others: Any): Expr = + FunctionExpr("multiply", numericFieldName, second, *others) /** - * Creates an expression that divides two expressions. + * Creates an expression that divides two numeric expressions. * - * @param left The expression to be divided. - * @param right The expression to divide by. + * @param dividend The numeric expression to be divided. + * @param divisor The numeric expression to divide by. * @return A new [Expr] representing the division operation. */ - @JvmStatic fun divide(left: Expr, right: Expr): Expr = FunctionExpr("divide", left, right) + @JvmStatic + fun divide(dividend: Expr, divisor: Expr): Expr = FunctionExpr("divide", dividend, divisor) /** - * Creates an expression that divides an expression by an expression by a value. + * Creates an expression that divides a numeric expression by a constant. * - * @param left The expression to be divided. - * @param right The value to divide by. + * @param dividend The numeric expression to be divided. + * @param divisor The constant to divide by. * @return A new [Expr] representing the division operation. */ - @JvmStatic fun divide(left: Expr, right: Any): Expr = FunctionExpr("divide", left, right) + @JvmStatic + fun divide(dividend: Expr, divisor: Number): Expr = FunctionExpr("divide", dividend, divisor) /** - * Creates an expression that divides a field's value by an expression. + * Creates an expression that divides numeric field by a numeric expression. * - * @param fieldName The field name to be divided. - * @param other The expression to divide by. + * @param dividendFieldName The numeric field name to be divided. + * @param divisor The numeric expression to divide by. * @return A new [Expr] representing the divide operation. */ @JvmStatic - fun divide(fieldName: String, other: Expr): Expr = FunctionExpr("divide", fieldName, other) + fun divide(dividendFieldName: String, divisor: Expr): Expr = + FunctionExpr("divide", dividendFieldName, divisor) /** - * Creates an expression that divides a field's value by a value. + * Creates an expression that divides a numeric field by a constant. * - * @param fieldName The field name to be divided. - * @param other The value to divide by. + * @param dividendFieldName The numeric field name to be divided. + * @param divisor The constant to divide by. * @return A new [Expr] representing the divide operation. */ @JvmStatic - fun divide(fieldName: String, other: Any): Expr = FunctionExpr("divide", fieldName, other) + fun divide(dividendFieldName: String, divisor: Number): Expr = + FunctionExpr("divide", dividendFieldName, divisor) - /** @return A new [Expr] representing the mod operation. */ - @JvmStatic fun mod(left: Expr, right: Expr): Expr = FunctionExpr("mod", left, right) + /** + * Creates an expression that calculates the modulo (remainder) of dividing two numeric + * expressions. + * + * @param dividend The numeric expression to be divided. + * @param divisor The numeric expression to divide by. + * @return A new [Expr] representing the modulo operation. + */ + @JvmStatic fun mod(dividend: Expr, divisor: Expr): Expr = FunctionExpr("mod", dividend, divisor) - /** @return A new [Expr] representing the mod operation. */ - @JvmStatic fun mod(left: Expr, right: Any): Expr = FunctionExpr("mod", left, right) + /** + * Creates an expression that calculates the modulo (remainder) of dividing a numeric expression + * by a constant. + * + * @param dividend The numeric expression to be divided. + * @param divisor The constant to divide by. + * @return A new [Expr] representing the modulo operation. + */ + @JvmStatic + fun mod(dividend: Expr, divisor: Number): Expr = FunctionExpr("mod", dividend, divisor) - /** @return A new [Expr] representing the mod operation. */ - @JvmStatic fun mod(fieldName: String, other: Expr): Expr = FunctionExpr("mod", fieldName, other) + /** + * Creates an expression that calculates the modulo (remainder) of dividing a numeric field by a + * constant. + * + * @param dividendFieldName The numeric field name to be divided. + * @param divisor The numeric expression to divide by. + * @return A new [Expr] representing the modulo operation. + */ + @JvmStatic + fun mod(dividendFieldName: String, divisor: Expr): Expr = + FunctionExpr("mod", dividendFieldName, divisor) - /** @return A new [Expr] representing the mod operation. */ - @JvmStatic fun mod(fieldName: String, other: Any): Expr = FunctionExpr("mod", fieldName, other) + /** + * Creates an expression that calculates the modulo (remainder) of dividing a numeric field by a + * constant. + * + * @param dividendFieldName The numeric field name to be divided. + * @param divisor The constant to divide by. + * @return A new [Expr] representing the modulo operation. + */ + @JvmStatic + fun mod(dividendFieldName: String, divisor: Number): Expr = + FunctionExpr("mod", dividendFieldName, divisor) /** * Creates an expression that checks if an [expression], when evaluated, is equal to any of the @@ -1463,7 +1571,7 @@ abstract class Expr internal constructor() { /** * Creates an expression that removes a key from the map produced by evaluating an expression. * - * @param mapExpr An expression return a map value. + * @param mapExpr An expression that evaluates to a map. * @param key The name of the key to remove from the input map. * @return A new [Expr] that evaluates to a modified map. */ @@ -1480,11 +1588,23 @@ abstract class Expr internal constructor() { @JvmStatic fun mapRemove(mapField: String, key: Expr): Expr = FunctionExpr("map_remove", mapField, key) - /** @return A new [Expr] representing the mapRemove operation. */ + /** + * Creates an expression that removes a key from the map produced by evaluating an expression. + * + * @param mapExpr An expression that evaluates to a map. + * @param key The name of the key to remove from the input map. + * @return A new [Expr] that evaluates to a modified map. + */ @JvmStatic - fun mapRemove(firstMap: Expr, key: String): Expr = FunctionExpr("map_remove", firstMap, key) + fun mapRemove(mapExpr: Expr, key: String): Expr = FunctionExpr("map_remove", mapExpr, key) - /** @return A new [Expr] representing the mapRemove operation. */ + /** + * Creates an expression that removes a key from the map produced by evaluating an expression. + * + * @param mapField The name of a field containing a map value. + * @param key The name of the key to remove from the input map. + * @return A new [Expr] that evaluates to a modified map. + */ @JvmStatic fun mapRemove(mapField: String, key: String): Expr = FunctionExpr("map_remove", mapField, key) @@ -2496,62 +2616,94 @@ abstract class Expr internal constructor() { fun documentId(): Expr = Companion.documentId(this) /** - * Creates an expression that adds this expression to another expression. + * Creates an expression that adds this numeric expression to other numeric expressions and + * constants. * - * @param second The second expression to add to this expression. - * @param others Additional expression or literal to add to this expression. + * @param second Numeric expression to add. + * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ fun add(second: Expr, vararg others: Any) = Companion.add(this, second, *others) /** - * Creates an expression that adds this expression to another expression. + * Creates an expression that adds this numeric expression to other numeric expressions and + * constants. * - * @param second The second expression or literal to add to this expression. - * @param others Additional expression or literal to add to this expression. + * @param second Constant to add. + * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ - fun add(second: Any, vararg others: Any) = Companion.add(this, second, *others) + fun add(second: Number, vararg others: Any) = Companion.add(this, second, *others) /** + * Creates an expression that subtracts a constant from this numeric expression. + * + * @param subtrahend Numeric expression to subtract. + * @return A new [Expr] representing the subtract operation. */ - fun subtract(other: Expr) = Companion.subtract(this, other) + fun subtract(subtrahend: Expr) = Companion.subtract(this, subtrahend) /** + * Creates an expression that subtracts a numeric expressions from this numeric expression. + * + * @param subtrahend Constant to subtract. + * @return A new [Expr] representing the subtract operation. */ - fun subtract(other: Any) = Companion.subtract(this, other) + fun subtract(subtrahend: Number) = Companion.subtract(this, subtrahend) /** + * Creates an expression that multiplies this numeric expression to other numeric expressions and + * constants. + * + * @param second Numeric expression to multiply. + * @param others Additional numeric expressions or constants to multiply. + * @return A new [Expr] representing the multiplication operation. */ - fun multiply(other: Expr) = Companion.multiply(this, other) + fun multiply(second: Expr, vararg others: Any) = Companion.multiply(this, second, *others) /** + * Creates an expression that multiplies this numeric expression to other numeric expressions and + * constants. + * + * @param second Constant to multiply. + * @param others Additional numeric expressions or constants to multiply. + * @return A new [Expr] representing the multiplication operation. */ - fun multiply(other: Any) = Companion.multiply(this, other) + fun multiply(second: Number, vararg others: Any) = Companion.multiply(this, second, *others) /** - * Creates an expression that divides this expression by another expression. + * Creates an expression that divides this numeric expression by another numeric expression. * - * @param other The expression to divide by. + * @param divisor Numeric expression to divide this numeric expression by. * @return A new [Expr] representing the division operation. */ - fun divide(other: Expr) = Companion.divide(this, other) + fun divide(divisor: Expr) = Companion.divide(this, divisor) /** - * Creates an expression that divides this expression by a value. + * Creates an expression that divides this numeric expression by a constant. * - * @param other The value to divide by. + * @param divisor Constant to divide this expression by. * @return A new [Expr] representing the division operation. */ - fun divide(other: Any) = Companion.divide(this, other) + fun divide(divisor: Number) = Companion.divide(this, divisor) /** + * Creates an expression that calculates the modulo (remainder) of dividing this numeric + * expressions by another numeric expression. + * + * @param divisor The numeric expression to divide this expression by. + * @return A new [Expr] representing the modulo operation. */ - fun mod(other: Expr) = Companion.mod(this, other) + fun mod(divisor: Expr) = Companion.mod(this, divisor) /** + * Creates an expression that calculates the modulo (remainder) of dividing this numeric + * expressions by a constant. + * + * @param divisor The constant to divide this expression by. + * @return A new [Expr] representing the modulo operation. */ - fun mod(other: Any) = Companion.mod(this, other) + fun mod(divisor: Number) = Companion.mod(this, divisor) /** * Creates an expression that rounds this numeric expression to nearest integer. @@ -2887,10 +3039,18 @@ abstract class Expr internal constructor() { Companion.mapMerge(this, mapExpr, *otherMaps) /** + * Creates an expression that removes a key from this map expression. + * + * @param key The name of the key to remove from this map expression. + * @return A new [Expr] that evaluates to a modified map. */ fun mapRemove(key: Expr) = Companion.mapRemove(this, key) /** + * Creates an expression that removes a key from this map expression. + * + * @param key The name of the key to remove from this map expression. + * @return A new [Expr] that evaluates to a modified map. */ fun mapRemove(key: String) = Companion.mapRemove(this, key) From 8a81354869015a4d3ada32c6c2a286c4a71441aa Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 6 May 2025 12:00:58 -0400 Subject: [PATCH 059/152] More expression work --- firebase-firestore/api.txt | 42 +-- .../firestore/pipeline/expressions.kt | 285 +++++++++++++++--- 2 files changed, 267 insertions(+), 60 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 4de6a67fff5..15ce01132b9 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -1097,30 +1097,33 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.Expr pow(Number exponent); method public static final com.google.firebase.firestore.pipeline.Expr pow(String numericField, com.google.firebase.firestore.pipeline.Expr exponent); method public static final com.google.firebase.firestore.pipeline.Expr pow(String numericField, Number exponent); + method public static final com.google.firebase.firestore.pipeline.Expr rand(); method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr pattern); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr stringExpression, String pattern); method public final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String fieldName, String pattern); method public final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr pattern); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr pattern); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr stringExpression, String pattern); method public final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public static final com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, String pattern); method public final com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public static final com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public static final com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr stringExpression, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceAll(String fieldName, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); method public final com.google.firebase.firestore.pipeline.Expr replaceAll(String find, String replace); method public static final com.google.firebase.firestore.pipeline.Expr replaceAll(String fieldName, String find, String replace); method public final com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public static final com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public static final com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr stringExpression, String find, String replace); + method public static final com.google.firebase.firestore.pipeline.Expr replaceFirst(String fieldName, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); method public final com.google.firebase.firestore.pipeline.Expr replaceFirst(String find, String replace); method public static final com.google.firebase.firestore.pipeline.Expr replaceFirst(String fieldName, String find, String replace); method public final com.google.firebase.firestore.pipeline.Expr reverse(); - method public static final com.google.firebase.firestore.pipeline.Expr reverse(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Expr reverse(com.google.firebase.firestore.pipeline.Expr stringExpression); method public static final com.google.firebase.firestore.pipeline.Expr reverse(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr round(); method public static final com.google.firebase.firestore.pipeline.Expr round(com.google.firebase.firestore.pipeline.Expr numericExpr); @@ -1390,21 +1393,24 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr pow(com.google.firebase.firestore.pipeline.Expr numericExpr, Number exponent); method public com.google.firebase.firestore.pipeline.Expr pow(String numericField, com.google.firebase.firestore.pipeline.Expr exponent); method public com.google.firebase.firestore.pipeline.Expr pow(String numericField, Number exponent); - method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); - method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public com.google.firebase.firestore.pipeline.Expr rand(); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(com.google.firebase.firestore.pipeline.Expr stringExpression, String pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexContains(String fieldName, String pattern); - method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr pattern); - method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr expr, String pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr pattern); + method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(com.google.firebase.firestore.pipeline.Expr stringExpression, String pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, com.google.firebase.firestore.pipeline.Expr pattern); method public com.google.firebase.firestore.pipeline.BooleanExpr regexMatch(String fieldName, String pattern); - method public com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public com.google.firebase.firestore.pipeline.Expr replaceAll(com.google.firebase.firestore.pipeline.Expr stringExpression, String find, String replace); + method public com.google.firebase.firestore.pipeline.Expr replaceAll(String fieldName, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); method public com.google.firebase.firestore.pipeline.Expr replaceAll(String fieldName, String find, String replace); - method public com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); - method public com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr value, String find, String replace); + method public com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); + method public com.google.firebase.firestore.pipeline.Expr replaceFirst(com.google.firebase.firestore.pipeline.Expr stringExpression, String find, String replace); + method public com.google.firebase.firestore.pipeline.Expr replaceFirst(String fieldName, com.google.firebase.firestore.pipeline.Expr find, com.google.firebase.firestore.pipeline.Expr replace); method public com.google.firebase.firestore.pipeline.Expr replaceFirst(String fieldName, String find, String replace); - method public com.google.firebase.firestore.pipeline.Expr reverse(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Expr reverse(com.google.firebase.firestore.pipeline.Expr stringExpression); method public com.google.firebase.firestore.pipeline.Expr reverse(String fieldName); method public com.google.firebase.firestore.pipeline.Expr round(com.google.firebase.firestore.pipeline.Expr numericExpr); method public com.google.firebase.firestore.pipeline.Expr round(String numericField); 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 d33f41dddda..4bc8983da21 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 @@ -1156,32 +1156,111 @@ abstract class Expr internal constructor() { */ @JvmStatic fun isNotNull(fieldName: String): BooleanExpr = BooleanExpr("is_not_null", fieldName) - /** @return A new [Expr] representing the replaceFirst operation. */ + /** + * Creates an expression that replaces the first occurrence of a substring within the + * [stringExpression]. + * + * @param stringExpression The expression representing the string to perform the replacement on. + * @param find The expression representing the substring to search for in [stringExpression]. + * @param replace The expression representing the replacement for the first occurrence of [find] + * . + * @return A new [Expr] representing the string with the first occurrence replaced. + */ @JvmStatic - fun replaceFirst(value: Expr, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_first", value, find, replace) + fun replaceFirst(stringExpression: Expr, find: Expr, replace: Expr): Expr = + FunctionExpr("replace_first", stringExpression, find, replace) - /** @return A new [Expr] representing the replaceFirst operation. */ + /** + * Creates an expression that replaces the first occurrence of a substring within the + * [stringExpression]. + * + * @param stringExpression The expression representing the string to perform the replacement on. + * @param find The substring to search for in [stringExpression]. + * @param replace The replacement for the first occurrence of [find] with. + * @return A new [Expr] representing the string with the first occurrence replaced. + */ @JvmStatic - fun replaceFirst(value: Expr, find: String, replace: String): Expr = - FunctionExpr("replace_first", value, find, replace) + fun replaceFirst(stringExpression: Expr, find: String, replace: String): Expr = + FunctionExpr("replace_first", stringExpression, find, replace) - /** @return A new [Expr] representing the replaceFirst operation. */ + /** + * Creates an expression that replaces the first occurrence of a substring within the specified + * string field. + * + * @param fieldName The name of the field representing the string to perform the replacement on. + * @param find The expression representing the substring to search for in specified string + * field. + * @param replace The expression representing the replacement for the first occurrence of [find] + * with. + * @return A new [Expr] representing the string with the first occurrence replaced. + */ + @JvmStatic + fun replaceFirst(fieldName: String, find: Expr, replace: Expr): Expr = + FunctionExpr("replace_first", fieldName, find, replace) + + /** + * Creates an expression that replaces the first occurrence of a substring within the specified + * string field. + * + * @param fieldName The name of the field representing the string to perform the replacement on. + * @param find The substring to search for in specified string field. + * @param replace The replacement for the first occurrence of [find] with. + * @return A new [Expr] representing the string with the first occurrence replaced. + */ @JvmStatic fun replaceFirst(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_first", fieldName, find, replace) - /** @return A new [Expr] representing the replaceAll operation. */ + /** + * Creates an expression that replaces all occurrences of a substring within the + * [stringExpression]. + * + * @param stringExpression The expression representing the string to perform the replacement on. + * @param find The expression representing the substring to search for in [stringExpression]. + * @param replace The expression representing the replacement for all occurrences of [find]. + * @return A new [Expr] representing the string with all occurrences replaced. + */ + @JvmStatic + fun replaceAll(stringExpression: Expr, find: Expr, replace: Expr): Expr = + FunctionExpr("replace_all", stringExpression, find, replace) + + /** + * Creates an expression that replaces all occurrences of a substring within the + * [stringExpression]. + * + * @param stringExpression The expression representing the string to perform the replacement on. + * @param find The substring to search for in [stringExpression]. + * @param replace The replacement for all occurrences of [find] with. + * @return A new [Expr] representing the string with all occurrences replaced. + */ @JvmStatic - fun replaceAll(value: Expr, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_all", value, find, replace) + fun replaceAll(stringExpression: Expr, find: String, replace: String): Expr = + FunctionExpr("replace_all", stringExpression, find, replace) - /** @return A new [Expr] representing the replaceAll operation. */ + /** + * Creates an expression that replaces all occurrences of a substring within the specified + * string field. + * + * @param fieldName The name of the field representing the string to perform the replacement on. + * @param find The expression representing the substring to search for in specified string + * field. + * @param replace The expression representing the replacement for all occurrences of [find] + * with. + * @return A new [Expr] representing the string with all occurrences replaced. + */ @JvmStatic - fun replaceAll(value: Expr, find: String, replace: String): Expr = - FunctionExpr("replace_all", value, find, replace) + fun replaceAll(fieldName: String, find: Expr, replace: Expr): Expr = + FunctionExpr("replace_all", fieldName, find, replace) - /** @return A new [Expr] representing the replaceAll operation. */ + /** + * Creates an expression that replaces all occurrences of a substring within the specified + * string field. + * + * @param fieldName The name of the field representing the string to perform the replacement on. + * @param find The substring to search for in specified string field. + * @param replace The replacement for all occurrences of [find] with. + * @return A new [Expr] representing the string with all occurrences replaced. + */ @JvmStatic fun replaceAll(fieldName: String, find: String, replace: String): Expr = FunctionExpr("replace_all", fieldName, find, replace) @@ -1266,42 +1345,102 @@ abstract class Expr internal constructor() { fun like(fieldName: String, pattern: String): BooleanExpr = BooleanExpr("like", fieldName, pattern) - /** @return A new [Expr] representing the regexContains operation. */ + /** + * Creates an expression that return a pseudo-random number of type double in the range of [0, + * 1), inclusive of 0 and exclusive of 1. + * + * @return A new [Expr] representing the random number operation. + */ + @JvmStatic fun rand(): Expr = FunctionExpr("rand") + + /** + * Creates an expression that checks if a string expression contains a specified regular + * expression as a substring. + * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param pattern The regular expression to use for the search. + * @return A new [BooleanExpr] representing the contains regular expression comparison. + */ @JvmStatic - fun regexContains(expr: Expr, pattern: Expr): BooleanExpr = - BooleanExpr("regex_contains", expr, pattern) + fun regexContains(stringExpression: Expr, pattern: Expr): BooleanExpr = + BooleanExpr("regex_contains", stringExpression, pattern) - /** @return A new [Expr] representing the regexContains operation. */ + /** + * Creates an expression that checks if a string expression contains a specified regular + * expression as a substring. + * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param pattern The regular expression to use for the search. + * @return A new [BooleanExpr] representing the contains regular expression comparison. + */ @JvmStatic - fun regexContains(expr: Expr, pattern: String): BooleanExpr = - BooleanExpr("regex_contains", expr, pattern) + fun regexContains(stringExpression: Expr, pattern: String): BooleanExpr = + BooleanExpr("regex_contains", stringExpression, pattern) - /** @return A new [Expr] representing the regexContains operation. */ + /** + * Creates an expression that checks if a string field contains a specified regular expression + * as a substring. + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the search. + * @return A new [BooleanExpr] representing the contains regular expression comparison. + */ @JvmStatic fun regexContains(fieldName: String, pattern: Expr) = BooleanExpr("regex_contains", fieldName, pattern) - /** @return A new [Expr] representing the regexContains operation. */ + /** + * Creates an expression that checks if a string field contains a specified regular expression + * as a substring. + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the search. + * @return A new [BooleanExpr] representing the contains regular expression comparison. + */ @JvmStatic fun regexContains(fieldName: String, pattern: String) = BooleanExpr("regex_contains", fieldName, pattern) - /** @return A new [Expr] representing the regexMatch operation. */ + /** + * Creates an expression that checks if a string field matches a specified regular expression. + * + * @param stringExpression The expression representing the string to match against. + * @param pattern The regular expression to use for the match. + * @return A new [BooleanExpr] representing the regular expression match comparison. + */ @JvmStatic - fun regexMatch(expr: Expr, pattern: Expr): BooleanExpr = - BooleanExpr("regex_match", expr, pattern) + fun regexMatch(stringExpression: Expr, pattern: Expr): BooleanExpr = + BooleanExpr("regex_match", stringExpression, pattern) - /** @return A new [Expr] representing the regexMatch operation. */ + /** + * Creates an expression that checks if a string field matches a specified regular expression. + * + * @param stringExpression The expression representing the string to match against. + * @param pattern The regular expression to use for the match. + * @return A new [BooleanExpr] representing the regular expression match comparison. + */ @JvmStatic - fun regexMatch(expr: Expr, pattern: String): BooleanExpr = - BooleanExpr("regex_match", expr, pattern) + fun regexMatch(stringExpression: Expr, pattern: String): BooleanExpr = + BooleanExpr("regex_match", stringExpression, pattern) - /** @return A new [Expr] representing the regexMatch operation. */ + /** + * Creates an expression that checks if a string field matches a specified regular expression. + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the match. + * @return A new [BooleanExpr] representing the regular expression match comparison. + */ @JvmStatic fun regexMatch(fieldName: String, pattern: Expr) = BooleanExpr("regex_match", fieldName, pattern) - /** @return A new [Expr] representing the regexMatch operation. */ + /** + * Creates an expression that checks if a string field matches a specified regular expression. + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the match. + * @return A new [BooleanExpr] representing the regular expression match comparison. + */ @JvmStatic fun regexMatch(fieldName: String, pattern: String) = BooleanExpr("regex_match", fieldName, pattern) @@ -1354,13 +1493,27 @@ abstract class Expr internal constructor() { fun logicalMinimum(fieldName: String, vararg others: Any): Expr = FunctionExpr("logical_min", fieldName, *others) - /** @return A new [Expr] representing the reverse operation. */ - @JvmStatic fun reverse(expr: Expr): Expr = FunctionExpr("reverse", expr) + /** + * Creates an expression that reverses a string. + * + * @param stringExpression An expression evaluating to a string value, which will be reversed. + * @return A new [Expr] representing the reversed string. + */ + @JvmStatic fun reverse(stringExpression: Expr): Expr = FunctionExpr("reverse", stringExpression) - /** @return A new [Expr] representing the reverse operation. */ + /** + * Creates an expression that reverses a string value from the specified field. + * + * @param fieldName The name of the field that contains the string to reverse. + * @return A new [Expr] representing the reversed string. + */ @JvmStatic fun reverse(fieldName: String): Expr = FunctionExpr("reverse", fieldName) - /** @return A new [Expr] representing the strContains operation. */ + /** + * Creates an expression that checks if a string expression contains a specified substring. + * + * @return A new [BooleanExpr] representing the contains comparison. + */ @JvmStatic fun strContains(expr: Expr, substring: Expr): BooleanExpr = BooleanExpr("str_contains", expr, substring) @@ -2712,7 +2865,7 @@ abstract class Expr internal constructor() { * * @return A new [Expr] representing an integer result from the round operation. */ - fun round() = Companion.round(this) + fun round(): Expr = Companion.round(this) /** * Creates an expression that rounds off this numeric expression to [decimalPlace] decimal places @@ -2722,7 +2875,7 @@ abstract class Expr internal constructor() { * @param decimalPlace The number of decimal places to round. * @return A new [Expr] representing the round operation. */ - fun roundToDecimal(decimalPlace: Int) = Companion.roundToDecimal(this, decimalPlace) + fun roundToDecimal(decimalPlace: Int): Expr = Companion.roundToDecimal(this, decimalPlace) /** * Creates an expression that rounds off this numeric expression to [decimalPlace] decimal places @@ -2732,7 +2885,7 @@ abstract class Expr internal constructor() { * @param decimalPlace The number of decimal places to round. * @return A new [Expr] representing the round operation. */ - fun roundToDecimal(decimalPlace: Expr) = Companion.roundToDecimal(this, decimalPlace) + fun roundToDecimal(decimalPlace: Expr): Expr = Companion.roundToDecimal(this, decimalPlace) /** * Creates an expression that returns the smalled integer that isn't less than this numeric @@ -2740,7 +2893,7 @@ abstract class Expr internal constructor() { * * @return A new [Expr] representing an integer result from the ceil operation. */ - fun ceil() = Companion.ceil(this) + fun ceil(): Expr = Companion.ceil(this) /** * Creates an expression that returns the largest integer that isn't less than this numeric @@ -2748,7 +2901,7 @@ abstract class Expr internal constructor() { * * @return A new [Expr] representing an integer result from the floor operation. */ - fun floor() = Companion.floor(this) + fun floor(): Expr = Companion.floor(this) /** * Creates an expression that returns this numeric expression raised to the power of the @@ -2758,7 +2911,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing a numeric result from raising this numeric expression to the * power of [exponent]. */ - fun pow(exponent: Number) = Companion.pow(this, exponent) + fun pow(exponent: Number): Expr = Companion.pow(this, exponent) /** * Creates an expression that returns this numeric expression raised to the power of the @@ -2768,14 +2921,14 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing a numeric result from raising this numeric expression to the * power of [exponent]. */ - fun pow(exponent: Expr) = Companion.pow(this, exponent) + fun pow(exponent: Expr): Expr = Companion.pow(this, exponent) /** * Creates an expression that returns the square root of this numeric expression. * * @return A new [Expr] representing the numeric result of the square root operation. */ - fun sqrt() = Companion.sqrt(this) + fun sqrt(): Expr = Companion.sqrt(this) /** * Creates an expression that checks if this expression, when evaluated, is equal to any of the @@ -2853,18 +3006,42 @@ abstract class Expr internal constructor() { fun isNotNull(): BooleanExpr = Companion.isNotNull(this) /** + * Creates an expression that replaces the first occurrence of a substring within this string + * expression. + * + * @param find The expression representing the substring to search for in this expressions. + * @param replace The expression representing the replacement for the first occurrence of [find]. + * @return A new [Expr] representing the string with the first occurrence replaced. */ fun replaceFirst(find: Expr, replace: Expr) = Companion.replaceFirst(this, find, replace) /** + * Creates an expression that replaces the first occurrence of a substring within this string + * expression. + * + * @param find The substring to search for in this string expression. + * @param replace The replacement for the first occurrence of [find] with. + * @return A new [Expr] representing the string with the first occurrence replaced. */ fun replaceFirst(find: String, replace: String) = Companion.replaceFirst(this, find, replace) /** + * Creates an expression that replaces all occurrences of a substring within this string + * expression. + * + * @param find The expression representing the substring to search for in this string expression. + * @param replace The expression representing the replacement for all occurrences of [find]. + * @return A new [Expr] representing the string with all occurrences replaced. */ fun replaceAll(find: Expr, replace: Expr) = Companion.replaceAll(this, find, replace) /** + * Creates an expression that replaces all occurrences of a substring within this string + * expression. + * + * @param find The substring to search for in this string expression. + * @param replace The replacement for all occurrences of [find] with. + * @return A new [Expr] representing the string with all occurrences replaced. */ fun replaceAll(find: String, replace: String) = Companion.replaceAll(this, find, replace) @@ -2900,18 +3077,38 @@ abstract class Expr internal constructor() { fun like(pattern: String): BooleanExpr = Companion.like(this, pattern) /** + * Creates an expression that checks if this string expression contains a specified regular + * expression as a substring. + * + * @param pattern The regular expression to use for the search. + * @return A new [BooleanExpr] representing the contains regular expression comparison. */ fun regexContains(pattern: Expr): BooleanExpr = Companion.regexContains(this, pattern) /** + * Creates an expression that checks if this string expression contains a specified regular + * expression as a substring. + * + * @param pattern The regular expression to use for the search. + * @return A new [BooleanExpr] representing the contains regular expression comparison. */ fun regexContains(pattern: String): BooleanExpr = Companion.regexContains(this, pattern) /** + * Creates an expression that checks if this string expression matches a specified regular + * expression. + * + * @param pattern The regular expression to use for the match. + * @return A new [BooleanExpr] representing the regular expression match comparison. */ fun regexMatch(pattern: Expr): BooleanExpr = Companion.regexMatch(this, pattern) /** + * Creates an expression that checks if this string expression matches a specified regular + * expression. + * + * @param pattern The regular expression to use for the match. + * @return A new [BooleanExpr] representing the regular expression match comparison. */ fun regexMatch(pattern: String): BooleanExpr = Companion.regexMatch(this, pattern) @@ -2952,6 +3149,9 @@ abstract class Expr internal constructor() { fun logicalMinimum(vararg others: Any): Expr = Companion.logicalMinimum(this, *others) /** + * Creates an expression that reverses this string expression. + * + * @return A new [Expr] representing the reversed string. */ fun reverse(): Expr = Companion.reverse(this) @@ -3529,6 +3729,7 @@ internal constructor( private val params: Array, private val options: InternalOptions = InternalOptions.EMPTY ) : Expr() { + internal constructor(name: String) : this(name, emptyArray()) internal constructor(name: String, param: Expr) : this(name, arrayOf(param)) internal constructor( name: String, From 8817f81f953b0563765a1fc92d6cc9e29b1ce479 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 6 May 2025 12:41:01 -0400 Subject: [PATCH 060/152] More expression work --- .../firestore/pipeline/expressions.kt | 129 ++++++++++++++---- 1 file changed, 99 insertions(+), 30 deletions(-) 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 4bc8983da21..985ce468850 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 @@ -1512,23 +1512,43 @@ abstract class Expr internal constructor() { /** * Creates an expression that checks if a string expression contains a specified substring. * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param substring The expression representing the substring to search for. * @return A new [BooleanExpr] representing the contains comparison. */ @JvmStatic - fun strContains(expr: Expr, substring: Expr): BooleanExpr = - BooleanExpr("str_contains", expr, substring) + fun strContains(stringExpression: Expr, substring: Expr): BooleanExpr = + BooleanExpr("str_contains", stringExpression, substring) - /** @return A new [Expr] representing the strContains operation. */ + /** + * Creates an expression that checks if a string expression contains a specified substring. + * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param substring The substring to search for. + * @return A new [BooleanExpr] representing the contains comparison. + */ @JvmStatic - fun strContains(expr: Expr, substring: String): BooleanExpr = - BooleanExpr("str_contains", expr, substring) + fun strContains(stringExpression: Expr, substring: String): BooleanExpr = + BooleanExpr("str_contains", stringExpression, substring) - /** @return A new [BooleanExpr] representing the strContains operation. */ + /** + * Creates an expression that checks if a string field contains a specified substring. + * + * @param fieldName The name of the field to perform the comparison on. + * @param substring The expression representing the substring to search for. + * @return A new [BooleanExpr] representing the contains comparison. + */ @JvmStatic fun strContains(fieldName: String, substring: Expr): BooleanExpr = BooleanExpr("str_contains", fieldName, substring) - /** @return A new [BooleanExpr] representing the strContains operation. */ + /** + * Creates an expression that checks if a string field contains a specified substring. + * + * @param fieldName The name of the field to perform the comparison on. + * @param substring The substring to search for. + * @return A new [BooleanExpr] representing the contains comparison. + */ @JvmStatic fun strContains(fieldName: String, substring: String): BooleanExpr = BooleanExpr("str_contains", fieldName, substring) @@ -1645,23 +1665,51 @@ abstract class Expr internal constructor() { /** @return A new [Expr] representing the trim operation. */ @JvmStatic fun trim(fieldName: String): Expr = FunctionExpr("trim", fieldName) - /** @return A new [Expr] representing the strConcat operation. */ + /** + * Creates an expression that concatenates string expressions together. + * + * @param firstString The expression representing the initial string value. + * @param otherStrings Optional additional string expressions to concatenate. + * @return A new [Expr] representing the concatenated string. + */ @JvmStatic - fun strConcat(first: Expr, vararg rest: Expr): Expr = FunctionExpr("str_concat", first, *rest) + fun strConcat(firstString: Expr, vararg otherStrings: Expr): Expr = + FunctionExpr("str_concat", firstString, *otherStrings) - /** @return A new [Expr] representing the strConcat operation. */ + /** + * Creates an expression that concatenates string expressions together. + * + * @param firstString The expression representing the initial string value. + * @param otherStrings Optional additional string expressions or string constants to + * concatenate. + * @return A new [Expr] representing the concatenated string. + */ @JvmStatic - fun strConcat(first: Expr, vararg rest: Any): Expr = FunctionExpr("str_concat", first, *rest) + fun strConcat(firstString: Expr, vararg otherStrings: Any): Expr = + FunctionExpr("str_concat", firstString, *otherStrings) - /** @return A new [Expr] representing the strConcat operation. */ + /** + * Creates an expression that concatenates string expressions together. + * + * @param fieldName The field name containing the initial string value. + * @param otherStrings Optional additional string expressions to concatenate. + * @return A new [Expr] representing the concatenated string. + */ @JvmStatic - fun strConcat(fieldName: String, vararg rest: Expr): Expr = - FunctionExpr("str_concat", fieldName, *rest) + fun strConcat(fieldName: String, vararg otherStrings: Expr): Expr = + FunctionExpr("str_concat", fieldName, *otherStrings) - /** @return A new [Expr] representing the strConcat operation. */ + /** + * Creates an expression that concatenates string expressions together. + * + * @param fieldName The field name containing the initial string value. + * @param otherStrings Optional additional string expressions or string constants to + * concatenate. + * @return A new [Expr] representing the concatenated string. + */ @JvmStatic - fun strConcat(fieldName: String, vararg rest: Any): Expr = - FunctionExpr("str_concat", fieldName, *rest) + fun strConcat(fieldName: String, vararg otherStrings: Any): Expr = + FunctionExpr("str_concat", fieldName, *otherStrings) internal fun map(elements: Array): Expr = FunctionExpr("map", elements) @@ -2776,7 +2824,7 @@ abstract class Expr internal constructor() { * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ - fun add(second: Expr, vararg others: Any) = Companion.add(this, second, *others) + fun add(second: Expr, vararg others: Any): Expr = Companion.add(this, second, *others) /** * Creates an expression that adds this numeric expression to other numeric expressions and @@ -2786,7 +2834,7 @@ abstract class Expr internal constructor() { * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ - fun add(second: Number, vararg others: Any) = Companion.add(this, second, *others) + fun add(second: Number, vararg others: Any): Expr = Companion.add(this, second, *others) /** * Creates an expression that subtracts a constant from this numeric expression. @@ -2794,7 +2842,7 @@ abstract class Expr internal constructor() { * @param subtrahend Numeric expression to subtract. * @return A new [Expr] representing the subtract operation. */ - fun subtract(subtrahend: Expr) = Companion.subtract(this, subtrahend) + fun subtract(subtrahend: Expr): Expr = Companion.subtract(this, subtrahend) /** * Creates an expression that subtracts a numeric expressions from this numeric expression. @@ -2802,7 +2850,7 @@ abstract class Expr internal constructor() { * @param subtrahend Constant to subtract. * @return A new [Expr] representing the subtract operation. */ - fun subtract(subtrahend: Number) = Companion.subtract(this, subtrahend) + fun subtract(subtrahend: Number): Expr = Companion.subtract(this, subtrahend) /** * Creates an expression that multiplies this numeric expression to other numeric expressions and @@ -2812,7 +2860,7 @@ abstract class Expr internal constructor() { * @param others Additional numeric expressions or constants to multiply. * @return A new [Expr] representing the multiplication operation. */ - fun multiply(second: Expr, vararg others: Any) = Companion.multiply(this, second, *others) + fun multiply(second: Expr, vararg others: Any): Expr = Companion.multiply(this, second, *others) /** * Creates an expression that multiplies this numeric expression to other numeric expressions and @@ -2822,7 +2870,7 @@ abstract class Expr internal constructor() { * @param others Additional numeric expressions or constants to multiply. * @return A new [Expr] representing the multiplication operation. */ - fun multiply(second: Number, vararg others: Any) = Companion.multiply(this, second, *others) + fun multiply(second: Number, vararg others: Any): Expr = Companion.multiply(this, second, *others) /** * Creates an expression that divides this numeric expression by another numeric expression. @@ -2830,7 +2878,7 @@ abstract class Expr internal constructor() { * @param divisor Numeric expression to divide this numeric expression by. * @return A new [Expr] representing the division operation. */ - fun divide(divisor: Expr) = Companion.divide(this, divisor) + fun divide(divisor: Expr): Expr = Companion.divide(this, divisor) /** * Creates an expression that divides this numeric expression by a constant. @@ -2838,7 +2886,7 @@ abstract class Expr internal constructor() { * @param divisor Constant to divide this expression by. * @return A new [Expr] representing the division operation. */ - fun divide(divisor: Number) = Companion.divide(this, divisor) + fun divide(divisor: Number): Expr = Companion.divide(this, divisor) /** * Creates an expression that calculates the modulo (remainder) of dividing this numeric @@ -2847,7 +2895,7 @@ abstract class Expr internal constructor() { * @param divisor The numeric expression to divide this expression by. * @return A new [Expr] representing the modulo operation. */ - fun mod(divisor: Expr) = Companion.mod(this, divisor) + fun mod(divisor: Expr): Expr = Companion.mod(this, divisor) /** * Creates an expression that calculates the modulo (remainder) of dividing this numeric @@ -2856,7 +2904,7 @@ abstract class Expr internal constructor() { * @param divisor The constant to divide this expression by. * @return A new [Expr] representing the modulo operation. */ - fun mod(divisor: Number) = Companion.mod(this, divisor) + fun mod(divisor: Number): Expr = Companion.mod(this, divisor) /** * Creates an expression that rounds this numeric expression to nearest integer. @@ -3156,10 +3204,18 @@ abstract class Expr internal constructor() { fun reverse(): Expr = Companion.reverse(this) /** + * Creates an expression that checks if this string expression contains a specified substring. + * + * @param substring The expression representing the substring to search for. + * @return A new [BooleanExpr] representing the contains comparison. */ fun strContains(substring: Expr): BooleanExpr = Companion.strContains(this, substring) /** + * Creates an expression that checks if this string expression contains a specified substring. + * + * @param substring The substring to search for. + * @return A new [BooleanExpr] representing the contains comparison. */ fun strContains(substring: String): BooleanExpr = Companion.strContains(this, substring) @@ -3208,16 +3264,29 @@ abstract class Expr internal constructor() { fun trim() = Companion.trim(this) /** + * Creates an expression that concatenates string expressions together. + * + * @param stringExpressions The string expressions to concatenate. + * @return A new [Expr] representing the concatenated string. */ - fun strConcat(vararg expr: Expr) = Companion.strConcat(this, *expr) + fun strConcat(vararg stringExpressions: Expr): Expr = + Companion.strConcat(this, *stringExpressions) /** + * Creates an expression that concatenates this string expression with string constants. + * + * @param strings The string constants to concatenate. + * @return A new [Expr] representing the concatenated string. */ - fun strConcat(vararg string: String) = Companion.strConcat(this, *string) + fun strConcat(vararg strings: String): Expr = Companion.strConcat(this, *strings) /** + * Creates an expression that concatenates string expressions and string constants together. + * + * @param strings The string expressions or string constants to concatenate. + * @return A new [Expr] representing the concatenated string. */ - fun strConcat(vararg string: Any) = Companion.strConcat(this, *string) + fun strConcat(vararg strings: Any): Expr = Companion.strConcat(this, *strings) /** * Accesses a map (object) value using the provided [key]. From 891cefd9fa57f7f42257352782813f7c016884b6 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 6 May 2025 16:38:42 -0400 Subject: [PATCH 061/152] More expression work --- firebase-firestore/api.txt | 50 +++---- .../firestore/pipeline/expressions.kt | 124 +++++++++++++++--- 2 files changed, 131 insertions(+), 43 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 15ce01132b9..d94afe25ee3 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -1128,12 +1128,12 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.Expr round(); method public static final com.google.firebase.firestore.pipeline.Expr round(com.google.firebase.firestore.pipeline.Expr numericExpr); method public static final com.google.firebase.firestore.pipeline.Expr round(String numericField); - method public final com.google.firebase.firestore.pipeline.Expr roundToDecimal(com.google.firebase.firestore.pipeline.Expr decimalPlace); - method public static final com.google.firebase.firestore.pipeline.Expr roundToDecimal(com.google.firebase.firestore.pipeline.Expr numericExpr, com.google.firebase.firestore.pipeline.Expr decimalPlace); - method public static final com.google.firebase.firestore.pipeline.Expr roundToDecimal(com.google.firebase.firestore.pipeline.Expr numericExpr, int decimalPlace); - method public final com.google.firebase.firestore.pipeline.Expr roundToDecimal(int decimalPlace); - method public static final com.google.firebase.firestore.pipeline.Expr roundToDecimal(String numericField, com.google.firebase.firestore.pipeline.Expr decimalPlace); - method public static final com.google.firebase.firestore.pipeline.Expr roundToDecimal(String numericField, int decimalPlace); + method public final com.google.firebase.firestore.pipeline.Expr roundToPrecision(com.google.firebase.firestore.pipeline.Expr decimalPlace); + method public static final com.google.firebase.firestore.pipeline.Expr roundToPrecision(com.google.firebase.firestore.pipeline.Expr numericExpr, com.google.firebase.firestore.pipeline.Expr decimalPlace); + method public static final com.google.firebase.firestore.pipeline.Expr roundToPrecision(com.google.firebase.firestore.pipeline.Expr numericExpr, int decimalPlace); + method public final com.google.firebase.firestore.pipeline.Expr roundToPrecision(int decimalPlace); + method public static final com.google.firebase.firestore.pipeline.Expr roundToPrecision(String numericField, com.google.firebase.firestore.pipeline.Expr decimalPlace); + method public static final com.google.firebase.firestore.pipeline.Expr roundToPrecision(String numericField, int decimalPlace); method public final com.google.firebase.firestore.pipeline.Expr sqrt(); method public static final com.google.firebase.firestore.pipeline.Expr sqrt(com.google.firebase.firestore.pipeline.Expr numericExpr); method public static final com.google.firebase.firestore.pipeline.Expr sqrt(String numericField); @@ -1143,16 +1143,16 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String prefix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr prefix); method public static final com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, String prefix); - method public static final com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); - method public static final com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); - method public final com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr... expr); - method public final com.google.firebase.firestore.pipeline.Expr strConcat(java.lang.Object... string); - method public static final com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); - method public static final com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, java.lang.Object... rest); - method public final com.google.firebase.firestore.pipeline.Expr strConcat(java.lang.String... string); + method public static final com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr firstString, com.google.firebase.firestore.pipeline.Expr... otherStrings); + method public static final com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr firstString, java.lang.Object... otherStrings); + method public final com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr... stringExpressions); + method public final com.google.firebase.firestore.pipeline.Expr strConcat(java.lang.Object... strings); + method public static final com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... otherStrings); + method public static final com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, java.lang.Object... otherStrings); + method public final com.google.firebase.firestore.pipeline.Expr strConcat(java.lang.String... strings); method public final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr substring); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr substring); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, String substring); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr substring); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr stringExpression, String substring); method public final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String substring); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, com.google.firebase.firestore.pipeline.Expr substring); method public static final com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, String substring); @@ -1414,22 +1414,22 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr reverse(String fieldName); method public com.google.firebase.firestore.pipeline.Expr round(com.google.firebase.firestore.pipeline.Expr numericExpr); method public com.google.firebase.firestore.pipeline.Expr round(String numericField); - method public com.google.firebase.firestore.pipeline.Expr roundToDecimal(com.google.firebase.firestore.pipeline.Expr numericExpr, com.google.firebase.firestore.pipeline.Expr decimalPlace); - method public com.google.firebase.firestore.pipeline.Expr roundToDecimal(com.google.firebase.firestore.pipeline.Expr numericExpr, int decimalPlace); - method public com.google.firebase.firestore.pipeline.Expr roundToDecimal(String numericField, com.google.firebase.firestore.pipeline.Expr decimalPlace); - method public com.google.firebase.firestore.pipeline.Expr roundToDecimal(String numericField, int decimalPlace); + method public com.google.firebase.firestore.pipeline.Expr roundToPrecision(com.google.firebase.firestore.pipeline.Expr numericExpr, com.google.firebase.firestore.pipeline.Expr decimalPlace); + method public com.google.firebase.firestore.pipeline.Expr roundToPrecision(com.google.firebase.firestore.pipeline.Expr numericExpr, int decimalPlace); + method public com.google.firebase.firestore.pipeline.Expr roundToPrecision(String numericField, com.google.firebase.firestore.pipeline.Expr decimalPlace); + method public com.google.firebase.firestore.pipeline.Expr roundToPrecision(String numericField, int decimalPlace); method public com.google.firebase.firestore.pipeline.Expr sqrt(com.google.firebase.firestore.pipeline.Expr numericExpr); method public com.google.firebase.firestore.pipeline.Expr sqrt(String numericField); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr stringExpr, com.google.firebase.firestore.pipeline.Expr prefix); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(com.google.firebase.firestore.pipeline.Expr stringExpr, String prefix); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, com.google.firebase.firestore.pipeline.Expr prefix); method public com.google.firebase.firestore.pipeline.BooleanExpr startsWith(String fieldName, String prefix); - method public com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr... rest); - method public com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr first, java.lang.Object... rest); - method public com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... rest); - method public com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, java.lang.Object... rest); - method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, com.google.firebase.firestore.pipeline.Expr substring); - method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr expr, String substring); + method public com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr firstString, com.google.firebase.firestore.pipeline.Expr... otherStrings); + method public com.google.firebase.firestore.pipeline.Expr strConcat(com.google.firebase.firestore.pipeline.Expr firstString, java.lang.Object... otherStrings); + method public com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, com.google.firebase.firestore.pipeline.Expr... otherStrings); + method public com.google.firebase.firestore.pipeline.Expr strConcat(String fieldName, java.lang.Object... otherStrings); + method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr stringExpression, com.google.firebase.firestore.pipeline.Expr substring); + method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(com.google.firebase.firestore.pipeline.Expr stringExpression, String substring); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, com.google.firebase.firestore.pipeline.Expr substring); method public com.google.firebase.firestore.pipeline.BooleanExpr strContains(String fieldName, String substring); method public com.google.firebase.firestore.pipeline.Expr subtract(com.google.firebase.firestore.pipeline.Expr minuend, com.google.firebase.firestore.pipeline.Expr subtrahend); 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 985ce468850..1497373ebd9 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 @@ -605,7 +605,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the round operation. */ @JvmStatic - fun roundToDecimal(numericExpr: Expr, decimalPlace: Int): Expr = + fun roundToPrecision(numericExpr: Expr, decimalPlace: Int): Expr = FunctionExpr("round", numericExpr, constant(decimalPlace)) /** @@ -618,7 +618,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the round operation. */ @JvmStatic - fun roundToDecimal(numericField: String, decimalPlace: Int): Expr = + fun roundToPrecision(numericField: String, decimalPlace: Int): Expr = FunctionExpr("round", numericField, constant(decimalPlace)) /** @@ -631,7 +631,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the round operation. */ @JvmStatic - fun roundToDecimal(numericExpr: Expr, decimalPlace: Expr): Expr = + fun roundToPrecision(numericExpr: Expr, decimalPlace: Expr): Expr = FunctionExpr("round", numericExpr, decimalPlace) /** @@ -644,7 +644,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the round operation. */ @JvmStatic - fun roundToDecimal(numericField: String, decimalPlace: Expr): Expr = + fun roundToPrecision(numericField: String, decimalPlace: Expr): Expr = FunctionExpr("round", numericField, decimalPlace) /** @@ -2067,42 +2067,106 @@ abstract class Expr internal constructor() { fun timestampToUnixSeconds(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_seconds", fieldName) - /** @return A new [Expr] representing the timestampAdd operation. */ + /** + * Creates an expression that adds a specified amount of time to a timestamp. + * + * @param timestamp The expression representing the timestamp. + * @param unit The expression representing the unit of time to add. Valid units include + * "microsecond", "millisecond", "second", "minute", "hour" and "day". + * @param amount The expression representing the amount of time to add. + * @return A new [Expr] representing the resulting timestamp. + */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) - /** @return A new [Expr] representing the timestampAdd operation. */ + /** + * Creates an expression that adds a specified amount of time to a timestamp. + * + * @param timestamp The expression representing the timestamp. + * @param unit The unit of time to add. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to add. + * @return A new [Expr] representing the resulting timestamp. + */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", timestamp, unit, amount) - /** @return A new [Expr] representing the timestampAdd operation. */ + /** + * Creates an expression that adds a specified amount of time to a timestamp. + * + * @param fieldName The name of the field that contains the timestamp. + * @param unit The expression representing the unit of time to add. Valid units include + * "microsecond", "millisecond", "second", "minute", "hour" and "day". + * @param amount The expression representing the amount of time to add. + * @return A new [Expr] representing the resulting timestamp. + */ @JvmStatic fun timestampAdd(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) - /** @return A new [Expr] representing the timestampAdd operation. */ + /** + * Creates an expression that adds a specified amount of time to a timestamp. + * + * @param fieldName The name of the field that contains the timestamp. + * @param unit The unit of time to add. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to add. + * @return A new [Expr] representing the resulting timestamp. + */ @JvmStatic fun timestampAdd(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_add", fieldName, unit, amount) - /** @return A new [Expr] representing the timestampSub operation. */ + /** + * Creates an expression that subtracts a specified amount of time to a timestamp. + * + * @param timestamp The expression representing the timestamp. + * @param unit The expression representing the unit of time to subtract. Valid units include + * "microsecond", "millisecond", "second", "minute", "hour" and "day". + * @param amount The expression representing the amount of time to subtract. + * @return A new [Expr] representing the resulting timestamp. + */ @JvmStatic fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) - /** @return A new [Expr] representing the timestampSub operation. */ + /** + * Creates an expression that subtracts a specified amount of time to a timestamp. + * + * @param timestamp The expression representing the timestamp. + * @param unit The unit of time to subtract. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to subtract. + * @return A new [Expr] representing the resulting timestamp. + */ @JvmStatic fun timestampSub(timestamp: Expr, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", timestamp, unit, amount) - /** @return A new [Expr] representing the timestampSub operation. */ + /** + * Creates an expression that subtracts a specified amount of time to a timestamp. + * + * @param fieldName The name of the field that contains the timestamp. + * @param unit The unit of time to subtract. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to subtract. + * @return A new [Expr] representing the resulting timestamp. + */ @JvmStatic fun timestampSub(fieldName: String, unit: Expr, amount: Expr): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) - /** @return A new [Expr] representing the timestampSub operation. */ + /** + * Creates an expression that subtracts a specified amount of time to a timestamp. + * + * @param fieldName The name of the field that contains the timestamp. + * @param unit The unit of time to subtract. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to subtract. + * @return A new [Expr] representing the resulting timestamp. + */ @JvmStatic fun timestampSub(fieldName: String, unit: String, amount: Double): Expr = FunctionExpr("timestamp_sub", fieldName, unit, amount) @@ -2923,7 +2987,7 @@ abstract class Expr internal constructor() { * @param decimalPlace The number of decimal places to round. * @return A new [Expr] representing the round operation. */ - fun roundToDecimal(decimalPlace: Int): Expr = Companion.roundToDecimal(this, decimalPlace) + fun roundToPrecision(decimalPlace: Int): Expr = Companion.roundToPrecision(this, decimalPlace) /** * Creates an expression that rounds off this numeric expression to [decimalPlace] decimal places @@ -2933,7 +2997,7 @@ abstract class Expr internal constructor() { * @param decimalPlace The number of decimal places to round. * @return A new [Expr] representing the round operation. */ - fun roundToDecimal(decimalPlace: Expr): Expr = Companion.roundToDecimal(this, decimalPlace) + fun roundToPrecision(decimalPlace: Expr): Expr = Companion.roundToPrecision(this, decimalPlace) /** * Creates an expression that returns the smalled integer that isn't less than this numeric @@ -3424,20 +3488,44 @@ abstract class Expr internal constructor() { fun timestampToUnixSeconds() = Companion.timestampToUnixSeconds(this) /** + * Creates an expression that adds a specified amount of time to this timestamp expression. + * + * @param unit The expression representing the unit of time to add. Valid units include + * "microsecond", "millisecond", "second", "minute", "hour" and "day". + * @param amount The expression representing the amount of time to add. + * @return A new [Expr] representing the resulting timestamp. */ - fun timestampAdd(unit: Expr, amount: Expr) = Companion.timestampAdd(this, unit, amount) + fun timestampAdd(unit: Expr, amount: Expr): Expr = Companion.timestampAdd(this, unit, amount) /** + * Creates an expression that adds a specified amount of time to this timestamp expression. + * + * @param unit The unit of time to add. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to add. + * @return A new [Expr] representing the resulting timestamp. */ - fun timestampAdd(unit: String, amount: Double) = Companion.timestampAdd(this, unit, amount) + fun timestampAdd(unit: String, amount: Double): Expr = Companion.timestampAdd(this, unit, amount) /** + * Creates an expression that subtracts a specified amount of time to this timestamp expression. + * + * @param unit The expression representing the unit of time to subtract. Valid units include + * "microsecond", "millisecond", "second", "minute", "hour" and "day". + * @param amount The expression representing the amount of time to subtract. + * @return A new [Expr] representing the resulting timestamp. */ - fun timestampSub(unit: Expr, amount: Expr) = Companion.timestampSub(this, unit, amount) + fun timestampSub(unit: Expr, amount: Expr): Expr = Companion.timestampSub(this, unit, amount) /** + * Creates an expression that subtracts a specified amount of time to this timestamp expression. + * + * @param unit The unit of time to subtract. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to subtract. + * @return A new [Expr] representing the resulting timestamp. */ - fun timestampSub(unit: String, amount: Double) = Companion.timestampSub(this, unit, amount) + fun timestampSub(unit: String, amount: Double): Expr = Companion.timestampSub(this, unit, amount) /** * Creates an expression that concatenates a field's array value with other arrays. From eb02ca420e97c4af94ea16e8ca4e63983c675006 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 7 May 2025 11:41:40 -0400 Subject: [PATCH 062/152] More expression work --- firebase-firestore/api.txt | 28 ++-- .../firestore/pipeline/expressions.kt | 136 ++++++++++++++---- 2 files changed, 123 insertions(+), 41 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index d94afe25ee3..5da5bd2fad6 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -1176,28 +1176,28 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.Expr timestampSub(String unit, double amount); method public static final com.google.firebase.firestore.pipeline.Expr timestampSub(String fieldName, String unit, double amount); method public final com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(); - method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(); - method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(); - method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr toLower(); - method public static final com.google.firebase.firestore.pipeline.Expr toLower(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Expr toLower(com.google.firebase.firestore.pipeline.Expr stringExpression); method public static final com.google.firebase.firestore.pipeline.Expr toLower(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr toUpper(); - method public static final com.google.firebase.firestore.pipeline.Expr toUpper(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Expr toUpper(com.google.firebase.firestore.pipeline.Expr stringExpression); method public static final com.google.firebase.firestore.pipeline.Expr toUpper(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr trim(); - method public static final com.google.firebase.firestore.pipeline.Expr trim(com.google.firebase.firestore.pipeline.Expr expr); + method public static final com.google.firebase.firestore.pipeline.Expr trim(com.google.firebase.firestore.pipeline.Expr stringExpression); method public static final com.google.firebase.firestore.pipeline.Expr trim(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(); method public static final com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); method public static final com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(); - method public static final com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(); method public static final com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); @@ -1444,21 +1444,21 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr timestampSub(com.google.firebase.firestore.pipeline.Expr timestamp, String unit, double amount); method public com.google.firebase.firestore.pipeline.Expr timestampSub(String fieldName, com.google.firebase.firestore.pipeline.Expr unit, com.google.firebase.firestore.pipeline.Expr amount); method public com.google.firebase.firestore.pipeline.Expr timestampSub(String fieldName, String unit, double amount); - method public com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.Expr timestampToUnixMicros(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.Expr timestampToUnixMillis(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.Expr timestampToUnixSeconds(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr toLower(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Expr toLower(com.google.firebase.firestore.pipeline.Expr stringExpression); method public com.google.firebase.firestore.pipeline.Expr toLower(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr toUpper(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Expr toUpper(com.google.firebase.firestore.pipeline.Expr stringExpression); method public com.google.firebase.firestore.pipeline.Expr toUpper(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr trim(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.Expr trim(com.google.firebase.firestore.pipeline.Expr stringExpression); method public com.google.firebase.firestore.pipeline.Expr trim(String fieldName); method public com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); method public com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(String fieldName); method public com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); method public com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(String fieldName); 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 1497373ebd9..4a5d2a4eb34 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 @@ -1641,28 +1641,54 @@ abstract class Expr internal constructor() { fun endsWith(fieldName: String, suffix: String): BooleanExpr = BooleanExpr("ends_with", fieldName, suffix) - /** @return A new [Expr] representing the toLower operation. */ - @JvmStatic fun toLower(expr: Expr): Expr = FunctionExpr("to_lower", expr) - - /** @return A new [Expr] representing the toLower operation. */ + /** + * Creates an expression that converts a string expression to lowercase. + * + * @param stringExpression The expression representing the string to convert to lowercase. + * @return A new [Expr] representing the lowercase string. + */ @JvmStatic - fun toLower( - fieldName: String, - ): Expr = FunctionExpr("to_lower", fieldName) + fun toLower(stringExpression: Expr): Expr = FunctionExpr("to_lower", stringExpression) - /** @return A new [Expr] representing the toUpper operation. */ - @JvmStatic fun toUpper(expr: Expr): Expr = FunctionExpr("to_upper", expr) + /** + * Creates an expression that converts a string field to lowercase. + * + * @param fieldName The name of the field containing the string to convert to lowercase. + * @return A new [Expr] representing the lowercase string. + */ + @JvmStatic fun toLower(fieldName: String): Expr = FunctionExpr("to_lower", fieldName) - /** @return A new [Expr] representing the toUpper operation. */ + /** + * Creates an expression that converts a string expression to uppercase. + * + * @param stringExpression The expression representing the string to convert to uppercase. + * @return A new [Expr] representing the lowercase string. + */ @JvmStatic - fun toUpper( - fieldName: String, - ): Expr = FunctionExpr("to_upper", fieldName) + fun toUpper(stringExpression: Expr): Expr = FunctionExpr("to_upper", stringExpression) + + /** + * Creates an expression that converts a string field to uppercase. + * + * @param fieldName The name of the field containing the string to convert to uppercase. + * @return A new [Expr] representing the lowercase string. + */ + @JvmStatic fun toUpper(fieldName: String): Expr = FunctionExpr("to_upper", fieldName) - /** @return A new [Expr] representing the trim operation. */ - @JvmStatic fun trim(expr: Expr): Expr = FunctionExpr("trim", expr) + /** + * Creates an expression that removes leading and trailing whitespace from a string expression. + * + * @param stringExpression The expression representing the string to trim. + * @return A new [Expr] representing the trimmed string. + */ + @JvmStatic fun trim(stringExpression: Expr): Expr = FunctionExpr("trim", stringExpression) - /** @return A new [Expr] representing the trim operation. */ + /** + * Creates an expression that removes leading and trailing whitespace from a string field. + * + * @param fieldName The name of the field containing the string to trim. + * @return A new [Expr] representing the trimmed string. + */ @JvmStatic fun trim(fieldName: String): Expr = FunctionExpr("trim", fieldName) /** @@ -2022,29 +2048,52 @@ abstract class Expr internal constructor() { fun unixMicrosToTimestamp(fieldName: String): Expr = FunctionExpr("unix_micros_to_timestamp", fieldName) - /** @return A new [Expr] representing the timestampToUnixMicros operation. */ + /** + * Creates an expression that converts a timestamp expression to the number of microseconds + * since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * @param expr The expression representing the timestamp. + * @return A new [Expr] representing the number of microseconds since epoch. + */ @JvmStatic - fun timestampToUnixMicros(input: Expr): Expr = FunctionExpr("timestamp_to_unix_micros", input) + fun timestampToUnixMicros(expr: Expr): Expr = FunctionExpr("timestamp_to_unix_micros", expr) - /** @return A new [Expr] representing the timestampToUnixMicros operation. */ + /** + * Creates an expression that converts a timestamp field to the number of microseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC). + * + * @param fieldName The name of the field that contains the timestamp. + * @return A new [Expr] representing the number of microseconds since epoch. + */ @JvmStatic fun timestampToUnixMicros(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_micros", fieldName) - /** @return A new [Expr] representing the unixMillisToTimestamp operation. */ @JvmStatic - fun unixMillisToTimestamp(input: Expr): Expr = FunctionExpr("unix_millis_to_timestamp", input) + fun unixMillisToTimestamp(expr: Expr): Expr = FunctionExpr("unix_millis_to_timestamp", expr) /** @return A new [Expr] representing the unixMillisToTimestamp operation. */ @JvmStatic fun unixMillisToTimestamp(fieldName: String): Expr = FunctionExpr("unix_millis_to_timestamp", fieldName) - /** @return A new [Expr] representing the timestampToUnixMillis operation. */ + /** + * Creates an expression that converts a timestamp expression to the number of milliseconds + * since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * @param expr The expression representing the timestamp. + * @return A new [Expr] representing the number of milliseconds since epoch. + */ @JvmStatic - fun timestampToUnixMillis(input: Expr): Expr = FunctionExpr("timestamp_to_unix_millis", input) + fun timestampToUnixMillis(expr: Expr): Expr = FunctionExpr("timestamp_to_unix_millis", expr) - /** @return A new [Expr] representing the timestampToUnixMillis operation. */ + /** + * Creates an expression that converts a timestamp field to the number of milliseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC). + * + * @param fieldName The name of the field that contains the timestamp. + * @return A new [Expr] representing the number of milliseconds since epoch. + */ @JvmStatic fun timestampToUnixMillis(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_millis", fieldName) @@ -2058,11 +2107,23 @@ abstract class Expr internal constructor() { fun unixSecondsToTimestamp(fieldName: String): Expr = FunctionExpr("unix_seconds_to_timestamp", fieldName) - /** @return A new [Expr] representing the timestampToUnixSeconds operation. */ + /** + * Creates an expression that converts a timestamp expression to the number of seconds since the + * Unix epoch (1970-01-01 00:00:00 UTC). + * + * @param expr The expression representing the timestamp. + * @return A new [Expr] representing the number of seconds since epoch. + */ @JvmStatic - fun timestampToUnixSeconds(input: Expr): Expr = FunctionExpr("timestamp_to_unix_seconds", input) + fun timestampToUnixSeconds(expr: Expr): Expr = FunctionExpr("timestamp_to_unix_seconds", expr) - /** @return A new [Expr] representing the timestampToUnixSeconds operation. */ + /** + * Creates an expression that converts a timestamp field to the number of seconds since the Unix + * epoch (1970-01-01 00:00:00 UTC). + * + * @param fieldName The name of the field that contains the timestamp. + * @return A new [Expr] representing the number of seconds since epoch. + */ @JvmStatic fun timestampToUnixSeconds(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_seconds", fieldName) @@ -3316,14 +3377,23 @@ abstract class Expr internal constructor() { fun endsWith(suffix: String) = Companion.endsWith(this, suffix) /** + * Creates an expression that converts this string expression to lowercase. + * + * @return A new [Expr] representing the lowercase string. */ fun toLower() = Companion.toLower(this) /** + * Creates an expression that converts this string expression to uppercase. + * + * @return A new [Expr] representing the lowercase string. */ fun toUpper() = Companion.toUpper(this) /** + * Creates an expression that removes leading and trailing whitespace from this string expression. + * + * @return A new [Expr] representing the trimmed string. */ fun trim() = Companion.trim(this) @@ -3468,6 +3538,10 @@ abstract class Expr internal constructor() { fun unixMicrosToTimestamp() = Companion.unixMicrosToTimestamp(this) /** + * Creates an expression that converts this timestamp expression to the number of microseconds + * since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * @return A new [Expr] representing the number of microseconds since epoch. */ fun timestampToUnixMicros() = Companion.timestampToUnixMicros(this) @@ -3476,6 +3550,10 @@ abstract class Expr internal constructor() { fun unixMillisToTimestamp() = Companion.unixMillisToTimestamp(this) /** + * Creates an expression that converts this timestamp expression to the number of milliseconds + * since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * @return A new [Expr] representing the number of milliseconds since epoch. */ fun timestampToUnixMillis() = Companion.timestampToUnixMillis(this) @@ -3484,6 +3562,10 @@ abstract class Expr internal constructor() { fun unixSecondsToTimestamp() = Companion.unixSecondsToTimestamp(this) /** + * Creates an expression that converts this timestamp expression to the number of seconds since + * the Unix epoch (1970-01-01 00:00:00 UTC). + * + * @return A new [Expr] representing the number of seconds since epoch. */ fun timestampToUnixSeconds() = Companion.timestampToUnixSeconds(this) From 8a58ed2b3aa038685032c9d6094d8b6fe27dfcbc Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 7 May 2025 17:39:15 -0400 Subject: [PATCH 063/152] More expression work --- firebase-firestore/api.txt | 48 +++---- .../firebase/firestore/PipelineTest.java | 6 +- .../firebase/firestore/pipeline/aggregates.kt | 105 +++++++++++++-- .../firestore/pipeline/expressions.kt | 126 +++++++++++++++--- 4 files changed, 223 insertions(+), 62 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 5da5bd2fad6..7ad2c871db7 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -691,35 +691,35 @@ package com.google.firebase.firestore.pipeline { public final class AggregateFunction { method public com.google.firebase.firestore.pipeline.AggregateWithAlias alias(String alias); - method public static com.google.firebase.firestore.pipeline.AggregateFunction avg(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateFunction avg(com.google.firebase.firestore.pipeline.Expr expression); method public static com.google.firebase.firestore.pipeline.AggregateFunction avg(String fieldName); - method public static com.google.firebase.firestore.pipeline.AggregateFunction count(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateFunction count(com.google.firebase.firestore.pipeline.Expr expression); method public static com.google.firebase.firestore.pipeline.AggregateFunction count(String fieldName); method public static com.google.firebase.firestore.pipeline.AggregateFunction countAll(); method public static com.google.firebase.firestore.pipeline.AggregateFunction countIf(com.google.firebase.firestore.pipeline.BooleanExpr condition); method public static com.google.firebase.firestore.pipeline.AggregateFunction generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); - method public static com.google.firebase.firestore.pipeline.AggregateFunction max(com.google.firebase.firestore.pipeline.Expr expr); - method public static com.google.firebase.firestore.pipeline.AggregateFunction max(String fieldName); - method public static com.google.firebase.firestore.pipeline.AggregateFunction min(com.google.firebase.firestore.pipeline.Expr expr); - method public static com.google.firebase.firestore.pipeline.AggregateFunction min(String fieldName); - method public static com.google.firebase.firestore.pipeline.AggregateFunction sum(com.google.firebase.firestore.pipeline.Expr expr); + method public static com.google.firebase.firestore.pipeline.AggregateFunction maximum(com.google.firebase.firestore.pipeline.Expr expression); + method public static com.google.firebase.firestore.pipeline.AggregateFunction maximum(String fieldName); + method public static com.google.firebase.firestore.pipeline.AggregateFunction minimum(com.google.firebase.firestore.pipeline.Expr expression); + method public static com.google.firebase.firestore.pipeline.AggregateFunction minimum(String fieldName); + method public static com.google.firebase.firestore.pipeline.AggregateFunction sum(com.google.firebase.firestore.pipeline.Expr expression); method public static com.google.firebase.firestore.pipeline.AggregateFunction sum(String fieldName); field public static final com.google.firebase.firestore.pipeline.AggregateFunction.Companion Companion; } public static final class AggregateFunction.Companion { - method public com.google.firebase.firestore.pipeline.AggregateFunction avg(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateFunction avg(com.google.firebase.firestore.pipeline.Expr expression); method public com.google.firebase.firestore.pipeline.AggregateFunction avg(String fieldName); - method public com.google.firebase.firestore.pipeline.AggregateFunction count(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateFunction count(com.google.firebase.firestore.pipeline.Expr expression); method public com.google.firebase.firestore.pipeline.AggregateFunction count(String fieldName); method public com.google.firebase.firestore.pipeline.AggregateFunction countAll(); method public com.google.firebase.firestore.pipeline.AggregateFunction countIf(com.google.firebase.firestore.pipeline.BooleanExpr condition); method public com.google.firebase.firestore.pipeline.AggregateFunction generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); - method public com.google.firebase.firestore.pipeline.AggregateFunction max(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.AggregateFunction max(String fieldName); - method public com.google.firebase.firestore.pipeline.AggregateFunction min(com.google.firebase.firestore.pipeline.Expr expr); - method public com.google.firebase.firestore.pipeline.AggregateFunction min(String fieldName); - method public com.google.firebase.firestore.pipeline.AggregateFunction sum(com.google.firebase.firestore.pipeline.Expr expr); + method public com.google.firebase.firestore.pipeline.AggregateFunction maximum(com.google.firebase.firestore.pipeline.Expr expression); + method public com.google.firebase.firestore.pipeline.AggregateFunction maximum(String fieldName); + method public com.google.firebase.firestore.pipeline.AggregateFunction minimum(com.google.firebase.firestore.pipeline.Expr expression); + method public com.google.firebase.firestore.pipeline.AggregateFunction minimum(String fieldName); + method public com.google.firebase.firestore.pipeline.AggregateFunction sum(com.google.firebase.firestore.pipeline.Expr expression); method public com.google.firebase.firestore.pipeline.AggregateFunction sum(String fieldName); } @@ -738,8 +738,8 @@ package com.google.firebase.firestore.pipeline { } public class BooleanExpr extends com.google.firebase.firestore.pipeline.FunctionExpr { - method public final com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.Expr then, com.google.firebase.firestore.pipeline.Expr otherwise); - method public final com.google.firebase.firestore.pipeline.Expr cond(Object then, Object otherwise); + method public final com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.Expr thenExpr, com.google.firebase.firestore.pipeline.Expr elseExpr); + method public final com.google.firebase.firestore.pipeline.Expr cond(Object thenValue, Object elseValue); method public final com.google.firebase.firestore.pipeline.AggregateFunction countIf(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); method public final com.google.firebase.firestore.pipeline.BooleanExpr not(); @@ -1062,8 +1062,8 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.Expr mapRemove(String key); method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, com.google.firebase.firestore.pipeline.Expr key); method public static final com.google.firebase.firestore.pipeline.Expr mapRemove(String mapField, String key); - method public final com.google.firebase.firestore.pipeline.AggregateFunction max(); - method public final com.google.firebase.firestore.pipeline.AggregateFunction min(); + method public final com.google.firebase.firestore.pipeline.AggregateFunction maximum(); + method public final com.google.firebase.firestore.pipeline.AggregateFunction minimum(); method public final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr divisor); method public static final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr dividend, com.google.firebase.firestore.pipeline.Expr divisor); method public static final com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr dividend, Number divisor); @@ -1194,18 +1194,18 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.Expr trim(com.google.firebase.firestore.pipeline.Expr stringExpression); method public static final com.google.firebase.firestore.pipeline.Expr trim(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(); - method public static final com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(); method public static final com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(String fieldName); method public final com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(); - method public static final com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public static final com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(String fieldName); method public static final com.google.firebase.firestore.pipeline.Expr vector(com.google.firebase.firestore.VectorValue vector); method public static final com.google.firebase.firestore.pipeline.Expr vector(double[] vector); method public final com.google.firebase.firestore.pipeline.Expr vectorLength(); - method public static final com.google.firebase.firestore.pipeline.Expr vectorLength(com.google.firebase.firestore.pipeline.Expr vector); + method public static final com.google.firebase.firestore.pipeline.Expr vectorLength(com.google.firebase.firestore.pipeline.Expr vectorExpression); method public static final com.google.firebase.firestore.pipeline.Expr vectorLength(String fieldName); method public static final com.google.firebase.firestore.pipeline.BooleanExpr xor(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); field public static final com.google.firebase.firestore.pipeline.Expr.Companion Companion; @@ -1456,15 +1456,15 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr toUpper(String fieldName); method public com.google.firebase.firestore.pipeline.Expr trim(com.google.firebase.firestore.pipeline.Expr stringExpression); method public com.google.firebase.firestore.pipeline.Expr trim(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.Expr unixMicrosToTimestamp(String fieldName); method public com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.Expr unixMillisToTimestamp(String fieldName); - method public com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr input); + method public com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.Expr unixSecondsToTimestamp(String fieldName); method public com.google.firebase.firestore.pipeline.Expr vector(com.google.firebase.firestore.VectorValue vector); method public com.google.firebase.firestore.pipeline.Expr vector(double[] vector); - method public com.google.firebase.firestore.pipeline.Expr vectorLength(com.google.firebase.firestore.pipeline.Expr vector); + method public com.google.firebase.firestore.pipeline.Expr vectorLength(com.google.firebase.firestore.pipeline.Expr vectorExpression); method public com.google.firebase.firestore.pipeline.Expr vectorLength(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr xor(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); } 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 12a4a5aa1d7..344f66fddfd 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 @@ -244,7 +244,7 @@ public void aggregateResultsMany() { .aggregate( AggregateFunction.countAll().alias("count"), AggregateFunction.avg("rating").alias("avgRating"), - field("rating").max().alias("maxRating")) + field("rating").maximum().alias("maxRating")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) @@ -304,8 +304,8 @@ public void minAndMaxAccumulations() { .collection(randomCol) .aggregate( AggregateFunction.countAll().alias("count"), - field("rating").max().alias("maxRating"), - field("published").min().alias("minPublished")) + field("rating").maximum().alias("maxRating"), + field("published").minimum().alias("minPublished")) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt index 60bdbdab8eb..0ce88ce385b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/aggregates.kt @@ -35,29 +35,108 @@ private constructor( @JvmStatic fun generic(name: String, vararg expr: Expr) = AggregateFunction(name, expr) + /** + * Creates an aggregation that counts the total number of stage inputs. + * + * @return A new [AggregateFunction] representing the countAll aggregation. + */ @JvmStatic fun countAll() = AggregateFunction("count") + /** + * Creates an aggregation that counts the number of stage inputs where the input field exists. + * + * @param fieldName The name of the field to count. + * @return A new [AggregateFunction] representing the 'count' aggregation. + */ @JvmStatic fun count(fieldName: String) = AggregateFunction("count", fieldName) - @JvmStatic fun count(expr: Expr) = AggregateFunction("count", expr) - + /** + * Creates an aggregation that counts the number of stage inputs with valid evaluations of the + * provided [expression]. + * + * @param expression The expression to count. + * @return A new [AggregateFunction] representing the 'count' aggregation. + */ + @JvmStatic fun count(expression: Expr) = AggregateFunction("count", expression) + + /** + * Creates an aggregation that counts the number of stage inputs where the provided boolean + * expression evaluates to true. + * + * @param condition The boolean expression to evaluate on each input. + * @return A new [AggregateFunction] representing the count aggregation. + */ @JvmStatic fun countIf(condition: BooleanExpr) = AggregateFunction("countIf", condition) + /** + * Creates an aggregation that calculates the sum of a field's values across multiple stage + * inputs. + * + * @param fieldName The name of the field containing numeric values to sum up. + * @return A new [AggregateFunction] representing the average aggregation. + */ @JvmStatic fun sum(fieldName: String) = AggregateFunction("sum", fieldName) - @JvmStatic fun sum(expr: Expr) = AggregateFunction("sum", expr) - + /** + * Creates an aggregation that calculates the sum of values from an expression across multiple + * stage inputs. + * + * @param expression The expression to sum up. + * @return A new [AggregateFunction] representing the sum aggregation. + */ + @JvmStatic fun sum(expression: Expr) = AggregateFunction("sum", expression) + + /** + * Creates an aggregation that calculates the average (mean) of a field's values across multiple + * stage inputs. + * + * @param fieldName The name of the field containing numeric values to average. + * @return A new [AggregateFunction] representing the average aggregation. + */ @JvmStatic fun avg(fieldName: String) = AggregateFunction("avg", fieldName) - @JvmStatic fun avg(expr: Expr) = AggregateFunction("avg", expr) - - @JvmStatic fun min(fieldName: String) = AggregateFunction("min", fieldName) - - @JvmStatic fun min(expr: Expr) = AggregateFunction("min", expr) - - @JvmStatic fun max(fieldName: String) = AggregateFunction("max", fieldName) - - @JvmStatic fun max(expr: Expr) = AggregateFunction("max", expr) + /** + * Creates an aggregation that calculates the average (mean) of values from an expression across + * multiple stage inputs. + * + * @param expression The expression representing the values to average. + * @return A new [AggregateFunction] representing the average aggregation. + */ + @JvmStatic fun avg(expression: Expr) = AggregateFunction("avg", expression) + + /** + * Creates an aggregation that finds the minimum value of a field across multiple stage inputs. + * + * @param fieldName The name of the field to find the minimum value of. + * @return A new [AggregateFunction] representing the minimum aggregation. + */ + @JvmStatic fun minimum(fieldName: String) = AggregateFunction("min", fieldName) + + /** + * Creates an aggregation that finds the minimum value of an expression across multiple stage + * inputs. + * + * @param expression The expression to find the minimum value of. + * @return A new [AggregateFunction] representing the minimum aggregation. + */ + @JvmStatic fun minimum(expression: Expr) = AggregateFunction("min", expression) + + /** + * Creates an aggregation that finds the maximum value of a field across multiple stage inputs. + * + * @param fieldName The name of the field to find the maximum value of. + * @return A new [AggregateFunction] representing the maximum aggregation. + */ + @JvmStatic fun maximum(fieldName: String) = AggregateFunction("max", fieldName) + + /** + * Creates an aggregation that finds the maximum value of an expression across multiple stage + * inputs. + * + * @param expression The expression to find the maximum value of. + * @return A new [AggregateFunction] representing the maximum aggregation. + */ + @JvmStatic fun maximum(expression: Expr) = AggregateFunction("max", expression) } /** 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 4a5d2a4eb34..ba56d2bcb07 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 @@ -329,7 +329,7 @@ abstract class Expr internal constructor() { * Creates an expression that negates a boolean expression. * * @param condition The boolean expression to negate. - * @return A new [Expr] representing the not operation. + * @return A new [BooleanExpr] representing the not operation. */ @JvmStatic fun not(condition: BooleanExpr): BooleanExpr = BooleanExpr("not", condition) @@ -2033,17 +2033,40 @@ abstract class Expr internal constructor() { fun euclideanDistance(vectorFieldName: String, vector: VectorValue): Expr = FunctionExpr("euclidean_distance", vectorFieldName, vector) - /** @return A new [Expr] representing the vectorLength operation. */ - @JvmStatic fun vectorLength(vector: Expr): Expr = FunctionExpr("vector_length", vector) + /** + * Creates an expression that calculates the length (dimension) of a Firestore Vector. + * + * @param vectorExpression The expression representing the Firestore Vector. + * @return A new [Expr] representing the length (dimension) of the vector. + */ + @JvmStatic + fun vectorLength(vectorExpression: Expr): Expr = FunctionExpr("vector_length", vectorExpression) - /** @return A new [Expr] representing the vectorLength operation. */ + /** + * Creates an expression that calculates the length (dimension) of a Firestore Vector. + * + * @param fieldName The name of the field containing the Firestore Vector. + * @return A new [Expr] representing the length (dimension) of the vector. + */ @JvmStatic fun vectorLength(fieldName: String): Expr = FunctionExpr("vector_length", fieldName) - /** @return A new [Expr] representing the unixMicrosToTimestamp operation. */ + /** + * Creates an expression that interprets an expression as the number of microseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @param expr The expression representing the number of microseconds since epoch. + * @return A new [Expr] representing the timestamp. + */ @JvmStatic - fun unixMicrosToTimestamp(input: Expr): Expr = FunctionExpr("unix_micros_to_timestamp", input) + fun unixMicrosToTimestamp(expr: Expr): Expr = FunctionExpr("unix_micros_to_timestamp", expr) - /** @return A new [Expr] representing the unixMicrosToTimestamp operation. */ + /** + * Creates an expression that interprets a field's value as the number of microseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @param fieldName The name of the field containing the number of microseconds since epoch. + * @return A new [Expr] representing the timestamp. + */ @JvmStatic fun unixMicrosToTimestamp(fieldName: String): Expr = FunctionExpr("unix_micros_to_timestamp", fieldName) @@ -2069,10 +2092,23 @@ abstract class Expr internal constructor() { fun timestampToUnixMicros(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_micros", fieldName) + /** + * Creates an expression that interprets an expression as the number of milliseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @param expr The expression representing the number of milliseconds since epoch. + * @return A new [Expr] representing the timestamp. + */ @JvmStatic fun unixMillisToTimestamp(expr: Expr): Expr = FunctionExpr("unix_millis_to_timestamp", expr) - /** @return A new [Expr] representing the unixMillisToTimestamp operation. */ + /** + * Creates an expression that interprets a field's value as the number of milliseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @param fieldName The name of the field containing the number of milliseconds since epoch. + * @return A new [Expr] representing the timestamp. + */ @JvmStatic fun unixMillisToTimestamp(fieldName: String): Expr = FunctionExpr("unix_millis_to_timestamp", fieldName) @@ -2098,11 +2134,23 @@ abstract class Expr internal constructor() { fun timestampToUnixMillis(fieldName: String): Expr = FunctionExpr("timestamp_to_unix_millis", fieldName) - /** @return A new [Expr] representing the unixSecondsToTimestamp operation. */ + /** + * Creates an expression that interprets an expression as the number of seconds since the Unix + * epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @param expr The expression representing the number of seconds since epoch. + * @return A new [Expr] representing the timestamp. + */ @JvmStatic - fun unixSecondsToTimestamp(input: Expr): Expr = FunctionExpr("unix_seconds_to_timestamp", input) + fun unixSecondsToTimestamp(expr: Expr): Expr = FunctionExpr("unix_seconds_to_timestamp", expr) - /** @return A new [Expr] representing the unixSecondsToTimestamp operation. */ + /** + * Creates an expression that interprets a field's value as the number of seconds since the Unix + * epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @param fieldName The name of the field containing the number of seconds since epoch. + * @return A new [Expr] representing the timestamp. + */ @JvmStatic fun unixSecondsToTimestamp(fieldName: String): Expr = FunctionExpr("unix_seconds_to_timestamp", fieldName) @@ -3530,10 +3578,17 @@ abstract class Expr internal constructor() { fun euclideanDistance(vector: VectorValue): Expr = Companion.euclideanDistance(this, vector) /** + * Creates an expression that calculates the length (dimension) of a Firestore Vector. + * + * @return A new [Expr] representing the length (dimension) of the vector. */ fun vectorLength() = Companion.vectorLength(this) /** + * Creates an expression that interprets this expression as the number of microseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @return A new [Expr] representing the timestamp. */ fun unixMicrosToTimestamp() = Companion.unixMicrosToTimestamp(this) @@ -3546,6 +3601,10 @@ abstract class Expr internal constructor() { fun timestampToUnixMicros() = Companion.timestampToUnixMicros(this) /** + * Creates an expression that interprets this expression as the number of milliseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @return A new [Expr] representing the timestamp. */ fun unixMillisToTimestamp() = Companion.unixMillisToTimestamp(this) @@ -3558,6 +3617,10 @@ abstract class Expr internal constructor() { fun timestampToUnixMillis() = Companion.timestampToUnixMillis(this) /** + * Creates an expression that interprets this expression as the number of seconds since the Unix + * epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @return A new [Expr] representing the timestamp. */ fun unixSecondsToTimestamp() = Companion.unixSecondsToTimestamp(this) @@ -3717,7 +3780,7 @@ abstract class Expr internal constructor() { * Creates an aggregation that counts the number of stage inputs with valid evaluations of the * this expression. * - * @return A new [AggregateFunction] representing the 'count' aggregation. + * @return A new [AggregateFunction] representing the count aggregation. */ fun count(): AggregateFunction = AggregateFunction.count(this) @@ -3725,7 +3788,7 @@ abstract class Expr internal constructor() { * Creates an aggregation that calculates the sum of this numeric expression across multiple stage * inputs. * - * @return A new [AggregateFunction] representing the 'sum' aggregation. + * @return A new [AggregateFunction] representing the sum aggregation. */ fun sum(): AggregateFunction = AggregateFunction.sum(this) @@ -3733,7 +3796,7 @@ abstract class Expr internal constructor() { * Creates an aggregation that calculates the average (mean) of this numeric expression across * multiple stage inputs. * - * @return A new [AggregateFunction] representing the 'avg' aggregation. + * @return A new [AggregateFunction] representing the average aggregation. */ fun avg(): AggregateFunction = AggregateFunction.avg(this) @@ -3741,17 +3804,17 @@ abstract class Expr internal constructor() { * Creates an aggregation that finds the minimum value of this expression across multiple stage * inputs. * - * @return A new [AggregateFunction] representing the 'min' aggregation. + * @return A new [AggregateFunction] representing the minimum aggregation. */ - fun min(): AggregateFunction = AggregateFunction.min(this) + fun minimum(): AggregateFunction = AggregateFunction.minimum(this) /** * Creates an aggregation that finds the maximum value of this expression across multiple stage * inputs. * - * @return A new [AggregateFunction] representing the 'max' aggregation. + * @return A new [AggregateFunction] representing the maximum aggregation. */ - fun max(): AggregateFunction = AggregateFunction.max(this) + fun maximum(): AggregateFunction = AggregateFunction.maximum(this) /** * Create an [Ordering] that sorts documents in ascending order based on value of this expression @@ -4033,20 +4096,39 @@ open class BooleanExpr internal constructor(name: String, params: Array Date: Thu, 8 May 2025 11:26:28 -0400 Subject: [PATCH 064/152] Fix from API Review --- firebase-firestore/api.txt | 5 ++--- .../com/google/firebase/firestore/Pipeline.kt | 17 ----------------- .../firebase/firestore/pipeline/expressions.kt | 2 +- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 7ad2c871db7..9e981a511c1 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -433,7 +433,6 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Pipeline findNearest(String vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline findNearest(String vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline genericStage(com.google.firebase.firestore.pipeline.GenericStage stage); - method public com.google.firebase.firestore.Pipeline genericStage(String name, java.lang.Object... arguments); method public com.google.firebase.firestore.Pipeline limit(int limit); method public com.google.firebase.firestore.Pipeline offset(int offset); method public com.google.firebase.firestore.Pipeline removeFields(com.google.firebase.firestore.pipeline.Field field, com.google.firebase.firestore.pipeline.Field... additionalFields); @@ -991,7 +990,7 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.Expr floor(); method public static final com.google.firebase.firestore.pipeline.Expr floor(com.google.firebase.firestore.pipeline.Expr numericExpr); method public static final com.google.firebase.firestore.pipeline.Expr floor(String numericField); - method public static final com.google.firebase.firestore.pipeline.FunctionExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public static final com.google.firebase.firestore.pipeline.Expr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); method public final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); @@ -1324,7 +1323,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Field field(String name); method public com.google.firebase.firestore.pipeline.Expr floor(com.google.firebase.firestore.pipeline.Expr numericExpr); method public com.google.firebase.firestore.pipeline.Expr floor(String numericField); - method public com.google.firebase.firestore.pipeline.FunctionExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public com.google.firebase.firestore.pipeline.Expr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr gt(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 06e4142d81b..5184ac8c3c7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -102,23 +102,6 @@ internal constructor( .addAllStages(stages.map { it.toProtoStage(userDataReader) }) .build() - /** - * Adds a stage to the pipeline by specifying the stage name as an argument. This does not offer - * any type safety on the stage params and requires the caller to know the order (and optionally - * names) of parameters accepted by the stage. - * - * This method provides a way to call stages that are supported by the Firestore backend but that - * are not implemented in the SDK version being used. - * - * For stages with named parameters, use the [GenericStage] class instead. - * - * @param name The unique name of the stage to add. - * @param arguments A list of ordered parameters to configure the stage's behavior. - * @return A new [Pipeline] object with this stage appended to the stage list. - */ - fun genericStage(name: String, vararg arguments: Any): Pipeline = - append(GenericStage.ofName(name).withArguments(arguments)) - /** * Adds a stage to the pipeline by specifying the stage name as an argument. This does not offer * any type safety on the stage params and requires the caller to know the order (and optionally 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 ba56d2bcb07..47f1a974ec5 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 @@ -290,7 +290,7 @@ abstract class Expr internal constructor() { return Field(fieldPath.internalPath) } - @JvmStatic fun generic(name: String, vararg expr: Expr) = FunctionExpr(name, expr) + @JvmStatic fun generic(name: String, vararg expr: Expr): Expr = FunctionExpr(name, expr) /** * Creates an expression that performs a logical 'AND' operation. From ae034da115aea803f5593153563dcbbae1df91f4 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 12 May 2025 11:48:40 -0400 Subject: [PATCH 065/152] Realtime Pipeline Proto changes --- .../google/firebase/firestore/proto/target.proto | 3 +++ .../src/proto/google/firestore/v1/firestore.proto | 12 ++++++++++++ .../src/proto/google/firestore/v1/write.proto | 6 ++++++ 3 files changed, 21 insertions(+) diff --git a/firebase-firestore/src/proto/google/firebase/firestore/proto/target.proto b/firebase-firestore/src/proto/google/firebase/firestore/proto/target.proto index 8bca7e4adfd..65a8cf90a0a 100644 --- a/firebase-firestore/src/proto/google/firebase/firestore/proto/target.proto +++ b/firebase-firestore/src/proto/google/firebase/firestore/proto/target.proto @@ -77,6 +77,9 @@ message Target { // A target specified by a set of document names. google.firestore.v1.Target.DocumentsTarget documents = 6; + + // A target specified by a pipeline query. + google.firestore.v1.Target.PipelineQueryTarget pipeline_query = 13; } // Denotes the maximum snapshot version at which the associated query view diff --git a/firebase-firestore/src/proto/google/firestore/v1/firestore.proto b/firebase-firestore/src/proto/google/firestore/v1/firestore.proto index d59c9e2decb..be7ce9065c3 100644 --- a/firebase-firestore/src/proto/google/firestore/v1/firestore.proto +++ b/firebase-firestore/src/proto/google/firestore/v1/firestore.proto @@ -866,6 +866,15 @@ message Target { } } + // A target specified by a pipeline query. + message PipelineQueryTarget { + // The pipeline to run. + oneof pipeline_type { + // A pipelined operation in structured format. + StructuredPipeline structured_pipeline = 1; + } + } + // The type of target to listen to. oneof target_type { // A target specified by a query. @@ -873,6 +882,9 @@ message Target { // A target specified by a set of document names. DocumentsTarget documents = 3; + + // A target specified by a pipeline query. + PipelineQueryTarget pipeline_query = 13; } // When to start listening. diff --git a/firebase-firestore/src/proto/google/firestore/v1/write.proto b/firebase-firestore/src/proto/google/firestore/v1/write.proto index f74b32e2782..a7655332e16 100644 --- a/firebase-firestore/src/proto/google/firestore/v1/write.proto +++ b/firebase-firestore/src/proto/google/firestore/v1/write.proto @@ -200,6 +200,12 @@ message WriteResult { // // Multiple [DocumentChange][google.firestore.v1.DocumentChange] messages may be // returned for the same logical change, if multiple targets are affected. +// +// For PipelineQueryTargets, `document` will be in the new pipeline format, +// (-- TODO(b/330735468): Insert link to spec. --) +// For a Listen stream with both QueryTargets and PipelineQueryTargets present, +// if a document matches both types of queries, then a separate DocumentChange +// messages will be sent out one for each set. message DocumentChange { // The new state of the [Document][google.firestore.v1.Document]. // From 4e4a4b23ef92b2945a3c2dc853a7abbd51a3384e Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 15 May 2025 13:21:16 -0400 Subject: [PATCH 066/152] RealtimePipeline evaluate initial implementation --- .../com/google/firebase/firestore/Pipeline.kt | 251 ++++++--- .../firestore/core/CompositeFilter.java | 11 +- .../firebase/firestore/core/pipeline.kt | 16 + .../google/firebase/firestore/model/Values.kt | 7 +- .../firestore/pipeline/EvaluateResult.kt | 19 + .../firebase/firestore/pipeline/evaluation.kt | 118 ++++ .../firestore/pipeline/expressions.kt | 513 ++++++++++++------ .../firebase/firestore/pipeline/options.kt | 6 + .../firebase/firestore/pipeline/stage.kt | 45 +- .../google/firebase/firestore/TestUtil.java | 9 + .../firebase/firestore/core/PipelineTests.kt | 29 + 11 files changed, 763 insertions(+), 261 deletions(-) create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/core/pipeline.kt create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 5184ac8c3c7..a0140d8fc0f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -32,15 +32,18 @@ import com.google.firebase.firestore.pipeline.DatabaseSource import com.google.firebase.firestore.pipeline.DistinctStage import com.google.firebase.firestore.pipeline.DocumentsSource import com.google.firebase.firestore.pipeline.Expr +import com.google.firebase.firestore.pipeline.Expr.Companion.field import com.google.firebase.firestore.pipeline.ExprWithAlias import com.google.firebase.firestore.pipeline.Field import com.google.firebase.firestore.pipeline.FindNearestStage import com.google.firebase.firestore.pipeline.FunctionExpr import com.google.firebase.firestore.pipeline.GenericStage +import com.google.firebase.firestore.pipeline.InternalOptions import com.google.firebase.firestore.pipeline.LimitStage import com.google.firebase.firestore.pipeline.OffsetStage import com.google.firebase.firestore.pipeline.Ordering import com.google.firebase.firestore.pipeline.PipelineOptions +import com.google.firebase.firestore.pipeline.RealtimePipelineOptions import com.google.firebase.firestore.pipeline.RemoveFieldsStage import com.google.firebase.firestore.pipeline.ReplaceStage import com.google.firebase.firestore.pipeline.SampleStage @@ -55,12 +58,81 @@ import com.google.firestore.v1.ExecutePipelineRequest import com.google.firestore.v1.StructuredPipeline import com.google.firestore.v1.Value -class Pipeline +open class AbstractPipeline internal constructor( internal val firestore: FirebaseFirestore, internal val userDataReader: UserDataReader, - private val stages: FluentIterable> + internal val stages: FluentIterable> ) { + private fun toStructuredPipelineProto(options: InternalOptions?): StructuredPipeline { + val builder = StructuredPipeline.newBuilder() + builder.pipeline = toPipelineProto() + options?.forEach(builder::putOptions) + return builder.build() + } + + internal fun toPipelineProto(): com.google.firestore.v1.Pipeline = + com.google.firestore.v1.Pipeline.newBuilder() + .addAllStages(stages.map { it.toProtoStage(userDataReader) }) + .build() + + private fun toExecutePipelineRequest(options: InternalOptions?): ExecutePipelineRequest { + val database = firestore.databaseId + val builder = ExecutePipelineRequest.newBuilder() + builder.database = "projects/${database.projectId}/databases/${database.databaseId}" + builder.structuredPipeline = toStructuredPipelineProto(options) + return builder.build() + } + + protected fun execute(options: InternalOptions?): Task { + val request = toExecutePipelineRequest(options) + val observerTask = ObserverSnapshotTask() + firestore.callClient { call -> call!!.executePipeline(request, observerTask) } + return observerTask.task + } + + private inner class ObserverSnapshotTask : PipelineResultObserver { + private val userDataWriter = + UserDataWriter(firestore, DocumentSnapshot.ServerTimestampBehavior.DEFAULT) + private val taskCompletionSource = TaskCompletionSource() + private val results: ImmutableList.Builder = ImmutableList.builder() + override fun onDocument( + key: DocumentKey?, + data: Map, + createTime: Timestamp?, + updateTime: Timestamp? + ) { + results.add( + PipelineResult( + firestore, + userDataWriter, + if (key == null) null else DocumentReference(key, firestore), + data, + createTime, + updateTime + ) + ) + } + + override fun onComplete(executionTime: Timestamp) { + taskCompletionSource.setResult(PipelineSnapshot(executionTime, results.build())) + } + + override fun onError(exception: FirebaseFirestoreException) { + taskCompletionSource.setException(exception) + } + + val task: Task + get() = taskCompletionSource.task + } +} + +class Pipeline +private constructor( + firestore: FirebaseFirestore, + userDataReader: UserDataReader, + stages: FluentIterable> +) : AbstractPipeline(firestore, userDataReader, stages) { internal constructor( firestore: FirebaseFirestore, userDataReader: UserDataReader, @@ -71,37 +143,14 @@ internal constructor( return Pipeline(firestore, userDataReader, stages.append(stage)) } - fun execute(): Task = execute(PipelineOptions.DEFAULT) + fun execute(): Task = execute(null) - fun execute(options: PipelineOptions): Task { - val observerTask = ObserverSnapshotTask() - firestore.callClient { call -> call!!.executePipeline(toProto(options), observerTask) } - return observerTask.task - } + fun execute(options: RealtimePipelineOptions): Task = execute(options.options) internal fun documentReference(key: DocumentKey): DocumentReference { return DocumentReference(key, firestore) } - private fun toProto(options: PipelineOptions): ExecutePipelineRequest { - val database = firestore.databaseId - val builder = ExecutePipelineRequest.newBuilder() - builder.database = "projects/${database.projectId}/databases/${database.databaseId}" - builder.structuredPipeline = toStructuredPipelineProto() - return builder.build() - } - - private fun toStructuredPipelineProto(): StructuredPipeline { - val builder = StructuredPipeline.newBuilder() - builder.pipeline = toPipelineProto() - return builder.build() - } - - internal fun toPipelineProto(): com.google.firestore.v1.Pipeline = - com.google.firestore.v1.Pipeline.newBuilder() - .addAllStages(stages.map { it.toProtoStage(userDataReader) }) - .build() - /** * Adds a stage to the pipeline by specifying the stage name as an argument. This does not offer * any type safety on the stage params and requires the caller to know the order (and optionally @@ -153,9 +202,7 @@ internal constructor( */ fun removeFields(field: String, vararg additionalFields: String): Pipeline = append( - RemoveFieldsStage( - arrayOf(Expr.field(field), *additionalFields.map(Expr::field).toTypedArray()) - ) + RemoveFieldsStage(arrayOf(field(field), *additionalFields.map(Expr::field).toTypedArray())) ) /** @@ -178,11 +225,7 @@ internal constructor( * @return A new [Pipeline] object with this stage appended to the stage list. */ fun select(selection: Selectable, vararg additionalSelections: Any): Pipeline = - append( - SelectStage( - arrayOf(selection, *additionalSelections.map(Selectable::toSelectable).toTypedArray()) - ) - ) + append(SelectStage.of(selection, *additionalSelections)) /** * Selects or creates a set of fields from the outputs of previous stages. @@ -204,14 +247,7 @@ internal constructor( * @return A new [Pipeline] object with this stage appended to the stage list. */ fun select(fieldName: String, vararg additionalSelections: Any): Pipeline = - append( - SelectStage( - arrayOf( - Expr.field(fieldName), - *additionalSelections.map(Selectable::toSelectable).toTypedArray() - ) - ) - ) + append(SelectStage.of(fieldName, *additionalSelections)) /** * Sorts the documents from previous stages based on one or more [Ordering] criteria. @@ -320,10 +356,7 @@ internal constructor( fun distinct(groupField: String, vararg additionalGroups: Any): Pipeline = append( DistinctStage( - arrayOf( - Expr.field(groupField), - *additionalGroups.map(Selectable::toSelectable).toTypedArray() - ) + arrayOf(field(groupField), *additionalGroups.map(Selectable::toSelectable).toTypedArray()) ) ) @@ -453,7 +486,7 @@ internal constructor( * @param field The [String] specifying the field name containing the nested map. * @return A new [Pipeline] object with this stage appended to the stage list. */ - fun replace(field: String): Pipeline = replace(Expr.field(field)) + fun replace(field: String): Pipeline = replace(field(field)) /** * Fully overwrites all fields in a document with those coming from a nested map. @@ -514,8 +547,7 @@ internal constructor( * @param alias The name of field to store emitted element of array. * @return A new [Pipeline] object with this stage appended to the stage list. */ - fun unnest(arrayField: String, alias: String): Pipeline = - unnest(Expr.field(arrayField).alias(alias)) + fun unnest(arrayField: String, alias: String): Pipeline = unnest(field(arrayField).alias(alias)) /** * Takes a specified array from the input documents and outputs a document for each element with @@ -550,41 +582,6 @@ internal constructor( * @return A new [Pipeline] object with this stage appended to the stage list. */ fun unnest(unnestStage: UnnestStage): Pipeline = append(unnestStage) - - private inner class ObserverSnapshotTask : PipelineResultObserver { - private val userDataWriter = - UserDataWriter(firestore, DocumentSnapshot.ServerTimestampBehavior.DEFAULT) - private val taskCompletionSource = TaskCompletionSource() - private val results: ImmutableList.Builder = ImmutableList.builder() - override fun onDocument( - key: DocumentKey?, - data: Map, - createTime: Timestamp?, - updateTime: Timestamp? - ) { - results.add( - PipelineResult( - firestore, - userDataWriter, - if (key == null) null else DocumentReference(key, firestore), - data, - createTime, - updateTime - ) - ) - } - - override fun onComplete(executionTime: Timestamp) { - taskCompletionSource.setResult(PipelineSnapshot(executionTime, results.build())) - } - - override fun onError(exception: FirebaseFirestoreException) { - taskCompletionSource.setException(exception) - } - - val task: Task - get() = taskCompletionSource.task - } } /** Start of a Firestore Pipeline */ @@ -644,7 +641,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * Set the pipeline's source to the collection specified by CollectionSource. * * @param stage A [CollectionSource] that will be the source of this pipeline. - * @return Pipeline with documents from target collection. + * @return A new [Pipeline] object with documents from target collection. * @throws [IllegalArgumentException] Thrown if the [stage] provided targets a different project * or database than the pipeline. */ @@ -659,6 +656,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * Set the pipeline's source to the collection group with the given id. * * @param collectionId The id of a collection group that will be the source of this pipeline. + * @return A new [Pipeline] object with documents from target collection group. */ fun collectionGroup(collectionId: String): Pipeline = pipeline(CollectionGroupSource.of((collectionId))) @@ -710,6 +708,87 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto } } +class RealtimePipelineSource internal constructor(private val firestore: FirebaseFirestore) { + + /** + * Set the pipeline's source to the collection specified by the given path. + * + * @param path A path to a collection that will be the source of this pipeline. + * @return A new [RealtimePipeline] object with documents from target collection. + */ + fun collection(path: String): RealtimePipeline = collection(CollectionSource.of(path)) + + /** + * Set the pipeline's source to the collection specified by the given [CollectionReference]. + * + * @param ref A [CollectionReference] for a collection that will be the source of this pipeline. + * @return A new [RealtimePipeline] object with documents from target collection. + * @throws [IllegalArgumentException] Thrown if the [ref] provided targets a different project or + * database than the pipeline. + */ + fun collection(ref: CollectionReference): RealtimePipeline = collection(CollectionSource.of(ref)) + + /** + * Set the pipeline's source to the collection specified by CollectionSource. + * + * @param stage A [CollectionSource] that will be the source of this pipeline. + * @return A new [RealtimePipeline] object with documents from target collection. + * @throws [IllegalArgumentException] Thrown if the [stage] provided targets a different project + * or database than the pipeline. + */ + fun collection(stage: CollectionSource): RealtimePipeline { + if (stage.firestore != null && stage.firestore.databaseId != firestore.databaseId) { + throw IllegalArgumentException("Provided collection is from a different Firestore instance.") + } + return RealtimePipeline(firestore, firestore.userDataReader, stage) + } + + /** + * Set the pipeline's source to the collection group with the given id. + * + * @param collectionId The id of a collection group that will be the source of this pipeline. + * @return A new [RealtimePipeline] object with documents from target collection group. + */ + fun collectionGroup(collectionId: String): RealtimePipeline = + pipeline(CollectionGroupSource.of((collectionId))) + + fun pipeline(stage: CollectionGroupSource): RealtimePipeline = + RealtimePipeline(firestore, firestore.userDataReader, stage) +} + +class RealtimePipeline +internal constructor( + firestore: FirebaseFirestore, + userDataReader: UserDataReader, + stages: FluentIterable> +) : AbstractPipeline(firestore, userDataReader, stages) { + internal constructor( + firestore: FirebaseFirestore, + userDataReader: UserDataReader, + stage: Stage<*> + ) : this(firestore, userDataReader, FluentIterable.of(stage)) + + private fun append(stage: Stage<*>): RealtimePipeline { + return RealtimePipeline(firestore, userDataReader, stages.append(stage)) + } + + fun execute(): Task = execute(null) + + fun execute(options: PipelineOptions): Task = execute(options.options) + + fun limit(limit: Int): RealtimePipeline = append(LimitStage(limit)) + + fun offset(offset: Int): RealtimePipeline = append(OffsetStage(offset)) + + fun select(selection: Selectable, vararg additionalSelections: Any): RealtimePipeline = + append(SelectStage.of(selection, *additionalSelections)) + + fun select(fieldName: String, vararg additionalSelections: Any): RealtimePipeline = + append(SelectStage.of(fieldName, *additionalSelections)) + + fun where(condition: BooleanExpr): RealtimePipeline = append(WhereStage(condition)) +} + /** */ class PipelineSnapshot diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java index ee471318f80..090ddf9c17b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java @@ -170,13 +170,16 @@ public String getCanonicalId() { @Override BooleanExpr toPipelineExpr() { - BooleanExpr[] booleanExprs = - filters.stream().map(Filter::toPipelineExpr).toArray(BooleanExpr[]::new); + BooleanExpr first = filters.get(0).toPipelineExpr(); + BooleanExpr[] additional = new BooleanExpr[filters.size() - 1]; + for (int i = 1, filtersSize = filters.size(); i < filtersSize; i++) { + additional[i - 1] = filters.get(i).toPipelineExpr(); + } switch (operator) { case AND: - return new BooleanExpr("and", booleanExprs); + return BooleanExpr.and(first, additional); case OR: - return new BooleanExpr("or", booleanExprs); + return BooleanExpr.or(first, additional); } // Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed throw new IllegalArgumentException("Unsupported operator: " + operator); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/pipeline.kt new file mode 100644 index 00000000000..480cf87af3b --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/pipeline.kt @@ -0,0 +1,16 @@ +package com.google.firebase.firestore.core + +import com.google.firebase.firestore.AbstractPipeline +import com.google.firebase.firestore.model.MutableDocument +import com.google.firebase.firestore.pipeline.EvaluationContext +import kotlinx.coroutines.flow.Flow + +internal fun runPipeline( + pipeline: AbstractPipeline, + input: Flow +): Flow { + val context = EvaluationContext(pipeline.userDataReader) + return pipeline.stages.fold(input) { documentFlow, stage -> + stage.evaluate(context, documentFlow) + } +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 3bcd2ed3c38..2a14d5acb70 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -599,8 +599,11 @@ internal object Values { .build() } - @JvmStatic - fun encodeValue(value: Boolean): Value = Value.newBuilder().setBooleanValue(value).build() + @JvmField val TRUE_VALUE: Value = Value.newBuilder().setBooleanValue(true).build() + + @JvmField val FALSE_VALUE: Value = Value.newBuilder().setBooleanValue(false).build() + + @JvmStatic fun encodeValue(value: Boolean): Value = if (value) TRUE_VALUE else FALSE_VALUE @JvmStatic fun encodeValue(geoPoint: GeoPoint): Value = diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt new file mode 100644 index 00000000000..60bed411728 --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt @@ -0,0 +1,19 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firebase.firestore.model.Values +import com.google.firestore.v1.Value + +internal sealed class EvaluateResult(val value: Value?) { + companion object { + val TRUE: EvaluateResultValue = EvaluateResultValue(Values.TRUE_VALUE) + val FALSE: EvaluateResultValue = EvaluateResultValue(Values.FALSE_VALUE) + val NULL: EvaluateResultValue = EvaluateResultValue(Values.NULL_VALUE) + fun booleanValue(boolean: Boolean) = if (boolean) TRUE else FALSE + } +} + +internal object EvaluateResultError : EvaluateResult(null) + +internal object EvaluateResultUnset : EvaluateResult(null) + +internal class EvaluateResultValue(value: Value) : EvaluateResult(value) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt new file mode 100644 index 00000000000..69f56e94e88 --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -0,0 +1,118 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firebase.firestore.UserDataReader +import com.google.firebase.firestore.model.Values +import com.google.firebase.firestore.util.Assert +import com.google.firestore.v1.Value + +internal class EvaluationContext(val userDataReader: UserDataReader) + +internal fun interface EvaluateFunction { + fun evaluate(params: Sequence): EvaluateResult +} + +private fun evaluateValue( + params: Sequence, + next: (value: Value) -> EvaluateResult?, + complete: () -> EvaluateResult +): EvaluateResult { + for (value in params.map(EvaluateResult::value)) { + if (value == null) return EvaluateResultError + val result = next(value) + if (result != null) return result + } + return complete() +} + +private fun evaluateValueShortCircuitNull( + function: (values: List) -> EvaluateResult +): EvaluateFunction { + return object : EvaluateFunction { + override fun evaluate(params: Sequence): EvaluateResult { + val values = buildList { + for (value in params.map(EvaluateResult::value)) { + if (value == null) return EvaluateResultError + if (value.hasNullValue()) return EvaluateResult.NULL + add(value) + } + } + return function.invoke(values) + } + } +} + +private fun evaluateBooleanValue( + function: (values: List) -> EvaluateResult +): EvaluateFunction { + return object : EvaluateFunction { + override fun evaluate(params: Sequence): EvaluateResult { + val values = buildList { + for (value in params.map(EvaluateResult::value)) { + if (value == null) return EvaluateResultError + if (value.hasNullValue()) return EvaluateResult.NULL + if (!value.hasBooleanValue()) return EvaluateResultError + add(value.booleanValue) + } + } + return function.invoke(values) + } + } +} + +private fun evaluateBooleanValue( + params: Sequence, + next: (value: Boolean) -> Boolean, + complete: () -> EvaluateResult +): EvaluateResult { + for (value in params.map(EvaluateResult::value)) { + if (value == null) return EvaluateResultError + if (value.hasNullValue()) return EvaluateResult.NULL + if (!value.hasBooleanValue()) return EvaluateResultError + if (!next(value.booleanValue)) break + } + return complete() +} + +internal val evaluateNotImplemented = EvaluateFunction { _ -> throw NotImplementedError() } + +internal val evaluateAnd = EvaluateFunction { params -> + var result: EvaluateResult = EvaluateResult.TRUE + evaluateValue( + params, + fun(value: Value): EvaluateResult? { + when (value.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> result = EvaluateResult.NULL + Value.ValueTypeCase.BOOLEAN_VALUE -> { + if (!value.booleanValue) return EvaluateResult.FALSE + } + else -> return EvaluateResultError + } + return null + }, + { result } + ) +} +internal val evaluateOr = EvaluateFunction { params -> + var result: EvaluateResult = EvaluateResult.FALSE + evaluateValue( + params, + fun(value: Value): EvaluateResult? { + when (value.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> result = EvaluateResult.NULL + Value.ValueTypeCase.BOOLEAN_VALUE -> { + if (value.booleanValue) return EvaluateResult.TRUE + } + else -> return EvaluateResultError + } + return null + }, + { result } + ) +} +internal val evaluateXor = evaluateBooleanValue { params -> + EvaluateResult.booleanValue(params.fold(false, Boolean::xor)) +} +internal val evaluateEq = evaluateValueShortCircuitNull { values -> + Assert.hardAssert(values.size == 2, "Eq function should have exactly 2 params") + EvaluateResult.booleanValue(Values.equals(values.get(0), values.get(1))) +} 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 47f1a974ec5..99f54e29de2 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 @@ -24,6 +24,7 @@ import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.VectorValue import com.google.firebase.firestore.model.DocumentKey import com.google.firebase.firestore.model.FieldPath as ModelFieldPath +import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.pipeline.Expr.Companion.field @@ -50,6 +51,9 @@ abstract class Expr internal constructor() { private class ValueConstant(val value: Value) : Expr() { override fun toProto(userDataReader: UserDataReader): Value = value + override fun evaluate(context: EvaluationContext) = { _: MutableDocument -> + EvaluateResultValue(value) + } } companion object { @@ -152,7 +156,7 @@ abstract class Expr internal constructor() { @JvmStatic fun constant(value: Boolean): BooleanExpr { val encodedValue = encodeValue(value) - return object : BooleanExpr("N/A", emptyArray()) { + return object : BooleanExpr("N/A", { EvaluateResultValue(encodedValue) }, emptyArray()) { override fun toProto(userDataReader: UserDataReader): Value { return encodedValue } @@ -213,6 +217,13 @@ abstract class Expr internal constructor() { userDataReader.validateDocumentReference(ref, ::IllegalArgumentException) return encodeValue(ref) } + + override fun evaluate( + context: EvaluationContext + ): (input: MutableDocument) -> EvaluateResult { + val result = EvaluateResultValue(toProto(context.userDataReader)) + return { _ -> result } + } } } @@ -290,40 +301,42 @@ abstract class Expr internal constructor() { return Field(fieldPath.internalPath) } - @JvmStatic fun generic(name: String, vararg expr: Expr): Expr = FunctionExpr(name, expr) + @JvmStatic + fun generic(name: String, vararg expr: Expr): Expr = + FunctionExpr(name, evaluateNotImplemented, expr) /** * Creates an expression that performs a logical 'AND' operation. * * @param condition The first [BooleanExpr]. - * @param conditions Addition [BooleanExpr]s. + * @param conditions Additional [BooleanExpr]s. * @return A new [BooleanExpr] representing the logical 'AND' operation. */ @JvmStatic fun and(condition: BooleanExpr, vararg conditions: BooleanExpr) = - BooleanExpr("and", condition, *conditions) + BooleanExpr("and", evaluateAnd, condition, *conditions) /** * Creates an expression that performs a logical 'OR' operation. * * @param condition The first [BooleanExpr]. - * @param conditions Addition [BooleanExpr]s. + * @param conditions Additional [BooleanExpr]s. * @return A new [BooleanExpr] representing the logical 'OR' operation. */ @JvmStatic fun or(condition: BooleanExpr, vararg conditions: BooleanExpr) = - BooleanExpr("or", condition, *conditions) + BooleanExpr("or", evaluateOr, condition, *conditions) /** * Creates an expression that performs a logical 'XOR' operation. * * @param condition The first [BooleanExpr]. - * @param conditions Addition [BooleanExpr]s. + * @param conditions Additional [BooleanExpr]s. * @return A new [BooleanExpr] representing the logical 'XOR' operation. */ @JvmStatic fun xor(condition: BooleanExpr, vararg conditions: BooleanExpr) = - BooleanExpr("xor", condition, *conditions) + BooleanExpr("xor", evaluateXor, condition, *conditions) /** * Creates an expression that negates a boolean expression. @@ -331,7 +344,9 @@ abstract class Expr internal constructor() { * @param condition The boolean expression to negate. * @return A new [BooleanExpr] representing the not operation. */ - @JvmStatic fun not(condition: BooleanExpr): BooleanExpr = BooleanExpr("not", condition) + @JvmStatic + fun not(condition: BooleanExpr): BooleanExpr = + BooleanExpr("not", evaluateNotImplemented, condition) /** * Creates an expression that applies a bitwise AND operation between two expressions. @@ -341,7 +356,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the bitwise AND operation. */ @JvmStatic - fun bitAnd(bits: Expr, bitsOther: Expr): Expr = FunctionExpr("bit_and", bits, bitsOther) + fun bitAnd(bits: Expr, bitsOther: Expr): Expr = + FunctionExpr("bit_and", evaluateNotImplemented, bits, bitsOther) /** * Creates an expression that applies a bitwise AND operation between an expression and a @@ -353,7 +369,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitAnd(bits: Expr, bitsOther: ByteArray): Expr = - FunctionExpr("bit_and", bits, constant(bitsOther)) + FunctionExpr("bit_and", evaluateNotImplemented, bits, constant(bitsOther)) /** * Creates an expression that applies a bitwise AND operation between an field and an @@ -386,7 +402,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the bitwise OR operation. */ @JvmStatic - fun bitOr(bits: Expr, bitsOther: Expr): Expr = FunctionExpr("bit_or", bits, bitsOther) + fun bitOr(bits: Expr, bitsOther: Expr): Expr = + FunctionExpr("bit_or", evaluateNotImplemented, bits, bitsOther) /** * Creates an expression that applies a bitwise OR operation between an expression and a @@ -398,7 +415,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitOr(bits: Expr, bitsOther: ByteArray): Expr = - FunctionExpr("bit_or", bits, constant(bitsOther)) + FunctionExpr("bit_or", evaluateNotImplemented, bits, constant(bitsOther)) /** * Creates an expression that applies a bitwise OR operation between an field and an expression. @@ -430,7 +447,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the bitwise XOR operation. */ @JvmStatic - fun bitXor(bits: Expr, bitsOther: Expr): Expr = FunctionExpr("bit_xor", bits, bitsOther) + fun bitXor(bits: Expr, bitsOther: Expr): Expr = + FunctionExpr("bit_xor", evaluateNotImplemented, bits, bitsOther) /** * Creates an expression that applies a bitwise XOR operation between an expression and a @@ -442,7 +460,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitXor(bits: Expr, bitsOther: ByteArray): Expr = - FunctionExpr("bit_xor", bits, constant(bitsOther)) + FunctionExpr("bit_xor", evaluateNotImplemented, bits, constant(bitsOther)) /** * Creates an expression that applies a bitwise XOR operation between an field and an @@ -473,7 +491,7 @@ abstract class Expr internal constructor() { * @param bits An expression that returns bits when evaluated. * @return A new [Expr] representing the bitwise NOT operation. */ - @JvmStatic fun bitNot(bits: Expr): Expr = FunctionExpr("bit_not", bits) + @JvmStatic fun bitNot(bits: Expr): Expr = FunctionExpr("bit_not", evaluateNotImplemented, bits) /** * Creates an expression that applies a bitwise NOT operation to a field. @@ -481,7 +499,9 @@ abstract class Expr internal constructor() { * @param bitsFieldName Name of field that contains bits data. * @return A new [Expr] representing the bitwise NOT operation. */ - @JvmStatic fun bitNot(bitsFieldName: String): Expr = FunctionExpr("bit_not", bitsFieldName) + @JvmStatic + fun bitNot(bitsFieldName: String): Expr = + FunctionExpr("bit_not", bitsFieldName, evaluateNotImplemented) /** * Creates an expression that applies a bitwise left shift operation between two expressions. @@ -492,7 +512,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitLeftShift(bits: Expr, numberExpr: Expr): Expr = - FunctionExpr("bit_left_shift", bits, numberExpr) + FunctionExpr("bit_left_shift", evaluateNotImplemented, bits, numberExpr) /** * Creates an expression that applies a bitwise left shift operation between an expression and a @@ -538,7 +558,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitRightShift(bits: Expr, numberExpr: Expr): Expr = - FunctionExpr("bit_right_shift", bits, numberExpr) + FunctionExpr("bit_right_shift", evaluateNotImplemented, bits, numberExpr) /** * Creates an expression that applies a bitwise right shift operation between an expression and @@ -583,7 +603,8 @@ abstract class Expr internal constructor() { * @param numericExpr An expression that returns number when evaluated. * @return A new [Expr] representing an integer result from the round operation. */ - @JvmStatic fun round(numericExpr: Expr): Expr = FunctionExpr("round", numericExpr) + @JvmStatic + fun round(numericExpr: Expr): Expr = FunctionExpr("round", evaluateNotImplemented, numericExpr) /** * Creates an expression that rounds [numericField] to nearest integer. @@ -593,7 +614,9 @@ abstract class Expr internal constructor() { * @param numericField Name of field that returns number when evaluated. * @return A new [Expr] representing an integer result from the round operation. */ - @JvmStatic fun round(numericField: String): Expr = FunctionExpr("round", numericField) + @JvmStatic + fun round(numericField: String): Expr = + FunctionExpr("round", numericField, evaluateNotImplemented) /** * Creates an expression that rounds off [numericExpr] to [decimalPlace] decimal places if @@ -606,7 +629,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun roundToPrecision(numericExpr: Expr, decimalPlace: Int): Expr = - FunctionExpr("round", numericExpr, constant(decimalPlace)) + FunctionExpr("round", evaluateNotImplemented, numericExpr, constant(decimalPlace)) /** * Creates an expression that rounds off [numericField] to [decimalPlace] decimal places if @@ -632,7 +655,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun roundToPrecision(numericExpr: Expr, decimalPlace: Expr): Expr = - FunctionExpr("round", numericExpr, decimalPlace) + FunctionExpr("round", evaluateNotImplemented, numericExpr, decimalPlace) /** * Creates an expression that rounds off [numericField] to [decimalPlace] decimal places if @@ -653,7 +676,8 @@ abstract class Expr internal constructor() { * @param numericExpr An expression that returns number when evaluated. * @return A new [Expr] representing an integer result from the ceil operation. */ - @JvmStatic fun ceil(numericExpr: Expr): Expr = FunctionExpr("ceil", numericExpr) + @JvmStatic + fun ceil(numericExpr: Expr): Expr = FunctionExpr("ceil", evaluateNotImplemented, numericExpr) /** * Creates an expression that returns the smalled integer that isn't less than [numericField]. @@ -661,7 +685,9 @@ abstract class Expr internal constructor() { * @param numericField Name of field that returns number when evaluated. * @return A new [Expr] representing an integer result from the ceil operation. */ - @JvmStatic fun ceil(numericField: String): Expr = FunctionExpr("ceil", numericField) + @JvmStatic + fun ceil(numericField: String): Expr = + FunctionExpr("ceil", numericField, evaluateNotImplemented) /** * Creates an expression that returns the largest integer that isn't less than [numericExpr]. @@ -669,7 +695,8 @@ abstract class Expr internal constructor() { * @param numericExpr An expression that returns number when evaluated. * @return A new [Expr] representing an integer result from the floor operation. */ - @JvmStatic fun floor(numericExpr: Expr): Expr = FunctionExpr("floor", numericExpr) + @JvmStatic + fun floor(numericExpr: Expr): Expr = FunctionExpr("floor", evaluateNotImplemented, numericExpr) /** * Creates an expression that returns the largest integer that isn't less than [numericField]. @@ -677,7 +704,9 @@ abstract class Expr internal constructor() { * @param numericField Name of field that returns number when evaluated. * @return A new [Expr] representing an integer result from the floor operation. */ - @JvmStatic fun floor(numericField: String): Expr = FunctionExpr("floor", numericField) + @JvmStatic + fun floor(numericField: String): Expr = + FunctionExpr("floor", numericField, evaluateNotImplemented) /** * Creates an expression that returns the [numericExpr] raised to the power of the [exponent]. @@ -690,7 +719,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun pow(numericExpr: Expr, exponent: Number): Expr = - FunctionExpr("pow", numericExpr, constant(exponent)) + FunctionExpr("pow", evaluateNotImplemented, numericExpr, constant(exponent)) /** * Creates an expression that returns the [numericField] raised to the power of the [exponent]. @@ -715,7 +744,8 @@ abstract class Expr internal constructor() { * [exponent]. */ @JvmStatic - fun pow(numericExpr: Expr, exponent: Expr): Expr = FunctionExpr("pow", numericExpr, exponent) + fun pow(numericExpr: Expr, exponent: Expr): Expr = + FunctionExpr("pow", evaluateNotImplemented, numericExpr, exponent) /** * Creates an expression that returns the [numericField] raised to the power of the [exponent]. @@ -736,7 +766,8 @@ abstract class Expr internal constructor() { * @param numericExpr An expression that returns number when evaluated. * @return A new [Expr] representing the numeric result of the square root operation. */ - @JvmStatic fun sqrt(numericExpr: Expr): Expr = FunctionExpr("sqrt", numericExpr) + @JvmStatic + fun sqrt(numericExpr: Expr): Expr = FunctionExpr("sqrt", evaluateNotImplemented, numericExpr) /** * Creates an expression that returns the square root of [numericField]. @@ -744,7 +775,9 @@ abstract class Expr internal constructor() { * @param numericField Name of field that returns number when evaluated. * @return A new [Expr] representing the numeric result of the square root operation. */ - @JvmStatic fun sqrt(numericField: String): Expr = FunctionExpr("sqrt", numericField) + @JvmStatic + fun sqrt(numericField: String): Expr = + FunctionExpr("sqrt", numericField, evaluateNotImplemented) /** * Creates an expression that adds numeric expressions and constants. @@ -803,7 +836,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun subtract(minuend: Expr, subtrahend: Expr): Expr = - FunctionExpr("subtract", minuend, subtrahend) + FunctionExpr("subtract", evaluateNotImplemented, minuend, subtrahend) /** * Creates an expression that subtracts a constant value from a numeric expression. @@ -894,7 +927,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the division operation. */ @JvmStatic - fun divide(dividend: Expr, divisor: Expr): Expr = FunctionExpr("divide", dividend, divisor) + fun divide(dividend: Expr, divisor: Expr): Expr = + FunctionExpr("divide", evaluateNotImplemented, dividend, divisor) /** * Creates an expression that divides a numeric expression by a constant. @@ -936,7 +970,9 @@ abstract class Expr internal constructor() { * @param divisor The numeric expression to divide by. * @return A new [Expr] representing the modulo operation. */ - @JvmStatic fun mod(dividend: Expr, divisor: Expr): Expr = FunctionExpr("mod", dividend, divisor) + @JvmStatic + fun mod(dividend: Expr, divisor: Expr): Expr = + FunctionExpr("mod", evaluateNotImplemented, dividend, divisor) /** * Creates an expression that calculates the modulo (remainder) of dividing a numeric expression @@ -996,7 +1032,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun eqAny(expression: Expr, arrayExpression: Expr): BooleanExpr = - BooleanExpr("eq_any", expression, arrayExpression) + BooleanExpr("eq_any", evaluateNotImplemented, expression, arrayExpression) /** * Creates an expression that checks if a field's value is equal to any of the provided [values] @@ -1021,7 +1057,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun eqAny(fieldName: String, arrayExpression: Expr): BooleanExpr = - BooleanExpr("eq_any", fieldName, arrayExpression) + BooleanExpr("eq_any", evaluateNotImplemented, fieldName, arrayExpression) /** * Creates an expression that checks if an [expression], when evaluated, is not equal to all the @@ -1046,7 +1082,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun notEqAny(expression: Expr, arrayExpression: Expr): BooleanExpr = - BooleanExpr("not_eq_any", expression, arrayExpression) + BooleanExpr("not_eq_any", evaluateNotImplemented, expression, arrayExpression) /** * Creates an expression that checks if a field's value is not equal to all of the provided @@ -1071,7 +1107,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun notEqAny(fieldName: String, arrayExpression: Expr): BooleanExpr = - BooleanExpr("not_eq_any", fieldName, arrayExpression) + BooleanExpr("not_eq_any", evaluateNotImplemented, fieldName, arrayExpression) /** * Creates an expression that returns true if a value is absent. Otherwise, returns false even @@ -1080,7 +1116,8 @@ abstract class Expr internal constructor() { * @param value The expression to check. * @return A new [BooleanExpr] representing the isAbsent operation. */ - @JvmStatic fun isAbsent(value: Expr): BooleanExpr = BooleanExpr("is_absent", value) + @JvmStatic + fun isAbsent(value: Expr): BooleanExpr = BooleanExpr("is_absent", evaluateNotImplemented, value) /** * Creates an expression that returns true if a field is absent. Otherwise, returns false even @@ -1089,7 +1126,9 @@ abstract class Expr internal constructor() { * @param fieldName The field to check. * @return A new [BooleanExpr] representing the isAbsent operation. */ - @JvmStatic fun isAbsent(fieldName: String): BooleanExpr = BooleanExpr("is_absent", fieldName) + @JvmStatic + fun isAbsent(fieldName: String): BooleanExpr = + BooleanExpr("is_absent", evaluateNotImplemented, fieldName) /** * Creates an expression that checks if an expression evaluates to 'NaN' (Not a Number). @@ -1097,7 +1136,8 @@ abstract class Expr internal constructor() { * @param expr The expression to check. * @return A new [BooleanExpr] representing the isNan operation. */ - @JvmStatic fun isNan(expr: Expr): BooleanExpr = BooleanExpr("is_nan", expr) + @JvmStatic + fun isNan(expr: Expr): BooleanExpr = BooleanExpr("is_nan", evaluateNotImplemented, expr) /** * Creates an expression that checks if [expr] evaluates to 'NaN' (Not a Number). @@ -1105,7 +1145,9 @@ abstract class Expr internal constructor() { * @param fieldName The field to check. * @return A new [BooleanExpr] representing the isNan operation. */ - @JvmStatic fun isNan(fieldName: String): BooleanExpr = BooleanExpr("is_nan", fieldName) + @JvmStatic + fun isNan(fieldName: String): BooleanExpr = + BooleanExpr("is_nan", evaluateNotImplemented, fieldName) /** * Creates an expression that checks if the results of [expr] is NOT 'NaN' (Not a Number). @@ -1113,7 +1155,8 @@ abstract class Expr internal constructor() { * @param expr The expression to check. * @return A new [BooleanExpr] representing the isNotNan operation. */ - @JvmStatic fun isNotNan(expr: Expr): BooleanExpr = BooleanExpr("is_not_nan", expr) + @JvmStatic + fun isNotNan(expr: Expr): BooleanExpr = BooleanExpr("is_not_nan", evaluateNotImplemented, expr) /** * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a @@ -1122,7 +1165,9 @@ abstract class Expr internal constructor() { * @param fieldName The field to check. * @return A new [BooleanExpr] representing the isNotNan operation. */ - @JvmStatic fun isNotNan(fieldName: String): BooleanExpr = BooleanExpr("is_not_nan", fieldName) + @JvmStatic + fun isNotNan(fieldName: String): BooleanExpr = + BooleanExpr("is_not_nan", evaluateNotImplemented, fieldName) /** * Creates an expression that checks if tbe result of [expr] is null. @@ -1130,7 +1175,8 @@ abstract class Expr internal constructor() { * @param expr The expression to check. * @return A new [BooleanExpr] representing the isNull operation. */ - @JvmStatic fun isNull(expr: Expr): BooleanExpr = BooleanExpr("is_null", expr) + @JvmStatic + fun isNull(expr: Expr): BooleanExpr = BooleanExpr("is_null", evaluateNotImplemented, expr) /** * Creates an expression that checks if tbe value of a field is null. @@ -1138,7 +1184,9 @@ abstract class Expr internal constructor() { * @param fieldName The field to check. * @return A new [BooleanExpr] representing the isNull operation. */ - @JvmStatic fun isNull(fieldName: String): BooleanExpr = BooleanExpr("is_null", fieldName) + @JvmStatic + fun isNull(fieldName: String): BooleanExpr = + BooleanExpr("is_null", evaluateNotImplemented, fieldName) /** * Creates an expression that checks if tbe result of [expr] is not null. @@ -1146,7 +1194,9 @@ abstract class Expr internal constructor() { * @param expr The expression to check. * @return A new [BooleanExpr] representing the isNotNull operation. */ - @JvmStatic fun isNotNull(expr: Expr): BooleanExpr = BooleanExpr("is_not_null", expr) + @JvmStatic + fun isNotNull(expr: Expr): BooleanExpr = + BooleanExpr("is_not_null", evaluateNotImplemented, expr) /** * Creates an expression that checks if tbe value of a field is not null. @@ -1154,7 +1204,9 @@ abstract class Expr internal constructor() { * @param fieldName The field to check. * @return A new [BooleanExpr] representing the isNotNull operation. */ - @JvmStatic fun isNotNull(fieldName: String): BooleanExpr = BooleanExpr("is_not_null", fieldName) + @JvmStatic + fun isNotNull(fieldName: String): BooleanExpr = + BooleanExpr("is_not_null", evaluateNotImplemented, fieldName) /** * Creates an expression that replaces the first occurrence of a substring within the @@ -1271,7 +1323,8 @@ abstract class Expr internal constructor() { * @param expr The expression representing the string. * @return A new [Expr] representing the charLength operation. */ - @JvmStatic fun charLength(expr: Expr): Expr = FunctionExpr("char_length", expr) + @JvmStatic + fun charLength(expr: Expr): Expr = FunctionExpr("char_length", evaluateNotImplemented, expr) /** * Creates an expression that calculates the character length of a string field in UTF8. @@ -1279,7 +1332,9 @@ abstract class Expr internal constructor() { * @param fieldName The name of the field containing the string. * @return A new [Expr] representing the charLength operation. */ - @JvmStatic fun charLength(fieldName: String): Expr = FunctionExpr("char_length", fieldName) + @JvmStatic + fun charLength(fieldName: String): Expr = + FunctionExpr("char_length", fieldName, evaluateNotImplemented) /** * Creates an expression that calculates the length of a string in UTF-8 bytes, or just the @@ -1288,7 +1343,8 @@ abstract class Expr internal constructor() { * @param value The expression representing the string. * @return A new [Expr] representing the length of the string in bytes. */ - @JvmStatic fun byteLength(value: Expr): Expr = FunctionExpr("byte_length", value) + @JvmStatic + fun byteLength(value: Expr): Expr = FunctionExpr("byte_length", evaluateNotImplemented, value) /** * Creates an expression that calculates the length of a string represented by a field in UTF-8 @@ -1297,7 +1353,9 @@ abstract class Expr internal constructor() { * @param fieldName The name of the field containing the string. * @return A new [Expr] representing the length of the string in bytes. */ - @JvmStatic fun byteLength(fieldName: String): Expr = FunctionExpr("byte_length", fieldName) + @JvmStatic + fun byteLength(fieldName: String): Expr = + FunctionExpr("byte_length", fieldName, evaluateNotImplemented) /** * Creates an expression that performs a case-sensitive wildcard string comparison. @@ -1308,7 +1366,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(stringExpression: Expr, pattern: Expr): BooleanExpr = - BooleanExpr("like", stringExpression, pattern) + BooleanExpr("like", evaluateNotImplemented, stringExpression, pattern) /** * Creates an expression that performs a case-sensitive wildcard string comparison. @@ -1319,7 +1377,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(stringExpression: Expr, pattern: String): BooleanExpr = - BooleanExpr("like", stringExpression, pattern) + BooleanExpr("like", evaluateNotImplemented, stringExpression, pattern) /** * Creates an expression that performs a case-sensitive wildcard string comparison against a @@ -1331,7 +1389,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(fieldName: String, pattern: Expr): BooleanExpr = - BooleanExpr("like", fieldName, pattern) + BooleanExpr("like", evaluateNotImplemented, fieldName, pattern) /** * Creates an expression that performs a case-sensitive wildcard string comparison against a @@ -1343,7 +1401,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(fieldName: String, pattern: String): BooleanExpr = - BooleanExpr("like", fieldName, pattern) + BooleanExpr("like", evaluateNotImplemented, fieldName, pattern) /** * Creates an expression that return a pseudo-random number of type double in the range of [0, @@ -1351,7 +1409,7 @@ abstract class Expr internal constructor() { * * @return A new [Expr] representing the random number operation. */ - @JvmStatic fun rand(): Expr = FunctionExpr("rand") + @JvmStatic fun rand(): Expr = FunctionExpr("rand", evaluateNotImplemented) /** * Creates an expression that checks if a string expression contains a specified regular @@ -1363,7 +1421,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(stringExpression: Expr, pattern: Expr): BooleanExpr = - BooleanExpr("regex_contains", stringExpression, pattern) + BooleanExpr("regex_contains", evaluateNotImplemented, stringExpression, pattern) /** * Creates an expression that checks if a string expression contains a specified regular @@ -1375,7 +1433,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(stringExpression: Expr, pattern: String): BooleanExpr = - BooleanExpr("regex_contains", stringExpression, pattern) + BooleanExpr("regex_contains", evaluateNotImplemented, stringExpression, pattern) /** * Creates an expression that checks if a string field contains a specified regular expression @@ -1387,7 +1445,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(fieldName: String, pattern: Expr) = - BooleanExpr("regex_contains", fieldName, pattern) + BooleanExpr("regex_contains", evaluateNotImplemented, fieldName, pattern) /** * Creates an expression that checks if a string field contains a specified regular expression @@ -1399,7 +1457,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(fieldName: String, pattern: String) = - BooleanExpr("regex_contains", fieldName, pattern) + BooleanExpr("regex_contains", evaluateNotImplemented, fieldName, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1410,7 +1468,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(stringExpression: Expr, pattern: Expr): BooleanExpr = - BooleanExpr("regex_match", stringExpression, pattern) + BooleanExpr("regex_match", evaluateNotImplemented, stringExpression, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1421,7 +1479,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(stringExpression: Expr, pattern: String): BooleanExpr = - BooleanExpr("regex_match", stringExpression, pattern) + BooleanExpr("regex_match", evaluateNotImplemented, stringExpression, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1432,7 +1490,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(fieldName: String, pattern: Expr) = - BooleanExpr("regex_match", fieldName, pattern) + BooleanExpr("regex_match", evaluateNotImplemented, fieldName, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1443,7 +1501,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(fieldName: String, pattern: String) = - BooleanExpr("regex_match", fieldName, pattern) + BooleanExpr("regex_match", evaluateNotImplemented, fieldName, pattern) /** * Creates an expression that returns the largest value between multiple input expressions or @@ -1499,7 +1557,9 @@ abstract class Expr internal constructor() { * @param stringExpression An expression evaluating to a string value, which will be reversed. * @return A new [Expr] representing the reversed string. */ - @JvmStatic fun reverse(stringExpression: Expr): Expr = FunctionExpr("reverse", stringExpression) + @JvmStatic + fun reverse(stringExpression: Expr): Expr = + FunctionExpr("reverse", evaluateNotImplemented, stringExpression) /** * Creates an expression that reverses a string value from the specified field. @@ -1507,7 +1567,9 @@ abstract class Expr internal constructor() { * @param fieldName The name of the field that contains the string to reverse. * @return A new [Expr] representing the reversed string. */ - @JvmStatic fun reverse(fieldName: String): Expr = FunctionExpr("reverse", fieldName) + @JvmStatic + fun reverse(fieldName: String): Expr = + FunctionExpr("reverse", fieldName, evaluateNotImplemented) /** * Creates an expression that checks if a string expression contains a specified substring. @@ -1518,7 +1580,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(stringExpression: Expr, substring: Expr): BooleanExpr = - BooleanExpr("str_contains", stringExpression, substring) + BooleanExpr("str_contains", evaluateNotImplemented, stringExpression, substring) /** * Creates an expression that checks if a string expression contains a specified substring. @@ -1529,7 +1591,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(stringExpression: Expr, substring: String): BooleanExpr = - BooleanExpr("str_contains", stringExpression, substring) + BooleanExpr("str_contains", evaluateNotImplemented, stringExpression, substring) /** * Creates an expression that checks if a string field contains a specified substring. @@ -1540,7 +1602,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(fieldName: String, substring: Expr): BooleanExpr = - BooleanExpr("str_contains", fieldName, substring) + BooleanExpr("str_contains", evaluateNotImplemented, fieldName, substring) /** * Creates an expression that checks if a string field contains a specified substring. @@ -1551,7 +1613,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(fieldName: String, substring: String): BooleanExpr = - BooleanExpr("str_contains", fieldName, substring) + BooleanExpr("str_contains", evaluateNotImplemented, fieldName, substring) /** * Creates an expression that checks if a string expression starts with a given [prefix]. @@ -1562,7 +1624,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun startsWith(stringExpr: Expr, prefix: Expr): BooleanExpr = - BooleanExpr("starts_with", stringExpr, prefix) + BooleanExpr("starts_with", evaluateNotImplemented, stringExpr, prefix) /** * Creates an expression that checks if a string expression starts with a given [prefix]. @@ -1573,7 +1635,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun startsWith(stringExpr: Expr, prefix: String): BooleanExpr = - BooleanExpr("starts_with", stringExpr, prefix) + BooleanExpr("starts_with", evaluateNotImplemented, stringExpr, prefix) /** * Creates an expression that checks if a string expression starts with a given [prefix]. @@ -1584,7 +1646,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun startsWith(fieldName: String, prefix: Expr): BooleanExpr = - BooleanExpr("starts_with", fieldName, prefix) + BooleanExpr("starts_with", evaluateNotImplemented, fieldName, prefix) /** * Creates an expression that checks if a string expression starts with a given [prefix]. @@ -1595,7 +1657,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun startsWith(fieldName: String, prefix: String): BooleanExpr = - BooleanExpr("starts_with", fieldName, prefix) + BooleanExpr("starts_with", evaluateNotImplemented, fieldName, prefix) /** * Creates an expression that checks if a string expression ends with a given [suffix]. @@ -1606,7 +1668,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun endsWith(stringExpr: Expr, suffix: Expr): BooleanExpr = - BooleanExpr("ends_with", stringExpr, suffix) + BooleanExpr("ends_with", evaluateNotImplemented, stringExpr, suffix) /** * Creates an expression that checks if a string expression ends with a given [suffix]. @@ -1617,7 +1679,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun endsWith(stringExpr: Expr, suffix: String): BooleanExpr = - BooleanExpr("ends_with", stringExpr, suffix) + BooleanExpr("ends_with", evaluateNotImplemented, stringExpr, suffix) /** * Creates an expression that checks if a string expression ends with a given [suffix]. @@ -1628,7 +1690,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun endsWith(fieldName: String, suffix: Expr): BooleanExpr = - BooleanExpr("ends_with", fieldName, suffix) + BooleanExpr("ends_with", evaluateNotImplemented, fieldName, suffix) /** * Creates an expression that checks if a string expression ends with a given [suffix]. @@ -1639,7 +1701,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun endsWith(fieldName: String, suffix: String): BooleanExpr = - BooleanExpr("ends_with", fieldName, suffix) + BooleanExpr("ends_with", evaluateNotImplemented, fieldName, suffix) /** * Creates an expression that converts a string expression to lowercase. @@ -1648,7 +1710,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the lowercase string. */ @JvmStatic - fun toLower(stringExpression: Expr): Expr = FunctionExpr("to_lower", stringExpression) + fun toLower(stringExpression: Expr): Expr = + FunctionExpr("to_lower", evaluateNotImplemented, stringExpression) /** * Creates an expression that converts a string field to lowercase. @@ -1656,7 +1719,9 @@ abstract class Expr internal constructor() { * @param fieldName The name of the field containing the string to convert to lowercase. * @return A new [Expr] representing the lowercase string. */ - @JvmStatic fun toLower(fieldName: String): Expr = FunctionExpr("to_lower", fieldName) + @JvmStatic + fun toLower(fieldName: String): Expr = + FunctionExpr("to_lower", fieldName, evaluateNotImplemented) /** * Creates an expression that converts a string expression to uppercase. @@ -1665,7 +1730,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the lowercase string. */ @JvmStatic - fun toUpper(stringExpression: Expr): Expr = FunctionExpr("to_upper", stringExpression) + fun toUpper(stringExpression: Expr): Expr = + FunctionExpr("to_upper", evaluateNotImplemented, stringExpression) /** * Creates an expression that converts a string field to uppercase. @@ -1673,7 +1739,9 @@ abstract class Expr internal constructor() { * @param fieldName The name of the field containing the string to convert to uppercase. * @return A new [Expr] representing the lowercase string. */ - @JvmStatic fun toUpper(fieldName: String): Expr = FunctionExpr("to_upper", fieldName) + @JvmStatic + fun toUpper(fieldName: String): Expr = + FunctionExpr("to_upper", fieldName, evaluateNotImplemented) /** * Creates an expression that removes leading and trailing whitespace from a string expression. @@ -1681,7 +1749,9 @@ abstract class Expr internal constructor() { * @param stringExpression The expression representing the string to trim. * @return A new [Expr] representing the trimmed string. */ - @JvmStatic fun trim(stringExpression: Expr): Expr = FunctionExpr("trim", stringExpression) + @JvmStatic + fun trim(stringExpression: Expr): Expr = + FunctionExpr("trim", evaluateNotImplemented, stringExpression) /** * Creates an expression that removes leading and trailing whitespace from a string field. @@ -1689,7 +1759,8 @@ abstract class Expr internal constructor() { * @param fieldName The name of the field containing the string to trim. * @return A new [Expr] representing the trimmed string. */ - @JvmStatic fun trim(fieldName: String): Expr = FunctionExpr("trim", fieldName) + @JvmStatic + fun trim(fieldName: String): Expr = FunctionExpr("trim", fieldName, evaluateNotImplemented) /** * Creates an expression that concatenates string expressions together. @@ -1737,7 +1808,8 @@ abstract class Expr internal constructor() { fun strConcat(fieldName: String, vararg otherStrings: Any): Expr = FunctionExpr("str_concat", fieldName, *otherStrings) - internal fun map(elements: Array): Expr = FunctionExpr("map", elements) + internal fun map(elements: Array): Expr = + FunctionExpr("map", evaluateNotImplemented, elements) /** * Creates an expression that creates a Firestore map value from an input object. @@ -1803,7 +1875,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] that evaluates to a modified map. */ @JvmStatic - fun mapRemove(mapExpr: Expr, key: Expr): Expr = FunctionExpr("map_remove", mapExpr, key) + fun mapRemove(mapExpr: Expr, key: Expr): Expr = + FunctionExpr("map_remove", evaluateNotImplemented, mapExpr, key) /** * Creates an expression that removes a key from the map produced by evaluating an expression. @@ -1844,7 +1917,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: Expr): Expr = - FunctionExpr("cosine_distance", vector1, vector2) + FunctionExpr("cosine_distance", evaluateNotImplemented, vector1, vector2) /** * Calculates the Cosine distance between vector expression and a vector literal. @@ -1855,7 +1928,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: DoubleArray): Expr = - FunctionExpr("cosine_distance", vector1, vector(vector2)) + FunctionExpr("cosine_distance", evaluateNotImplemented, vector1, vector(vector2)) /** * Calculates the Cosine distance between vector expression and a vector literal. @@ -1910,7 +1983,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vector1: Expr, vector2: Expr): Expr = - FunctionExpr("dot_product", vector1, vector2) + FunctionExpr("dot_product", evaluateNotImplemented, vector1, vector2) /** * Calculates the dot product distance between vector expression and a vector literal. @@ -1921,7 +1994,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vector1: Expr, vector2: DoubleArray): Expr = - FunctionExpr("dot_product", vector1, vector(vector2)) + FunctionExpr("dot_product", evaluateNotImplemented, vector1, vector(vector2)) /** * Calculates the dot product distance between vector expression and a vector literal. @@ -1976,7 +2049,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: Expr): Expr = - FunctionExpr("euclidean_distance", vector1, vector2) + FunctionExpr("euclidean_distance", evaluateNotImplemented, vector1, vector2) /** * Calculates the Euclidean distance between vector expression and a vector literal. @@ -1987,7 +2060,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: DoubleArray): Expr = - FunctionExpr("euclidean_distance", vector1, vector(vector2)) + FunctionExpr("euclidean_distance", evaluateNotImplemented, vector1, vector(vector2)) /** * Calculates the Euclidean distance between vector expression and a vector literal. @@ -2040,7 +2113,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the length (dimension) of the vector. */ @JvmStatic - fun vectorLength(vectorExpression: Expr): Expr = FunctionExpr("vector_length", vectorExpression) + fun vectorLength(vectorExpression: Expr): Expr = + FunctionExpr("vector_length", evaluateNotImplemented, vectorExpression) /** * Creates an expression that calculates the length (dimension) of a Firestore Vector. @@ -2048,7 +2122,9 @@ abstract class Expr internal constructor() { * @param fieldName The name of the field containing the Firestore Vector. * @return A new [Expr] representing the length (dimension) of the vector. */ - @JvmStatic fun vectorLength(fieldName: String): Expr = FunctionExpr("vector_length", fieldName) + @JvmStatic + fun vectorLength(fieldName: String): Expr = + FunctionExpr("vector_length", fieldName, evaluateNotImplemented) /** * Creates an expression that interprets an expression as the number of microseconds since the @@ -2058,7 +2134,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the timestamp. */ @JvmStatic - fun unixMicrosToTimestamp(expr: Expr): Expr = FunctionExpr("unix_micros_to_timestamp", expr) + fun unixMicrosToTimestamp(expr: Expr): Expr = + FunctionExpr("unix_micros_to_timestamp", evaluateNotImplemented, expr) /** * Creates an expression that interprets a field's value as the number of microseconds since the @@ -2069,7 +2146,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixMicrosToTimestamp(fieldName: String): Expr = - FunctionExpr("unix_micros_to_timestamp", fieldName) + FunctionExpr("unix_micros_to_timestamp", fieldName, evaluateNotImplemented) /** * Creates an expression that converts a timestamp expression to the number of microseconds @@ -2079,7 +2156,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the number of microseconds since epoch. */ @JvmStatic - fun timestampToUnixMicros(expr: Expr): Expr = FunctionExpr("timestamp_to_unix_micros", expr) + fun timestampToUnixMicros(expr: Expr): Expr = + FunctionExpr("timestamp_to_unix_micros", evaluateNotImplemented, expr) /** * Creates an expression that converts a timestamp field to the number of microseconds since the @@ -2090,7 +2168,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixMicros(fieldName: String): Expr = - FunctionExpr("timestamp_to_unix_micros", fieldName) + FunctionExpr("timestamp_to_unix_micros", fieldName, evaluateNotImplemented) /** * Creates an expression that interprets an expression as the number of milliseconds since the @@ -2100,7 +2178,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the timestamp. */ @JvmStatic - fun unixMillisToTimestamp(expr: Expr): Expr = FunctionExpr("unix_millis_to_timestamp", expr) + fun unixMillisToTimestamp(expr: Expr): Expr = + FunctionExpr("unix_millis_to_timestamp", evaluateNotImplemented, expr) /** * Creates an expression that interprets a field's value as the number of milliseconds since the @@ -2111,7 +2190,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixMillisToTimestamp(fieldName: String): Expr = - FunctionExpr("unix_millis_to_timestamp", fieldName) + FunctionExpr("unix_millis_to_timestamp", fieldName, evaluateNotImplemented) /** * Creates an expression that converts a timestamp expression to the number of milliseconds @@ -2121,7 +2200,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the number of milliseconds since epoch. */ @JvmStatic - fun timestampToUnixMillis(expr: Expr): Expr = FunctionExpr("timestamp_to_unix_millis", expr) + fun timestampToUnixMillis(expr: Expr): Expr = + FunctionExpr("timestamp_to_unix_millis", evaluateNotImplemented, expr) /** * Creates an expression that converts a timestamp field to the number of milliseconds since the @@ -2132,7 +2212,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixMillis(fieldName: String): Expr = - FunctionExpr("timestamp_to_unix_millis", fieldName) + FunctionExpr("timestamp_to_unix_millis", fieldName, evaluateNotImplemented) /** * Creates an expression that interprets an expression as the number of seconds since the Unix @@ -2142,7 +2222,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the timestamp. */ @JvmStatic - fun unixSecondsToTimestamp(expr: Expr): Expr = FunctionExpr("unix_seconds_to_timestamp", expr) + fun unixSecondsToTimestamp(expr: Expr): Expr = + FunctionExpr("unix_seconds_to_timestamp", evaluateNotImplemented, expr) /** * Creates an expression that interprets a field's value as the number of seconds since the Unix @@ -2153,7 +2234,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixSecondsToTimestamp(fieldName: String): Expr = - FunctionExpr("unix_seconds_to_timestamp", fieldName) + FunctionExpr("unix_seconds_to_timestamp", fieldName, evaluateNotImplemented) /** * Creates an expression that converts a timestamp expression to the number of seconds since the @@ -2163,7 +2244,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the number of seconds since epoch. */ @JvmStatic - fun timestampToUnixSeconds(expr: Expr): Expr = FunctionExpr("timestamp_to_unix_seconds", expr) + fun timestampToUnixSeconds(expr: Expr): Expr = + FunctionExpr("timestamp_to_unix_seconds", evaluateNotImplemented, expr) /** * Creates an expression that converts a timestamp field to the number of seconds since the Unix @@ -2174,7 +2256,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixSeconds(fieldName: String): Expr = - FunctionExpr("timestamp_to_unix_seconds", fieldName) + FunctionExpr("timestamp_to_unix_seconds", fieldName, evaluateNotImplemented) /** * Creates an expression that adds a specified amount of time to a timestamp. @@ -2287,7 +2369,8 @@ abstract class Expr internal constructor() { * @param right The second expression to compare to. * @return A new [BooleanExpr] representing the equality comparison. */ - @JvmStatic fun eq(left: Expr, right: Expr): BooleanExpr = BooleanExpr("eq", left, right) + @JvmStatic + fun eq(left: Expr, right: Expr): BooleanExpr = BooleanExpr("eq", evaluateEq, left, right) /** * Creates an expression that checks if an expression is equal to a value. @@ -2296,7 +2379,8 @@ abstract class Expr internal constructor() { * @param right The value to compare to. * @return A new [BooleanExpr] representing the equality comparison. */ - @JvmStatic fun eq(left: Expr, right: Any): BooleanExpr = BooleanExpr("eq", left, right) + @JvmStatic + fun eq(left: Expr, right: Any): BooleanExpr = BooleanExpr("eq", evaluateEq, left, right) /** * Creates an expression that checks if a field's value is equal to an expression. @@ -2307,7 +2391,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun eq(fieldName: String, expression: Expr): BooleanExpr = - BooleanExpr("eq", fieldName, expression) + BooleanExpr("eq", evaluateEq, fieldName, expression) /** * Creates an expression that checks if a field's value is equal to another value. @@ -2317,7 +2401,8 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the equality comparison. */ @JvmStatic - fun eq(fieldName: String, value: Any): BooleanExpr = BooleanExpr("eq", fieldName, value) + fun eq(fieldName: String, value: Any): BooleanExpr = + BooleanExpr("eq", evaluateEq, fieldName, value) /** * Creates an expression that checks if two expressions are not equal. @@ -2326,7 +2411,9 @@ abstract class Expr internal constructor() { * @param right The second expression to compare to. * @return A new [BooleanExpr] representing the inequality comparison. */ - @JvmStatic fun neq(left: Expr, right: Expr): BooleanExpr = BooleanExpr("neq", left, right) + @JvmStatic + fun neq(left: Expr, right: Expr): BooleanExpr = + BooleanExpr("neq", evaluateNotImplemented, left, right) /** * Creates an expression that checks if an expression is not equal to a value. @@ -2335,7 +2422,9 @@ abstract class Expr internal constructor() { * @param right The value to compare to. * @return A new [BooleanExpr] representing the inequality comparison. */ - @JvmStatic fun neq(left: Expr, right: Any): BooleanExpr = BooleanExpr("neq", left, right) + @JvmStatic + fun neq(left: Expr, right: Any): BooleanExpr = + BooleanExpr("neq", evaluateNotImplemented, left, right) /** * Creates an expression that checks if a field's value is not equal to an expression. @@ -2346,7 +2435,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun neq(fieldName: String, expression: Expr): BooleanExpr = - BooleanExpr("neq", fieldName, expression) + BooleanExpr("neq", evaluateNotImplemented, fieldName, expression) /** * Creates an expression that checks if a field's value is not equal to another value. @@ -2356,7 +2445,8 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the inequality comparison. */ @JvmStatic - fun neq(fieldName: String, value: Any): BooleanExpr = BooleanExpr("neq", fieldName, value) + fun neq(fieldName: String, value: Any): BooleanExpr = + BooleanExpr("neq", evaluateNotImplemented, fieldName, value) /** * Creates an expression that checks if the first expression is greater than the second @@ -2366,7 +2456,9 @@ abstract class Expr internal constructor() { * @param right The second expression to compare to. * @return A new [BooleanExpr] representing the greater than comparison. */ - @JvmStatic fun gt(left: Expr, right: Expr): BooleanExpr = BooleanExpr("gt", left, right) + @JvmStatic + fun gt(left: Expr, right: Expr): BooleanExpr = + BooleanExpr("gt", evaluateNotImplemented, left, right) /** * Creates an expression that checks if an expression is greater than a value. @@ -2375,7 +2467,9 @@ abstract class Expr internal constructor() { * @param right The value to compare to. * @return A new [BooleanExpr] representing the greater than comparison. */ - @JvmStatic fun gt(left: Expr, right: Any): BooleanExpr = BooleanExpr("gt", left, right) + @JvmStatic + fun gt(left: Expr, right: Any): BooleanExpr = + BooleanExpr("gt", evaluateNotImplemented, left, right) /** * Creates an expression that checks if a field's value is greater than an expression. @@ -2386,7 +2480,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun gt(fieldName: String, expression: Expr): BooleanExpr = - BooleanExpr("gt", fieldName, expression) + BooleanExpr("gt", evaluateNotImplemented, fieldName, expression) /** * Creates an expression that checks if a field's value is greater than another value. @@ -2396,7 +2490,8 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the greater than comparison. */ @JvmStatic - fun gt(fieldName: String, value: Any): BooleanExpr = BooleanExpr("gt", fieldName, value) + fun gt(fieldName: String, value: Any): BooleanExpr = + BooleanExpr("gt", evaluateNotImplemented, fieldName, value) /** * Creates an expression that checks if the first expression is greater than or equal to the @@ -2406,7 +2501,9 @@ abstract class Expr internal constructor() { * @param right The second expression to compare to. * @return A new [BooleanExpr] representing the greater than or equal to comparison. */ - @JvmStatic fun gte(left: Expr, right: Expr): BooleanExpr = BooleanExpr("gte", left, right) + @JvmStatic + fun gte(left: Expr, right: Expr): BooleanExpr = + BooleanExpr("gte", evaluateNotImplemented, left, right) /** * Creates an expression that checks if an expression is greater than or equal to a value. @@ -2415,7 +2512,9 @@ abstract class Expr internal constructor() { * @param right The value to compare to. * @return A new [BooleanExpr] representing the greater than or equal to comparison. */ - @JvmStatic fun gte(left: Expr, right: Any): BooleanExpr = BooleanExpr("gte", left, right) + @JvmStatic + fun gte(left: Expr, right: Any): BooleanExpr = + BooleanExpr("gte", evaluateNotImplemented, left, right) /** * Creates an expression that checks if a field's value is greater than or equal to an @@ -2427,7 +2526,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun gte(fieldName: String, expression: Expr): BooleanExpr = - BooleanExpr("gte", fieldName, expression) + BooleanExpr("gte", evaluateNotImplemented, fieldName, expression) /** * Creates an expression that checks if a field's value is greater than or equal to another @@ -2438,7 +2537,8 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the greater than or equal to comparison. */ @JvmStatic - fun gte(fieldName: String, value: Any): BooleanExpr = BooleanExpr("gte", fieldName, value) + fun gte(fieldName: String, value: Any): BooleanExpr = + BooleanExpr("gte", evaluateNotImplemented, fieldName, value) /** * Creates an expression that checks if the first expression is less than the second expression. @@ -2447,7 +2547,9 @@ abstract class Expr internal constructor() { * @param right The second expression to compare to. * @return A new [BooleanExpr] representing the less than comparison. */ - @JvmStatic fun lt(left: Expr, right: Expr): BooleanExpr = BooleanExpr("lt", left, right) + @JvmStatic + fun lt(left: Expr, right: Expr): BooleanExpr = + BooleanExpr("lt", evaluateNotImplemented, left, right) /** * Creates an expression that checks if an expression is less than a value. @@ -2456,7 +2558,9 @@ abstract class Expr internal constructor() { * @param right The value to compare to. * @return A new [BooleanExpr] representing the less than comparison. */ - @JvmStatic fun lt(left: Expr, right: Any): BooleanExpr = BooleanExpr("lt", left, right) + @JvmStatic + fun lt(left: Expr, right: Any): BooleanExpr = + BooleanExpr("lt", evaluateNotImplemented, left, right) /** * Creates an expression that checks if a field's value is less than an expression. @@ -2467,7 +2571,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun lt(fieldName: String, expression: Expr): BooleanExpr = - BooleanExpr("lt", fieldName, expression) + BooleanExpr("lt", evaluateNotImplemented, fieldName, expression) /** * Creates an expression that checks if a field's value is less than another value. @@ -2477,7 +2581,8 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the less than comparison. */ @JvmStatic - fun lt(fieldName: String, right: Any): BooleanExpr = BooleanExpr("lt", fieldName, right) + fun lt(fieldName: String, right: Any): BooleanExpr = + BooleanExpr("lt", evaluateNotImplemented, fieldName, right) /** * Creates an expression that checks if the first expression is less than or equal to the second @@ -2487,7 +2592,9 @@ abstract class Expr internal constructor() { * @param right The second expression to compare to. * @return A new [BooleanExpr] representing the less than or equal to comparison. */ - @JvmStatic fun lte(left: Expr, right: Expr): BooleanExpr = BooleanExpr("lte", left, right) + @JvmStatic + fun lte(left: Expr, right: Expr): BooleanExpr = + BooleanExpr("lte", evaluateNotImplemented, left, right) /** * Creates an expression that checks if an expression is less than or equal to a value. @@ -2496,7 +2603,9 @@ abstract class Expr internal constructor() { * @param right The value to compare to. * @return A new [BooleanExpr] representing the less than or equal to comparison. */ - @JvmStatic fun lte(left: Expr, right: Any): BooleanExpr = BooleanExpr("lte", left, right) + @JvmStatic + fun lte(left: Expr, right: Any): BooleanExpr = + BooleanExpr("lte", evaluateNotImplemented, left, right) /** * Creates an expression that checks if a field's value is less than or equal to an expression. @@ -2507,7 +2616,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun lte(fieldName: String, expression: Expr): BooleanExpr = - BooleanExpr("lte", fieldName, expression) + BooleanExpr("lte", evaluateNotImplemented, fieldName, expression) /** * Creates an expression that checks if a field's value is less than or equal to another value. @@ -2517,7 +2626,8 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the less than or equal to comparison. */ @JvmStatic - fun lte(fieldName: String, value: Any): BooleanExpr = BooleanExpr("lte", fieldName, value) + fun lte(fieldName: String, value: Any): BooleanExpr = + BooleanExpr("lte", evaluateNotImplemented, fieldName, value) /** * Creates an expression that concatenates an array with other arrays. @@ -2573,7 +2683,9 @@ abstract class Expr internal constructor() { * @param array The array expression to reverse. * @return A new [Expr] representing the arrayReverse operation. */ - @JvmStatic fun arrayReverse(array: Expr): Expr = FunctionExpr("array_reverse", array) + @JvmStatic + fun arrayReverse(array: Expr): Expr = + FunctionExpr("array_reverse", evaluateNotImplemented, array) /** * Reverses the order of elements in the array field. @@ -2582,7 +2694,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the arrayReverse operation. */ @JvmStatic - fun arrayReverse(arrayFieldName: String): Expr = FunctionExpr("array_reverse", arrayFieldName) + fun arrayReverse(arrayFieldName: String): Expr = + FunctionExpr("array_reverse", arrayFieldName, evaluateNotImplemented) /** * Creates an expression that checks if the array contains a specific [element]. @@ -2593,7 +2706,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContains(array: Expr, element: Expr): BooleanExpr = - BooleanExpr("array_contains", array, element) + BooleanExpr("array_contains", evaluateNotImplemented, array, element) /** * Creates an expression that checks if the array field contains a specific [element]. @@ -2604,7 +2717,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContains(arrayFieldName: String, element: Expr) = - BooleanExpr("array_contains", arrayFieldName, element) + BooleanExpr("array_contains", evaluateNotImplemented, arrayFieldName, element) /** * Creates an expression that checks if the [array] contains a specific [element]. @@ -2615,7 +2728,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContains(array: Expr, element: Any): BooleanExpr = - BooleanExpr("array_contains", array, element) + BooleanExpr("array_contains", evaluateNotImplemented, array, element) /** * Creates an expression that checks if the array field contains a specific [element]. @@ -2626,7 +2739,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContains(arrayFieldName: String, element: Any) = - BooleanExpr("array_contains", arrayFieldName, element) + BooleanExpr("array_contains", evaluateNotImplemented, arrayFieldName, element) /** * Creates an expression that checks if [array] contains all the specified [values]. @@ -2648,7 +2761,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAll(array: Expr, arrayExpression: Expr) = - BooleanExpr("array_contains_all", array, arrayExpression) + BooleanExpr("array_contains_all", evaluateNotImplemented, array, arrayExpression) /** * Creates an expression that checks if array field contains all the specified [values]. @@ -2661,6 +2774,7 @@ abstract class Expr internal constructor() { fun arrayContainsAll(arrayFieldName: String, values: List) = BooleanExpr( "array_contains_all", + evaluateNotImplemented, arrayFieldName, ListOfExprs(toArrayOfExprOrConstant(values)) ) @@ -2674,7 +2788,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAll(arrayFieldName: String, arrayExpression: Expr) = - BooleanExpr("array_contains_all", arrayFieldName, arrayExpression) + BooleanExpr("array_contains_all", evaluateNotImplemented, arrayFieldName, arrayExpression) /** * Creates an expression that checks if [array] contains any of the specified [values]. @@ -2685,7 +2799,12 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAny(array: Expr, values: List) = - BooleanExpr("array_contains_any", array, ListOfExprs(toArrayOfExprOrConstant(values))) + BooleanExpr( + "array_contains_any", + evaluateNotImplemented, + array, + ListOfExprs(toArrayOfExprOrConstant(values)) + ) /** * Creates an expression that checks if [array] contains any elements of [arrayExpression]. @@ -2696,7 +2815,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAny(array: Expr, arrayExpression: Expr) = - BooleanExpr("array_contains_any", array, arrayExpression) + BooleanExpr("array_contains_any", evaluateNotImplemented, array, arrayExpression) /** * Creates an expression that checks if array field contains any of the specified [values]. @@ -2709,6 +2828,7 @@ abstract class Expr internal constructor() { fun arrayContainsAny(arrayFieldName: String, values: List) = BooleanExpr( "array_contains_any", + evaluateNotImplemented, arrayFieldName, ListOfExprs(toArrayOfExprOrConstant(values)) ) @@ -2722,7 +2842,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAny(arrayFieldName: String, arrayExpression: Expr) = - BooleanExpr("array_contains_any", arrayFieldName, arrayExpression) + BooleanExpr("array_contains_any", evaluateNotImplemented, arrayFieldName, arrayExpression) /** * Creates an expression that calculates the length of an [array] expression. @@ -2730,7 +2850,8 @@ abstract class Expr internal constructor() { * @param array The array expression to calculate the length of. * @return A new [Expr] representing the length of the array. */ - @JvmStatic fun arrayLength(array: Expr): Expr = FunctionExpr("array_length", array) + @JvmStatic + fun arrayLength(array: Expr): Expr = FunctionExpr("array_length", evaluateNotImplemented, array) /** * Creates an expression that calculates the length of an array field. @@ -2739,7 +2860,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the length of the array. */ @JvmStatic - fun arrayLength(arrayFieldName: String): Expr = FunctionExpr("array_length", arrayFieldName) + fun arrayLength(arrayFieldName: String): Expr = + FunctionExpr("array_length", arrayFieldName, evaluateNotImplemented) /** * Creates an expression that indexes into an array from the beginning or end and return the @@ -2751,7 +2873,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the arrayOffset operation. */ @JvmStatic - fun arrayOffset(array: Expr, offset: Expr): Expr = FunctionExpr("array_offset", array, offset) + fun arrayOffset(array: Expr, offset: Expr): Expr = + FunctionExpr("array_offset", evaluateNotImplemented, array, offset) /** * Creates an expression that indexes into an array from the beginning or end and return the @@ -2764,7 +2887,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayOffset(array: Expr, offset: Int): Expr = - FunctionExpr("array_offset", array, constant(offset)) + FunctionExpr("array_offset", evaluateNotImplemented, array, constant(offset)) /** * Creates an expression that indexes into an array from the beginning or end and return the @@ -2824,7 +2947,8 @@ abstract class Expr internal constructor() { * @param value An expression evaluates to the name of the field to check. * @return A new [Expr] representing the exists check. */ - @JvmStatic fun exists(value: Expr): BooleanExpr = BooleanExpr("exists", value) + @JvmStatic + fun exists(value: Expr): BooleanExpr = BooleanExpr("exists", evaluateNotImplemented, value) /** * Creates an expression that checks if a field exists. @@ -2832,7 +2956,9 @@ abstract class Expr internal constructor() { * @param fieldName The field name to check. * @return A new [Expr] representing the exists check. */ - @JvmStatic fun exists(fieldName: String): BooleanExpr = BooleanExpr("exists", fieldName) + @JvmStatic + fun exists(fieldName: String): BooleanExpr = + BooleanExpr("exists", evaluateNotImplemented, fieldName) /** * Creates an expression that returns the [catchExpr] argument if there is an error, else return @@ -2844,7 +2970,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the ifError operation. */ @JvmStatic - fun ifError(tryExpr: Expr, catchExpr: Expr): Expr = FunctionExpr("if_error", tryExpr, catchExpr) + fun ifError(tryExpr: Expr, catchExpr: Expr): Expr = + FunctionExpr("if_error", evaluateNotImplemented, tryExpr, catchExpr) /** * Creates an expression that returns the [catchValue] argument if there is an error, else @@ -2864,7 +2991,9 @@ abstract class Expr internal constructor() { * @param documentPath An expression the evaluates to document path. * @return A new [Expr] representing the documentId operation. */ - @JvmStatic fun documentId(documentPath: Expr): Expr = FunctionExpr("document_id", documentPath) + @JvmStatic + fun documentId(documentPath: Expr): Expr = + FunctionExpr("document_id", evaluateNotImplemented, documentPath) /** * Creates an expression that returns the document ID from a path. @@ -3956,6 +4085,10 @@ abstract class Expr internal constructor() { fun ifError(catchValue: Any): Expr = Companion.ifError(this, catchValue) internal abstract fun toProto(userDataReader: UserDataReader): Value + + internal abstract fun evaluate( + context: EvaluationContext + ): (input: MutableDocument) -> EvaluateResult } /** Expressions that have an alias are [Selectable] */ @@ -3981,6 +4114,7 @@ class ExprWithAlias internal constructor(private val alias: String, private val override fun getAlias() = alias override fun getExpr() = expr override fun toProto(userDataReader: UserDataReader): Value = expr.toProto(userDataReader) + override fun evaluate(context: EvaluationContext) = expr.evaluate(context) } /** @@ -4010,11 +4144,22 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select internal fun toProto(): Value = Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() + + override fun evaluate(context: EvaluationContext) = ::evaluateInternal + + private fun evaluateInternal(input: MutableDocument): EvaluateResult { + val value: Value? = input.getField(fieldPath) + return if (value == null) EvaluateResultUnset else EvaluateResultValue(value) + } } internal class ListOfExprs(private val expressions: Array) : Expr() { override fun toProto(userDataReader: UserDataReader): Value = encodeValue(expressions.map { it.toProto(userDataReader) }) + + override fun evaluate(context: EvaluationContext): (input: MutableDocument) -> EvaluateResult { + TODO("Not yet implemented") + } } /** @@ -4028,33 +4173,50 @@ internal class ListOfExprs(private val expressions: Array) : Expr() { open class FunctionExpr internal constructor( private val name: String, + private val function: EvaluateFunction, private val params: Array, private val options: InternalOptions = InternalOptions.EMPTY ) : Expr() { - internal constructor(name: String) : this(name, emptyArray()) - internal constructor(name: String, param: Expr) : this(name, arrayOf(param)) + internal constructor( + name: String, + function: EvaluateFunction + ) : this(name, function, emptyArray()) + internal constructor( + name: String, + function: EvaluateFunction, + param: Expr + ) : this(name, function, arrayOf(param)) internal constructor( name: String, param: Expr, vararg params: Any - ) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) + ) : this(name, evaluateNotImplemented, arrayOf(param, *toArrayOfExprOrConstant(params))) internal constructor( name: String, + function: EvaluateFunction, param1: Expr, param2: Expr - ) : this(name, arrayOf(param1, param2)) + ) : this(name, function, arrayOf(param1, param2)) internal constructor( name: String, param1: Expr, param2: Expr, vararg params: Any - ) : this(name, arrayOf(param1, param2, *toArrayOfExprOrConstant(params))) - internal constructor(name: String, fieldName: String) : this(name, arrayOf(field(fieldName))) + ) : this(name, evaluateNotImplemented, arrayOf(param1, param2, *toArrayOfExprOrConstant(params))) + internal constructor( + name: String, + fieldName: String, + function: EvaluateFunction + ) : this(name, function, arrayOf(field(fieldName))) internal constructor( name: String, fieldName: String, vararg params: Any - ) : this(name, arrayOf(field(fieldName), *toArrayOfExprOrConstant(params))) + ) : this( + name, + evaluateNotImplemented, + arrayOf(field(fieldName), *toArrayOfExprOrConstant(params)) + ) override fun toProto(userDataReader: UserDataReader): Value { val builder = com.google.firestore.v1.Function.newBuilder() @@ -4065,34 +4227,55 @@ internal constructor( options.forEach(builder::putOptions) return Value.newBuilder().setFunctionValue(builder).build() } + + final override fun evaluate( + context: EvaluationContext + ): (input: MutableDocument) -> EvaluateResult { + val evaluateParams = params.map { it.evaluate(context) }.asSequence() + return { input -> function.evaluate(evaluateParams.map { it.invoke(input) }) } + } } /** A class that represents a filter condition. */ -open class BooleanExpr internal constructor(name: String, params: Array) : - FunctionExpr(name, params, InternalOptions.EMPTY) { - internal constructor(name: String, param: Expr) : this(name, arrayOf(param)) +open class BooleanExpr +internal constructor(name: String, function: EvaluateFunction, params: Array) : + FunctionExpr(name, function, params, InternalOptions.EMPTY) { internal constructor( name: String, + function: EvaluateFunction, + param: Expr + ) : this(name, function, arrayOf(param)) + internal constructor( + name: String, + function: EvaluateFunction, param: Expr, vararg params: Any - ) : this(name, arrayOf(param, *toArrayOfExprOrConstant(params))) + ) : this(name, function, arrayOf(param, *toArrayOfExprOrConstant(params))) internal constructor( name: String, + function: EvaluateFunction, param1: Expr, param2: Expr - ) : this(name, arrayOf(param1, param2)) - internal constructor(name: String, fieldName: String) : this(name, arrayOf(field(fieldName))) + ) : this(name, function, arrayOf(param1, param2)) internal constructor( name: String, + function: EvaluateFunction, + fieldName: String + ) : this(name, function, arrayOf(field(fieldName))) + internal constructor( + name: String, + function: EvaluateFunction, fieldName: String, vararg params: Any - ) : this(name, arrayOf(field(fieldName), *toArrayOfExprOrConstant(params))) + ) : this(name, function, arrayOf(field(fieldName), *toArrayOfExprOrConstant(params))) companion object { /** */ - @JvmStatic fun generic(name: String, vararg expr: Expr): BooleanExpr = BooleanExpr(name, expr) + @JvmStatic + fun generic(name: String, vararg expr: Expr): BooleanExpr = + BooleanExpr(name, evaluateNotImplemented, expr) } /** diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt index 27d5375f3ab..a1c6caf2b85 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/options.kt @@ -159,6 +159,12 @@ class PipelineOptions private constructor(options: InternalOptions) : with("explain_options", options.options) } +class RealtimePipelineOptions private constructor(options: InternalOptions) : + AbstractOptions(options) { + + override fun self(options: InternalOptions) = RealtimePipelineOptions(options) +} + class ExplainOptions private constructor(options: InternalOptions) : AbstractOptions(options) { override fun self(options: InternalOptions) = ExplainOptions(options) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index e3d02d19652..5a1a56dfd8f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -18,6 +18,7 @@ import com.google.firebase.firestore.CollectionReference import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.VectorValue +import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.ResourcePath import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue @@ -26,6 +27,8 @@ import com.google.firebase.firestore.pipeline.Expr.Companion.field import com.google.firebase.firestore.util.Preconditions import com.google.firestore.v1.Pipeline import com.google.firestore.v1.Value +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter abstract class Stage> internal constructor(protected val name: String, internal val options: InternalOptions) { @@ -86,6 +89,13 @@ internal constructor(protected val name: String, internal val options: InternalO * @return New stage with named parameter. */ fun with(key: String, value: Field): T = with(key, value.toProto()) + + internal open fun evaluate( + context: EvaluationContext, + inputs: Flow + ): Flow { + throw NotImplementedError("Stage does not support offline evaluation") + } } /** @@ -212,6 +222,15 @@ internal constructor( } fun withForceIndex(value: String) = with("force_index", value) + + override fun evaluate( + context: EvaluationContext, + inputs: Flow + ): Flow { + return inputs.filter { input -> + input.isFoundDocument && input.key.collectionPath.canonicalString() == path + } + } } class CollectionGroupSource @@ -353,6 +372,14 @@ internal constructor( override fun self(options: InternalOptions) = WhereStage(condition, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(condition.toProto(userDataReader)) + + override fun evaluate( + context: EvaluationContext, + inputs: Flow + ): Flow { + val conditionFunction = condition.evaluate(context) + return inputs.filter { input -> conditionFunction.invoke(input).value?.booleanValue ?: false } + } } /** @@ -492,10 +519,20 @@ internal constructor(private val offset: Int, options: InternalOptions = Interna } internal class SelectStage -internal constructor( - private val fields: Array, - options: InternalOptions = InternalOptions.EMPTY -) : Stage("select", options) { +private constructor(private val fields: Array, options: InternalOptions) : + Stage("select", options) { + companion object { + @JvmStatic + fun of(selection: Selectable, vararg additionalSelections: Any): SelectStage = + SelectStage( + arrayOf(selection, *additionalSelections.map(Selectable::toSelectable).toTypedArray()), + InternalOptions.EMPTY + ) + + @JvmStatic + fun of(fieldName: String, vararg additionalSelections: Any): SelectStage = + of(field(fieldName), *additionalSelections) + } override fun self(options: InternalOptions) = SelectStage(fields, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto(userDataReader) })) diff --git a/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java b/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java index eae5d3d01b3..df19245d01f 100644 --- a/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java +++ b/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java @@ -18,12 +18,14 @@ import static com.google.firebase.firestore.testutil.TestUtil.docSet; import static com.google.firebase.firestore.testutil.TestUtil.key; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.google.android.gms.tasks.Task; import com.google.firebase.database.collection.ImmutableSortedSet; import com.google.firebase.firestore.core.DocumentViewChange; import com.google.firebase.firestore.core.DocumentViewChange.Type; import com.google.firebase.firestore.core.ViewSnapshot; +import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.DocumentSet; @@ -39,6 +41,13 @@ public class TestUtil { private static final FirebaseFirestore FIRESTORE = mock(FirebaseFirestore.class); + private static final DatabaseId DATABASE_ID = DatabaseId.forProject("project"); + private static final UserDataReader USER_DATA_READER = new UserDataReader(DATABASE_ID); + + static { + when(FIRESTORE.getDatabaseId()).thenReturn(DATABASE_ID); + when(FIRESTORE.getUserDataReader()).thenReturn(USER_DATA_READER); + } public static FirebaseFirestore firestore() { return FIRESTORE; diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt new file mode 100644 index 00000000000..459e9c173d9 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt @@ -0,0 +1,29 @@ +package com.google.firebase.firestore.core + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.firestore.RealtimePipelineSource +import com.google.firebase.firestore.TestUtil +import com.google.firebase.firestore.model.MutableDocument +import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.Test + +internal class PipelineTests { + + @Test + fun `runPipeline executes without error`(): Unit = runBlocking { + val firestore = TestUtil.firestore() + val pipeline = RealtimePipelineSource(firestore).collection("foo").where(field("bar").eq(42)) + + val doc1: MutableDocument = doc("foo/1", 0, mapOf("bar" to 42)) + val doc2: MutableDocument = doc("foo/2", 0, mapOf("bar" to "43")) + val doc3: MutableDocument = doc("xxx/1", 0, mapOf("bar" to 42)) + + val list = runPipeline(pipeline, flowOf(doc1, doc2, doc3)).toList() + + assertThat(list).hasSize(1) + } +} From 8bbbfaafcd8483d9703ed327644836458a294453 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 15 May 2025 13:22:15 -0400 Subject: [PATCH 067/152] Fix --- .../com/google/firebase/firestore/PipelineTest.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 344f66fddfd..3531ffab2b1 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 @@ -279,13 +279,15 @@ public void groupAndAccumulateResultsGeneric() { firestore .pipeline() .collection(randomCol) - .genericStage("where", lt(field("published"), 1984)) + .genericStage(GenericStage.ofName("where").withArguments(lt(field("published"), 1984))) .genericStage( - "aggregate", - ImmutableMap.of("avgRating", AggregateFunction.avg("rating")), - ImmutableMap.of("genre", field("genre"))) + GenericStage.ofName("aggregate") + .withArguments( + ImmutableMap.of("avgRating", AggregateFunction.avg("rating")), + ImmutableMap.of("genre", field("genre")))) .genericStage(GenericStage.ofName("where").withArguments(gt("avgRating", 4.3))) - .genericStage("sort", field("avgRating").descending()) + .genericStage( + GenericStage.ofName("sort").withArguments(field("avgRating").descending())) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) From 0cdb9096bc853916457e638315c8488b8fdeb35d Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 15 May 2025 13:28:27 -0400 Subject: [PATCH 068/152] API --- firebase-firestore/api.txt | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 9e981a511c1..0a0b3cd69d5 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -1,6 +1,10 @@ // Signature format: 3.0 package com.google.firebase.firestore { + public class AbstractPipeline { + method protected final com.google.android.gms.tasks.Task execute(com.google.firebase.firestore.pipeline.InternalOptions? options); + } + public abstract class AggregateField { method public static com.google.firebase.firestore.AggregateField.AverageAggregateField average(com.google.firebase.firestore.FieldPath); method public static com.google.firebase.firestore.AggregateField.AverageAggregateField average(String); @@ -419,14 +423,14 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.PersistentCacheSettings.Builder setSizeBytes(long); } - public final class Pipeline { + public final class Pipeline extends com.google.firebase.firestore.AbstractPipeline { method public com.google.firebase.firestore.Pipeline addFields(com.google.firebase.firestore.pipeline.Selectable field, com.google.firebase.firestore.pipeline.Selectable... additionalFields); method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateStage aggregateStage); method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateWithAlias accumulator, com.google.firebase.firestore.pipeline.AggregateWithAlias... additionalAccumulators); method public com.google.firebase.firestore.Pipeline distinct(com.google.firebase.firestore.pipeline.Selectable group, java.lang.Object... additionalGroups); method public com.google.firebase.firestore.Pipeline distinct(String groupField, java.lang.Object... additionalGroups); method public com.google.android.gms.tasks.Task execute(); - method public com.google.android.gms.tasks.Task execute(com.google.firebase.firestore.pipeline.PipelineOptions options); + method public com.google.android.gms.tasks.Task execute(com.google.firebase.firestore.pipeline.RealtimePipelineOptions options); method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.FindNearestStage stage); @@ -560,6 +564,24 @@ package com.google.firebase.firestore { method public java.util.List toObjects(Class, com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior); } + public final class RealtimePipeline extends com.google.firebase.firestore.AbstractPipeline { + method public com.google.android.gms.tasks.Task execute(); + method public com.google.android.gms.tasks.Task execute(com.google.firebase.firestore.pipeline.PipelineOptions options); + method public com.google.firebase.firestore.RealtimePipeline limit(int limit); + method public com.google.firebase.firestore.RealtimePipeline offset(int offset); + method public com.google.firebase.firestore.RealtimePipeline select(com.google.firebase.firestore.pipeline.Selectable selection, java.lang.Object... additionalSelections); + method public com.google.firebase.firestore.RealtimePipeline select(String fieldName, java.lang.Object... additionalSelections); + method public com.google.firebase.firestore.RealtimePipeline where(com.google.firebase.firestore.pipeline.BooleanExpr condition); + } + + public final class RealtimePipelineSource { + method public com.google.firebase.firestore.RealtimePipeline collection(com.google.firebase.firestore.CollectionReference ref); + method public com.google.firebase.firestore.RealtimePipeline collection(com.google.firebase.firestore.pipeline.CollectionSource stage); + method public com.google.firebase.firestore.RealtimePipeline collection(String path); + method public com.google.firebase.firestore.RealtimePipeline collectionGroup(String collectionId); + method public com.google.firebase.firestore.RealtimePipeline pipeline(com.google.firebase.firestore.pipeline.CollectionGroupSource stage); + } + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD}) public @interface ServerTimestamp { } @@ -1573,6 +1595,9 @@ package com.google.firebase.firestore.pipeline { public static final class PipelineOptions.IndexMode.Companion { } + public final class RealtimePipelineOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { + } + public final class SampleStage extends com.google.firebase.firestore.pipeline.Stage { method public static com.google.firebase.firestore.pipeline.SampleStage withDocLimit(int documents); method public static com.google.firebase.firestore.pipeline.SampleStage withPercentage(double percentage); From c2d442ca0de875e9dc2aa0bba2f4cbf7caba71e8 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 15 May 2025 13:58:50 -0400 Subject: [PATCH 069/152] Cleanup --- .../firestore/pipeline/expressions.kt | 248 ++++++++++-------- 1 file changed, 134 insertions(+), 114 deletions(-) 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 99f54e29de2..21c4a1a3a70 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 @@ -381,7 +381,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitAnd(bitsFieldName: String, bitsOther: Expr): Expr = - FunctionExpr("bit_and", bitsFieldName, bitsOther) + FunctionExpr("bit_and", evaluateNotImplemented, bitsFieldName, bitsOther) /** * Creates an expression that applies a bitwise AND operation between an field and constant. @@ -392,7 +392,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitAnd(bitsFieldName: String, bitsOther: ByteArray): Expr = - FunctionExpr("bit_and", bitsFieldName, constant(bitsOther)) + FunctionExpr("bit_and", evaluateNotImplemented, bitsFieldName, constant(bitsOther)) /** * Creates an expression that applies a bitwise OR operation between two expressions. @@ -426,7 +426,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitOr(bitsFieldName: String, bitsOther: Expr): Expr = - FunctionExpr("bit_or", bitsFieldName, bitsOther) + FunctionExpr("bit_or", evaluateNotImplemented, bitsFieldName, bitsOther) /** * Creates an expression that applies a bitwise OR operation between an field and constant. @@ -437,7 +437,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitOr(bitsFieldName: String, bitsOther: ByteArray): Expr = - FunctionExpr("bit_or", bitsFieldName, constant(bitsOther)) + FunctionExpr("bit_or", evaluateNotImplemented, bitsFieldName, constant(bitsOther)) /** * Creates an expression that applies a bitwise XOR operation between two expressions. @@ -472,7 +472,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitXor(bitsFieldName: String, bitsOther: Expr): Expr = - FunctionExpr("bit_xor", bitsFieldName, bitsOther) + FunctionExpr("bit_xor", evaluateNotImplemented, bitsFieldName, bitsOther) /** * Creates an expression that applies a bitwise XOR operation between an field and constant. @@ -483,7 +483,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitXor(bitsFieldName: String, bitsOther: ByteArray): Expr = - FunctionExpr("bit_xor", bitsFieldName, constant(bitsOther)) + FunctionExpr("bit_xor", evaluateNotImplemented, bitsFieldName, constant(bitsOther)) /** * Creates an expression that applies a bitwise NOT operation to an expression. @@ -501,7 +501,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitNot(bitsFieldName: String): Expr = - FunctionExpr("bit_not", bitsFieldName, evaluateNotImplemented) + FunctionExpr("bit_not", evaluateNotImplemented, bitsFieldName) /** * Creates an expression that applies a bitwise left shift operation between two expressions. @@ -523,7 +523,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the bitwise left shift operation. */ @JvmStatic - fun bitLeftShift(bits: Expr, number: Int): Expr = FunctionExpr("bit_left_shift", bits, number) + fun bitLeftShift(bits: Expr, number: Int): Expr = + FunctionExpr("bit_left_shift", evaluateNotImplemented, bits, number) /** * Creates an expression that applies a bitwise left shift operation between a field and an @@ -535,7 +536,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitLeftShift(bitsFieldName: String, numberExpr: Expr): Expr = - FunctionExpr("bit_left_shift", bitsFieldName, numberExpr) + FunctionExpr("bit_left_shift", evaluateNotImplemented, bitsFieldName, numberExpr) /** * Creates an expression that applies a bitwise left shift operation between a field and a @@ -547,7 +548,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitLeftShift(bitsFieldName: String, number: Int): Expr = - FunctionExpr("bit_left_shift", bitsFieldName, number) + FunctionExpr("bit_left_shift", evaluateNotImplemented, bitsFieldName, number) /** * Creates an expression that applies a bitwise right shift operation between two expressions. @@ -569,7 +570,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the bitwise right shift operation. */ @JvmStatic - fun bitRightShift(bits: Expr, number: Int): Expr = FunctionExpr("bit_right_shift", bits, number) + fun bitRightShift(bits: Expr, number: Int): Expr = + FunctionExpr("bit_right_shift", evaluateNotImplemented, bits, number) /** * Creates an expression that applies a bitwise right shift operation between a field and an @@ -581,7 +583,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitRightShift(bitsFieldName: String, numberExpr: Expr): Expr = - FunctionExpr("bit_right_shift", bitsFieldName, numberExpr) + FunctionExpr("bit_right_shift", evaluateNotImplemented, bitsFieldName, numberExpr) /** * Creates an expression that applies a bitwise right shift operation between a field and a @@ -593,7 +595,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitRightShift(bitsFieldName: String, number: Int): Expr = - FunctionExpr("bit_right_shift", bitsFieldName, number) + FunctionExpr("bit_right_shift", evaluateNotImplemented, bitsFieldName, number) /** * Creates an expression that rounds [numericExpr] to nearest integer. @@ -616,7 +618,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun round(numericField: String): Expr = - FunctionExpr("round", numericField, evaluateNotImplemented) + FunctionExpr("round", evaluateNotImplemented, numericField) /** * Creates an expression that rounds off [numericExpr] to [decimalPlace] decimal places if @@ -642,7 +644,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun roundToPrecision(numericField: String, decimalPlace: Int): Expr = - FunctionExpr("round", numericField, constant(decimalPlace)) + FunctionExpr("round", evaluateNotImplemented, numericField, constant(decimalPlace)) /** * Creates an expression that rounds off [numericExpr] to [decimalPlace] decimal places if @@ -668,7 +670,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun roundToPrecision(numericField: String, decimalPlace: Expr): Expr = - FunctionExpr("round", numericField, decimalPlace) + FunctionExpr("round", evaluateNotImplemented, numericField, decimalPlace) /** * Creates an expression that returns the smalled integer that isn't less than [numericExpr]. @@ -687,7 +689,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun ceil(numericField: String): Expr = - FunctionExpr("ceil", numericField, evaluateNotImplemented) + FunctionExpr("ceil", evaluateNotImplemented, numericField) /** * Creates an expression that returns the largest integer that isn't less than [numericExpr]. @@ -706,7 +708,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun floor(numericField: String): Expr = - FunctionExpr("floor", numericField, evaluateNotImplemented) + FunctionExpr("floor", evaluateNotImplemented, numericField) /** * Creates an expression that returns the [numericExpr] raised to the power of the [exponent]. @@ -732,7 +734,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun pow(numericField: String, exponent: Number): Expr = - FunctionExpr("pow", numericField, constant(exponent)) + FunctionExpr("pow", evaluateNotImplemented, numericField, constant(exponent)) /** * Creates an expression that returns the [numericExpr] raised to the power of the [exponent]. @@ -758,7 +760,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun pow(numericField: String, exponent: Expr): Expr = - FunctionExpr("pow", numericField, exponent) + FunctionExpr("pow", evaluateNotImplemented, numericField, exponent) /** * Creates an expression that returns the square root of [numericExpr]. @@ -777,7 +779,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun sqrt(numericField: String): Expr = - FunctionExpr("sqrt", numericField, evaluateNotImplemented) + FunctionExpr("sqrt", evaluateNotImplemented, numericField) /** * Creates an expression that adds numeric expressions and constants. @@ -789,7 +791,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun add(first: Expr, second: Expr, vararg others: Any): Expr = - FunctionExpr("add", first, second, *others) + FunctionExpr("add", evaluateNotImplemented, first, second, *others) /** * Creates an expression that adds numeric expressions and constants. @@ -801,7 +803,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun add(first: Expr, second: Number, vararg others: Any): Expr = - FunctionExpr("add", first, second, *others) + FunctionExpr("add", evaluateNotImplemented, first, second, *others) /** * Creates an expression that adds a numeric field with numeric expressions and constants. @@ -813,7 +815,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun add(numericFieldName: String, second: Expr, vararg others: Any): Expr = - FunctionExpr("add", numericFieldName, second, *others) + FunctionExpr("add", evaluateNotImplemented, numericFieldName, second, *others) /** * Creates an expression that adds a numeric field with numeric expressions and constants. @@ -825,7 +827,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun add(numericFieldName: String, second: Number, vararg others: Any): Expr = - FunctionExpr("add", numericFieldName, second, *others) + FunctionExpr("add", evaluateNotImplemented, numericFieldName, second, *others) /** * Creates an expression that subtracts two expressions. @@ -847,7 +849,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun subtract(minuend: Expr, subtrahend: Number): Expr = - FunctionExpr("subtract", minuend, subtrahend) + FunctionExpr("subtract", evaluateNotImplemented, minuend, subtrahend) /** * Creates an expression that subtracts a numeric expressions from numeric field. @@ -858,7 +860,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun subtract(numericFieldName: String, subtrahend: Expr): Expr = - FunctionExpr("subtract", numericFieldName, subtrahend) + FunctionExpr("subtract", evaluateNotImplemented, numericFieldName, subtrahend) /** * Creates an expression that subtracts a constant from numeric field. @@ -869,7 +871,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun subtract(numericFieldName: String, subtrahend: Number): Expr = - FunctionExpr("subtract", numericFieldName, subtrahend) + FunctionExpr("subtract", evaluateNotImplemented, numericFieldName, subtrahend) /** * Creates an expression that multiplies numeric expressions and constants. @@ -881,7 +883,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun multiply(first: Expr, second: Expr, vararg others: Any): Expr = - FunctionExpr("multiply", first, second, *others) + FunctionExpr("multiply", evaluateNotImplemented, first, second, *others) /** * Creates an expression that multiplies numeric expressions and constants. @@ -893,7 +895,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun multiply(first: Expr, second: Number, vararg others: Any): Expr = - FunctionExpr("multiply", first, second, *others) + FunctionExpr("multiply", evaluateNotImplemented, first, second, *others) /** * Creates an expression that multiplies a numeric field with numeric expressions and constants. @@ -905,7 +907,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun multiply(numericFieldName: String, second: Expr, vararg others: Any): Expr = - FunctionExpr("multiply", numericFieldName, second, *others) + FunctionExpr("multiply", evaluateNotImplemented, numericFieldName, second, *others) /** * Creates an expression that multiplies a numeric field with numeric expressions and constants. @@ -917,7 +919,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun multiply(numericFieldName: String, second: Number, vararg others: Any): Expr = - FunctionExpr("multiply", numericFieldName, second, *others) + FunctionExpr("multiply", evaluateNotImplemented, numericFieldName, second, *others) /** * Creates an expression that divides two numeric expressions. @@ -938,7 +940,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the division operation. */ @JvmStatic - fun divide(dividend: Expr, divisor: Number): Expr = FunctionExpr("divide", dividend, divisor) + fun divide(dividend: Expr, divisor: Number): Expr = + FunctionExpr("divide", evaluateNotImplemented, dividend, divisor) /** * Creates an expression that divides numeric field by a numeric expression. @@ -949,7 +952,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun divide(dividendFieldName: String, divisor: Expr): Expr = - FunctionExpr("divide", dividendFieldName, divisor) + FunctionExpr("divide", evaluateNotImplemented, dividendFieldName, divisor) /** * Creates an expression that divides a numeric field by a constant. @@ -960,7 +963,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun divide(dividendFieldName: String, divisor: Number): Expr = - FunctionExpr("divide", dividendFieldName, divisor) + FunctionExpr("divide", evaluateNotImplemented, dividendFieldName, divisor) /** * Creates an expression that calculates the modulo (remainder) of dividing two numeric @@ -983,7 +986,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the modulo operation. */ @JvmStatic - fun mod(dividend: Expr, divisor: Number): Expr = FunctionExpr("mod", dividend, divisor) + fun mod(dividend: Expr, divisor: Number): Expr = + FunctionExpr("mod", evaluateNotImplemented, dividend, divisor) /** * Creates an expression that calculates the modulo (remainder) of dividing a numeric field by a @@ -995,7 +999,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mod(dividendFieldName: String, divisor: Expr): Expr = - FunctionExpr("mod", dividendFieldName, divisor) + FunctionExpr("mod", evaluateNotImplemented, dividendFieldName, divisor) /** * Creates an expression that calculates the modulo (remainder) of dividing a numeric field by a @@ -1007,7 +1011,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mod(dividendFieldName: String, divisor: Number): Expr = - FunctionExpr("mod", dividendFieldName, divisor) + FunctionExpr("mod", evaluateNotImplemented, dividendFieldName, divisor) /** * Creates an expression that checks if an [expression], when evaluated, is equal to any of the @@ -1220,7 +1224,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(stringExpression: Expr, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_first", stringExpression, find, replace) + FunctionExpr("replace_first", evaluateNotImplemented, stringExpression, find, replace) /** * Creates an expression that replaces the first occurrence of a substring within the @@ -1233,7 +1237,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(stringExpression: Expr, find: String, replace: String): Expr = - FunctionExpr("replace_first", stringExpression, find, replace) + FunctionExpr("replace_first", evaluateNotImplemented, stringExpression, find, replace) /** * Creates an expression that replaces the first occurrence of a substring within the specified @@ -1248,7 +1252,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(fieldName: String, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_first", fieldName, find, replace) + FunctionExpr("replace_first", evaluateNotImplemented, fieldName, find, replace) /** * Creates an expression that replaces the first occurrence of a substring within the specified @@ -1261,7 +1265,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(fieldName: String, find: String, replace: String): Expr = - FunctionExpr("replace_first", fieldName, find, replace) + FunctionExpr("replace_first", evaluateNotImplemented, fieldName, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the @@ -1274,7 +1278,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(stringExpression: Expr, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_all", stringExpression, find, replace) + FunctionExpr("replace_all", evaluateNotImplemented, stringExpression, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the @@ -1287,7 +1291,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(stringExpression: Expr, find: String, replace: String): Expr = - FunctionExpr("replace_all", stringExpression, find, replace) + FunctionExpr("replace_all", evaluateNotImplemented, stringExpression, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the specified @@ -1302,7 +1306,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(fieldName: String, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_all", fieldName, find, replace) + FunctionExpr("replace_all", evaluateNotImplemented, fieldName, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the specified @@ -1315,7 +1319,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(fieldName: String, find: String, replace: String): Expr = - FunctionExpr("replace_all", fieldName, find, replace) + FunctionExpr("replace_all", evaluateNotImplemented, fieldName, find, replace) /** * Creates an expression that calculates the character length of a string expression in UTF8. @@ -1334,7 +1338,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun charLength(fieldName: String): Expr = - FunctionExpr("char_length", fieldName, evaluateNotImplemented) + FunctionExpr("char_length", evaluateNotImplemented, fieldName) /** * Creates an expression that calculates the length of a string in UTF-8 bytes, or just the @@ -1355,7 +1359,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun byteLength(fieldName: String): Expr = - FunctionExpr("byte_length", fieldName, evaluateNotImplemented) + FunctionExpr("byte_length", evaluateNotImplemented, fieldName) /** * Creates an expression that performs a case-sensitive wildcard string comparison. @@ -1513,7 +1517,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMaximum(expr: Expr, vararg others: Any): Expr = - FunctionExpr("logical_max", expr, *others) + FunctionExpr("logical_max", evaluateNotImplemented, expr, *others) /** * Creates an expression that returns the largest value between multiple input expressions or @@ -1525,7 +1529,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMaximum(fieldName: String, vararg others: Any): Expr = - FunctionExpr("logical_max", fieldName, *others) + FunctionExpr("logical_max", evaluateNotImplemented, fieldName, *others) /** * Creates an expression that returns the smallest value between multiple input expressions or @@ -1537,7 +1541,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMinimum(expr: Expr, vararg others: Any): Expr = - FunctionExpr("logical_min", expr, *others) + FunctionExpr("logical_min", evaluateNotImplemented, expr, *others) /** * Creates an expression that returns the smallest value between multiple input expressions or @@ -1549,7 +1553,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMinimum(fieldName: String, vararg others: Any): Expr = - FunctionExpr("logical_min", fieldName, *others) + FunctionExpr("logical_min", evaluateNotImplemented, fieldName, *others) /** * Creates an expression that reverses a string. @@ -1569,7 +1573,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun reverse(fieldName: String): Expr = - FunctionExpr("reverse", fieldName, evaluateNotImplemented) + FunctionExpr("reverse", evaluateNotImplemented, fieldName) /** * Creates an expression that checks if a string expression contains a specified substring. @@ -1721,7 +1725,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun toLower(fieldName: String): Expr = - FunctionExpr("to_lower", fieldName, evaluateNotImplemented) + FunctionExpr("to_lower", evaluateNotImplemented, fieldName) /** * Creates an expression that converts a string expression to uppercase. @@ -1741,7 +1745,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun toUpper(fieldName: String): Expr = - FunctionExpr("to_upper", fieldName, evaluateNotImplemented) + FunctionExpr("to_upper", evaluateNotImplemented, fieldName) /** * Creates an expression that removes leading and trailing whitespace from a string expression. @@ -1760,7 +1764,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the trimmed string. */ @JvmStatic - fun trim(fieldName: String): Expr = FunctionExpr("trim", fieldName, evaluateNotImplemented) + fun trim(fieldName: String): Expr = FunctionExpr("trim", evaluateNotImplemented, fieldName) /** * Creates an expression that concatenates string expressions together. @@ -1771,7 +1775,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strConcat(firstString: Expr, vararg otherStrings: Expr): Expr = - FunctionExpr("str_concat", firstString, *otherStrings) + FunctionExpr("str_concat", evaluateNotImplemented, firstString, *otherStrings) /** * Creates an expression that concatenates string expressions together. @@ -1783,7 +1787,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strConcat(firstString: Expr, vararg otherStrings: Any): Expr = - FunctionExpr("str_concat", firstString, *otherStrings) + FunctionExpr("str_concat", evaluateNotImplemented, firstString, *otherStrings) /** * Creates an expression that concatenates string expressions together. @@ -1794,7 +1798,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strConcat(fieldName: String, vararg otherStrings: Expr): Expr = - FunctionExpr("str_concat", fieldName, *otherStrings) + FunctionExpr("str_concat", evaluateNotImplemented, fieldName, *otherStrings) /** * Creates an expression that concatenates string expressions together. @@ -1806,7 +1810,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strConcat(fieldName: String, vararg otherStrings: Any): Expr = - FunctionExpr("str_concat", fieldName, *otherStrings) + FunctionExpr("str_concat", evaluateNotImplemented, fieldName, *otherStrings) internal fun map(elements: Array): Expr = FunctionExpr("map", evaluateNotImplemented, elements) @@ -1829,7 +1833,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the value associated with the given key in the map. */ @JvmStatic - fun mapGet(mapExpression: Expr, key: String): Expr = FunctionExpr("map_get", mapExpression, key) + fun mapGet(mapExpression: Expr, key: String): Expr = + FunctionExpr("map_get", evaluateNotImplemented, mapExpression, key) /** * Accesses a value from a map (object) field using the provided [key]. @@ -1839,7 +1844,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the value associated with the given key in the map. */ @JvmStatic - fun mapGet(fieldName: String, key: String): Expr = FunctionExpr("map_get", fieldName, key) + fun mapGet(fieldName: String, key: String): Expr = + FunctionExpr("map_get", evaluateNotImplemented, fieldName, key) /** * Creates an expression that merges multiple maps into a single map. If multiple maps have the @@ -1852,7 +1858,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapMerge(firstMap: Expr, secondMap: Expr, vararg otherMaps: Expr): Expr = - FunctionExpr("map_merge", firstMap, secondMap, *otherMaps) + FunctionExpr("map_merge", evaluateNotImplemented, firstMap, secondMap, *otherMaps) /** * Creates an expression that merges multiple maps into a single map. If multiple maps have the @@ -1865,7 +1871,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapMerge(firstMapFieldName: String, secondMap: Expr, vararg otherMaps: Expr): Expr = - FunctionExpr("map_merge", firstMapFieldName, secondMap, *otherMaps) + FunctionExpr("map_merge", evaluateNotImplemented, firstMapFieldName, secondMap, *otherMaps) /** * Creates an expression that removes a key from the map produced by evaluating an expression. @@ -1886,7 +1892,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] that evaluates to a modified map. */ @JvmStatic - fun mapRemove(mapField: String, key: Expr): Expr = FunctionExpr("map_remove", mapField, key) + fun mapRemove(mapField: String, key: Expr): Expr = + FunctionExpr("map_remove", evaluateNotImplemented, mapField, key) /** * Creates an expression that removes a key from the map produced by evaluating an expression. @@ -1896,7 +1903,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] that evaluates to a modified map. */ @JvmStatic - fun mapRemove(mapExpr: Expr, key: String): Expr = FunctionExpr("map_remove", mapExpr, key) + fun mapRemove(mapExpr: Expr, key: String): Expr = + FunctionExpr("map_remove", evaluateNotImplemented, mapExpr, key) /** * Creates an expression that removes a key from the map produced by evaluating an expression. @@ -1906,7 +1914,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] that evaluates to a modified map. */ @JvmStatic - fun mapRemove(mapField: String, key: String): Expr = FunctionExpr("map_remove", mapField, key) + fun mapRemove(mapField: String, key: String): Expr = + FunctionExpr("map_remove", evaluateNotImplemented, mapField, key) /** * Calculates the Cosine distance between two vector expressions. @@ -1939,7 +1948,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: VectorValue): Expr = - FunctionExpr("cosine_distance", vector1, vector2) + FunctionExpr("cosine_distance", evaluateNotImplemented, vector1, vector2) /** * Calculates the Cosine distance between a vector field and a vector expression. @@ -1950,7 +1959,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vectorFieldName: String, vector: Expr): Expr = - FunctionExpr("cosine_distance", vectorFieldName, vector) + FunctionExpr("cosine_distance", evaluateNotImplemented, vectorFieldName, vector) /** * Calculates the Cosine distance between a vector field and a vector literal. @@ -1961,7 +1970,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vectorFieldName: String, vector: DoubleArray): Expr = - FunctionExpr("cosine_distance", vectorFieldName, vector(vector)) + FunctionExpr("cosine_distance", evaluateNotImplemented, vectorFieldName, vector(vector)) /** * Calculates the Cosine distance between a vector field and a vector literal. @@ -1972,7 +1981,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vectorFieldName: String, vector: VectorValue): Expr = - FunctionExpr("cosine_distance", vectorFieldName, vector) + FunctionExpr("cosine_distance", evaluateNotImplemented, vectorFieldName, vector) /** * Calculates the dot product distance between two vector expressions. @@ -2005,7 +2014,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vector1: Expr, vector2: VectorValue): Expr = - FunctionExpr("dot_product", vector1, vector2) + FunctionExpr("dot_product", evaluateNotImplemented, vector1, vector2) /** * Calculates the dot product distance between a vector field and a vector expression. @@ -2016,7 +2025,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vectorFieldName: String, vector: Expr): Expr = - FunctionExpr("dot_product", vectorFieldName, vector) + FunctionExpr("dot_product", evaluateNotImplemented, vectorFieldName, vector) /** * Calculates the dot product distance between vector field and a vector literal. @@ -2027,7 +2036,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vectorFieldName: String, vector: DoubleArray): Expr = - FunctionExpr("dot_product", vectorFieldName, vector(vector)) + FunctionExpr("dot_product", evaluateNotImplemented, vectorFieldName, vector(vector)) /** * Calculates the dot product distance between a vector field and a vector literal. @@ -2038,7 +2047,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vectorFieldName: String, vector: VectorValue): Expr = - FunctionExpr("dot_product", vectorFieldName, vector) + FunctionExpr("dot_product", evaluateNotImplemented, vectorFieldName, vector) /** * Calculates the Euclidean distance between two vector expressions. @@ -2071,7 +2080,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: VectorValue): Expr = - FunctionExpr("euclidean_distance", vector1, vector2) + FunctionExpr("euclidean_distance", evaluateNotImplemented, vector1, vector2) /** * Calculates the Euclidean distance between a vector field and a vector expression. @@ -2082,7 +2091,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vectorFieldName: String, vector: Expr): Expr = - FunctionExpr("euclidean_distance", vectorFieldName, vector) + FunctionExpr("euclidean_distance", evaluateNotImplemented, vectorFieldName, vector) /** * Calculates the Euclidean distance between a vector field and a vector literal. @@ -2093,7 +2102,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vectorFieldName: String, vector: DoubleArray): Expr = - FunctionExpr("euclidean_distance", vectorFieldName, vector(vector)) + FunctionExpr("euclidean_distance", evaluateNotImplemented, vectorFieldName, vector(vector)) /** * Calculates the Euclidean distance between a vector field and a vector literal. @@ -2104,7 +2113,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vectorFieldName: String, vector: VectorValue): Expr = - FunctionExpr("euclidean_distance", vectorFieldName, vector) + FunctionExpr("euclidean_distance", evaluateNotImplemented, vectorFieldName, vector) /** * Creates an expression that calculates the length (dimension) of a Firestore Vector. @@ -2124,7 +2133,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun vectorLength(fieldName: String): Expr = - FunctionExpr("vector_length", fieldName, evaluateNotImplemented) + FunctionExpr("vector_length", evaluateNotImplemented, fieldName) /** * Creates an expression that interprets an expression as the number of microseconds since the @@ -2146,7 +2155,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixMicrosToTimestamp(fieldName: String): Expr = - FunctionExpr("unix_micros_to_timestamp", fieldName, evaluateNotImplemented) + FunctionExpr("unix_micros_to_timestamp", evaluateNotImplemented, fieldName) /** * Creates an expression that converts a timestamp expression to the number of microseconds @@ -2168,7 +2177,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixMicros(fieldName: String): Expr = - FunctionExpr("timestamp_to_unix_micros", fieldName, evaluateNotImplemented) + FunctionExpr("timestamp_to_unix_micros", evaluateNotImplemented, fieldName) /** * Creates an expression that interprets an expression as the number of milliseconds since the @@ -2190,7 +2199,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixMillisToTimestamp(fieldName: String): Expr = - FunctionExpr("unix_millis_to_timestamp", fieldName, evaluateNotImplemented) + FunctionExpr("unix_millis_to_timestamp", evaluateNotImplemented, fieldName) /** * Creates an expression that converts a timestamp expression to the number of milliseconds @@ -2212,7 +2221,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixMillis(fieldName: String): Expr = - FunctionExpr("timestamp_to_unix_millis", fieldName, evaluateNotImplemented) + FunctionExpr("timestamp_to_unix_millis", evaluateNotImplemented, fieldName) /** * Creates an expression that interprets an expression as the number of seconds since the Unix @@ -2234,7 +2243,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixSecondsToTimestamp(fieldName: String): Expr = - FunctionExpr("unix_seconds_to_timestamp", fieldName, evaluateNotImplemented) + FunctionExpr("unix_seconds_to_timestamp", evaluateNotImplemented, fieldName) /** * Creates an expression that converts a timestamp expression to the number of seconds since the @@ -2256,7 +2265,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixSeconds(fieldName: String): Expr = - FunctionExpr("timestamp_to_unix_seconds", fieldName, evaluateNotImplemented) + FunctionExpr("timestamp_to_unix_seconds", evaluateNotImplemented, fieldName) /** * Creates an expression that adds a specified amount of time to a timestamp. @@ -2269,7 +2278,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr): Expr = - FunctionExpr("timestamp_add", timestamp, unit, amount) + FunctionExpr("timestamp_add", evaluateNotImplemented, timestamp, unit, amount) /** * Creates an expression that adds a specified amount of time to a timestamp. @@ -2282,7 +2291,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: String, amount: Double): Expr = - FunctionExpr("timestamp_add", timestamp, unit, amount) + FunctionExpr("timestamp_add", evaluateNotImplemented, timestamp, unit, amount) /** * Creates an expression that adds a specified amount of time to a timestamp. @@ -2295,7 +2304,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampAdd(fieldName: String, unit: Expr, amount: Expr): Expr = - FunctionExpr("timestamp_add", fieldName, unit, amount) + FunctionExpr("timestamp_add", evaluateNotImplemented, fieldName, unit, amount) /** * Creates an expression that adds a specified amount of time to a timestamp. @@ -2308,7 +2317,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampAdd(fieldName: String, unit: String, amount: Double): Expr = - FunctionExpr("timestamp_add", fieldName, unit, amount) + FunctionExpr("timestamp_add", evaluateNotImplemented, fieldName, unit, amount) /** * Creates an expression that subtracts a specified amount of time to a timestamp. @@ -2321,7 +2330,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr): Expr = - FunctionExpr("timestamp_sub", timestamp, unit, amount) + FunctionExpr("timestamp_sub", evaluateNotImplemented, timestamp, unit, amount) /** * Creates an expression that subtracts a specified amount of time to a timestamp. @@ -2334,7 +2343,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampSub(timestamp: Expr, unit: String, amount: Double): Expr = - FunctionExpr("timestamp_sub", timestamp, unit, amount) + FunctionExpr("timestamp_sub", evaluateNotImplemented, timestamp, unit, amount) /** * Creates an expression that subtracts a specified amount of time to a timestamp. @@ -2347,7 +2356,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampSub(fieldName: String, unit: Expr, amount: Expr): Expr = - FunctionExpr("timestamp_sub", fieldName, unit, amount) + FunctionExpr("timestamp_sub", evaluateNotImplemented, fieldName, unit, amount) /** * Creates an expression that subtracts a specified amount of time to a timestamp. @@ -2360,7 +2369,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampSub(fieldName: String, unit: String, amount: Double): Expr = - FunctionExpr("timestamp_sub", fieldName, unit, amount) + FunctionExpr("timestamp_sub", evaluateNotImplemented, fieldName, unit, amount) /** * Creates an expression that checks if two expressions are equal. @@ -2639,7 +2648,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayConcat(firstArray: Expr, secondArray: Expr, vararg otherArrays: Any): Expr = - FunctionExpr("array_concat", firstArray, secondArray, *otherArrays) + FunctionExpr("array_concat", evaluateNotImplemented, firstArray, secondArray, *otherArrays) /** * Creates an expression that concatenates an array with other arrays. @@ -2651,7 +2660,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayConcat(firstArray: Expr, secondArray: Any, vararg otherArrays: Any): Expr = - FunctionExpr("array_concat", firstArray, secondArray, *otherArrays) + FunctionExpr("array_concat", evaluateNotImplemented, firstArray, secondArray, *otherArrays) /** * Creates an expression that concatenates a field's array value with other arrays. @@ -2663,7 +2672,13 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayConcat(firstArrayField: String, secondArray: Expr, vararg otherArrays: Any): Expr = - FunctionExpr("array_concat", firstArrayField, secondArray, *otherArrays) + FunctionExpr( + "array_concat", + evaluateNotImplemented, + firstArrayField, + secondArray, + *otherArrays + ) /** * Creates an expression that concatenates a field's array value with other arrays. @@ -2675,7 +2690,13 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayConcat(firstArrayField: String, secondArray: Any, vararg otherArrays: Any): Expr = - FunctionExpr("array_concat", firstArrayField, secondArray, *otherArrays) + FunctionExpr( + "array_concat", + evaluateNotImplemented, + firstArrayField, + secondArray, + *otherArrays + ) /** * Reverses the order of elements in the [array]. @@ -2695,7 +2716,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayReverse(arrayFieldName: String): Expr = - FunctionExpr("array_reverse", arrayFieldName, evaluateNotImplemented) + FunctionExpr("array_reverse", evaluateNotImplemented, arrayFieldName) /** * Creates an expression that checks if the array contains a specific [element]. @@ -2861,7 +2882,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayLength(arrayFieldName: String): Expr = - FunctionExpr("array_length", arrayFieldName, evaluateNotImplemented) + FunctionExpr("array_length", evaluateNotImplemented, arrayFieldName) /** * Creates an expression that indexes into an array from the beginning or end and return the @@ -2900,7 +2921,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayOffset(arrayFieldName: String, offset: Expr): Expr = - FunctionExpr("array_offset", arrayFieldName, offset) + FunctionExpr("array_offset", evaluateNotImplemented, arrayFieldName, offset) /** * Creates an expression that indexes into an array from the beginning or end and return the @@ -2913,7 +2934,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayOffset(arrayFieldName: String, offset: Int): Expr = - FunctionExpr("array_offset", arrayFieldName, constant(offset)) + FunctionExpr("array_offset", evaluateNotImplemented, arrayFieldName, constant(offset)) /** * Creates a conditional expression that evaluates to a [thenExpr] expression if a condition is @@ -2926,7 +2947,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cond(condition: BooleanExpr, thenExpr: Expr, elseExpr: Expr): Expr = - FunctionExpr("cond", condition, thenExpr, elseExpr) + FunctionExpr("cond", evaluateNotImplemented, condition, thenExpr, elseExpr) /** * Creates a conditional expression that evaluates to a [thenValue] if a condition is true or an @@ -2939,7 +2960,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cond(condition: BooleanExpr, thenValue: Any, elseValue: Any): Expr = - FunctionExpr("cond", condition, thenValue, elseValue) + FunctionExpr("cond", evaluateNotImplemented, condition, thenValue, elseValue) /** * Creates an expression that checks if a field exists. @@ -2983,7 +3004,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun ifError(tryExpr: Expr, catchValue: Any): Expr = - FunctionExpr("if_error", tryExpr, catchValue) + FunctionExpr("if_error", evaluateNotImplemented, tryExpr, catchValue) /** * Creates an expression that returns the document ID from a path. @@ -4188,9 +4209,10 @@ internal constructor( ) : this(name, function, arrayOf(param)) internal constructor( name: String, + function: EvaluateFunction, param: Expr, vararg params: Any - ) : this(name, evaluateNotImplemented, arrayOf(param, *toArrayOfExprOrConstant(params))) + ) : this(name, function, arrayOf(param, *toArrayOfExprOrConstant(params))) internal constructor( name: String, function: EvaluateFunction, @@ -4199,24 +4221,22 @@ internal constructor( ) : this(name, function, arrayOf(param1, param2)) internal constructor( name: String, + function: EvaluateFunction, param1: Expr, param2: Expr, vararg params: Any - ) : this(name, evaluateNotImplemented, arrayOf(param1, param2, *toArrayOfExprOrConstant(params))) + ) : this(name, function, arrayOf(param1, param2, *toArrayOfExprOrConstant(params))) internal constructor( name: String, - fieldName: String, - function: EvaluateFunction + function: EvaluateFunction, + fieldName: String ) : this(name, function, arrayOf(field(fieldName))) internal constructor( name: String, + function: EvaluateFunction, fieldName: String, vararg params: Any - ) : this( - name, - evaluateNotImplemented, - arrayOf(field(fieldName), *toArrayOfExprOrConstant(params)) - ) + ) : this(name, function, arrayOf(field(fieldName), *toArrayOfExprOrConstant(params))) override fun toProto(userDataReader: UserDataReader): Value { val builder = com.google.firestore.v1.Function.newBuilder() From db7c444c35fb64dc847c4b25b488a88f397294dd Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 21 May 2025 18:14:12 -0400 Subject: [PATCH 070/152] Additional Realtime Expression Support --- firebase-firestore/api.txt | 4 +- .../google/firebase/firestore/model/Values.kt | 79 +- .../firestore/pipeline/EvaluateResult.kt | 18 +- .../firebase/firestore/pipeline/evaluation.kt | 694 +++++++++++++++--- .../firestore/pipeline/expressions.kt | 530 ++++++------- .../firebase/firestore/pipeline/stage.kt | 4 +- 6 files changed, 948 insertions(+), 381 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 0a0b3cd69d5..5da39ff80a2 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -1063,7 +1063,7 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(Object value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object value); method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); @@ -1377,7 +1377,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); - method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object value); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 2a14d5acb70..e216fa34e5e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -107,6 +107,26 @@ internal object Values { } } + fun strictEquals(left: Value, right: Value): Boolean { + val leftType = typeOrder(left) + val rightType = typeOrder(right) + if (leftType != rightType) { + return false + } + + return when (leftType) { + TYPE_ORDER_NULL -> false + TYPE_ORDER_NUMBER -> strictNumberEquals(left, right) + TYPE_ORDER_ARRAY -> strictArrayEquals(left, right) + TYPE_ORDER_VECTOR, + TYPE_ORDER_MAP -> strictObjectEquals(left, right) + TYPE_ORDER_SERVER_TIMESTAMP -> + ServerTimestamps.getLocalWriteTime(left) == ServerTimestamps.getLocalWriteTime(right) + TYPE_ORDER_MAX_VALUE -> true + else -> left == right + } + } + @JvmStatic fun equals(left: Value?, right: Value?): Boolean { if (left === right) { @@ -135,6 +155,17 @@ internal object Values { } } + private fun strictNumberEquals(left: Value, right: Value): Boolean { + if (left.valueTypeCase != right.valueTypeCase) { + return false + } + return when (left.valueTypeCase) { + ValueTypeCase.INTEGER_VALUE -> left.integerValue == right.integerValue + ValueTypeCase.DOUBLE_VALUE -> left.doubleValue == right.doubleValue + else -> false + } + } + private fun numberEquals(left: Value, right: Value): Boolean { if (left.valueTypeCase != right.valueTypeCase) { return false @@ -147,6 +178,23 @@ internal object Values { } } + private fun strictArrayEquals(left: Value, right: Value): Boolean { + val leftArray = left.arrayValue + val rightArray = right.arrayValue + + if (leftArray.valuesCount != rightArray.valuesCount) { + return false + } + + for (i in 0 until leftArray.valuesCount) { + if (!strictEquals(leftArray.getValues(i), rightArray.getValues(i))) { + return false + } + } + + return true + } + private fun arrayEquals(left: Value, right: Value): Boolean { val leftArray = left.arrayValue val rightArray = right.arrayValue @@ -164,6 +212,24 @@ internal object Values { return true } + private fun strictObjectEquals(left: Value, right: Value): Boolean { + val leftMap = left.mapValue + val rightMap = right.mapValue + + if (leftMap.fieldsCount != rightMap.fieldsCount) { + return false + } + + for ((key, value) in leftMap.fieldsMap) { + val otherEntry = rightMap.fieldsMap[key] ?: return false + if (!strictEquals(value, otherEntry)) { + return false + } + } + + return true + } + private fun objectEquals(left: Value, right: Value): Boolean { val leftMap = left.mapValue val rightMap = right.mapValue @@ -173,7 +239,7 @@ internal object Values { } for ((key, value) in leftMap.fieldsMap) { - val otherEntry = rightMap.fieldsMap[key] + val otherEntry = rightMap.fieldsMap[key] ?: return false if (!equals(value, otherEntry)) { return false } @@ -592,13 +658,14 @@ internal object Values { // the backend to do that. val truncatedNanoseconds: Int = timestamp.nanoseconds / 1000 * 1000 - return Value.newBuilder() - .setTimestampValue( - Timestamp.newBuilder().setSeconds(timestamp.seconds).setNanos(truncatedNanoseconds) - ) - .build() + return encodeValue( + Timestamp.newBuilder().setSeconds(timestamp.seconds).setNanos(truncatedNanoseconds).build() + ) } + @JvmStatic + fun encodeValue(value: Timestamp): Value = Value.newBuilder().setTimestampValue(value).build() + @JvmField val TRUE_VALUE: Value = Value.newBuilder().setBooleanValue(true).build() @JvmField val FALSE_VALUE: Value = Value.newBuilder().setBooleanValue(false).build() diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt index 60bed411728..e1c9ea26bc0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt @@ -1,15 +1,31 @@ package com.google.firebase.firestore.pipeline import com.google.firebase.firestore.model.Values +import com.google.firebase.firestore.model.Values.encodeValue import com.google.firestore.v1.Value +import com.google.protobuf.Timestamp internal sealed class EvaluateResult(val value: Value?) { companion object { val TRUE: EvaluateResultValue = EvaluateResultValue(Values.TRUE_VALUE) val FALSE: EvaluateResultValue = EvaluateResultValue(Values.FALSE_VALUE) val NULL: EvaluateResultValue = EvaluateResultValue(Values.NULL_VALUE) - fun booleanValue(boolean: Boolean) = if (boolean) TRUE else FALSE + val DOUBLE_ZERO: EvaluateResultValue = double(0.0) + val LONG_ZERO: EvaluateResultValue = long(0) + fun boolean(boolean: Boolean) = if (boolean) TRUE else FALSE + fun double(double: Double) = EvaluateResultValue(encodeValue(double)) + fun long(long: Long) = EvaluateResultValue(encodeValue(long)) + fun long(int: Int) = EvaluateResultValue(encodeValue(int.toLong())) + fun string(string: String) = EvaluateResultValue(encodeValue(string)) + fun timestamp(seconds: Long, nanos: Int): EvaluateResult = + if (seconds !in -62_135_596_800 until 253_402_300_800) EvaluateResultError + else + EvaluateResultValue( + encodeValue(Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build()) + ) } + internal inline fun evaluateNonNull(f: (Value) -> EvaluateResult): EvaluateResult = + if (value?.hasNullValue() == true) f(value) else this } internal object EvaluateResultError : EvaluateResult(null) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index 69f56e94e88..36796d18f53 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -1,118 +1,648 @@ package com.google.firebase.firestore.pipeline +import com.google.common.math.LongMath +import com.google.common.math.LongMath.checkedAdd +import com.google.common.math.LongMath.checkedMultiply import com.google.firebase.firestore.UserDataReader +import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values +import com.google.firebase.firestore.model.Values.isNanValue import com.google.firebase.firestore.util.Assert import com.google.firestore.v1.Value +import com.google.protobuf.ByteString +import com.google.protobuf.Timestamp +import java.math.BigDecimal +import java.math.RoundingMode +import kotlin.math.absoluteValue +import kotlin.math.floor +import kotlin.math.log10 +import kotlin.math.pow +import kotlin.math.sqrt internal class EvaluationContext(val userDataReader: UserDataReader) -internal fun interface EvaluateFunction { - fun evaluate(params: Sequence): EvaluateResult -} +internal typealias EvaluateDocument = (input: MutableDocument) -> EvaluateResult -private fun evaluateValue( - params: Sequence, - next: (value: Value) -> EvaluateResult?, - complete: () -> EvaluateResult -): EvaluateResult { - for (value in params.map(EvaluateResult::value)) { - if (value == null) return EvaluateResultError - val result = next(value) - if (result != null) return result - } - return complete() -} - -private fun evaluateValueShortCircuitNull( - function: (values: List) -> EvaluateResult -): EvaluateFunction { - return object : EvaluateFunction { - override fun evaluate(params: Sequence): EvaluateResult { - val values = buildList { - for (value in params.map(EvaluateResult::value)) { - if (value == null) return EvaluateResultError - if (value.hasNullValue()) return EvaluateResult.NULL - add(value) +internal typealias EvaluateFunction = (params: List) -> EvaluateDocument + +internal val notImplemented: EvaluateFunction = { _ -> throw NotImplementedError() } + +// === Logical Functions === + +internal val evaluateExists: EvaluateFunction = notImplemented + +internal val evaluateAnd: EvaluateFunction = { params -> + fun(input: MutableDocument): EvaluateResult { + // We only propagate NULL if all no FALSE parameters exist. + var result: EvaluateResult = EvaluateResult.TRUE + for (param in params) { + val value = param(input).value ?: return EvaluateResultError + when (value.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> result = EvaluateResult.NULL + Value.ValueTypeCase.BOOLEAN_VALUE -> { + if (!value.booleanValue) return EvaluateResult.FALSE } + else -> return EvaluateResultError } - return function.invoke(values) } + return result } } -private fun evaluateBooleanValue( - function: (values: List) -> EvaluateResult -): EvaluateFunction { - return object : EvaluateFunction { - override fun evaluate(params: Sequence): EvaluateResult { - val values = buildList { - for (value in params.map(EvaluateResult::value)) { - if (value == null) return EvaluateResultError - if (value.hasNullValue()) return EvaluateResult.NULL - if (!value.hasBooleanValue()) return EvaluateResultError - add(value.booleanValue) +internal val evaluateOr: EvaluateFunction = { params -> + fun(input: MutableDocument): EvaluateResult { + // We only propagate NULL if all no TRUE parameters exist. + var result: EvaluateResult = EvaluateResult.FALSE + for (param in params) { + val value = param(input).value ?: return EvaluateResultError + when (value.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> result = EvaluateResult.NULL + Value.ValueTypeCase.BOOLEAN_VALUE -> { + if (value.booleanValue) return EvaluateResult.TRUE } + else -> return EvaluateResultError } - return function.invoke(values) } + return result + } +} + +internal val evaluateXor: EvaluateFunction = variadicFunction { values: BooleanArray -> + EvaluateResult.boolean(values.fold(false, Boolean::xor)) +} + +// === Comparison Functions === + +internal val evaluateEq: EvaluateFunction = comparison(Values::strictEquals) + +internal val evaluateNeq: EvaluateFunction = comparison { v1, v2 -> !Values.strictEquals(v1, v2) } + +internal val evaluateGt: EvaluateFunction = comparison { v1, v2 -> Values.compare(v1, v2) > 0 } + +internal val evaluateGte: EvaluateFunction = comparison { v1, v2 -> Values.compare(v1, v2) >= 0 } + +internal val evaluateLt: EvaluateFunction = comparison { v1, v2 -> Values.compare(v1, v2) < 0 } + +internal val evaluateLte: EvaluateFunction = comparison { v1, v2 -> Values.compare(v1, v2) <= 0 } + +internal val evaluateNot: EvaluateFunction = unaryFunction { b: Boolean -> + EvaluateResult.boolean(b.not()) +} + +// === Type Functions === + +internal val evaluateIsNaN: EvaluateFunction = unaryFunction { v: Value -> + EvaluateResult.boolean(isNanValue(v)) +} + +internal val evaluateIsNotNaN: EvaluateFunction = unaryFunction { v: Value -> + EvaluateResult.boolean(!isNanValue(v)) +} + +internal val evaluateIsNull: EvaluateFunction = { params -> + if (params.size != 1) + throw Assert.fail( + "IsNull function should have exactly 1 params, but %d were given.", + params.size + ) + val p = params[0] + fun(input: MutableDocument): EvaluateResult { + val v = p(input).value ?: return EvaluateResultError + return EvaluateResult.boolean(v.hasNullValue()) } } -private fun evaluateBooleanValue( - params: Sequence, - next: (value: Boolean) -> Boolean, - complete: () -> EvaluateResult -): EvaluateResult { - for (value in params.map(EvaluateResult::value)) { - if (value == null) return EvaluateResultError - if (value.hasNullValue()) return EvaluateResult.NULL - if (!value.hasBooleanValue()) return EvaluateResultError - if (!next(value.booleanValue)) break +internal val evaluateIsNotNull: EvaluateFunction = { params -> + if (params.size != 1) + throw Assert.fail( + "IsNotNull function should have exactly 1 params, but %d were given.", + params.size + ) + val p = params[0] + fun(input: MutableDocument): EvaluateResult { + val v = p(input).value ?: return EvaluateResultError + return EvaluateResult.boolean(!v.hasNullValue()) } - return complete() } -internal val evaluateNotImplemented = EvaluateFunction { _ -> throw NotImplementedError() } +// === Arithmetic Functions === -internal val evaluateAnd = EvaluateFunction { params -> - var result: EvaluateResult = EvaluateResult.TRUE - evaluateValue( - params, - fun(value: Value): EvaluateResult? { - when (value.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> result = EvaluateResult.NULL - Value.ValueTypeCase.BOOLEAN_VALUE -> { - if (!value.booleanValue) return EvaluateResult.FALSE - } - else -> return EvaluateResultError +internal val evaluateAdd: EvaluateFunction = arithmeticPrimitive(LongMath::checkedAdd, Double::plus) + +internal val evaluateCeil = arithmeticPrimitive({ it }, Math::ceil) + +internal val evaluateDivide = arithmeticPrimitive(Long::div, Double::div) + +internal val evaluateFloor = arithmeticPrimitive({ it }, Math::floor) + +internal val evaluateMod = arithmeticPrimitive(Long::mod, Double::mod) + +internal val evaluateMultiply: EvaluateFunction = + arithmeticPrimitive(Math::multiplyExact, Double::times) + +internal val evaluatePow: EvaluateFunction = arithmeticPrimitive(Math::pow) + +internal val evaluateRound = + arithmeticPrimitive( + { it }, + { input -> + if (input.isInfinite()) { + val remainder = (input % 1) + val truncated = input - remainder + if (remainder.absoluteValue >= 0.5) truncated + (if (input < 0) -1 else 1) else truncated + } else input + } + ) + +internal val evaluateRoundToPrecision = + arithmetic( + { value: Long, places: Long -> + // If has no decimal places to round off. + if (places >= 0) { + return@arithmetic EvaluateResult.long(value) + } + // Predict and return when the rounded value will be 0, preventing edge cases where the + // traditional conversion could underflow. + val numDigits = floor(log10(value.absoluteValue.toDouble())).toLong() + 1 + if (-places >= numDigits) { + return@arithmetic EvaluateResult.LONG_ZERO + } + + val roundingFactor: Long = 10.0.pow(-places.toDouble()).toLong() + val truncated: Long = value - (value % roundingFactor) + + // Case for when we don't need to round up. + if (truncated.absoluteValue < (roundingFactor / 2).absoluteValue) { + return@arithmetic EvaluateResult.long(truncated) + } + + if (value < 0) { + if (value < -Long.MAX_VALUE + roundingFactor) EvaluateResultError + else EvaluateResult.long(truncated - roundingFactor) + } else { + if (value > Long.MAX_VALUE - roundingFactor) EvaluateResultError + else EvaluateResult.long(truncated + roundingFactor) } - return null }, - { result } + { value: Double, places: Long -> + // A double can only represent up to 16 decimal places. Here we return the original value if + // attempting to round to more decimal places than the double can represent. + if (places >= 16 || !value.isInfinite()) { + return@arithmetic EvaluateResult.double(value) + } + + // Predict and return when the rounded value will be 0, preventing edge cases where the + // traditional conversion could underflow. + val numDigits = floor(log10(value.absoluteValue)).toLong() + 1 + if (-places >= numDigits) { + return@arithmetic EvaluateResult.DOUBLE_ZERO + } + + val rounded: BigDecimal = + BigDecimal.valueOf(value).setScale(places.toInt(), RoundingMode.HALF_UP) + val result: Double = rounded.toDouble() + + if (result.isInfinite()) EvaluateResult.double(result) + else EvaluateResultError // overflow error + } ) + +internal val evaluateSqrt = arithmetic { value: Double -> + if (value < 0) EvaluateResultError else EvaluateResult.double(sqrt(value)) } -internal val evaluateOr = EvaluateFunction { params -> - var result: EvaluateResult = EvaluateResult.FALSE - evaluateValue( - params, - fun(value: Value): EvaluateResult? { - when (value.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> result = EvaluateResult.NULL - Value.ValueTypeCase.BOOLEAN_VALUE -> { - if (value.booleanValue) return EvaluateResult.TRUE + +internal val evaluateSubtract = arithmeticPrimitive(Math::subtractExact, Double::minus) + +// === Array Functions === + +internal val evaluateEqAny = notImplemented + +internal val evaluateNotEqAny = notImplemented + +internal val evaluateArrayContains = notImplemented + +internal val evaluateArrayContainsAny = notImplemented + +internal val evaluateArrayLength = notImplemented + +// === String Functions === + +internal val evaluateStrConcat = variadicFunction { strings: List -> + EvaluateResult.string(buildString { strings.forEach(::append) }) +} + +internal val evaluateStartsWith = binaryFunction { value: String, prefix: String -> + EvaluateResult.boolean(value.startsWith(prefix)) +} + +internal val evaluateEndsWith = binaryFunction { value: String, suffix: String -> + EvaluateResult.boolean(value.endsWith(suffix)) +} + +internal val evaluateByteLength = + unaryFunction( + { b: ByteString -> EvaluateResult.long(b.size()) }, + { s: String -> EvaluateResult.long(s.toByteArray(Charsets.UTF_8).size) } + ) + +internal val evaluateCharLength = unaryFunction { s: String -> + // For strings containing only BMP characters, #length() and #codePointCount() will return + // the same value. Once we exceed the first plane, #length() will not provide the correct + // result. It is safe to use #length() within #codePointCount() because beyond the BMP, + // #length() always yields a larger number. + EvaluateResult.long(s.codePointCount(0, s.length)) +} + +internal val evaluateToLowercase = notImplemented + +internal val evaluateToUppercase = notImplemented + +internal val evaluateReverse = notImplemented + +internal val evaluateSplit = notImplemented // TODO: Does not exist in expressions.kt yet. + +internal val evaluateSubstring = notImplemented // TODO: Does not exist in expressions.kt yet. + +internal val evaluateTrim = notImplemented + +internal val evaluateLTrim = notImplemented // TODO: Does not exist in expressions.kt yet. + +internal val evaluateRTrim = notImplemented // TODO: Does not exist in expressions.kt yet. + +internal val evaluateStrJoin = notImplemented // TODO: Does not exist in expressions.kt yet. + +// === Date / Timestamp Functions === + +internal val evaluateTimestampAdd = notImplemented + +internal val evaluateTimestampSub = notImplemented + +internal val evaluateTimestampTrunc = notImplemented // TODO: Does not exist in expressions.kt yet. + +internal val evaluateTimestampToUnixMicros = unaryFunction { t: Timestamp -> + EvaluateResult.long( + if (t.seconds < Long.MIN_VALUE / 1_000_000) { + // To avoid overflow when very close to Long.MIN_VALUE, add 1 second, multiply, then subtract + // again. + val micros = checkedMultiply(t.seconds + 1, 1_000_000) + val adjustment = t.nanos.toLong() / 1_000 - 1_000_000 + checkedAdd(micros, adjustment) + } else { + val micros = checkedMultiply(t.seconds, 1_000_000) + checkedAdd(micros, t.nanos.toLong() / 1_000) + } + ) +} + +internal val evaluateTimestampToUnixMillis = unaryFunction { t: Timestamp -> + EvaluateResult.long( + if (t.seconds < 0 && t.nanos > 0) { + val millis = checkedMultiply(t.seconds + 1, 1000) + val adjustment = t.nanos.toLong() / 1000_000 - 1000 + checkedAdd(millis, adjustment) + } else { + val millis = checkedMultiply(t.seconds, 1000) + checkedAdd(millis, t.nanos.toLong() / 1000_000) + } + ) +} + +internal val evaluateTimestampToUnixSeconds = unaryFunction { t: Timestamp -> + if (t.nanos !in 0 until 1_000_000_000) EvaluateResultError else EvaluateResult.long(t.seconds) +} + +internal val evaluateUnixMicrosToTimestamp = unaryFunction { micros: Long -> + EvaluateResult.timestamp(Math.floorDiv(micros, 1000_000), Math.floorMod(micros, 1000_000)) +} + +internal val evaluateUnixMillisToTimestamp = unaryFunction { millis: Long -> + EvaluateResult.timestamp(Math.floorDiv(millis, 1000), Math.floorMod(millis, 1000)) +} + +internal val evaluateUnixSecondsToTimestamp = unaryFunction { seconds: Long -> + EvaluateResult.timestamp(seconds, 0) +} + +// === Helper Functions === + +private inline fun catch(f: () -> EvaluateResult): EvaluateResult = + try { + f() + } catch (e: Exception) { + EvaluateResultError + } + +@JvmName("unaryValueFunction") +private inline fun unaryFunction( + crossinline function: (Value) -> EvaluateResult +): EvaluateFunction = { params -> + if (params.size != 1) + throw Assert.fail("Function should have exactly 1 params, but %d were given.", params.size) + val p = params[0] + block@{ input: MutableDocument -> + val v = p(input).value ?: return@block EvaluateResultError + if (v.hasNullValue()) return@block EvaluateResult.NULL + catch { function(v) } + } +} + +@JvmName("unaryBooleanFunction") +private inline fun unaryFunction(crossinline stringOp: (Boolean) -> EvaluateResult) = + unaryFunctionType( + Value.ValueTypeCase.BOOLEAN_VALUE, + Value::getBooleanValue, + stringOp, + ) + +@JvmName("unaryStringFunction") +private inline fun unaryFunction(crossinline stringOp: (String) -> EvaluateResult) = + unaryFunctionType( + Value.ValueTypeCase.STRING_VALUE, + Value::getStringValue, + stringOp, + ) + +@JvmName("unaryLongFunction") +private inline fun unaryFunction(crossinline longOp: (Long) -> EvaluateResult) = + unaryFunctionType( + Value.ValueTypeCase.INTEGER_VALUE, + Value::getIntegerValue, + longOp, + ) + +@JvmName("unaryTimestampFunction") +private inline fun unaryFunction(crossinline timestampOp: (Timestamp) -> EvaluateResult) = + unaryFunctionType( + Value.ValueTypeCase.TIMESTAMP_VALUE, + Value::getTimestampValue, + timestampOp, + ) + +private inline fun unaryFunction( + crossinline byteOp: (ByteString) -> EvaluateResult, + crossinline stringOp: (String) -> EvaluateResult +) = + unaryFunctionType( + Value.ValueTypeCase.BYTES_VALUE, + Value::getBytesValue, + byteOp, + Value.ValueTypeCase.STRING_VALUE, + Value::getStringValue, + stringOp, + ) + +private inline fun unaryFunctionType( + valueTypeCase: Value.ValueTypeCase, + crossinline valueExtractor: (Value) -> T, + crossinline function: (T) -> EvaluateResult +): EvaluateFunction = { params -> + if (params.size != 1) + throw Assert.fail("Function should have exactly 1 params, but %d were given.", params.size) + val p = params[0] + block@{ input: MutableDocument -> + val v = p(input).value ?: return@block EvaluateResultError + when (v.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL + valueTypeCase -> catch { function(valueExtractor(v)) } + else -> EvaluateResultError + } + } +} + +private inline fun unaryFunctionType( + valueTypeCase1: Value.ValueTypeCase, + crossinline valueExtractor1: (Value) -> T1, + crossinline function1: (T1) -> EvaluateResult, + valueTypeCase2: Value.ValueTypeCase, + crossinline valueExtractor2: (Value) -> T2, + crossinline function2: (T2) -> EvaluateResult +): EvaluateFunction = { params -> + if (params.size != 1) + throw Assert.fail("Function should have exactly 1 params, but %d were given.", params.size) + val p = params[0] + block@{ input: MutableDocument -> + val v = p(input).value ?: return@block EvaluateResultError + when (v.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL + valueTypeCase1 -> catch { function1(valueExtractor1(v)) } + valueTypeCase2 -> catch { function2(valueExtractor2(v)) } + else -> EvaluateResultError + } + } +} + +@JvmName("binaryValueValueFunction") +private inline fun binaryFunction( + crossinline function: (Value, Value) -> EvaluateResult +): EvaluateFunction = { params -> + if (params.size != 2) + throw Assert.fail("Function should have exactly 2 params, but %d were given.", params.size) + val p1 = params[0] + val p2 = params[1] + block@{ input: MutableDocument -> + val v1 = p1(input).value ?: return@block EvaluateResultError + val v2 = p2(input).value ?: return@block EvaluateResultError + if (v1.hasNullValue() || v2.hasNullValue()) return@block EvaluateResult.NULL + catch { function(v1, v2) } + } +} + +@JvmName("binaryStringStringFunction") +private inline fun binaryFunction(crossinline function: (String, String) -> EvaluateResult) = + binaryFunctionType( + Value.ValueTypeCase.STRING_VALUE, + Value::getStringValue, + Value.ValueTypeCase.STRING_VALUE, + Value::getStringValue, + function + ) + +private inline fun binaryFunctionType( + valueTypeCase1: Value.ValueTypeCase, + crossinline valueExtractor1: (Value) -> T1, + valueTypeCase2: Value.ValueTypeCase, + crossinline valueExtractor2: (Value) -> T2, + crossinline function: (T1, T2) -> EvaluateResult +): EvaluateFunction = { params -> + if (params.size != 2) + throw Assert.fail("Function should have exactly 2 params, but %d were given.", params.size) + val p1 = params[0] + val p2 = params[1] + block@{ input: MutableDocument -> + val v1 = p1(input).value ?: return@block EvaluateResultError + val v2 = p2(input).value ?: return@block EvaluateResultError + when (v1.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> + when (v2.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL + valueTypeCase2 -> EvaluateResult.NULL + else -> EvaluateResultError } - else -> return EvaluateResultError + valueTypeCase1 -> + when (v2.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL + valueTypeCase2 -> catch { function(valueExtractor1(v1), valueExtractor2(v2)) } + else -> EvaluateResultError + } + else -> EvaluateResultError + } + } +} + +@JvmName("variadicValueFunction") +private inline fun variadicFunction( + crossinline function: (List) -> EvaluateResult +): EvaluateFunction = { params -> + block@{ input: MutableDocument -> + val values = ArrayList(params.size) + var nullFound = false + for (param in params) { + val v = param(input).value ?: return@block EvaluateResultError + if (v.hasNullValue()) nullFound = true + values.add(v) + } + if (nullFound) EvaluateResult.NULL else catch { function(values) } + } +} + +@JvmName("variadicStringFunction") +private inline fun variadicFunction( + crossinline function: (List) -> EvaluateResult +): EvaluateFunction = + variadicFunctionType(Value.ValueTypeCase.STRING_VALUE, Value::getStringValue, function) + +private inline fun variadicFunctionType( + valueTypeCase: Value.ValueTypeCase, + crossinline valueExtractor: (Value) -> T, + crossinline function: (List) -> EvaluateResult, +): EvaluateFunction = { params -> + block@{ input: MutableDocument -> + val values = ArrayList(params.size) + var nullFound = false + for (param in params) { + val v = param(input).value ?: return@block EvaluateResultError + when (v.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> nullFound = true + valueTypeCase -> values.add(valueExtractor(v)) + else -> return@block EvaluateResultError } - return null - }, - { result } + } + if (nullFound) EvaluateResult.NULL else catch { function(values) } + } +} + +@JvmName("variadicBooleanFunction") +private inline fun variadicFunction( + crossinline function: (BooleanArray) -> EvaluateResult +): EvaluateFunction = { params -> + block@{ input: MutableDocument -> + val values = BooleanArray(params.size) + var nullFound = false + params.forEachIndexed { i, param -> + val v = param(input).value ?: return@block EvaluateResultError + when (v.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> nullFound = true + Value.ValueTypeCase.BOOLEAN_VALUE -> values[i] = v.booleanValue + else -> return@block EvaluateResultError + } + } + if (nullFound) EvaluateResult.NULL else catch { function(values) } + } +} + +private inline fun comparison(crossinline predicate: (Value, Value) -> Boolean): EvaluateFunction = + binaryFunction { p1: Value, p2: Value -> + if (isNanValue(p1) or isNanValue(p2)) EvaluateResult.FALSE + else catch { EvaluateResult.boolean(predicate(p1, p2)) } + } + +private inline fun arithmeticPrimitive( + crossinline intOp: (Long) -> Long, + crossinline doubleOp: (Double) -> Double +): EvaluateFunction = + arithmetic( + { x: Long -> EvaluateResult.long(intOp(x)) }, + { x: Double -> EvaluateResult.double(doubleOp(x)) } + ) + +private inline fun arithmeticPrimitive( + crossinline intOp: (Long, Long) -> Long, + crossinline doubleOp: (Double, Double) -> Double +): EvaluateFunction = + arithmetic( + { x: Long, y: Long -> EvaluateResult.long(intOp(x, y)) }, + { x: Double, y: Double -> EvaluateResult.double(doubleOp(x, y)) } + ) + +private inline fun arithmeticPrimitive( + crossinline doubleOp: (Double, Double) -> Double +): EvaluateFunction = arithmetic { x: Double, y: Double -> EvaluateResult.double(doubleOp(x, y)) } + +private inline fun arithmetic(crossinline doubleOp: (Double) -> EvaluateResult): EvaluateFunction = + arithmetic({ n: Long -> doubleOp(n.toDouble()) }, doubleOp) + +private inline fun arithmetic( + crossinline intOp: (Long) -> EvaluateResult, + crossinline doubleOp: (Double) -> EvaluateResult +): EvaluateFunction = + unaryFunctionType( + Value.ValueTypeCase.INTEGER_VALUE, + Value::getIntegerValue, + intOp, + Value.ValueTypeCase.DOUBLE_VALUE, + Value::getDoubleValue, + doubleOp, ) + +@JvmName("arithmeticNumberLong") +private inline fun arithmetic( + crossinline intOp: (Long, Long) -> EvaluateResult, + crossinline doubleOp: (Double, Long) -> EvaluateResult +): EvaluateFunction = binaryFunction { p1: Value, p2: Value -> + if (p2.hasIntegerValue()) + when (p1.valueTypeCase) { + Value.ValueTypeCase.INTEGER_VALUE -> intOp(p1.integerValue, p2.integerValue) + Value.ValueTypeCase.DOUBLE_VALUE -> doubleOp(p1.doubleValue, p2.integerValue) + else -> EvaluateResultError + } + else EvaluateResultError } -internal val evaluateXor = evaluateBooleanValue { params -> - EvaluateResult.booleanValue(params.fold(false, Boolean::xor)) + +private inline fun arithmetic( + crossinline intOp: (Long, Long) -> EvaluateResult, + crossinline doubleOp: (Double, Double) -> EvaluateResult +): EvaluateFunction = binaryFunction { p1: Value, p2: Value -> + when (p1.valueTypeCase) { + Value.ValueTypeCase.INTEGER_VALUE -> + when (p2.valueTypeCase) { + Value.ValueTypeCase.INTEGER_VALUE -> intOp(p1.integerValue, p2.integerValue) + Value.ValueTypeCase.DOUBLE_VALUE -> doubleOp(p1.integerValue.toDouble(), p2.doubleValue) + else -> EvaluateResultError + } + Value.ValueTypeCase.DOUBLE_VALUE -> + when (p2.valueTypeCase) { + Value.ValueTypeCase.INTEGER_VALUE -> doubleOp(p1.doubleValue, p2.integerValue.toDouble()) + Value.ValueTypeCase.DOUBLE_VALUE -> doubleOp(p1.doubleValue, p2.doubleValue) + else -> EvaluateResultError + } + else -> EvaluateResultError + } } -internal val evaluateEq = evaluateValueShortCircuitNull { values -> - Assert.hardAssert(values.size == 2, "Eq function should have exactly 2 params") - EvaluateResult.booleanValue(Values.equals(values.get(0), values.get(1))) + +private inline fun arithmetic( + crossinline op: (Double, Double) -> EvaluateResult +): EvaluateFunction = binaryFunction { p1: Value, p2: Value -> + val v1: Double = + when (p1.valueTypeCase) { + Value.ValueTypeCase.INTEGER_VALUE -> p1.integerValue.toDouble() + Value.ValueTypeCase.DOUBLE_VALUE -> p1.doubleValue + else -> return@binaryFunction EvaluateResultError + } + val v2: Double = + when (p2.valueTypeCase) { + Value.ValueTypeCase.INTEGER_VALUE -> p2.integerValue.toDouble() + Value.ValueTypeCase.DOUBLE_VALUE -> p2.doubleValue + else -> return@binaryFunction EvaluateResultError + } + op(v1, v2) } 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 21c4a1a3a70..dafbae42d5f 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 @@ -32,7 +32,6 @@ import com.google.firebase.firestore.util.CustomClassMapper import com.google.firestore.v1.MapValue import com.google.firestore.v1.Value import java.util.Date -import kotlin.reflect.KFunction1 /** * Represents an expression that can be evaluated to a value within the execution of a [Pipeline]. @@ -51,7 +50,7 @@ abstract class Expr internal constructor() { private class ValueConstant(val value: Value) : Expr() { override fun toProto(userDataReader: UserDataReader): Value = value - override fun evaluate(context: EvaluationContext) = { _: MutableDocument -> + override fun evaluateContext(context: EvaluationContext) = { _: MutableDocument -> EvaluateResultValue(value) } } @@ -65,7 +64,7 @@ abstract class Expr internal constructor() { toExpr(value, ::pojoToExprOrConstant) ?: throw IllegalArgumentException("Unknown type: $value") - private fun toExpr(value: Any?, toExpr: KFunction1): Expr? { + private inline fun toExpr(value: Any?, toExpr: (Any?) -> Expr): Expr? { if (value == null) return NULL return when (value) { is Expr -> value @@ -95,7 +94,7 @@ abstract class Expr internal constructor() { } } - internal fun toArrayOfExprOrConstant(others: Iterable): Array = + private fun toArrayOfExprOrConstant(others: Iterable): Array = others.map(::toExprOrConstant).toTypedArray() internal fun toArrayOfExprOrConstant(others: Array): Array = @@ -156,7 +155,8 @@ abstract class Expr internal constructor() { @JvmStatic fun constant(value: Boolean): BooleanExpr { val encodedValue = encodeValue(value) - return object : BooleanExpr("N/A", { EvaluateResultValue(encodedValue) }, emptyArray()) { + val evaluateResultValue = EvaluateResultValue(encodedValue) + return object : BooleanExpr("N/A", { _ -> { _ -> evaluateResultValue } }, emptyArray()) { override fun toProto(userDataReader: UserDataReader): Value { return encodedValue } @@ -218,7 +218,7 @@ abstract class Expr internal constructor() { return encodeValue(ref) } - override fun evaluate( + override fun evaluateContext( context: EvaluationContext ): (input: MutableDocument) -> EvaluateResult { val result = EvaluateResultValue(toProto(context.userDataReader)) @@ -302,8 +302,7 @@ abstract class Expr internal constructor() { } @JvmStatic - fun generic(name: String, vararg expr: Expr): Expr = - FunctionExpr(name, evaluateNotImplemented, expr) + fun generic(name: String, vararg expr: Expr): Expr = FunctionExpr(name, notImplemented, expr) /** * Creates an expression that performs a logical 'AND' operation. @@ -345,8 +344,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the not operation. */ @JvmStatic - fun not(condition: BooleanExpr): BooleanExpr = - BooleanExpr("not", evaluateNotImplemented, condition) + fun not(condition: BooleanExpr): BooleanExpr = BooleanExpr("not", evaluateNot, condition) /** * Creates an expression that applies a bitwise AND operation between two expressions. @@ -357,7 +355,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitAnd(bits: Expr, bitsOther: Expr): Expr = - FunctionExpr("bit_and", evaluateNotImplemented, bits, bitsOther) + FunctionExpr("bit_and", notImplemented, bits, bitsOther) /** * Creates an expression that applies a bitwise AND operation between an expression and a @@ -369,7 +367,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitAnd(bits: Expr, bitsOther: ByteArray): Expr = - FunctionExpr("bit_and", evaluateNotImplemented, bits, constant(bitsOther)) + FunctionExpr("bit_and", notImplemented, bits, constant(bitsOther)) /** * Creates an expression that applies a bitwise AND operation between an field and an @@ -381,7 +379,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitAnd(bitsFieldName: String, bitsOther: Expr): Expr = - FunctionExpr("bit_and", evaluateNotImplemented, bitsFieldName, bitsOther) + FunctionExpr("bit_and", notImplemented, bitsFieldName, bitsOther) /** * Creates an expression that applies a bitwise AND operation between an field and constant. @@ -392,7 +390,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitAnd(bitsFieldName: String, bitsOther: ByteArray): Expr = - FunctionExpr("bit_and", evaluateNotImplemented, bitsFieldName, constant(bitsOther)) + FunctionExpr("bit_and", notImplemented, bitsFieldName, constant(bitsOther)) /** * Creates an expression that applies a bitwise OR operation between two expressions. @@ -403,7 +401,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitOr(bits: Expr, bitsOther: Expr): Expr = - FunctionExpr("bit_or", evaluateNotImplemented, bits, bitsOther) + FunctionExpr("bit_or", notImplemented, bits, bitsOther) /** * Creates an expression that applies a bitwise OR operation between an expression and a @@ -415,7 +413,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitOr(bits: Expr, bitsOther: ByteArray): Expr = - FunctionExpr("bit_or", evaluateNotImplemented, bits, constant(bitsOther)) + FunctionExpr("bit_or", notImplemented, bits, constant(bitsOther)) /** * Creates an expression that applies a bitwise OR operation between an field and an expression. @@ -426,7 +424,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitOr(bitsFieldName: String, bitsOther: Expr): Expr = - FunctionExpr("bit_or", evaluateNotImplemented, bitsFieldName, bitsOther) + FunctionExpr("bit_or", notImplemented, bitsFieldName, bitsOther) /** * Creates an expression that applies a bitwise OR operation between an field and constant. @@ -437,7 +435,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitOr(bitsFieldName: String, bitsOther: ByteArray): Expr = - FunctionExpr("bit_or", evaluateNotImplemented, bitsFieldName, constant(bitsOther)) + FunctionExpr("bit_or", notImplemented, bitsFieldName, constant(bitsOther)) /** * Creates an expression that applies a bitwise XOR operation between two expressions. @@ -448,7 +446,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitXor(bits: Expr, bitsOther: Expr): Expr = - FunctionExpr("bit_xor", evaluateNotImplemented, bits, bitsOther) + FunctionExpr("bit_xor", notImplemented, bits, bitsOther) /** * Creates an expression that applies a bitwise XOR operation between an expression and a @@ -460,7 +458,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitXor(bits: Expr, bitsOther: ByteArray): Expr = - FunctionExpr("bit_xor", evaluateNotImplemented, bits, constant(bitsOther)) + FunctionExpr("bit_xor", notImplemented, bits, constant(bitsOther)) /** * Creates an expression that applies a bitwise XOR operation between an field and an @@ -472,7 +470,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitXor(bitsFieldName: String, bitsOther: Expr): Expr = - FunctionExpr("bit_xor", evaluateNotImplemented, bitsFieldName, bitsOther) + FunctionExpr("bit_xor", notImplemented, bitsFieldName, bitsOther) /** * Creates an expression that applies a bitwise XOR operation between an field and constant. @@ -483,7 +481,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitXor(bitsFieldName: String, bitsOther: ByteArray): Expr = - FunctionExpr("bit_xor", evaluateNotImplemented, bitsFieldName, constant(bitsOther)) + FunctionExpr("bit_xor", notImplemented, bitsFieldName, constant(bitsOther)) /** * Creates an expression that applies a bitwise NOT operation to an expression. @@ -491,7 +489,7 @@ abstract class Expr internal constructor() { * @param bits An expression that returns bits when evaluated. * @return A new [Expr] representing the bitwise NOT operation. */ - @JvmStatic fun bitNot(bits: Expr): Expr = FunctionExpr("bit_not", evaluateNotImplemented, bits) + @JvmStatic fun bitNot(bits: Expr): Expr = FunctionExpr("bit_not", notImplemented, bits) /** * Creates an expression that applies a bitwise NOT operation to a field. @@ -500,8 +498,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the bitwise NOT operation. */ @JvmStatic - fun bitNot(bitsFieldName: String): Expr = - FunctionExpr("bit_not", evaluateNotImplemented, bitsFieldName) + fun bitNot(bitsFieldName: String): Expr = FunctionExpr("bit_not", notImplemented, bitsFieldName) /** * Creates an expression that applies a bitwise left shift operation between two expressions. @@ -512,7 +509,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitLeftShift(bits: Expr, numberExpr: Expr): Expr = - FunctionExpr("bit_left_shift", evaluateNotImplemented, bits, numberExpr) + FunctionExpr("bit_left_shift", notImplemented, bits, numberExpr) /** * Creates an expression that applies a bitwise left shift operation between an expression and a @@ -524,7 +521,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitLeftShift(bits: Expr, number: Int): Expr = - FunctionExpr("bit_left_shift", evaluateNotImplemented, bits, number) + FunctionExpr("bit_left_shift", notImplemented, bits, number) /** * Creates an expression that applies a bitwise left shift operation between a field and an @@ -536,7 +533,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitLeftShift(bitsFieldName: String, numberExpr: Expr): Expr = - FunctionExpr("bit_left_shift", evaluateNotImplemented, bitsFieldName, numberExpr) + FunctionExpr("bit_left_shift", notImplemented, bitsFieldName, numberExpr) /** * Creates an expression that applies a bitwise left shift operation between a field and a @@ -548,7 +545,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitLeftShift(bitsFieldName: String, number: Int): Expr = - FunctionExpr("bit_left_shift", evaluateNotImplemented, bitsFieldName, number) + FunctionExpr("bit_left_shift", notImplemented, bitsFieldName, number) /** * Creates an expression that applies a bitwise right shift operation between two expressions. @@ -559,7 +556,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitRightShift(bits: Expr, numberExpr: Expr): Expr = - FunctionExpr("bit_right_shift", evaluateNotImplemented, bits, numberExpr) + FunctionExpr("bit_right_shift", notImplemented, bits, numberExpr) /** * Creates an expression that applies a bitwise right shift operation between an expression and @@ -571,7 +568,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitRightShift(bits: Expr, number: Int): Expr = - FunctionExpr("bit_right_shift", evaluateNotImplemented, bits, number) + FunctionExpr("bit_right_shift", notImplemented, bits, number) /** * Creates an expression that applies a bitwise right shift operation between a field and an @@ -583,7 +580,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitRightShift(bitsFieldName: String, numberExpr: Expr): Expr = - FunctionExpr("bit_right_shift", evaluateNotImplemented, bitsFieldName, numberExpr) + FunctionExpr("bit_right_shift", notImplemented, bitsFieldName, numberExpr) /** * Creates an expression that applies a bitwise right shift operation between a field and a @@ -595,7 +592,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun bitRightShift(bitsFieldName: String, number: Int): Expr = - FunctionExpr("bit_right_shift", evaluateNotImplemented, bitsFieldName, number) + FunctionExpr("bit_right_shift", notImplemented, bitsFieldName, number) /** * Creates an expression that rounds [numericExpr] to nearest integer. @@ -606,7 +603,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing an integer result from the round operation. */ @JvmStatic - fun round(numericExpr: Expr): Expr = FunctionExpr("round", evaluateNotImplemented, numericExpr) + fun round(numericExpr: Expr): Expr = FunctionExpr("round", evaluateRound, numericExpr) /** * Creates an expression that rounds [numericField] to nearest integer. @@ -617,8 +614,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing an integer result from the round operation. */ @JvmStatic - fun round(numericField: String): Expr = - FunctionExpr("round", evaluateNotImplemented, numericField) + fun round(numericField: String): Expr = FunctionExpr("round", evaluateRound, numericField) /** * Creates an expression that rounds off [numericExpr] to [decimalPlace] decimal places if @@ -631,7 +627,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun roundToPrecision(numericExpr: Expr, decimalPlace: Int): Expr = - FunctionExpr("round", evaluateNotImplemented, numericExpr, constant(decimalPlace)) + FunctionExpr("round", evaluateRoundToPrecision, numericExpr, constant(decimalPlace)) /** * Creates an expression that rounds off [numericField] to [decimalPlace] decimal places if @@ -644,7 +640,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun roundToPrecision(numericField: String, decimalPlace: Int): Expr = - FunctionExpr("round", evaluateNotImplemented, numericField, constant(decimalPlace)) + FunctionExpr("round", evaluateRoundToPrecision, numericField, constant(decimalPlace)) /** * Creates an expression that rounds off [numericExpr] to [decimalPlace] decimal places if @@ -657,7 +653,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun roundToPrecision(numericExpr: Expr, decimalPlace: Expr): Expr = - FunctionExpr("round", evaluateNotImplemented, numericExpr, decimalPlace) + FunctionExpr("round", evaluateRoundToPrecision, numericExpr, decimalPlace) /** * Creates an expression that rounds off [numericField] to [decimalPlace] decimal places if @@ -670,7 +666,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun roundToPrecision(numericField: String, decimalPlace: Expr): Expr = - FunctionExpr("round", evaluateNotImplemented, numericField, decimalPlace) + FunctionExpr("round", evaluateRoundToPrecision, numericField, decimalPlace) /** * Creates an expression that returns the smalled integer that isn't less than [numericExpr]. @@ -678,8 +674,7 @@ abstract class Expr internal constructor() { * @param numericExpr An expression that returns number when evaluated. * @return A new [Expr] representing an integer result from the ceil operation. */ - @JvmStatic - fun ceil(numericExpr: Expr): Expr = FunctionExpr("ceil", evaluateNotImplemented, numericExpr) + @JvmStatic fun ceil(numericExpr: Expr): Expr = FunctionExpr("ceil", evaluateCeil, numericExpr) /** * Creates an expression that returns the smalled integer that isn't less than [numericField]. @@ -688,8 +683,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing an integer result from the ceil operation. */ @JvmStatic - fun ceil(numericField: String): Expr = - FunctionExpr("ceil", evaluateNotImplemented, numericField) + fun ceil(numericField: String): Expr = FunctionExpr("ceil", evaluateCeil, numericField) /** * Creates an expression that returns the largest integer that isn't less than [numericExpr]. @@ -698,7 +692,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing an integer result from the floor operation. */ @JvmStatic - fun floor(numericExpr: Expr): Expr = FunctionExpr("floor", evaluateNotImplemented, numericExpr) + fun floor(numericExpr: Expr): Expr = FunctionExpr("floor", evaluateFloor, numericExpr) /** * Creates an expression that returns the largest integer that isn't less than [numericField]. @@ -707,8 +701,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing an integer result from the floor operation. */ @JvmStatic - fun floor(numericField: String): Expr = - FunctionExpr("floor", evaluateNotImplemented, numericField) + fun floor(numericField: String): Expr = FunctionExpr("floor", evaluateFloor, numericField) /** * Creates an expression that returns the [numericExpr] raised to the power of the [exponent]. @@ -721,7 +714,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun pow(numericExpr: Expr, exponent: Number): Expr = - FunctionExpr("pow", evaluateNotImplemented, numericExpr, constant(exponent)) + FunctionExpr("pow", evaluatePow, numericExpr, constant(exponent)) /** * Creates an expression that returns the [numericField] raised to the power of the [exponent]. @@ -734,7 +727,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun pow(numericField: String, exponent: Number): Expr = - FunctionExpr("pow", evaluateNotImplemented, numericField, constant(exponent)) + FunctionExpr("pow", evaluatePow, numericField, constant(exponent)) /** * Creates an expression that returns the [numericExpr] raised to the power of the [exponent]. @@ -747,7 +740,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun pow(numericExpr: Expr, exponent: Expr): Expr = - FunctionExpr("pow", evaluateNotImplemented, numericExpr, exponent) + FunctionExpr("pow", evaluatePow, numericExpr, exponent) /** * Creates an expression that returns the [numericField] raised to the power of the [exponent]. @@ -760,7 +753,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun pow(numericField: String, exponent: Expr): Expr = - FunctionExpr("pow", evaluateNotImplemented, numericField, exponent) + FunctionExpr("pow", evaluatePow, numericField, exponent) /** * Creates an expression that returns the square root of [numericExpr]. @@ -768,8 +761,7 @@ abstract class Expr internal constructor() { * @param numericExpr An expression that returns number when evaluated. * @return A new [Expr] representing the numeric result of the square root operation. */ - @JvmStatic - fun sqrt(numericExpr: Expr): Expr = FunctionExpr("sqrt", evaluateNotImplemented, numericExpr) + @JvmStatic fun sqrt(numericExpr: Expr): Expr = FunctionExpr("sqrt", evaluateSqrt, numericExpr) /** * Creates an expression that returns the square root of [numericField]. @@ -778,8 +770,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the numeric result of the square root operation. */ @JvmStatic - fun sqrt(numericField: String): Expr = - FunctionExpr("sqrt", evaluateNotImplemented, numericField) + fun sqrt(numericField: String): Expr = FunctionExpr("sqrt", evaluateSqrt, numericField) /** * Creates an expression that adds numeric expressions and constants. @@ -791,7 +782,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun add(first: Expr, second: Expr, vararg others: Any): Expr = - FunctionExpr("add", evaluateNotImplemented, first, second, *others) + FunctionExpr("add", evaluateAdd, first, second, *others) /** * Creates an expression that adds numeric expressions and constants. @@ -803,7 +794,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun add(first: Expr, second: Number, vararg others: Any): Expr = - FunctionExpr("add", evaluateNotImplemented, first, second, *others) + FunctionExpr("add", evaluateAdd, first, second, *others) /** * Creates an expression that adds a numeric field with numeric expressions and constants. @@ -815,7 +806,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun add(numericFieldName: String, second: Expr, vararg others: Any): Expr = - FunctionExpr("add", evaluateNotImplemented, numericFieldName, second, *others) + FunctionExpr("add", evaluateAdd, numericFieldName, second, *others) /** * Creates an expression that adds a numeric field with numeric expressions and constants. @@ -827,7 +818,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun add(numericFieldName: String, second: Number, vararg others: Any): Expr = - FunctionExpr("add", evaluateNotImplemented, numericFieldName, second, *others) + FunctionExpr("add", evaluateAdd, numericFieldName, second, *others) /** * Creates an expression that subtracts two expressions. @@ -838,7 +829,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun subtract(minuend: Expr, subtrahend: Expr): Expr = - FunctionExpr("subtract", evaluateNotImplemented, minuend, subtrahend) + FunctionExpr("subtract", evaluateSubtract, minuend, subtrahend) /** * Creates an expression that subtracts a constant value from a numeric expression. @@ -849,7 +840,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun subtract(minuend: Expr, subtrahend: Number): Expr = - FunctionExpr("subtract", evaluateNotImplemented, minuend, subtrahend) + FunctionExpr("subtract", evaluateSubtract, minuend, subtrahend) /** * Creates an expression that subtracts a numeric expressions from numeric field. @@ -860,7 +851,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun subtract(numericFieldName: String, subtrahend: Expr): Expr = - FunctionExpr("subtract", evaluateNotImplemented, numericFieldName, subtrahend) + FunctionExpr("subtract", evaluateSubtract, numericFieldName, subtrahend) /** * Creates an expression that subtracts a constant from numeric field. @@ -871,7 +862,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun subtract(numericFieldName: String, subtrahend: Number): Expr = - FunctionExpr("subtract", evaluateNotImplemented, numericFieldName, subtrahend) + FunctionExpr("subtract", evaluateSubtract, numericFieldName, subtrahend) /** * Creates an expression that multiplies numeric expressions and constants. @@ -883,7 +874,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun multiply(first: Expr, second: Expr, vararg others: Any): Expr = - FunctionExpr("multiply", evaluateNotImplemented, first, second, *others) + FunctionExpr("multiply", evaluateMultiply, first, second, *others) /** * Creates an expression that multiplies numeric expressions and constants. @@ -895,7 +886,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun multiply(first: Expr, second: Number, vararg others: Any): Expr = - FunctionExpr("multiply", evaluateNotImplemented, first, second, *others) + FunctionExpr("multiply", evaluateMultiply, first, second, *others) /** * Creates an expression that multiplies a numeric field with numeric expressions and constants. @@ -907,7 +898,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun multiply(numericFieldName: String, second: Expr, vararg others: Any): Expr = - FunctionExpr("multiply", evaluateNotImplemented, numericFieldName, second, *others) + FunctionExpr("multiply", evaluateMultiply, numericFieldName, second, *others) /** * Creates an expression that multiplies a numeric field with numeric expressions and constants. @@ -919,7 +910,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun multiply(numericFieldName: String, second: Number, vararg others: Any): Expr = - FunctionExpr("multiply", evaluateNotImplemented, numericFieldName, second, *others) + FunctionExpr("multiply", evaluateMultiply, numericFieldName, second, *others) /** * Creates an expression that divides two numeric expressions. @@ -930,7 +921,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun divide(dividend: Expr, divisor: Expr): Expr = - FunctionExpr("divide", evaluateNotImplemented, dividend, divisor) + FunctionExpr("divide", evaluateDivide, dividend, divisor) /** * Creates an expression that divides a numeric expression by a constant. @@ -941,7 +932,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun divide(dividend: Expr, divisor: Number): Expr = - FunctionExpr("divide", evaluateNotImplemented, dividend, divisor) + FunctionExpr("divide", evaluateDivide, dividend, divisor) /** * Creates an expression that divides numeric field by a numeric expression. @@ -952,7 +943,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun divide(dividendFieldName: String, divisor: Expr): Expr = - FunctionExpr("divide", evaluateNotImplemented, dividendFieldName, divisor) + FunctionExpr("divide", evaluateDivide, dividendFieldName, divisor) /** * Creates an expression that divides a numeric field by a constant. @@ -963,7 +954,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun divide(dividendFieldName: String, divisor: Number): Expr = - FunctionExpr("divide", evaluateNotImplemented, dividendFieldName, divisor) + FunctionExpr("divide", evaluateDivide, dividendFieldName, divisor) /** * Creates an expression that calculates the modulo (remainder) of dividing two numeric @@ -975,7 +966,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mod(dividend: Expr, divisor: Expr): Expr = - FunctionExpr("mod", evaluateNotImplemented, dividend, divisor) + FunctionExpr("mod", evaluateMod, dividend, divisor) /** * Creates an expression that calculates the modulo (remainder) of dividing a numeric expression @@ -987,7 +978,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mod(dividend: Expr, divisor: Number): Expr = - FunctionExpr("mod", evaluateNotImplemented, dividend, divisor) + FunctionExpr("mod", evaluateMod, dividend, divisor) /** * Creates an expression that calculates the modulo (remainder) of dividing a numeric field by a @@ -999,7 +990,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mod(dividendFieldName: String, divisor: Expr): Expr = - FunctionExpr("mod", evaluateNotImplemented, dividendFieldName, divisor) + FunctionExpr("mod", evaluateMod, dividendFieldName, divisor) /** * Creates an expression that calculates the modulo (remainder) of dividing a numeric field by a @@ -1011,7 +1002,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mod(dividendFieldName: String, divisor: Number): Expr = - FunctionExpr("mod", evaluateNotImplemented, dividendFieldName, divisor) + FunctionExpr("mod", evaluateMod, dividendFieldName, divisor) /** * Creates an expression that checks if an [expression], when evaluated, is equal to any of the @@ -1036,7 +1027,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun eqAny(expression: Expr, arrayExpression: Expr): BooleanExpr = - BooleanExpr("eq_any", evaluateNotImplemented, expression, arrayExpression) + BooleanExpr("eq_any", evaluateEqAny, expression, arrayExpression) /** * Creates an expression that checks if a field's value is equal to any of the provided [values] @@ -1061,7 +1052,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun eqAny(fieldName: String, arrayExpression: Expr): BooleanExpr = - BooleanExpr("eq_any", evaluateNotImplemented, fieldName, arrayExpression) + BooleanExpr("eq_any", evaluateEqAny, fieldName, arrayExpression) /** * Creates an expression that checks if an [expression], when evaluated, is not equal to all the @@ -1086,7 +1077,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun notEqAny(expression: Expr, arrayExpression: Expr): BooleanExpr = - BooleanExpr("not_eq_any", evaluateNotImplemented, expression, arrayExpression) + BooleanExpr("not_eq_any", evaluateNotEqAny, expression, arrayExpression) /** * Creates an expression that checks if a field's value is not equal to all of the provided @@ -1111,7 +1102,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun notEqAny(fieldName: String, arrayExpression: Expr): BooleanExpr = - BooleanExpr("not_eq_any", evaluateNotImplemented, fieldName, arrayExpression) + BooleanExpr("not_eq_any", evaluateNotEqAny, fieldName, arrayExpression) /** * Creates an expression that returns true if a value is absent. Otherwise, returns false even @@ -1121,7 +1112,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the isAbsent operation. */ @JvmStatic - fun isAbsent(value: Expr): BooleanExpr = BooleanExpr("is_absent", evaluateNotImplemented, value) + fun isAbsent(value: Expr): BooleanExpr = BooleanExpr("is_absent", notImplemented, value) /** * Creates an expression that returns true if a field is absent. Otherwise, returns false even @@ -1132,7 +1123,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun isAbsent(fieldName: String): BooleanExpr = - BooleanExpr("is_absent", evaluateNotImplemented, fieldName) + BooleanExpr("is_absent", notImplemented, fieldName) /** * Creates an expression that checks if an expression evaluates to 'NaN' (Not a Number). @@ -1140,8 +1131,7 @@ abstract class Expr internal constructor() { * @param expr The expression to check. * @return A new [BooleanExpr] representing the isNan operation. */ - @JvmStatic - fun isNan(expr: Expr): BooleanExpr = BooleanExpr("is_nan", evaluateNotImplemented, expr) + @JvmStatic fun isNan(expr: Expr): BooleanExpr = BooleanExpr("is_nan", evaluateIsNaN, expr) /** * Creates an expression that checks if [expr] evaluates to 'NaN' (Not a Number). @@ -1150,8 +1140,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the isNan operation. */ @JvmStatic - fun isNan(fieldName: String): BooleanExpr = - BooleanExpr("is_nan", evaluateNotImplemented, fieldName) + fun isNan(fieldName: String): BooleanExpr = BooleanExpr("is_nan", evaluateIsNaN, fieldName) /** * Creates an expression that checks if the results of [expr] is NOT 'NaN' (Not a Number). @@ -1160,7 +1149,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the isNotNan operation. */ @JvmStatic - fun isNotNan(expr: Expr): BooleanExpr = BooleanExpr("is_not_nan", evaluateNotImplemented, expr) + fun isNotNan(expr: Expr): BooleanExpr = BooleanExpr("is_not_nan", evaluateIsNotNaN, expr) /** * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a @@ -1171,7 +1160,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun isNotNan(fieldName: String): BooleanExpr = - BooleanExpr("is_not_nan", evaluateNotImplemented, fieldName) + BooleanExpr("is_not_nan", evaluateIsNotNaN, fieldName) /** * Creates an expression that checks if tbe result of [expr] is null. @@ -1179,8 +1168,7 @@ abstract class Expr internal constructor() { * @param expr The expression to check. * @return A new [BooleanExpr] representing the isNull operation. */ - @JvmStatic - fun isNull(expr: Expr): BooleanExpr = BooleanExpr("is_null", evaluateNotImplemented, expr) + @JvmStatic fun isNull(expr: Expr): BooleanExpr = BooleanExpr("is_null", evaluateIsNull, expr) /** * Creates an expression that checks if tbe value of a field is null. @@ -1189,8 +1177,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the isNull operation. */ @JvmStatic - fun isNull(fieldName: String): BooleanExpr = - BooleanExpr("is_null", evaluateNotImplemented, fieldName) + fun isNull(fieldName: String): BooleanExpr = BooleanExpr("is_null", evaluateIsNull, fieldName) /** * Creates an expression that checks if tbe result of [expr] is not null. @@ -1199,8 +1186,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the isNotNull operation. */ @JvmStatic - fun isNotNull(expr: Expr): BooleanExpr = - BooleanExpr("is_not_null", evaluateNotImplemented, expr) + fun isNotNull(expr: Expr): BooleanExpr = BooleanExpr("is_not_null", evaluateIsNotNull, expr) /** * Creates an expression that checks if tbe value of a field is not null. @@ -1210,7 +1196,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun isNotNull(fieldName: String): BooleanExpr = - BooleanExpr("is_not_null", evaluateNotImplemented, fieldName) + BooleanExpr("is_not_null", evaluateIsNotNull, fieldName) /** * Creates an expression that replaces the first occurrence of a substring within the @@ -1224,7 +1210,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(stringExpression: Expr, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_first", evaluateNotImplemented, stringExpression, find, replace) + FunctionExpr("replace_first", notImplemented, stringExpression, find, replace) /** * Creates an expression that replaces the first occurrence of a substring within the @@ -1237,7 +1223,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(stringExpression: Expr, find: String, replace: String): Expr = - FunctionExpr("replace_first", evaluateNotImplemented, stringExpression, find, replace) + FunctionExpr("replace_first", notImplemented, stringExpression, find, replace) /** * Creates an expression that replaces the first occurrence of a substring within the specified @@ -1252,7 +1238,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(fieldName: String, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_first", evaluateNotImplemented, fieldName, find, replace) + FunctionExpr("replace_first", notImplemented, fieldName, find, replace) /** * Creates an expression that replaces the first occurrence of a substring within the specified @@ -1265,7 +1251,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(fieldName: String, find: String, replace: String): Expr = - FunctionExpr("replace_first", evaluateNotImplemented, fieldName, find, replace) + FunctionExpr("replace_first", notImplemented, fieldName, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the @@ -1278,7 +1264,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(stringExpression: Expr, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_all", evaluateNotImplemented, stringExpression, find, replace) + FunctionExpr("replace_all", notImplemented, stringExpression, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the @@ -1291,7 +1277,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(stringExpression: Expr, find: String, replace: String): Expr = - FunctionExpr("replace_all", evaluateNotImplemented, stringExpression, find, replace) + FunctionExpr("replace_all", notImplemented, stringExpression, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the specified @@ -1306,7 +1292,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(fieldName: String, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_all", evaluateNotImplemented, fieldName, find, replace) + FunctionExpr("replace_all", notImplemented, fieldName, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the specified @@ -1319,7 +1305,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(fieldName: String, find: String, replace: String): Expr = - FunctionExpr("replace_all", evaluateNotImplemented, fieldName, find, replace) + FunctionExpr("replace_all", notImplemented, fieldName, find, replace) /** * Creates an expression that calculates the character length of a string expression in UTF8. @@ -1328,7 +1314,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the charLength operation. */ @JvmStatic - fun charLength(expr: Expr): Expr = FunctionExpr("char_length", evaluateNotImplemented, expr) + fun charLength(expr: Expr): Expr = FunctionExpr("char_length", evaluateCharLength, expr) /** * Creates an expression that calculates the character length of a string field in UTF8. @@ -1338,7 +1324,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun charLength(fieldName: String): Expr = - FunctionExpr("char_length", evaluateNotImplemented, fieldName) + FunctionExpr("char_length", evaluateCharLength, fieldName) /** * Creates an expression that calculates the length of a string in UTF-8 bytes, or just the @@ -1348,7 +1334,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the length of the string in bytes. */ @JvmStatic - fun byteLength(value: Expr): Expr = FunctionExpr("byte_length", evaluateNotImplemented, value) + fun byteLength(value: Expr): Expr = FunctionExpr("byte_length", evaluateByteLength, value) /** * Creates an expression that calculates the length of a string represented by a field in UTF-8 @@ -1359,7 +1345,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun byteLength(fieldName: String): Expr = - FunctionExpr("byte_length", evaluateNotImplemented, fieldName) + FunctionExpr("byte_length", evaluateByteLength, fieldName) /** * Creates an expression that performs a case-sensitive wildcard string comparison. @@ -1370,7 +1356,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(stringExpression: Expr, pattern: Expr): BooleanExpr = - BooleanExpr("like", evaluateNotImplemented, stringExpression, pattern) + BooleanExpr("like", notImplemented, stringExpression, pattern) /** * Creates an expression that performs a case-sensitive wildcard string comparison. @@ -1381,7 +1367,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(stringExpression: Expr, pattern: String): BooleanExpr = - BooleanExpr("like", evaluateNotImplemented, stringExpression, pattern) + BooleanExpr("like", notImplemented, stringExpression, pattern) /** * Creates an expression that performs a case-sensitive wildcard string comparison against a @@ -1393,7 +1379,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(fieldName: String, pattern: Expr): BooleanExpr = - BooleanExpr("like", evaluateNotImplemented, fieldName, pattern) + BooleanExpr("like", notImplemented, fieldName, pattern) /** * Creates an expression that performs a case-sensitive wildcard string comparison against a @@ -1405,7 +1391,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(fieldName: String, pattern: String): BooleanExpr = - BooleanExpr("like", evaluateNotImplemented, fieldName, pattern) + BooleanExpr("like", notImplemented, fieldName, pattern) /** * Creates an expression that return a pseudo-random number of type double in the range of [0, @@ -1413,7 +1399,7 @@ abstract class Expr internal constructor() { * * @return A new [Expr] representing the random number operation. */ - @JvmStatic fun rand(): Expr = FunctionExpr("rand", evaluateNotImplemented) + @JvmStatic fun rand(): Expr = FunctionExpr("rand", notImplemented) /** * Creates an expression that checks if a string expression contains a specified regular @@ -1425,7 +1411,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(stringExpression: Expr, pattern: Expr): BooleanExpr = - BooleanExpr("regex_contains", evaluateNotImplemented, stringExpression, pattern) + BooleanExpr("regex_contains", notImplemented, stringExpression, pattern) /** * Creates an expression that checks if a string expression contains a specified regular @@ -1437,7 +1423,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(stringExpression: Expr, pattern: String): BooleanExpr = - BooleanExpr("regex_contains", evaluateNotImplemented, stringExpression, pattern) + BooleanExpr("regex_contains", notImplemented, stringExpression, pattern) /** * Creates an expression that checks if a string field contains a specified regular expression @@ -1449,7 +1435,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(fieldName: String, pattern: Expr) = - BooleanExpr("regex_contains", evaluateNotImplemented, fieldName, pattern) + BooleanExpr("regex_contains", notImplemented, fieldName, pattern) /** * Creates an expression that checks if a string field contains a specified regular expression @@ -1461,7 +1447,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(fieldName: String, pattern: String) = - BooleanExpr("regex_contains", evaluateNotImplemented, fieldName, pattern) + BooleanExpr("regex_contains", notImplemented, fieldName, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1472,7 +1458,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(stringExpression: Expr, pattern: Expr): BooleanExpr = - BooleanExpr("regex_match", evaluateNotImplemented, stringExpression, pattern) + BooleanExpr("regex_match", notImplemented, stringExpression, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1483,7 +1469,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(stringExpression: Expr, pattern: String): BooleanExpr = - BooleanExpr("regex_match", evaluateNotImplemented, stringExpression, pattern) + BooleanExpr("regex_match", notImplemented, stringExpression, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1494,7 +1480,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(fieldName: String, pattern: Expr) = - BooleanExpr("regex_match", evaluateNotImplemented, fieldName, pattern) + BooleanExpr("regex_match", notImplemented, fieldName, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1505,7 +1491,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(fieldName: String, pattern: String) = - BooleanExpr("regex_match", evaluateNotImplemented, fieldName, pattern) + BooleanExpr("regex_match", notImplemented, fieldName, pattern) /** * Creates an expression that returns the largest value between multiple input expressions or @@ -1517,7 +1503,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMaximum(expr: Expr, vararg others: Any): Expr = - FunctionExpr("logical_max", evaluateNotImplemented, expr, *others) + FunctionExpr("logical_max", notImplemented, expr, *others) /** * Creates an expression that returns the largest value between multiple input expressions or @@ -1529,7 +1515,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMaximum(fieldName: String, vararg others: Any): Expr = - FunctionExpr("logical_max", evaluateNotImplemented, fieldName, *others) + FunctionExpr("logical_max", notImplemented, fieldName, *others) /** * Creates an expression that returns the smallest value between multiple input expressions or @@ -1541,7 +1527,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMinimum(expr: Expr, vararg others: Any): Expr = - FunctionExpr("logical_min", evaluateNotImplemented, expr, *others) + FunctionExpr("logical_min", notImplemented, expr, *others) /** * Creates an expression that returns the smallest value between multiple input expressions or @@ -1553,7 +1539,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMinimum(fieldName: String, vararg others: Any): Expr = - FunctionExpr("logical_min", evaluateNotImplemented, fieldName, *others) + FunctionExpr("logical_min", notImplemented, fieldName, *others) /** * Creates an expression that reverses a string. @@ -1563,7 +1549,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun reverse(stringExpression: Expr): Expr = - FunctionExpr("reverse", evaluateNotImplemented, stringExpression) + FunctionExpr("reverse", evaluateReverse, stringExpression) /** * Creates an expression that reverses a string value from the specified field. @@ -1572,8 +1558,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the reversed string. */ @JvmStatic - fun reverse(fieldName: String): Expr = - FunctionExpr("reverse", evaluateNotImplemented, fieldName) + fun reverse(fieldName: String): Expr = FunctionExpr("reverse", evaluateReverse, fieldName) /** * Creates an expression that checks if a string expression contains a specified substring. @@ -1584,7 +1569,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(stringExpression: Expr, substring: Expr): BooleanExpr = - BooleanExpr("str_contains", evaluateNotImplemented, stringExpression, substring) + BooleanExpr("str_contains", notImplemented, stringExpression, substring) /** * Creates an expression that checks if a string expression contains a specified substring. @@ -1595,7 +1580,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(stringExpression: Expr, substring: String): BooleanExpr = - BooleanExpr("str_contains", evaluateNotImplemented, stringExpression, substring) + BooleanExpr("str_contains", notImplemented, stringExpression, substring) /** * Creates an expression that checks if a string field contains a specified substring. @@ -1606,7 +1591,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(fieldName: String, substring: Expr): BooleanExpr = - BooleanExpr("str_contains", evaluateNotImplemented, fieldName, substring) + BooleanExpr("str_contains", notImplemented, fieldName, substring) /** * Creates an expression that checks if a string field contains a specified substring. @@ -1617,7 +1602,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(fieldName: String, substring: String): BooleanExpr = - BooleanExpr("str_contains", evaluateNotImplemented, fieldName, substring) + BooleanExpr("str_contains", notImplemented, fieldName, substring) /** * Creates an expression that checks if a string expression starts with a given [prefix]. @@ -1628,7 +1613,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun startsWith(stringExpr: Expr, prefix: Expr): BooleanExpr = - BooleanExpr("starts_with", evaluateNotImplemented, stringExpr, prefix) + BooleanExpr("starts_with", evaluateStartsWith, stringExpr, prefix) /** * Creates an expression that checks if a string expression starts with a given [prefix]. @@ -1639,7 +1624,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun startsWith(stringExpr: Expr, prefix: String): BooleanExpr = - BooleanExpr("starts_with", evaluateNotImplemented, stringExpr, prefix) + BooleanExpr("starts_with", evaluateStartsWith, stringExpr, prefix) /** * Creates an expression that checks if a string expression starts with a given [prefix]. @@ -1650,7 +1635,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun startsWith(fieldName: String, prefix: Expr): BooleanExpr = - BooleanExpr("starts_with", evaluateNotImplemented, fieldName, prefix) + BooleanExpr("starts_with", evaluateStartsWith, fieldName, prefix) /** * Creates an expression that checks if a string expression starts with a given [prefix]. @@ -1661,7 +1646,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun startsWith(fieldName: String, prefix: String): BooleanExpr = - BooleanExpr("starts_with", evaluateNotImplemented, fieldName, prefix) + BooleanExpr("starts_with", evaluateStartsWith, fieldName, prefix) /** * Creates an expression that checks if a string expression ends with a given [suffix]. @@ -1672,7 +1657,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun endsWith(stringExpr: Expr, suffix: Expr): BooleanExpr = - BooleanExpr("ends_with", evaluateNotImplemented, stringExpr, suffix) + BooleanExpr("ends_with", evaluateEndsWith, stringExpr, suffix) /** * Creates an expression that checks if a string expression ends with a given [suffix]. @@ -1683,7 +1668,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun endsWith(stringExpr: Expr, suffix: String): BooleanExpr = - BooleanExpr("ends_with", evaluateNotImplemented, stringExpr, suffix) + BooleanExpr("ends_with", evaluateEndsWith, stringExpr, suffix) /** * Creates an expression that checks if a string expression ends with a given [suffix]. @@ -1694,7 +1679,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun endsWith(fieldName: String, suffix: Expr): BooleanExpr = - BooleanExpr("ends_with", evaluateNotImplemented, fieldName, suffix) + BooleanExpr("ends_with", evaluateEndsWith, fieldName, suffix) /** * Creates an expression that checks if a string expression ends with a given [suffix]. @@ -1705,7 +1690,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun endsWith(fieldName: String, suffix: String): BooleanExpr = - BooleanExpr("ends_with", evaluateNotImplemented, fieldName, suffix) + BooleanExpr("ends_with", evaluateEndsWith, fieldName, suffix) /** * Creates an expression that converts a string expression to lowercase. @@ -1715,7 +1700,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun toLower(stringExpression: Expr): Expr = - FunctionExpr("to_lower", evaluateNotImplemented, stringExpression) + FunctionExpr("to_lowercase", evaluateToLowercase, stringExpression) /** * Creates an expression that converts a string field to lowercase. @@ -1725,7 +1710,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun toLower(fieldName: String): Expr = - FunctionExpr("to_lower", evaluateNotImplemented, fieldName) + FunctionExpr("to_lowercase", evaluateToLowercase, fieldName) /** * Creates an expression that converts a string expression to uppercase. @@ -1735,7 +1720,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun toUpper(stringExpression: Expr): Expr = - FunctionExpr("to_upper", evaluateNotImplemented, stringExpression) + FunctionExpr("to_uppercase", evaluateToUppercase, stringExpression) /** * Creates an expression that converts a string field to uppercase. @@ -1745,7 +1730,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun toUpper(fieldName: String): Expr = - FunctionExpr("to_upper", evaluateNotImplemented, fieldName) + FunctionExpr("to_uppercase", evaluateToUppercase, fieldName) /** * Creates an expression that removes leading and trailing whitespace from a string expression. @@ -1754,8 +1739,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the trimmed string. */ @JvmStatic - fun trim(stringExpression: Expr): Expr = - FunctionExpr("trim", evaluateNotImplemented, stringExpression) + fun trim(stringExpression: Expr): Expr = FunctionExpr("trim", evaluateTrim, stringExpression) /** * Creates an expression that removes leading and trailing whitespace from a string field. @@ -1763,8 +1747,7 @@ abstract class Expr internal constructor() { * @param fieldName The name of the field containing the string to trim. * @return A new [Expr] representing the trimmed string. */ - @JvmStatic - fun trim(fieldName: String): Expr = FunctionExpr("trim", evaluateNotImplemented, fieldName) + @JvmStatic fun trim(fieldName: String): Expr = FunctionExpr("trim", evaluateTrim, fieldName) /** * Creates an expression that concatenates string expressions together. @@ -1775,7 +1758,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strConcat(firstString: Expr, vararg otherStrings: Expr): Expr = - FunctionExpr("str_concat", evaluateNotImplemented, firstString, *otherStrings) + FunctionExpr("str_concat", evaluateStrConcat, firstString, *otherStrings) /** * Creates an expression that concatenates string expressions together. @@ -1787,7 +1770,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strConcat(firstString: Expr, vararg otherStrings: Any): Expr = - FunctionExpr("str_concat", evaluateNotImplemented, firstString, *otherStrings) + FunctionExpr("str_concat", evaluateStrConcat, firstString, *otherStrings) /** * Creates an expression that concatenates string expressions together. @@ -1798,7 +1781,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strConcat(fieldName: String, vararg otherStrings: Expr): Expr = - FunctionExpr("str_concat", evaluateNotImplemented, fieldName, *otherStrings) + FunctionExpr("str_concat", evaluateStrConcat, fieldName, *otherStrings) /** * Creates an expression that concatenates string expressions together. @@ -1810,10 +1793,10 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strConcat(fieldName: String, vararg otherStrings: Any): Expr = - FunctionExpr("str_concat", evaluateNotImplemented, fieldName, *otherStrings) + FunctionExpr("str_concat", evaluateStrConcat, fieldName, *otherStrings) internal fun map(elements: Array): Expr = - FunctionExpr("map", evaluateNotImplemented, elements) + FunctionExpr("map", notImplemented, elements) /** * Creates an expression that creates a Firestore map value from an input object. @@ -1834,7 +1817,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapGet(mapExpression: Expr, key: String): Expr = - FunctionExpr("map_get", evaluateNotImplemented, mapExpression, key) + FunctionExpr("map_get", notImplemented, mapExpression, key) /** * Accesses a value from a map (object) field using the provided [key]. @@ -1845,7 +1828,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapGet(fieldName: String, key: String): Expr = - FunctionExpr("map_get", evaluateNotImplemented, fieldName, key) + FunctionExpr("map_get", notImplemented, fieldName, key) /** * Creates an expression that merges multiple maps into a single map. If multiple maps have the @@ -1858,7 +1841,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapMerge(firstMap: Expr, secondMap: Expr, vararg otherMaps: Expr): Expr = - FunctionExpr("map_merge", evaluateNotImplemented, firstMap, secondMap, *otherMaps) + FunctionExpr("map_merge", notImplemented, firstMap, secondMap, *otherMaps) /** * Creates an expression that merges multiple maps into a single map. If multiple maps have the @@ -1871,7 +1854,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapMerge(firstMapFieldName: String, secondMap: Expr, vararg otherMaps: Expr): Expr = - FunctionExpr("map_merge", evaluateNotImplemented, firstMapFieldName, secondMap, *otherMaps) + FunctionExpr("map_merge", notImplemented, firstMapFieldName, secondMap, *otherMaps) /** * Creates an expression that removes a key from the map produced by evaluating an expression. @@ -1882,7 +1865,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapRemove(mapExpr: Expr, key: Expr): Expr = - FunctionExpr("map_remove", evaluateNotImplemented, mapExpr, key) + FunctionExpr("map_remove", notImplemented, mapExpr, key) /** * Creates an expression that removes a key from the map produced by evaluating an expression. @@ -1893,7 +1876,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapRemove(mapField: String, key: Expr): Expr = - FunctionExpr("map_remove", evaluateNotImplemented, mapField, key) + FunctionExpr("map_remove", notImplemented, mapField, key) /** * Creates an expression that removes a key from the map produced by evaluating an expression. @@ -1904,7 +1887,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapRemove(mapExpr: Expr, key: String): Expr = - FunctionExpr("map_remove", evaluateNotImplemented, mapExpr, key) + FunctionExpr("map_remove", notImplemented, mapExpr, key) /** * Creates an expression that removes a key from the map produced by evaluating an expression. @@ -1915,7 +1898,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapRemove(mapField: String, key: String): Expr = - FunctionExpr("map_remove", evaluateNotImplemented, mapField, key) + FunctionExpr("map_remove", notImplemented, mapField, key) /** * Calculates the Cosine distance between two vector expressions. @@ -1926,7 +1909,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: Expr): Expr = - FunctionExpr("cosine_distance", evaluateNotImplemented, vector1, vector2) + FunctionExpr("cosine_distance", notImplemented, vector1, vector2) /** * Calculates the Cosine distance between vector expression and a vector literal. @@ -1937,7 +1920,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: DoubleArray): Expr = - FunctionExpr("cosine_distance", evaluateNotImplemented, vector1, vector(vector2)) + FunctionExpr("cosine_distance", notImplemented, vector1, vector(vector2)) /** * Calculates the Cosine distance between vector expression and a vector literal. @@ -1948,7 +1931,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vector1: Expr, vector2: VectorValue): Expr = - FunctionExpr("cosine_distance", evaluateNotImplemented, vector1, vector2) + FunctionExpr("cosine_distance", notImplemented, vector1, vector2) /** * Calculates the Cosine distance between a vector field and a vector expression. @@ -1959,7 +1942,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vectorFieldName: String, vector: Expr): Expr = - FunctionExpr("cosine_distance", evaluateNotImplemented, vectorFieldName, vector) + FunctionExpr("cosine_distance", notImplemented, vectorFieldName, vector) /** * Calculates the Cosine distance between a vector field and a vector literal. @@ -1970,7 +1953,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vectorFieldName: String, vector: DoubleArray): Expr = - FunctionExpr("cosine_distance", evaluateNotImplemented, vectorFieldName, vector(vector)) + FunctionExpr("cosine_distance", notImplemented, vectorFieldName, vector(vector)) /** * Calculates the Cosine distance between a vector field and a vector literal. @@ -1981,7 +1964,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cosineDistance(vectorFieldName: String, vector: VectorValue): Expr = - FunctionExpr("cosine_distance", evaluateNotImplemented, vectorFieldName, vector) + FunctionExpr("cosine_distance", notImplemented, vectorFieldName, vector) /** * Calculates the dot product distance between two vector expressions. @@ -1992,7 +1975,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vector1: Expr, vector2: Expr): Expr = - FunctionExpr("dot_product", evaluateNotImplemented, vector1, vector2) + FunctionExpr("dot_product", notImplemented, vector1, vector2) /** * Calculates the dot product distance between vector expression and a vector literal. @@ -2003,7 +1986,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vector1: Expr, vector2: DoubleArray): Expr = - FunctionExpr("dot_product", evaluateNotImplemented, vector1, vector(vector2)) + FunctionExpr("dot_product", notImplemented, vector1, vector(vector2)) /** * Calculates the dot product distance between vector expression and a vector literal. @@ -2014,7 +1997,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vector1: Expr, vector2: VectorValue): Expr = - FunctionExpr("dot_product", evaluateNotImplemented, vector1, vector2) + FunctionExpr("dot_product", notImplemented, vector1, vector2) /** * Calculates the dot product distance between a vector field and a vector expression. @@ -2025,7 +2008,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vectorFieldName: String, vector: Expr): Expr = - FunctionExpr("dot_product", evaluateNotImplemented, vectorFieldName, vector) + FunctionExpr("dot_product", notImplemented, vectorFieldName, vector) /** * Calculates the dot product distance between vector field and a vector literal. @@ -2036,7 +2019,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vectorFieldName: String, vector: DoubleArray): Expr = - FunctionExpr("dot_product", evaluateNotImplemented, vectorFieldName, vector(vector)) + FunctionExpr("dot_product", notImplemented, vectorFieldName, vector(vector)) /** * Calculates the dot product distance between a vector field and a vector literal. @@ -2047,7 +2030,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun dotProduct(vectorFieldName: String, vector: VectorValue): Expr = - FunctionExpr("dot_product", evaluateNotImplemented, vectorFieldName, vector) + FunctionExpr("dot_product", notImplemented, vectorFieldName, vector) /** * Calculates the Euclidean distance between two vector expressions. @@ -2058,7 +2041,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: Expr): Expr = - FunctionExpr("euclidean_distance", evaluateNotImplemented, vector1, vector2) + FunctionExpr("euclidean_distance", notImplemented, vector1, vector2) /** * Calculates the Euclidean distance between vector expression and a vector literal. @@ -2069,7 +2052,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: DoubleArray): Expr = - FunctionExpr("euclidean_distance", evaluateNotImplemented, vector1, vector(vector2)) + FunctionExpr("euclidean_distance", notImplemented, vector1, vector(vector2)) /** * Calculates the Euclidean distance between vector expression and a vector literal. @@ -2080,7 +2063,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vector1: Expr, vector2: VectorValue): Expr = - FunctionExpr("euclidean_distance", evaluateNotImplemented, vector1, vector2) + FunctionExpr("euclidean_distance", notImplemented, vector1, vector2) /** * Calculates the Euclidean distance between a vector field and a vector expression. @@ -2091,7 +2074,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vectorFieldName: String, vector: Expr): Expr = - FunctionExpr("euclidean_distance", evaluateNotImplemented, vectorFieldName, vector) + FunctionExpr("euclidean_distance", notImplemented, vectorFieldName, vector) /** * Calculates the Euclidean distance between a vector field and a vector literal. @@ -2102,7 +2085,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vectorFieldName: String, vector: DoubleArray): Expr = - FunctionExpr("euclidean_distance", evaluateNotImplemented, vectorFieldName, vector(vector)) + FunctionExpr("euclidean_distance", notImplemented, vectorFieldName, vector(vector)) /** * Calculates the Euclidean distance between a vector field and a vector literal. @@ -2113,7 +2096,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun euclideanDistance(vectorFieldName: String, vector: VectorValue): Expr = - FunctionExpr("euclidean_distance", evaluateNotImplemented, vectorFieldName, vector) + FunctionExpr("euclidean_distance", notImplemented, vectorFieldName, vector) /** * Creates an expression that calculates the length (dimension) of a Firestore Vector. @@ -2123,7 +2106,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun vectorLength(vectorExpression: Expr): Expr = - FunctionExpr("vector_length", evaluateNotImplemented, vectorExpression) + FunctionExpr("vector_length", notImplemented, vectorExpression) /** * Creates an expression that calculates the length (dimension) of a Firestore Vector. @@ -2133,7 +2116,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun vectorLength(fieldName: String): Expr = - FunctionExpr("vector_length", evaluateNotImplemented, fieldName) + FunctionExpr("vector_length", notImplemented, fieldName) /** * Creates an expression that interprets an expression as the number of microseconds since the @@ -2144,7 +2127,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixMicrosToTimestamp(expr: Expr): Expr = - FunctionExpr("unix_micros_to_timestamp", evaluateNotImplemented, expr) + FunctionExpr("unix_micros_to_timestamp", evaluateUnixMicrosToTimestamp, expr) /** * Creates an expression that interprets a field's value as the number of microseconds since the @@ -2155,7 +2138,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixMicrosToTimestamp(fieldName: String): Expr = - FunctionExpr("unix_micros_to_timestamp", evaluateNotImplemented, fieldName) + FunctionExpr("unix_micros_to_timestamp", evaluateUnixMicrosToTimestamp, fieldName) /** * Creates an expression that converts a timestamp expression to the number of microseconds @@ -2166,7 +2149,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixMicros(expr: Expr): Expr = - FunctionExpr("timestamp_to_unix_micros", evaluateNotImplemented, expr) + FunctionExpr("timestamp_to_unix_micros", evaluateTimestampToUnixMicros, expr) /** * Creates an expression that converts a timestamp field to the number of microseconds since the @@ -2177,7 +2160,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixMicros(fieldName: String): Expr = - FunctionExpr("timestamp_to_unix_micros", evaluateNotImplemented, fieldName) + FunctionExpr("timestamp_to_unix_micros", evaluateTimestampToUnixMicros, fieldName) /** * Creates an expression that interprets an expression as the number of milliseconds since the @@ -2188,7 +2171,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixMillisToTimestamp(expr: Expr): Expr = - FunctionExpr("unix_millis_to_timestamp", evaluateNotImplemented, expr) + FunctionExpr("unix_millis_to_timestamp", evaluateUnixMillisToTimestamp, expr) /** * Creates an expression that interprets a field's value as the number of milliseconds since the @@ -2199,7 +2182,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixMillisToTimestamp(fieldName: String): Expr = - FunctionExpr("unix_millis_to_timestamp", evaluateNotImplemented, fieldName) + FunctionExpr("unix_millis_to_timestamp", evaluateUnixMillisToTimestamp, fieldName) /** * Creates an expression that converts a timestamp expression to the number of milliseconds @@ -2210,7 +2193,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixMillis(expr: Expr): Expr = - FunctionExpr("timestamp_to_unix_millis", evaluateNotImplemented, expr) + FunctionExpr("timestamp_to_unix_millis", evaluateTimestampToUnixMillis, expr) /** * Creates an expression that converts a timestamp field to the number of milliseconds since the @@ -2221,7 +2204,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixMillis(fieldName: String): Expr = - FunctionExpr("timestamp_to_unix_millis", evaluateNotImplemented, fieldName) + FunctionExpr("timestamp_to_unix_millis", evaluateTimestampToUnixMillis, fieldName) /** * Creates an expression that interprets an expression as the number of seconds since the Unix @@ -2232,7 +2215,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixSecondsToTimestamp(expr: Expr): Expr = - FunctionExpr("unix_seconds_to_timestamp", evaluateNotImplemented, expr) + FunctionExpr("unix_seconds_to_timestamp", evaluateUnixSecondsToTimestamp, expr) /** * Creates an expression that interprets a field's value as the number of seconds since the Unix @@ -2243,7 +2226,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun unixSecondsToTimestamp(fieldName: String): Expr = - FunctionExpr("unix_seconds_to_timestamp", evaluateNotImplemented, fieldName) + FunctionExpr("unix_seconds_to_timestamp", evaluateUnixSecondsToTimestamp, fieldName) /** * Creates an expression that converts a timestamp expression to the number of seconds since the @@ -2254,7 +2237,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixSeconds(expr: Expr): Expr = - FunctionExpr("timestamp_to_unix_seconds", evaluateNotImplemented, expr) + FunctionExpr("timestamp_to_unix_seconds", evaluateTimestampToUnixSeconds, expr) /** * Creates an expression that converts a timestamp field to the number of seconds since the Unix @@ -2265,7 +2248,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampToUnixSeconds(fieldName: String): Expr = - FunctionExpr("timestamp_to_unix_seconds", evaluateNotImplemented, fieldName) + FunctionExpr("timestamp_to_unix_seconds", evaluateTimestampToUnixSeconds, fieldName) /** * Creates an expression that adds a specified amount of time to a timestamp. @@ -2278,7 +2261,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: Expr, amount: Expr): Expr = - FunctionExpr("timestamp_add", evaluateNotImplemented, timestamp, unit, amount) + FunctionExpr("timestamp_add", evaluateTimestampAdd, timestamp, unit, amount) /** * Creates an expression that adds a specified amount of time to a timestamp. @@ -2291,7 +2274,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampAdd(timestamp: Expr, unit: String, amount: Double): Expr = - FunctionExpr("timestamp_add", evaluateNotImplemented, timestamp, unit, amount) + FunctionExpr("timestamp_add", evaluateTimestampAdd, timestamp, unit, amount) /** * Creates an expression that adds a specified amount of time to a timestamp. @@ -2304,7 +2287,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampAdd(fieldName: String, unit: Expr, amount: Expr): Expr = - FunctionExpr("timestamp_add", evaluateNotImplemented, fieldName, unit, amount) + FunctionExpr("timestamp_add", evaluateTimestampAdd, fieldName, unit, amount) /** * Creates an expression that adds a specified amount of time to a timestamp. @@ -2317,7 +2300,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampAdd(fieldName: String, unit: String, amount: Double): Expr = - FunctionExpr("timestamp_add", evaluateNotImplemented, fieldName, unit, amount) + FunctionExpr("timestamp_add", evaluateTimestampAdd, fieldName, unit, amount) /** * Creates an expression that subtracts a specified amount of time to a timestamp. @@ -2330,7 +2313,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampSub(timestamp: Expr, unit: Expr, amount: Expr): Expr = - FunctionExpr("timestamp_sub", evaluateNotImplemented, timestamp, unit, amount) + FunctionExpr("timestamp_sub", evaluateTimestampSub, timestamp, unit, amount) /** * Creates an expression that subtracts a specified amount of time to a timestamp. @@ -2343,7 +2326,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampSub(timestamp: Expr, unit: String, amount: Double): Expr = - FunctionExpr("timestamp_sub", evaluateNotImplemented, timestamp, unit, amount) + FunctionExpr("timestamp_sub", evaluateTimestampSub, timestamp, unit, amount) /** * Creates an expression that subtracts a specified amount of time to a timestamp. @@ -2356,7 +2339,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampSub(fieldName: String, unit: Expr, amount: Expr): Expr = - FunctionExpr("timestamp_sub", evaluateNotImplemented, fieldName, unit, amount) + FunctionExpr("timestamp_sub", evaluateTimestampSub, fieldName, unit, amount) /** * Creates an expression that subtracts a specified amount of time to a timestamp. @@ -2369,7 +2352,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun timestampSub(fieldName: String, unit: String, amount: Double): Expr = - FunctionExpr("timestamp_sub", evaluateNotImplemented, fieldName, unit, amount) + FunctionExpr("timestamp_sub", evaluateTimestampSub, fieldName, unit, amount) /** * Creates an expression that checks if two expressions are equal. @@ -2421,8 +2404,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the inequality comparison. */ @JvmStatic - fun neq(left: Expr, right: Expr): BooleanExpr = - BooleanExpr("neq", evaluateNotImplemented, left, right) + fun neq(left: Expr, right: Expr): BooleanExpr = BooleanExpr("neq", evaluateNeq, left, right) /** * Creates an expression that checks if an expression is not equal to a value. @@ -2432,8 +2414,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the inequality comparison. */ @JvmStatic - fun neq(left: Expr, right: Any): BooleanExpr = - BooleanExpr("neq", evaluateNotImplemented, left, right) + fun neq(left: Expr, right: Any): BooleanExpr = BooleanExpr("neq", evaluateNeq, left, right) /** * Creates an expression that checks if a field's value is not equal to an expression. @@ -2444,7 +2425,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun neq(fieldName: String, expression: Expr): BooleanExpr = - BooleanExpr("neq", evaluateNotImplemented, fieldName, expression) + BooleanExpr("neq", evaluateNeq, fieldName, expression) /** * Creates an expression that checks if a field's value is not equal to another value. @@ -2455,7 +2436,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun neq(fieldName: String, value: Any): BooleanExpr = - BooleanExpr("neq", evaluateNotImplemented, fieldName, value) + BooleanExpr("neq", evaluateNeq, fieldName, value) /** * Creates an expression that checks if the first expression is greater than the second @@ -2466,8 +2447,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the greater than comparison. */ @JvmStatic - fun gt(left: Expr, right: Expr): BooleanExpr = - BooleanExpr("gt", evaluateNotImplemented, left, right) + fun gt(left: Expr, right: Expr): BooleanExpr = BooleanExpr("gt", evaluateGt, left, right) /** * Creates an expression that checks if an expression is greater than a value. @@ -2477,8 +2457,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the greater than comparison. */ @JvmStatic - fun gt(left: Expr, right: Any): BooleanExpr = - BooleanExpr("gt", evaluateNotImplemented, left, right) + fun gt(left: Expr, right: Any): BooleanExpr = BooleanExpr("gt", evaluateGt, left, right) /** * Creates an expression that checks if a field's value is greater than an expression. @@ -2489,7 +2468,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun gt(fieldName: String, expression: Expr): BooleanExpr = - BooleanExpr("gt", evaluateNotImplemented, fieldName, expression) + BooleanExpr("gt", evaluateGt, fieldName, expression) /** * Creates an expression that checks if a field's value is greater than another value. @@ -2500,7 +2479,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun gt(fieldName: String, value: Any): BooleanExpr = - BooleanExpr("gt", evaluateNotImplemented, fieldName, value) + BooleanExpr("gt", evaluateGt, fieldName, value) /** * Creates an expression that checks if the first expression is greater than or equal to the @@ -2511,8 +2490,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the greater than or equal to comparison. */ @JvmStatic - fun gte(left: Expr, right: Expr): BooleanExpr = - BooleanExpr("gte", evaluateNotImplemented, left, right) + fun gte(left: Expr, right: Expr): BooleanExpr = BooleanExpr("gte", evaluateGte, left, right) /** * Creates an expression that checks if an expression is greater than or equal to a value. @@ -2522,8 +2500,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the greater than or equal to comparison. */ @JvmStatic - fun gte(left: Expr, right: Any): BooleanExpr = - BooleanExpr("gte", evaluateNotImplemented, left, right) + fun gte(left: Expr, right: Any): BooleanExpr = BooleanExpr("gte", evaluateGte, left, right) /** * Creates an expression that checks if a field's value is greater than or equal to an @@ -2535,7 +2512,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun gte(fieldName: String, expression: Expr): BooleanExpr = - BooleanExpr("gte", evaluateNotImplemented, fieldName, expression) + BooleanExpr("gte", evaluateGte, fieldName, expression) /** * Creates an expression that checks if a field's value is greater than or equal to another @@ -2547,7 +2524,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun gte(fieldName: String, value: Any): BooleanExpr = - BooleanExpr("gte", evaluateNotImplemented, fieldName, value) + BooleanExpr("gte", evaluateGte, fieldName, value) /** * Creates an expression that checks if the first expression is less than the second expression. @@ -2557,8 +2534,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the less than comparison. */ @JvmStatic - fun lt(left: Expr, right: Expr): BooleanExpr = - BooleanExpr("lt", evaluateNotImplemented, left, right) + fun lt(left: Expr, right: Expr): BooleanExpr = BooleanExpr("lt", evaluateLt, left, right) /** * Creates an expression that checks if an expression is less than a value. @@ -2568,8 +2544,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the less than comparison. */ @JvmStatic - fun lt(left: Expr, right: Any): BooleanExpr = - BooleanExpr("lt", evaluateNotImplemented, left, right) + fun lt(left: Expr, right: Any): BooleanExpr = BooleanExpr("lt", evaluateLt, left, right) /** * Creates an expression that checks if a field's value is less than an expression. @@ -2580,7 +2555,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun lt(fieldName: String, expression: Expr): BooleanExpr = - BooleanExpr("lt", evaluateNotImplemented, fieldName, expression) + BooleanExpr("lt", evaluateLt, fieldName, expression) /** * Creates an expression that checks if a field's value is less than another value. @@ -2590,8 +2565,8 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the less than comparison. */ @JvmStatic - fun lt(fieldName: String, right: Any): BooleanExpr = - BooleanExpr("lt", evaluateNotImplemented, fieldName, right) + fun lt(fieldName: String, value: Any): BooleanExpr = + BooleanExpr("lt", evaluateLt, fieldName, value) /** * Creates an expression that checks if the first expression is less than or equal to the second @@ -2602,8 +2577,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the less than or equal to comparison. */ @JvmStatic - fun lte(left: Expr, right: Expr): BooleanExpr = - BooleanExpr("lte", evaluateNotImplemented, left, right) + fun lte(left: Expr, right: Expr): BooleanExpr = BooleanExpr("lte", evaluateLte, left, right) /** * Creates an expression that checks if an expression is less than or equal to a value. @@ -2613,8 +2587,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the less than or equal to comparison. */ @JvmStatic - fun lte(left: Expr, right: Any): BooleanExpr = - BooleanExpr("lte", evaluateNotImplemented, left, right) + fun lte(left: Expr, right: Any): BooleanExpr = BooleanExpr("lte", evaluateLte, left, right) /** * Creates an expression that checks if a field's value is less than or equal to an expression. @@ -2625,7 +2598,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun lte(fieldName: String, expression: Expr): BooleanExpr = - BooleanExpr("lte", evaluateNotImplemented, fieldName, expression) + BooleanExpr("lte", evaluateLte, fieldName, expression) /** * Creates an expression that checks if a field's value is less than or equal to another value. @@ -2636,7 +2609,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun lte(fieldName: String, value: Any): BooleanExpr = - BooleanExpr("lte", evaluateNotImplemented, fieldName, value) + BooleanExpr("lte", evaluateLte, fieldName, value) /** * Creates an expression that concatenates an array with other arrays. @@ -2648,7 +2621,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayConcat(firstArray: Expr, secondArray: Expr, vararg otherArrays: Any): Expr = - FunctionExpr("array_concat", evaluateNotImplemented, firstArray, secondArray, *otherArrays) + FunctionExpr("array_concat", notImplemented, firstArray, secondArray, *otherArrays) /** * Creates an expression that concatenates an array with other arrays. @@ -2660,7 +2633,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayConcat(firstArray: Expr, secondArray: Any, vararg otherArrays: Any): Expr = - FunctionExpr("array_concat", evaluateNotImplemented, firstArray, secondArray, *otherArrays) + FunctionExpr("array_concat", notImplemented, firstArray, secondArray, *otherArrays) /** * Creates an expression that concatenates a field's array value with other arrays. @@ -2672,13 +2645,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayConcat(firstArrayField: String, secondArray: Expr, vararg otherArrays: Any): Expr = - FunctionExpr( - "array_concat", - evaluateNotImplemented, - firstArrayField, - secondArray, - *otherArrays - ) + FunctionExpr("array_concat", notImplemented, firstArrayField, secondArray, *otherArrays) /** * Creates an expression that concatenates a field's array value with other arrays. @@ -2690,13 +2657,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayConcat(firstArrayField: String, secondArray: Any, vararg otherArrays: Any): Expr = - FunctionExpr( - "array_concat", - evaluateNotImplemented, - firstArrayField, - secondArray, - *otherArrays - ) + FunctionExpr("array_concat", notImplemented, firstArrayField, secondArray, *otherArrays) /** * Reverses the order of elements in the [array]. @@ -2705,8 +2666,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the arrayReverse operation. */ @JvmStatic - fun arrayReverse(array: Expr): Expr = - FunctionExpr("array_reverse", evaluateNotImplemented, array) + fun arrayReverse(array: Expr): Expr = FunctionExpr("array_reverse", notImplemented, array) /** * Reverses the order of elements in the array field. @@ -2716,7 +2676,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayReverse(arrayFieldName: String): Expr = - FunctionExpr("array_reverse", evaluateNotImplemented, arrayFieldName) + FunctionExpr("array_reverse", notImplemented, arrayFieldName) /** * Creates an expression that checks if the array contains a specific [element]. @@ -2727,7 +2687,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContains(array: Expr, element: Expr): BooleanExpr = - BooleanExpr("array_contains", evaluateNotImplemented, array, element) + BooleanExpr("array_contains", evaluateArrayContains, array, element) /** * Creates an expression that checks if the array field contains a specific [element]. @@ -2738,7 +2698,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContains(arrayFieldName: String, element: Expr) = - BooleanExpr("array_contains", evaluateNotImplemented, arrayFieldName, element) + BooleanExpr("array_contains", evaluateArrayContains, arrayFieldName, element) /** * Creates an expression that checks if the [array] contains a specific [element]. @@ -2749,7 +2709,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContains(array: Expr, element: Any): BooleanExpr = - BooleanExpr("array_contains", evaluateNotImplemented, array, element) + BooleanExpr("array_contains", evaluateArrayContains, array, element) /** * Creates an expression that checks if the array field contains a specific [element]. @@ -2760,7 +2720,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContains(arrayFieldName: String, element: Any) = - BooleanExpr("array_contains", evaluateNotImplemented, arrayFieldName, element) + BooleanExpr("array_contains", evaluateArrayContains, arrayFieldName, element) /** * Creates an expression that checks if [array] contains all the specified [values]. @@ -2782,7 +2742,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAll(array: Expr, arrayExpression: Expr) = - BooleanExpr("array_contains_all", evaluateNotImplemented, array, arrayExpression) + BooleanExpr("array_contains_all", notImplemented, array, arrayExpression) /** * Creates an expression that checks if array field contains all the specified [values]. @@ -2795,7 +2755,7 @@ abstract class Expr internal constructor() { fun arrayContainsAll(arrayFieldName: String, values: List) = BooleanExpr( "array_contains_all", - evaluateNotImplemented, + notImplemented, arrayFieldName, ListOfExprs(toArrayOfExprOrConstant(values)) ) @@ -2809,7 +2769,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAll(arrayFieldName: String, arrayExpression: Expr) = - BooleanExpr("array_contains_all", evaluateNotImplemented, arrayFieldName, arrayExpression) + BooleanExpr("array_contains_all", notImplemented, arrayFieldName, arrayExpression) /** * Creates an expression that checks if [array] contains any of the specified [values]. @@ -2822,7 +2782,7 @@ abstract class Expr internal constructor() { fun arrayContainsAny(array: Expr, values: List) = BooleanExpr( "array_contains_any", - evaluateNotImplemented, + evaluateArrayContainsAny, array, ListOfExprs(toArrayOfExprOrConstant(values)) ) @@ -2836,7 +2796,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAny(array: Expr, arrayExpression: Expr) = - BooleanExpr("array_contains_any", evaluateNotImplemented, array, arrayExpression) + BooleanExpr("array_contains_any", evaluateArrayContainsAny, array, arrayExpression) /** * Creates an expression that checks if array field contains any of the specified [values]. @@ -2849,7 +2809,7 @@ abstract class Expr internal constructor() { fun arrayContainsAny(arrayFieldName: String, values: List) = BooleanExpr( "array_contains_any", - evaluateNotImplemented, + evaluateArrayContainsAny, arrayFieldName, ListOfExprs(toArrayOfExprOrConstant(values)) ) @@ -2863,7 +2823,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAny(arrayFieldName: String, arrayExpression: Expr) = - BooleanExpr("array_contains_any", evaluateNotImplemented, arrayFieldName, arrayExpression) + BooleanExpr("array_contains_any", evaluateArrayContainsAny, arrayFieldName, arrayExpression) /** * Creates an expression that calculates the length of an [array] expression. @@ -2872,7 +2832,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the length of the array. */ @JvmStatic - fun arrayLength(array: Expr): Expr = FunctionExpr("array_length", evaluateNotImplemented, array) + fun arrayLength(array: Expr): Expr = FunctionExpr("array_length", evaluateArrayLength, array) /** * Creates an expression that calculates the length of an array field. @@ -2882,7 +2842,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayLength(arrayFieldName: String): Expr = - FunctionExpr("array_length", evaluateNotImplemented, arrayFieldName) + FunctionExpr("array_length", evaluateArrayLength, arrayFieldName) /** * Creates an expression that indexes into an array from the beginning or end and return the @@ -2895,7 +2855,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayOffset(array: Expr, offset: Expr): Expr = - FunctionExpr("array_offset", evaluateNotImplemented, array, offset) + FunctionExpr("array_offset", notImplemented, array, offset) /** * Creates an expression that indexes into an array from the beginning or end and return the @@ -2908,7 +2868,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayOffset(array: Expr, offset: Int): Expr = - FunctionExpr("array_offset", evaluateNotImplemented, array, constant(offset)) + FunctionExpr("array_offset", notImplemented, array, constant(offset)) /** * Creates an expression that indexes into an array from the beginning or end and return the @@ -2921,7 +2881,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayOffset(arrayFieldName: String, offset: Expr): Expr = - FunctionExpr("array_offset", evaluateNotImplemented, arrayFieldName, offset) + FunctionExpr("array_offset", notImplemented, arrayFieldName, offset) /** * Creates an expression that indexes into an array from the beginning or end and return the @@ -2934,7 +2894,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayOffset(arrayFieldName: String, offset: Int): Expr = - FunctionExpr("array_offset", evaluateNotImplemented, arrayFieldName, constant(offset)) + FunctionExpr("array_offset", notImplemented, arrayFieldName, constant(offset)) /** * Creates a conditional expression that evaluates to a [thenExpr] expression if a condition is @@ -2947,7 +2907,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cond(condition: BooleanExpr, thenExpr: Expr, elseExpr: Expr): Expr = - FunctionExpr("cond", evaluateNotImplemented, condition, thenExpr, elseExpr) + FunctionExpr("cond", notImplemented, condition, thenExpr, elseExpr) /** * Creates a conditional expression that evaluates to a [thenValue] if a condition is true or an @@ -2960,7 +2920,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cond(condition: BooleanExpr, thenValue: Any, elseValue: Any): Expr = - FunctionExpr("cond", evaluateNotImplemented, condition, thenValue, elseValue) + FunctionExpr("cond", notImplemented, condition, thenValue, elseValue) /** * Creates an expression that checks if a field exists. @@ -2968,8 +2928,7 @@ abstract class Expr internal constructor() { * @param value An expression evaluates to the name of the field to check. * @return A new [Expr] representing the exists check. */ - @JvmStatic - fun exists(value: Expr): BooleanExpr = BooleanExpr("exists", evaluateNotImplemented, value) + @JvmStatic fun exists(value: Expr): BooleanExpr = BooleanExpr("exists", evaluateExists, value) /** * Creates an expression that checks if a field exists. @@ -2978,8 +2937,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the exists check. */ @JvmStatic - fun exists(fieldName: String): BooleanExpr = - BooleanExpr("exists", evaluateNotImplemented, fieldName) + fun exists(fieldName: String): BooleanExpr = BooleanExpr("exists", evaluateExists, fieldName) /** * Creates an expression that returns the [catchExpr] argument if there is an error, else return @@ -2992,7 +2950,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun ifError(tryExpr: Expr, catchExpr: Expr): Expr = - FunctionExpr("if_error", evaluateNotImplemented, tryExpr, catchExpr) + FunctionExpr("if_error", notImplemented, tryExpr, catchExpr) /** * Creates an expression that returns the [catchValue] argument if there is an error, else @@ -3004,7 +2962,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun ifError(tryExpr: Expr, catchValue: Any): Expr = - FunctionExpr("if_error", evaluateNotImplemented, tryExpr, catchValue) + FunctionExpr("if_error", notImplemented, tryExpr, catchValue) /** * Creates an expression that returns the document ID from a path. @@ -3014,7 +2972,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun documentId(documentPath: Expr): Expr = - FunctionExpr("document_id", evaluateNotImplemented, documentPath) + FunctionExpr("document_id", notImplemented, documentPath) /** * Creates an expression that returns the document ID from a path. @@ -4107,9 +4065,7 @@ abstract class Expr internal constructor() { internal abstract fun toProto(userDataReader: UserDataReader): Value - internal abstract fun evaluate( - context: EvaluationContext - ): (input: MutableDocument) -> EvaluateResult + internal abstract fun evaluateContext(context: EvaluationContext): EvaluateDocument } /** Expressions that have an alias are [Selectable] */ @@ -4135,7 +4091,7 @@ class ExprWithAlias internal constructor(private val alias: String, private val override fun getAlias() = alias override fun getExpr() = expr override fun toProto(userDataReader: UserDataReader): Value = expr.toProto(userDataReader) - override fun evaluate(context: EvaluationContext) = expr.evaluate(context) + override fun evaluateContext(context: EvaluationContext) = expr.evaluateContext(context) } /** @@ -4166,7 +4122,7 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select internal fun toProto(): Value = Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() - override fun evaluate(context: EvaluationContext) = ::evaluateInternal + override fun evaluateContext(context: EvaluationContext) = ::evaluateInternal private fun evaluateInternal(input: MutableDocument): EvaluateResult { val value: Value? = input.getField(fieldPath) @@ -4178,7 +4134,9 @@ internal class ListOfExprs(private val expressions: Array) : Expr() { override fun toProto(userDataReader: UserDataReader): Value = encodeValue(expressions.map { it.toProto(userDataReader) }) - override fun evaluate(context: EvaluationContext): (input: MutableDocument) -> EvaluateResult { + override fun evaluateContext( + context: EvaluationContext + ): (input: MutableDocument) -> EvaluateResult { TODO("Not yet implemented") } } @@ -4248,12 +4206,8 @@ internal constructor( return Value.newBuilder().setFunctionValue(builder).build() } - final override fun evaluate( - context: EvaluationContext - ): (input: MutableDocument) -> EvaluateResult { - val evaluateParams = params.map { it.evaluate(context) }.asSequence() - return { input -> function.evaluate(evaluateParams.map { it.invoke(input) }) } - } + final override fun evaluateContext(context: EvaluationContext): EvaluateDocument = + function(params.map { expr -> expr.evaluateContext(context) }) } /** A class that represents a filter condition. */ @@ -4295,7 +4249,7 @@ internal constructor(name: String, function: EvaluateFunction, params: Array ): Flow { - val conditionFunction = condition.evaluate(context) - return inputs.filter { input -> conditionFunction.invoke(input).value?.booleanValue ?: false } + val conditionFunction = condition.evaluateContext(context) + return inputs.filter { input -> conditionFunction(input).value?.booleanValue ?: false } } } From b4f04accde06db3d5e92f1d0941be451d5d7afab Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 22 May 2025 10:39:41 -0400 Subject: [PATCH 071/152] Fix add and multiply --- .../firestore/pipeline/expressions.kt | 82 ++++++++----------- 1 file changed, 33 insertions(+), 49 deletions(-) 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 47f1a974ec5..4b4d783ee89 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 @@ -747,52 +747,48 @@ abstract class Expr internal constructor() { @JvmStatic fun sqrt(numericField: String): Expr = FunctionExpr("sqrt", numericField) /** - * Creates an expression that adds numeric expressions and constants. + * Creates an expression that adds numeric expressions. * * @param first Numeric expression to add. * @param second Numeric expression to add. - * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ @JvmStatic - fun add(first: Expr, second: Expr, vararg others: Any): Expr = - FunctionExpr("add", first, second, *others) + fun add(first: Expr, second: Expr): Expr = + FunctionExpr("add", first, second) /** - * Creates an expression that adds numeric expressions and constants. + * Creates an expression that adds numeric expressions with a constant. * * @param first Numeric expression to add. * @param second Constant to add. - * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ @JvmStatic - fun add(first: Expr, second: Number, vararg others: Any): Expr = - FunctionExpr("add", first, second, *others) + fun add(first: Expr, second: Number): Expr = + FunctionExpr("add", first, second) /** - * Creates an expression that adds a numeric field with numeric expressions and constants. + * Creates an expression that adds a numeric field with a numeric expression. * * @param numericFieldName Numeric field to add. * @param second Numeric expression to add to field value. - * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ @JvmStatic - fun add(numericFieldName: String, second: Expr, vararg others: Any): Expr = - FunctionExpr("add", numericFieldName, second, *others) + fun add(numericFieldName: String, second: Expr): Expr = + FunctionExpr("add", numericFieldName, second) /** - * Creates an expression that adds a numeric field with numeric expressions and constants. + * Creates an expression that adds a numeric field with constant. * * @param numericFieldName Numeric field to add. * @param second Constant to add. - * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ @JvmStatic - fun add(numericFieldName: String, second: Number, vararg others: Any): Expr = - FunctionExpr("add", numericFieldName, second, *others) + fun add(numericFieldName: String, second: Number): Expr = + FunctionExpr("add", numericFieldName, second) /** * Creates an expression that subtracts two expressions. @@ -839,52 +835,48 @@ abstract class Expr internal constructor() { FunctionExpr("subtract", numericFieldName, subtrahend) /** - * Creates an expression that multiplies numeric expressions and constants. + * Creates an expression that multiplies numeric expressions. * * @param first Numeric expression to multiply. * @param second Numeric expression to multiply. - * @param others Additional numeric expressions or constants to multiply. * @return A new [Expr] representing the multiplication operation. */ @JvmStatic - fun multiply(first: Expr, second: Expr, vararg others: Any): Expr = - FunctionExpr("multiply", first, second, *others) + fun multiply(first: Expr, second: Expr): Expr = + FunctionExpr("multiply", first, second) /** - * Creates an expression that multiplies numeric expressions and constants. + * Creates an expression that multiplies numeric expressions with a constant. * * @param first Numeric expression to multiply. * @param second Constant to multiply. - * @param others Additional numeric expressions or constants to multiply. * @return A new [Expr] representing the multiplication operation. */ @JvmStatic - fun multiply(first: Expr, second: Number, vararg others: Any): Expr = - FunctionExpr("multiply", first, second, *others) + fun multiply(first: Expr, second: Number): Expr = + FunctionExpr("multiply", first, second) /** - * Creates an expression that multiplies a numeric field with numeric expressions and constants. + * Creates an expression that multiplies a numeric field with a numeric expression. * * @param numericFieldName Numeric field to multiply. - * @param second Numeric expression to add to field multiply. - * @param others Additional numeric expressions or constants to multiply. + * @param second Numeric expression to multiply. * @return A new [Expr] representing the multiplication operation. */ @JvmStatic - fun multiply(numericFieldName: String, second: Expr, vararg others: Any): Expr = - FunctionExpr("multiply", numericFieldName, second, *others) + fun multiply(numericFieldName: String, second: Expr): Expr = + FunctionExpr("multiply", numericFieldName, second) /** - * Creates an expression that multiplies a numeric field with numeric expressions and constants. + * Creates an expression that multiplies a numeric field with a constant. * * @param numericFieldName Numeric field to multiply. * @param second Constant to multiply. - * @param others Additional numeric expressions or constants to multiply. * @return A new [Expr] representing the multiplication operation. */ @JvmStatic - fun multiply(numericFieldName: String, second: Number, vararg others: Any): Expr = - FunctionExpr("multiply", numericFieldName, second, *others) + fun multiply(numericFieldName: String, second: Number): Expr = + FunctionExpr("multiply", numericFieldName, second) /** * Creates an expression that divides two numeric expressions. @@ -2990,24 +2982,20 @@ abstract class Expr internal constructor() { fun documentId(): Expr = Companion.documentId(this) /** - * Creates an expression that adds this numeric expression to other numeric expressions and - * constants. + * Creates an expression that adds this numeric expression to another numeric expression. * * @param second Numeric expression to add. - * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ - fun add(second: Expr, vararg others: Any): Expr = Companion.add(this, second, *others) + fun add(second: Expr): Expr = Companion.add(this, second) /** - * Creates an expression that adds this numeric expression to other numeric expressions and - * constants. + * Creates an expression that adds this numeric expression to a constants. * * @param second Constant to add. - * @param others Additional numeric expressions or constants to add. * @return A new [Expr] representing the addition operation. */ - fun add(second: Number, vararg others: Any): Expr = Companion.add(this, second, *others) + fun add(second: Number): Expr = Companion.add(this, second) /** * Creates an expression that subtracts a constant from this numeric expression. @@ -3026,24 +3014,20 @@ abstract class Expr internal constructor() { fun subtract(subtrahend: Number): Expr = Companion.subtract(this, subtrahend) /** - * Creates an expression that multiplies this numeric expression to other numeric expressions and - * constants. + * Creates an expression that multiplies this numeric expression with another numeric expression. * * @param second Numeric expression to multiply. - * @param others Additional numeric expressions or constants to multiply. * @return A new [Expr] representing the multiplication operation. */ - fun multiply(second: Expr, vararg others: Any): Expr = Companion.multiply(this, second, *others) + fun multiply(second: Expr): Expr = Companion.multiply(this, second) /** - * Creates an expression that multiplies this numeric expression to other numeric expressions and - * constants. + * Creates an expression that multiplies this numeric expression with a constant. * * @param second Constant to multiply. - * @param others Additional numeric expressions or constants to multiply. * @return A new [Expr] representing the multiplication operation. */ - fun multiply(second: Number, vararg others: Any): Expr = Companion.multiply(this, second, *others) + fun multiply(second: Number): Expr = Companion.multiply(this, second) /** * Creates an expression that divides this numeric expression by another numeric expression. From 9ea166bb33eeac0d05f24df436361965bf007456 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 22 May 2025 14:37:06 -0400 Subject: [PATCH 072/152] Timestamp expressions WIP --- .../google/firebase/firestore/model/Values.kt | 4 + .../firestore/pipeline/EvaluateResult.kt | 7 +- .../firebase/firestore/pipeline/evaluation.kt | 133 ++++++++++++++++-- .../firestore/pipeline/expressions.kt | 6 +- .../firebase/firestore/core/PipelineTests.kt | 33 +++++ 5 files changed, 162 insertions(+), 21 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index e216fa34e5e..8e7ef8be1f8 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -731,4 +731,8 @@ internal object Values { is VectorValue -> encodeValue(value) else -> throw IllegalArgumentException("Unexpected type: $value") } + + @JvmStatic + fun timestamp(seconds: Long, nanos: Int): Timestamp = + Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build() } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt index e1c9ea26bc0..84ee187cfd9 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt @@ -17,12 +17,11 @@ internal sealed class EvaluateResult(val value: Value?) { fun long(long: Long) = EvaluateResultValue(encodeValue(long)) fun long(int: Int) = EvaluateResultValue(encodeValue(int.toLong())) fun string(string: String) = EvaluateResultValue(encodeValue(string)) + fun timestamp(timestamp: Timestamp): EvaluateResult = + EvaluateResultValue(encodeValue(timestamp)) fun timestamp(seconds: Long, nanos: Int): EvaluateResult = if (seconds !in -62_135_596_800 until 253_402_300_800) EvaluateResultError - else - EvaluateResultValue( - encodeValue(Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build()) - ) + else timestamp(Values.timestamp(seconds, nanos)) } internal inline fun evaluateNonNull(f: (Value) -> EvaluateResult): EvaluateResult = if (value?.hasNullValue() == true) f(value) else this diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index 36796d18f53..0b6876659b5 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -3,6 +3,7 @@ package com.google.firebase.firestore.pipeline import com.google.common.math.LongMath import com.google.common.math.LongMath.checkedAdd import com.google.common.math.LongMath.checkedMultiply +import com.google.common.math.LongMath.checkedSubtract import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values @@ -273,9 +274,71 @@ internal val evaluateStrJoin = notImplemented // TODO: Does not exist in express // === Date / Timestamp Functions === -internal val evaluateTimestampAdd = notImplemented +private const val L_NANOS_PER_SECOND: Long = 1000_000_000 +private const val I_NANOS_PER_SECOND: Int = 1000_000_000 -internal val evaluateTimestampSub = notImplemented +private const val L_MICROS_PER_SECOND: Long = 1000_000 +private const val I_MICROS_PER_SECOND: Int = 1000_000 + +private const val L_MILLIS_PER_SECOND: Long = 1000 +private const val I_MILLIS_PER_SECOND: Int = 1000 + +internal fun plus(t: Timestamp, seconds: Long, nanos: Long): Timestamp = + if (nanos == 0L) { + plus(t, seconds) + } else { + val nanoSum = t.nanos + nanos // Overflow not possible since nanos is 0 to 1 000 000. + val secondsSum: Long = checkedAdd(checkedAdd(t.seconds, seconds), nanoSum / L_NANOS_PER_SECOND) + Values.timestamp(secondsSum, (nanoSum % I_NANOS_PER_SECOND).toInt()) + } + +private fun plus(t: Timestamp, seconds: Long): Timestamp = + if (seconds == 0L) t + else Values.timestamp(checkedAdd(t.seconds, seconds), t.nanos) + +internal fun minus(t: Timestamp, seconds: Long, nanos: Long): Timestamp = + if (nanos == 0L) { + minus(t, seconds) + } else { + val nanoSum = t.nanos - nanos // Overflow not possible since nanos is 0 to 1 000 000. + val secondsSum: Long = checkedSubtract(t.seconds, checkedSubtract(seconds, nanoSum / L_NANOS_PER_SECOND)) + Values.timestamp(secondsSum, (nanoSum % I_NANOS_PER_SECOND).toInt()) + } + +private fun minus(t: Timestamp, seconds: Long): Timestamp = + if (seconds == 0L) t + else Values.timestamp(checkedSubtract(t.seconds, seconds), t.nanos) + + +internal val evaluateTimestampAdd = + ternaryTimestampFunction { t: Timestamp, u: String, n: Long -> + EvaluateResult.timestamp( + when (u) { + "microsecond" -> plus(t, n / L_MICROS_PER_SECOND, (n % L_MICROS_PER_SECOND) * 1000) + "millisecond" -> plus(t, n / L_MILLIS_PER_SECOND, (n % L_MILLIS_PER_SECOND) * 1000_000) + "second" -> plus(t, n) + "minute" -> plus(t, checkedMultiply(n, 60)) + "hour" -> plus(t, checkedMultiply(n, 3600)) + "day" -> plus(t, checkedMultiply(n, 86400)) + else -> return@ternaryTimestampFunction EvaluateResultError + } + ) + } + +internal val evaluateTimestampSub = + ternaryTimestampFunction { t: Timestamp, u: String, n: Long -> + EvaluateResult.timestamp( + when (u) { + "microsecond" -> minus(t, n / L_MICROS_PER_SECOND, (n % L_MICROS_PER_SECOND) * 1000) + "millisecond" -> minus(t, n / L_MILLIS_PER_SECOND, (n % L_MILLIS_PER_SECOND) * 1000_000) + "second" -> minus(t, n) + "minute" -> minus(t, checkedMultiply(n, 60)) + "hour" -> minus(t, checkedMultiply(n, 3600)) + "day" -> minus(t, checkedMultiply(n, 86400)) + else -> return@ternaryTimestampFunction EvaluateResultError + } + ) + } internal val evaluateTimestampTrunc = notImplemented // TODO: Does not exist in expressions.kt yet. @@ -284,12 +347,12 @@ internal val evaluateTimestampToUnixMicros = unaryFunction { t: Timestamp -> if (t.seconds < Long.MIN_VALUE / 1_000_000) { // To avoid overflow when very close to Long.MIN_VALUE, add 1 second, multiply, then subtract // again. - val micros = checkedMultiply(t.seconds + 1, 1_000_000) - val adjustment = t.nanos.toLong() / 1_000 - 1_000_000 + val micros = checkedMultiply(t.seconds + 1, L_MICROS_PER_SECOND) + val adjustment = t.nanos.toLong() / L_MILLIS_PER_SECOND - L_MICROS_PER_SECOND checkedAdd(micros, adjustment) } else { - val micros = checkedMultiply(t.seconds, 1_000_000) - checkedAdd(micros, t.nanos.toLong() / 1_000) + val micros = checkedMultiply(t.seconds, L_MICROS_PER_SECOND) + checkedAdd(micros, t.nanos.toLong() / L_MILLIS_PER_SECOND) } ) } @@ -297,26 +360,33 @@ internal val evaluateTimestampToUnixMicros = unaryFunction { t: Timestamp -> internal val evaluateTimestampToUnixMillis = unaryFunction { t: Timestamp -> EvaluateResult.long( if (t.seconds < 0 && t.nanos > 0) { - val millis = checkedMultiply(t.seconds + 1, 1000) - val adjustment = t.nanos.toLong() / 1000_000 - 1000 + val millis = checkedMultiply(t.seconds + 1, L_MILLIS_PER_SECOND) + val adjustment = t.nanos.toLong() / L_MICROS_PER_SECOND - L_MILLIS_PER_SECOND checkedAdd(millis, adjustment) } else { - val millis = checkedMultiply(t.seconds, 1000) - checkedAdd(millis, t.nanos.toLong() / 1000_000) + val millis = checkedMultiply(t.seconds, L_MILLIS_PER_SECOND) + checkedAdd(millis, t.nanos.toLong() / L_MICROS_PER_SECOND) } ) } internal val evaluateTimestampToUnixSeconds = unaryFunction { t: Timestamp -> - if (t.nanos !in 0 until 1_000_000_000) EvaluateResultError else EvaluateResult.long(t.seconds) + if (t.nanos !in 0 until L_NANOS_PER_SECOND) EvaluateResultError + else EvaluateResult.long(t.seconds) } internal val evaluateUnixMicrosToTimestamp = unaryFunction { micros: Long -> - EvaluateResult.timestamp(Math.floorDiv(micros, 1000_000), Math.floorMod(micros, 1000_000)) + EvaluateResult.timestamp( + Math.floorDiv(micros, L_MICROS_PER_SECOND), + Math.floorMod(micros, I_MICROS_PER_SECOND) + ) } internal val evaluateUnixMillisToTimestamp = unaryFunction { millis: Long -> - EvaluateResult.timestamp(Math.floorDiv(millis, 1000), Math.floorMod(millis, 1000)) + EvaluateResult.timestamp( + Math.floorDiv(millis, L_MILLIS_PER_SECOND), + Math.floorMod(millis, I_MILLIS_PER_SECOND) + ) } internal val evaluateUnixSecondsToTimestamp = unaryFunction { seconds: Long -> @@ -457,6 +527,43 @@ private inline fun binaryFunction(crossinline function: (String, String) -> Eval function ) +private inline fun ternaryTimestampFunction( + crossinline function: (Timestamp, String, Long) -> EvaluateResult +): EvaluateFunction = ternaryNullableValueFunction { timestamp: Value, unit: Value, number: Value -> + val t: Timestamp = + when (timestamp.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL + Value.ValueTypeCase.TIMESTAMP_VALUE -> timestamp.timestampValue + else -> return@ternaryNullableValueFunction EvaluateResultError + } + val u: String = + if (unit.hasStringValue()) unit.stringValue + else return@ternaryNullableValueFunction EvaluateResultError + val n: Long = + when (number.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL + Value.ValueTypeCase.INTEGER_VALUE -> number.integerValue + else -> return@ternaryNullableValueFunction EvaluateResultError + } + function(t, u, n) +} + +private inline fun ternaryNullableValueFunction( + crossinline function: (Value, Value, Value) -> EvaluateResult +): EvaluateFunction = { params -> + if (params.size != 3) + throw Assert.fail("Function should have exactly 3 params, but %d were given.", params.size) + val p1 = params[0] + val p2 = params[1] + val p3 = params[2] + block@{ input: MutableDocument -> + val v1 = p1(input).value ?: return@block EvaluateResultError + val v2 = p2(input).value ?: return@block EvaluateResultError + val v3 = p3(input).value ?: return@block EvaluateResultError + catch { function(v1, v2, v3) } + } +} + private inline fun binaryFunctionType( valueTypeCase1: Value.ValueTypeCase, crossinline valueExtractor1: (Value) -> T1, 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 bac4ea15e5d..60894864904 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 @@ -780,8 +780,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the addition operation. */ @JvmStatic - fun add(first: Expr, second: Expr): Expr = - FunctionExpr("add", evaluateAdd, first, second) + fun add(first: Expr, second: Expr): Expr = FunctionExpr("add", evaluateAdd, first, second) /** * Creates an expression that adds numeric expressions with a constant. @@ -791,8 +790,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the addition operation. */ @JvmStatic - fun add(first: Expr, second: Number): Expr = - FunctionExpr("add", evaluateAdd, first, second) + fun add(first: Expr, second: Number): Expr = FunctionExpr("add", evaluateAdd, first, second) /** * Creates an expression that adds a numeric field with a numeric expression. diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt index 459e9c173d9..85d21574cf6 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt @@ -4,8 +4,12 @@ import com.google.common.truth.Truth.assertThat import com.google.firebase.firestore.RealtimePipelineSource import com.google.firebase.firestore.TestUtil import com.google.firebase.firestore.model.MutableDocument +import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.pipeline.minus +import com.google.firebase.firestore.pipeline.plus import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import com.google.protobuf.Timestamp import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking @@ -26,4 +30,33 @@ internal class PipelineTests { assertThat(list).hasSize(1) } + + @Test + fun xxx(): Unit = runBlocking { + val zero: Timestamp = Values.timestamp(0, 0) + + assertThat(plus(zero, 0, 0)) + .isEqualTo(zero) + + assertThat(plus(Values.timestamp(1, 1), 1, 1)) + .isEqualTo(Values.timestamp(2, 2)) + + assertThat(plus(Values.timestamp(1, 1), 0, 1)) + .isEqualTo(Values.timestamp(1, 2)) + + assertThat(plus(Values.timestamp(1, 1), 1, 0)) + .isEqualTo(Values.timestamp(2, 1)) + + assertThat(minus(zero, 0, 0)) + .isEqualTo(zero) + + assertThat(minus(Values.timestamp(1, 1), 1, 1)) + .isEqualTo(zero) + + assertThat(minus(Values.timestamp(1, 1), 0, 1)) + .isEqualTo(Values.timestamp(1, 0)) + + assertThat(minus(Values.timestamp(1, 1), 1, 0)) + .isEqualTo(Values.timestamp(0, 1)) + } } From 065235f867dcde12a12630020964b23925f6ff10 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 22 May 2025 14:47:16 -0400 Subject: [PATCH 073/152] Fix --- .../firebase/firestore/core/CompositeFilter.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java index ee471318f80..090ddf9c17b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java @@ -170,13 +170,16 @@ public String getCanonicalId() { @Override BooleanExpr toPipelineExpr() { - BooleanExpr[] booleanExprs = - filters.stream().map(Filter::toPipelineExpr).toArray(BooleanExpr[]::new); + BooleanExpr first = filters.get(0).toPipelineExpr(); + BooleanExpr[] additional = new BooleanExpr[filters.size() - 1]; + for (int i = 1, filtersSize = filters.size(); i < filtersSize; i++) { + additional[i - 1] = filters.get(i).toPipelineExpr(); + } switch (operator) { case AND: - return new BooleanExpr("and", booleanExprs); + return BooleanExpr.and(first, additional); case OR: - return new BooleanExpr("or", booleanExprs); + return BooleanExpr.or(first, additional); } // Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed throw new IllegalArgumentException("Unsupported operator: " + operator); From 6369f3c276af6aaf94613cd9ebcaf7b8b6e47966 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 23 May 2025 09:20:42 -0400 Subject: [PATCH 074/152] rename generic stage --- .../firebase/firestore/PipelineTest.java | 14 ++--- .../com/google/firebase/firestore/Pipeline.kt | 14 ++--- .../google/firebase/firestore/core/Query.java | 4 +- .../firebase/firestore/pipeline/stage.kt | 54 +++++++++---------- 4 files changed, 43 insertions(+), 43 deletions(-) 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 3531ffab2b1..5c9e26f5acb 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 @@ -47,7 +47,7 @@ import com.google.firebase.firestore.pipeline.AggregateFunction; import com.google.firebase.firestore.pipeline.AggregateStage; import com.google.firebase.firestore.pipeline.Expr; -import com.google.firebase.firestore.pipeline.GenericStage; +import com.google.firebase.firestore.pipeline.Stage; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import java.util.Collections; import java.util.LinkedHashMap; @@ -279,15 +279,15 @@ public void groupAndAccumulateResultsGeneric() { firestore .pipeline() .collection(randomCol) - .genericStage(GenericStage.ofName("where").withArguments(lt(field("published"), 1984))) - .genericStage( - GenericStage.ofName("aggregate") + .addStage(Stage.ofName("where").withArguments(lt(field("published"), 1984))) + .addStage( + Stage.ofName("aggregate") .withArguments( ImmutableMap.of("avgRating", AggregateFunction.avg("rating")), ImmutableMap.of("genre", field("genre")))) - .genericStage(GenericStage.ofName("where").withArguments(gt("avgRating", 4.3))) - .genericStage( - GenericStage.ofName("sort").withArguments(field("avgRating").descending())) + .addStage(Stage.ofName("where").withArguments(gt("avgRating", 4.3))) + .addStage( + Stage.ofName("sort").withArguments(field("avgRating").descending())) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 5184ac8c3c7..fb7d8313737 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -36,7 +36,7 @@ import com.google.firebase.firestore.pipeline.ExprWithAlias import com.google.firebase.firestore.pipeline.Field import com.google.firebase.firestore.pipeline.FindNearestStage import com.google.firebase.firestore.pipeline.FunctionExpr -import com.google.firebase.firestore.pipeline.GenericStage +import com.google.firebase.firestore.pipeline.Stage import com.google.firebase.firestore.pipeline.LimitStage import com.google.firebase.firestore.pipeline.OffsetStage import com.google.firebase.firestore.pipeline.Ordering @@ -47,7 +47,7 @@ import com.google.firebase.firestore.pipeline.SampleStage import com.google.firebase.firestore.pipeline.SelectStage import com.google.firebase.firestore.pipeline.Selectable import com.google.firebase.firestore.pipeline.SortStage -import com.google.firebase.firestore.pipeline.Stage +import com.google.firebase.firestore.pipeline.BaseStage import com.google.firebase.firestore.pipeline.UnionStage import com.google.firebase.firestore.pipeline.UnnestStage import com.google.firebase.firestore.pipeline.WhereStage @@ -59,15 +59,15 @@ class Pipeline internal constructor( internal val firestore: FirebaseFirestore, internal val userDataReader: UserDataReader, - private val stages: FluentIterable> + private val stages: FluentIterable> ) { internal constructor( firestore: FirebaseFirestore, userDataReader: UserDataReader, - stage: Stage<*> + stage: BaseStage<*> ) : this(firestore, userDataReader, FluentIterable.of(stage)) - private fun append(stage: Stage<*>): Pipeline { + private fun append(stage: BaseStage<*>): Pipeline { return Pipeline(firestore, userDataReader, stages.append(stage)) } @@ -110,10 +110,10 @@ internal constructor( * This method provides a way to call stages that are supported by the Firestore backend but that * are not implemented in the SDK version being used. * - * @param stage An [GenericStage] object that specifies stage name and parameters. + * @param stage An [Stage] object that specifies stage name and parameters. * @return A new [Pipeline] object with this stage appended to the stage list. */ - fun genericStage(stage: GenericStage): Pipeline = append(stage) + fun addStage(stage: Stage): Pipeline = append(stage) /** * Adds new fields to outputs from previous stages. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index 629135fe888..2aebd7e988c 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -37,7 +37,7 @@ import com.google.firebase.firestore.pipeline.FunctionExpr; import com.google.firebase.firestore.pipeline.InternalOptions; import com.google.firebase.firestore.pipeline.Ordering; -import com.google.firebase.firestore.pipeline.Stage; +import com.google.firebase.firestore.pipeline.BaseStage; import com.google.firestore.v1.Value; import java.util.ArrayList; import java.util.Collections; @@ -600,7 +600,7 @@ private static BooleanExpr whereConditionsFromCursor( } @NonNull - private Stage pipelineSource(FirebaseFirestore firestore) { + private BaseStage pipelineSource(FirebaseFirestore firestore) { if (isDocumentQuery()) { return new DocumentsSource(path.canonicalString()); } else if (isCollectionGroupQuery()) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index e3d02d19652..6b7e4546557 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -27,7 +27,7 @@ import com.google.firebase.firestore.util.Preconditions import com.google.firestore.v1.Pipeline import com.google.firestore.v1.Value -abstract class Stage> +abstract class BaseStage> internal constructor(protected val name: String, internal val options: InternalOptions) { internal fun toProtoStage(userDataReader: UserDataReader): Pipeline.Stage { val builder = Pipeline.Stage.newBuilder() @@ -96,32 +96,32 @@ internal constructor(protected val name: String, internal val options: InternalO * This class provides a way to call stages that are supported by the Firestore backend but that are * not implemented in the SDK version being used. */ -class GenericStage +class Stage private constructor( name: String, private val arguments: List, options: InternalOptions = InternalOptions.EMPTY -) : Stage(name, options) { +) : BaseStage(name, options) { companion object { /** * Specify name of stage * * @param name The unique name of the stage to add. - * @return [GenericStage] with specified parameters. + * @return [Stage] with specified parameters. */ - @JvmStatic fun ofName(name: String) = GenericStage(name, emptyList(), InternalOptions.EMPTY) + @JvmStatic fun ofName(name: String) = Stage(name, emptyList(), InternalOptions.EMPTY) } - override fun self(options: InternalOptions) = GenericStage(name, arguments, options) + override fun self(options: InternalOptions) = Stage(name, arguments, options) /** * Specify arguments to stage. * * @param arguments A list of ordered parameters to configure the stage's behavior. - * @return [GenericStage] with specified parameters. + * @return [Stage] with specified parameters. */ - fun withArguments(vararg arguments: Any): GenericStage = - GenericStage(name, arguments.map(GenericArg::from), options) + fun withArguments(vararg arguments: Any): Stage = + Stage(name, arguments.map(GenericArg::from), options) override fun args(userDataReader: UserDataReader): Sequence = arguments.asSequence().map { it.toProto(userDataReader) } @@ -167,7 +167,7 @@ internal sealed class GenericArg { internal class DatabaseSource @JvmOverloads internal constructor(options: InternalOptions = InternalOptions.EMPTY) : - Stage("database", options) { + BaseStage("database", options) { override fun self(options: InternalOptions) = DatabaseSource(options) override fun args(userDataReader: UserDataReader): Sequence = emptySequence() } @@ -178,7 +178,7 @@ internal constructor( // We validate [firestore.databaseId] when adding to pipeline. internal val firestore: FirebaseFirestore?, options: InternalOptions -) : Stage("collection", options) { +) : BaseStage("collection", options) { override fun self(options: InternalOptions): CollectionSource = CollectionSource(path, firestore, options) override fun args(userDataReader: UserDataReader): Sequence = @@ -216,7 +216,7 @@ internal constructor( class CollectionGroupSource private constructor(private val collectionId: String, options: InternalOptions) : - Stage("collection_group", options) { + BaseStage("collection_group", options) { override fun self(options: InternalOptions) = CollectionGroupSource(collectionId, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setReferenceValue("").build(), encodeValue(collectionId)) @@ -246,7 +246,7 @@ internal class DocumentsSource internal constructor( private val documents: Array, options: InternalOptions = InternalOptions.EMPTY -) : Stage("documents", options) { +) : BaseStage("documents", options) { internal constructor(document: String) : this(arrayOf(document)) override fun self(options: InternalOptions) = DocumentsSource(documents, options) override fun args(userDataReader: UserDataReader): Sequence = @@ -257,7 +257,7 @@ internal class AddFieldsStage internal constructor( private val fields: Array, options: InternalOptions = InternalOptions.EMPTY -) : Stage("add_fields", options) { +) : BaseStage("add_fields", options) { override fun self(options: InternalOptions) = AddFieldsStage(fields, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto(userDataReader) })) @@ -284,7 +284,7 @@ internal constructor( private val accumulators: Map, private val groups: Map, options: InternalOptions = InternalOptions.EMPTY -) : Stage("aggregate", options) { +) : BaseStage("aggregate", options) { private constructor(accumulators: Map) : this(accumulators, emptyMap()) companion object { @@ -349,7 +349,7 @@ internal class WhereStage internal constructor( private val condition: BooleanExpr, options: InternalOptions = InternalOptions.EMPTY -) : Stage("where", options) { +) : BaseStage("where", options) { override fun self(options: InternalOptions) = WhereStage(condition, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(condition.toProto(userDataReader)) @@ -365,7 +365,7 @@ internal constructor( private val vector: Expr, private val distanceMeasure: DistanceMeasure, options: InternalOptions = InternalOptions.EMPTY -) : Stage("find_nearest", options) { +) : BaseStage("find_nearest", options) { companion object { @@ -477,7 +477,7 @@ internal constructor( internal class LimitStage internal constructor(private val limit: Int, options: InternalOptions = InternalOptions.EMPTY) : - Stage("limit", options) { + BaseStage("limit", options) { override fun self(options: InternalOptions) = LimitStage(limit, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(limit)) @@ -485,7 +485,7 @@ internal constructor(private val limit: Int, options: InternalOptions = Internal internal class OffsetStage internal constructor(private val offset: Int, options: InternalOptions = InternalOptions.EMPTY) : - Stage("offset", options) { + BaseStage("offset", options) { override fun self(options: InternalOptions) = OffsetStage(offset, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(offset)) @@ -495,7 +495,7 @@ internal class SelectStage internal constructor( private val fields: Array, options: InternalOptions = InternalOptions.EMPTY -) : Stage("select", options) { +) : BaseStage("select", options) { override fun self(options: InternalOptions) = SelectStage(fields, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto(userDataReader) })) @@ -505,7 +505,7 @@ internal class SortStage internal constructor( private val orders: Array, options: InternalOptions = InternalOptions.EMPTY -) : Stage("sort", options) { +) : BaseStage("sort", options) { override fun self(options: InternalOptions) = SortStage(orders, options) override fun args(userDataReader: UserDataReader): Sequence = orders.asSequence().map { it.toProto(userDataReader) } @@ -515,7 +515,7 @@ internal class DistinctStage internal constructor( private val groups: Array, options: InternalOptions = InternalOptions.EMPTY -) : Stage("distinct", options) { +) : BaseStage("distinct", options) { override fun self(options: InternalOptions) = DistinctStage(groups, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(groups.associate { it.getAlias() to it.toProto(userDataReader) })) @@ -525,7 +525,7 @@ internal class RemoveFieldsStage internal constructor( private val fields: Array, options: InternalOptions = InternalOptions.EMPTY -) : Stage("remove_fields", options) { +) : BaseStage("remove_fields", options) { override fun self(options: InternalOptions) = RemoveFieldsStage(fields, options) override fun args(userDataReader: UserDataReader): Sequence = fields.asSequence().map(Field::toProto) @@ -536,7 +536,7 @@ internal constructor( private val mapValue: Expr, private val mode: Mode, options: InternalOptions = InternalOptions.EMPTY -) : Stage("replace", options) { +) : BaseStage("replace", options) { class Mode private constructor(internal val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) companion object { @@ -563,7 +563,7 @@ private constructor( private val size: Number, private val mode: Mode, options: InternalOptions = InternalOptions.EMPTY -) : Stage("sample", options) { +) : BaseStage("sample", options) { override fun self(options: InternalOptions) = SampleStage(size, mode, options) class Mode private constructor(internal val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) @@ -606,7 +606,7 @@ internal class UnionStage internal constructor( private val other: com.google.firebase.firestore.Pipeline, options: InternalOptions = InternalOptions.EMPTY -) : Stage("union", options) { +) : BaseStage("union", options) { override fun self(options: InternalOptions) = UnionStage(other, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setPipelineValue(other.toPipelineProto()).build()) @@ -620,7 +620,7 @@ class UnnestStage internal constructor( private val selectable: Selectable, options: InternalOptions = InternalOptions.EMPTY -) : Stage("unnest", options) { +) : BaseStage("unnest", options) { companion object { /** From d452eb30f42d340ea0c4be5cd000d74fc15456fb Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 23 May 2025 10:07:37 -0400 Subject: [PATCH 075/152] Make SDK version 23 compatible. --- .../google/firebase/firestore/core/Query.java | 35 +++++++++++++++---- .../firebase/firestore/util/BiFunction.java | 7 ++++ .../firebase/firestore/util/IntFunction.java | 7 ++++ 3 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/util/BiFunction.java create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/util/IntFunction.java diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index 2aebd7e988c..ac4d499854f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -38,6 +38,9 @@ import com.google.firebase.firestore.pipeline.InternalOptions; import com.google.firebase.firestore.pipeline.Ordering; import com.google.firebase.firestore.pipeline.BaseStage; +import com.google.firebase.firestore.util.BiFunction; +import com.google.firebase.firestore.util.Function; +import com.google.firebase.firestore.util.IntFunction; import com.google.firestore.v1.Value; import java.util.ArrayList; import java.util.Collections; @@ -46,7 +49,6 @@ import java.util.List; import java.util.SortedSet; import java.util.TreeSet; -import java.util.function.BiFunction; /** * Encapsulates all the query attributes we support in the SDK. It can be run against the @@ -547,8 +549,7 @@ public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataR if (fields.size() == 1) { p = p.where(fields.get(0).exists()); } else { - BooleanExpr[] conditions = - fields.stream().skip(1).map(Expr.Companion::exists).toArray(BooleanExpr[]::new); + BooleanExpr[] conditions = skipFirstToArray(fields, BooleanExpr[]::new, Expr.Companion::exists); p = p.where(and(fields.get(0).exists(), conditions)); } @@ -564,23 +565,43 @@ public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataR if (hasLimit()) { // TODO: Handle situation where user enters limit larger than integer. if (limitType == LimitType.LIMIT_TO_FIRST) { - p = p.sort(orderings.get(0), orderings.stream().skip(1).toArray(Ordering[]::new)); + p = p.sort(orderings.get(0), skipFirstToArray(orderings, Ordering[]::new)); p = p.limit((int) limit); } else { p = p.sort( orderings.get(0).reverse(), - orderings.stream().skip(1).map(Ordering::reverse).toArray(Ordering[]::new)); + skipFirstToArray(orderings, Ordering[]::new, Ordering::reverse)); p = p.limit((int) limit); - p = p.sort(orderings.get(0), orderings.stream().skip(1).toArray(Ordering[]::new)); + p = p.sort(orderings.get(0), skipFirstToArray(orderings, Ordering[]::new)); } } else { - p = p.sort(orderings.get(0), orderings.stream().skip(1).toArray(Ordering[]::new)); + p = p.sort(orderings.get(0), skipFirstToArray(orderings, Ordering[]::new)); } return p; } + // Many Pipelines require first parameter to be separated out from rest. + private static T[] skipFirstToArray(List list, IntFunction generator) { + int size = list.size(); + T[] result = generator.apply(size - 1); + for (int i = 1; i < size; i++) { + result[i-1] = list.get(i); + } + return result; + } + + // Many Pipelines require first parameter to be separated out from rest. + private static R[] skipFirstToArray(List list, IntFunction generator, Function map) { + int size = list.size(); + R[] result = generator.apply(size - 1); + for (int i = 1; i < size; i++) { + result[i-1] = map.apply(list.get(i)); + } + return result; + } + private static BooleanExpr whereConditionsFromCursor( Bound bound, List fields, BiFunction cmp) { List boundPosition = bound.getPosition(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BiFunction.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BiFunction.java new file mode 100644 index 00000000000..1afb87fbb1d --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BiFunction.java @@ -0,0 +1,7 @@ +package com.google.firebase.firestore.util; + +/** A port of {@link java.util.function.BiFunction} */ +@FunctionalInterface +public interface BiFunction { + R apply(T t, U u); +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/IntFunction.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/IntFunction.java new file mode 100644 index 00000000000..2407db54808 --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/IntFunction.java @@ -0,0 +1,7 @@ +package com.google.firebase.firestore.util; + +/** A port of {@link java.util.function.IntFunction} */ +@FunctionalInterface +public interface IntFunction { + R apply(int value); +} From 93a44a7fba95297851fc45b4febb80b1ab2d0611 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 23 May 2025 15:30:52 -0400 Subject: [PATCH 076/152] Fix query to pipeline logic. --- .../firebase/firestore/QueryToPipelineTest.java | 2 +- .../firestore/testutil/IntegrationTestUtil.java | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java index 4bf391df465..f4da3ce2856 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java @@ -356,7 +356,7 @@ public void testQueriesCanUseNotEqualFilters() { expectedDocsMap.remove("i"); expectedDocsMap.remove("j"); snapshot = - waitFor(db.pipeline().convertFrom(collection.whereEqualTo("zip", Double.NaN)).execute()); + waitFor(db.pipeline().convertFrom(collection.whereNotEqualTo("zip", Double.NaN)).execute()); assertEquals(Lists.newArrayList(expectedDocsMap.values()), pipelineSnapshotToValues(snapshot)); } diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index bea8743e0ba..717e3f02ebc 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -567,9 +567,18 @@ public static void checkOnlineAndOfflineResultsMatch(Query query, String... expe * @param expectedDocs Ordered list of document keys that are expected to match the query */ public static void checkQueryAndPipelineResultsMatch(Query query, String... expectedDocs) { - QuerySnapshot docsFromQuery = waitFor(query.get(Source.SERVER)); - PipelineSnapshot docsFromPipeline = - waitFor(query.getFirestore().pipeline().convertFrom(query).execute()); + QuerySnapshot docsFromQuery; + try { + docsFromQuery = waitFor(query.get(Source.SERVER)); + } catch (Exception e) { + throw new RuntimeException("Classic Query FAILED", e); + } + PipelineSnapshot docsFromPipeline; + try { + docsFromPipeline = waitFor(query.getFirestore().pipeline().convertFrom(query).execute()); + } catch (Exception e) { + throw new RuntimeException("Pipeline FAILED", e); + } assertEquals(querySnapshotToIds(docsFromQuery), pipelineSnapshotToIds(docsFromPipeline)); List expected = asList(expectedDocs); From 96aac51e7ca60ee6c2abb4077f34650ca7693bcb Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 23 May 2025 15:32:51 -0400 Subject: [PATCH 077/152] Fix query to pipeline logic. --- firebase-firestore/api.txt | 94 ++++++++++--------- .../firebase/firestore/core/FieldFilter.java | 47 ++++++++-- .../google/firebase/firestore/model/Values.kt | 8 +- .../firestore/pipeline/expressions.kt | 19 ++++ 4 files changed, 114 insertions(+), 54 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index 9e981a511c1..cb86b4b0932 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -421,6 +421,7 @@ package com.google.firebase.firestore { public final class Pipeline { method public com.google.firebase.firestore.Pipeline addFields(com.google.firebase.firestore.pipeline.Selectable field, com.google.firebase.firestore.pipeline.Selectable... additionalFields); + method public com.google.firebase.firestore.Pipeline addStage(com.google.firebase.firestore.pipeline.Stage stage); method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateStage aggregateStage); method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateWithAlias accumulator, com.google.firebase.firestore.pipeline.AggregateWithAlias... additionalAccumulators); method public com.google.firebase.firestore.Pipeline distinct(com.google.firebase.firestore.pipeline.Selectable group, java.lang.Object... additionalGroups); @@ -432,7 +433,6 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.FindNearestStage stage); method public com.google.firebase.firestore.Pipeline findNearest(String vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline findNearest(String vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); - method public com.google.firebase.firestore.Pipeline genericStage(com.google.firebase.firestore.pipeline.GenericStage stage); method public com.google.firebase.firestore.Pipeline limit(int limit); method public com.google.firebase.firestore.Pipeline offset(int offset); method public com.google.firebase.firestore.Pipeline removeFields(com.google.firebase.firestore.pipeline.Field field, com.google.firebase.firestore.pipeline.Field... additionalFields); @@ -722,7 +722,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.AggregateFunction sum(String fieldName); } - public final class AggregateStage extends com.google.firebase.firestore.pipeline.Stage { + public final class AggregateStage extends com.google.firebase.firestore.pipeline.BaseStage { method public static com.google.firebase.firestore.pipeline.AggregateStage withAccumulators(com.google.firebase.firestore.pipeline.AggregateWithAlias accumulator, com.google.firebase.firestore.pipeline.AggregateWithAlias... additionalAccumulators); method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(com.google.firebase.firestore.pipeline.Selectable group, java.lang.Object... additionalGroups); method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(String groupField, java.lang.Object... additionalGroups); @@ -736,6 +736,17 @@ package com.google.firebase.firestore.pipeline { public final class AggregateWithAlias { } + public abstract class BaseStage> { + method protected final String getName(); + method public final T with(String key, boolean value); + method public final T with(String key, com.google.firebase.firestore.pipeline.Field value); + method public final T with(String key, double value); + method protected final T with(String key, error.NonExistentClass value); + method public final T with(String key, String value); + method public final T with(String key, long value); + property protected final String name; + } + public class BooleanExpr extends com.google.firebase.firestore.pipeline.FunctionExpr { method public final com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.Expr thenExpr, com.google.firebase.firestore.pipeline.Expr elseExpr); method public final com.google.firebase.firestore.pipeline.Expr cond(Object thenValue, Object elseValue); @@ -749,7 +760,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); } - public final class CollectionGroupSource extends com.google.firebase.firestore.pipeline.Stage { + public final class CollectionGroupSource extends com.google.firebase.firestore.pipeline.BaseStage { method public static com.google.firebase.firestore.pipeline.CollectionGroupSource of(String collectionId); method public error.NonExistentClass withForceIndex(String value); field public static final com.google.firebase.firestore.pipeline.CollectionGroupSource.Companion Companion; @@ -759,7 +770,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.CollectionGroupSource of(String collectionId); } - public final class CollectionSource extends com.google.firebase.firestore.pipeline.Stage { + public final class CollectionSource extends com.google.firebase.firestore.pipeline.BaseStage { method public static com.google.firebase.firestore.pipeline.CollectionSource of(com.google.firebase.firestore.CollectionReference ref); method public static com.google.firebase.firestore.pipeline.CollectionSource of(String path); method public error.NonExistentClass withForceIndex(String value); @@ -825,12 +836,12 @@ package com.google.firebase.firestore.pipeline { } public abstract class Expr { - method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, Number second, java.lang.Object... others); - method public final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public final com.google.firebase.firestore.pipeline.Expr add(Number second, java.lang.Object... others); - method public static final com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public static final com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, Number second, java.lang.Object... others); + method public final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr second); + method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second); + method public static final com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, Number second); + method public final com.google.firebase.firestore.pipeline.Expr add(Number second); + method public static final com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second); + method public static final com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, Number second); method public com.google.firebase.firestore.pipeline.ExprWithAlias alias(String alias); method public static final com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); @@ -1003,6 +1014,7 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.BooleanExpr gte(Object value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); method public static final com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, Object value); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr ifError(com.google.firebase.firestore.pipeline.BooleanExpr tryExpr, com.google.firebase.firestore.pipeline.BooleanExpr catchExpr); method public final com.google.firebase.firestore.pipeline.Expr ifError(com.google.firebase.firestore.pipeline.Expr catchExpr); method public static final com.google.firebase.firestore.pipeline.Expr ifError(com.google.firebase.firestore.pipeline.Expr tryExpr, com.google.firebase.firestore.pipeline.Expr catchExpr); method public static final com.google.firebase.firestore.pipeline.Expr ifError(com.google.firebase.firestore.pipeline.Expr tryExpr, Object catchValue); @@ -1069,12 +1081,12 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.Expr mod(Number divisor); method public static final com.google.firebase.firestore.pipeline.Expr mod(String dividendFieldName, com.google.firebase.firestore.pipeline.Expr divisor); method public static final com.google.firebase.firestore.pipeline.Expr mod(String dividendFieldName, Number divisor); - method public static final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public static final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, Number second, java.lang.Object... others); - method public final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public final com.google.firebase.firestore.pipeline.Expr multiply(Number second, java.lang.Object... others); - method public static final com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public static final com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, Number second, java.lang.Object... others); + method public final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr second); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, Number second); + method public final com.google.firebase.firestore.pipeline.Expr multiply(Number second); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second); + method public static final com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, Number second); method public final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); @@ -1211,10 +1223,10 @@ package com.google.firebase.firestore.pipeline { } public static final class Expr.Companion { - method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, Number second, java.lang.Object... others); - method public com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, Number second, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second); + method public com.google.firebase.firestore.pipeline.Expr add(com.google.firebase.firestore.pipeline.Expr first, Number second); + method public com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second); + method public com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, Number second); method public com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, Object secondArray, java.lang.Object... otherArrays); @@ -1332,6 +1344,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr gte(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); method public com.google.firebase.firestore.pipeline.BooleanExpr gte(String fieldName, Object value); + method public com.google.firebase.firestore.pipeline.BooleanExpr ifError(com.google.firebase.firestore.pipeline.BooleanExpr tryExpr, com.google.firebase.firestore.pipeline.BooleanExpr catchExpr); method public com.google.firebase.firestore.pipeline.Expr ifError(com.google.firebase.firestore.pipeline.Expr tryExpr, com.google.firebase.firestore.pipeline.Expr catchExpr); method public com.google.firebase.firestore.pipeline.Expr ifError(com.google.firebase.firestore.pipeline.Expr tryExpr, Object catchValue); method public com.google.firebase.firestore.pipeline.BooleanExpr isAbsent(com.google.firebase.firestore.pipeline.Expr value); @@ -1373,10 +1386,10 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr mod(com.google.firebase.firestore.pipeline.Expr dividend, Number divisor); method public com.google.firebase.firestore.pipeline.Expr mod(String dividendFieldName, com.google.firebase.firestore.pipeline.Expr divisor); method public com.google.firebase.firestore.pipeline.Expr mod(String dividendFieldName, Number divisor); - method public com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, Number second, java.lang.Object... others); - method public com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second, java.lang.Object... others); - method public com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, Number second, java.lang.Object... others); + method public com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, com.google.firebase.firestore.pipeline.Expr second); + method public com.google.firebase.firestore.pipeline.Expr multiply(com.google.firebase.firestore.pipeline.Expr first, Number second); + method public com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second); + method public com.google.firebase.firestore.pipeline.Expr multiply(String numericFieldName, Number second); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr neq(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); @@ -1479,7 +1492,7 @@ package com.google.firebase.firestore.pipeline { public static final class Field.Companion { } - public final class FindNearestStage extends com.google.firebase.firestore.pipeline.Stage { + public final class FindNearestStage extends com.google.firebase.firestore.pipeline.BaseStage { method public static com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public static com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public static com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); @@ -1518,16 +1531,6 @@ package com.google.firebase.firestore.pipeline { public static final class GenericOptions.Companion { } - public final class GenericStage extends com.google.firebase.firestore.pipeline.Stage { - method public static com.google.firebase.firestore.pipeline.GenericStage ofName(String name); - method public com.google.firebase.firestore.pipeline.GenericStage withArguments(java.lang.Object... arguments); - field public static final com.google.firebase.firestore.pipeline.GenericStage.Companion Companion; - } - - public static final class GenericStage.Companion { - method public com.google.firebase.firestore.pipeline.GenericStage ofName(String name); - } - public final class InternalOptions { field public static final com.google.firebase.firestore.pipeline.InternalOptions.Companion Companion; field public static final com.google.firebase.firestore.pipeline.InternalOptions EMPTY; @@ -1573,7 +1576,7 @@ package com.google.firebase.firestore.pipeline { public static final class PipelineOptions.IndexMode.Companion { } - public final class SampleStage extends com.google.firebase.firestore.pipeline.Stage { + public final class SampleStage extends com.google.firebase.firestore.pipeline.BaseStage { method public static com.google.firebase.firestore.pipeline.SampleStage withDocLimit(int documents); method public static com.google.firebase.firestore.pipeline.SampleStage withPercentage(double percentage); field public static final com.google.firebase.firestore.pipeline.SampleStage.Companion Companion; @@ -1599,18 +1602,17 @@ package com.google.firebase.firestore.pipeline { ctor public Selectable(); } - public abstract class Stage> { - method protected final String getName(); - method public final T with(String key, boolean value); - method public final T with(String key, com.google.firebase.firestore.pipeline.Field value); - method public final T with(String key, double value); - method protected final T with(String key, error.NonExistentClass value); - method public final T with(String key, String value); - method public final T with(String key, long value); - property protected final String name; + public final class Stage extends com.google.firebase.firestore.pipeline.BaseStage { + method public static com.google.firebase.firestore.pipeline.Stage ofName(String name); + method public com.google.firebase.firestore.pipeline.Stage withArguments(java.lang.Object... arguments); + field public static final com.google.firebase.firestore.pipeline.Stage.Companion Companion; + } + + public static final class Stage.Companion { + method public com.google.firebase.firestore.pipeline.Stage ofName(String name); } - public final class UnnestStage extends com.google.firebase.firestore.pipeline.Stage { + public final class UnnestStage extends com.google.firebase.firestore.pipeline.BaseStage { method public static com.google.firebase.firestore.pipeline.UnnestStage withField(com.google.firebase.firestore.pipeline.Selectable arrayWithAlias); method public static com.google.firebase.firestore.pipeline.UnnestStage withField(String arrayField, String alias); method public com.google.firebase.firestore.pipeline.UnnestStage withIndexField(String indexField); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java index 20dd9ad585a..f8ceb77baa8 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java @@ -14,17 +14,23 @@ package com.google.firebase.firestore.core; +import static com.google.firebase.firestore.model.Values.isNanValue; import static com.google.firebase.firestore.pipeline.Expr.and; +import static com.google.firebase.firestore.pipeline.Expr.ifError; import static com.google.firebase.firestore.util.Assert.hardAssert; -import static java.lang.Double.isNaN; + +import androidx.annotation.NonNull; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.FieldPath; import com.google.firebase.firestore.model.Values; import com.google.firebase.firestore.pipeline.BooleanExpr; +import com.google.firebase.firestore.pipeline.Expr; import com.google.firebase.firestore.pipeline.Field; import com.google.firebase.firestore.util.Assert; import com.google.firestore.v1.Value; + +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -188,16 +194,18 @@ BooleanExpr toPipelineExpr() { case EQUAL: if (value.hasNullValue()) { return and(exists, x.isNull()); - } else if (value.hasDoubleValue() && isNaN(value.getDoubleValue())) { - return and(exists, x.isNan()); + } else if (isNanValue(value)) { + // The isNotNan will error on non-numeric values. + return and(exists, ifError(x.isNan(), Expr.constant(false))); } else { return and(exists, x.eq(value)); } case NOT_EQUAL: if (value.hasNullValue()) { return and(exists, x.isNotNull()); - } else if (value.hasDoubleValue() && isNaN(value.getDoubleValue())) { - return and(exists, x.isNotNan()); + } else if (isNanValue(value)) { + // The isNotNan will error on non-numeric values. + return and(exists, ifError(x.isNotNan(), Expr.constant(true))); } else { return and(exists, x.neq(value)); } @@ -211,14 +219,39 @@ BooleanExpr toPipelineExpr() { return and(exists, x.arrayContainsAny(value.getArrayValue().getValuesList())); case IN: return and(exists, x.eqAny(value.getArrayValue().getValuesList())); - case NOT_IN: - return and(exists, x.notEqAny(value.getArrayValue().getValuesList())); + case NOT_IN: { + List list = value.getArrayValue().getValuesList(); + if (hasNaN(list)) { + return and(exists, x.notEqAny(filterNaN(list)), ifError(x.isNotNan(), Expr.constant(true))); + } else { + return and(exists, x.notEqAny(list)); + } + } default: // Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed throw new IllegalArgumentException("Unsupported operator: " + operator); } } + private static boolean hasNaN(List list) { + for (Value v : list) { + if (isNanValue(v)) { + return true; + } + } + return false; + } + + @NonNull + private static List filterNaN(List list) { + List listWithoutNan = new ArrayList<>(list.size() - 1); + for (Value v : list) { + if (isNanValue(v)) continue; + listWithoutNan.add(v); + } + return listWithoutNan; + } + @Override public String toString() { return getCanonicalId(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 3bcd2ed3c38..568a64899ec 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -599,8 +599,14 @@ internal object Values { .build() } + @JvmField + val TRUE: Value = Value.newBuilder().setBooleanValue(true).build() + + @JvmField + val FALSE: Value = Value.newBuilder().setBooleanValue(false).build() + @JvmStatic - fun encodeValue(value: Boolean): Value = Value.newBuilder().setBooleanValue(value).build() + fun encodeValue(value: Boolean): Value = if (value) TRUE else FALSE @JvmStatic fun encodeValue(geoPoint: GeoPoint): Value = 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 4b4d783ee89..5eb5e6c3698 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 @@ -2838,6 +2838,20 @@ abstract class Expr internal constructor() { @JvmStatic fun ifError(tryExpr: Expr, catchExpr: Expr): Expr = FunctionExpr("if_error", tryExpr, catchExpr) + /** + * Creates an expression that returns the [catchExpr] argument if there is an error, else return + * the result of the [tryExpr] argument evaluation. + * + * This overload will return [BooleanExpr] when both parameters are also [BooleanExpr]. + * + * @param tryExpr The try boolean expression. + * @param catchExpr The catch boolean expression that will be evaluated and returned if the [tryExpr] + * produces an error. + * @return A new [BooleanExpr] representing the ifError operation. + */ + @JvmStatic + fun ifError(tryExpr: BooleanExpr, catchExpr: BooleanExpr): BooleanExpr = BooleanExpr("if_error", tryExpr, catchExpr) + /** * Creates an expression that returns the [catchValue] argument if there is an error, else * return the result of the [tryExpr] argument evaluation. @@ -4055,6 +4069,11 @@ internal constructor( open class BooleanExpr internal constructor(name: String, params: Array) : FunctionExpr(name, params, InternalOptions.EMPTY) { internal constructor(name: String, param: Expr) : this(name, arrayOf(param)) + internal constructor( + name: String, + param1: Expr, + param2: Any + ) : this(name, arrayOf(param1, toExprOrConstant(param2))) internal constructor( name: String, param: Expr, From 385d3cff0fab32b5abf5662c817161e006352049 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 23 May 2025 15:37:02 -0400 Subject: [PATCH 078/152] Fix query to pipeline logic. --- .../java/com/google/firebase/firestore/core/FieldFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java index f8ceb77baa8..b702746f066 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java @@ -195,7 +195,7 @@ BooleanExpr toPipelineExpr() { if (value.hasNullValue()) { return and(exists, x.isNull()); } else if (isNanValue(value)) { - // The isNotNan will error on non-numeric values. + // The isNan will error on non-numeric values. return and(exists, ifError(x.isNan(), Expr.constant(false))); } else { return and(exists, x.eq(value)); From 9f9f95ae44fc3baf698e744ddf8c002390a416c0 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 23 May 2025 16:15:12 -0400 Subject: [PATCH 079/152] small refactor --- .../firestore/QueryToPipelineTest.java | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java index f4da3ce2856..942d6291668 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java @@ -613,53 +613,41 @@ public void testQueriesCanUseArrayContainsAnyFilters() { FirebaseFirestore db = collection.firestore; // Search for "array" to contain [42, 43]. - PipelineSnapshot snapshot = - waitFor( - db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", asList(42L, 43L))) - .execute()); + Pipeline pipeline = db.pipeline() + .convertFrom(collection.whereArrayContainsAny("array", asList(42L, 43L))); + PipelineSnapshot snapshot = waitFor(pipeline.execute()); assertEquals(asList(docA, docB, docD, docE), pipelineSnapshotToValues(snapshot)); // With objects. - snapshot = - waitFor( - db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", asList(map("a", 42L)))) - .execute()); + pipeline = db.pipeline() + .convertFrom(collection.whereArrayContainsAny("array", asList(map("a", 42L)))); + snapshot = waitFor(pipeline.execute()); assertEquals(asList(docF), pipelineSnapshotToValues(snapshot)); // With null. - snapshot = - waitFor( - db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", nullList())) - .execute()); + pipeline = db.pipeline() + .convertFrom(collection.whereArrayContainsAny("array", nullList())); + snapshot = waitFor(pipeline.execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With null and a value. List inputList = nullList(); inputList.add(43L); - snapshot = - waitFor( - db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", inputList)) - .execute()); + pipeline = db.pipeline() + .convertFrom(collection.whereArrayContainsAny("array", inputList)); + snapshot = waitFor(pipeline.execute()); assertEquals(asList(docE), pipelineSnapshotToValues(snapshot)); // With NaN. - snapshot = - waitFor( - db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", asList(Double.NaN))) - .execute()); + pipeline = db.pipeline() + .convertFrom(collection.whereArrayContainsAny("array", asList(Double.NaN))); + snapshot = waitFor(pipeline.execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With NaN and a value. - snapshot = - waitFor( - db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", asList(Double.NaN, 43L))) - .execute()); + pipeline = db.pipeline() + .convertFrom(collection.whereArrayContainsAny("array", asList(Double.NaN, 43L))); + snapshot = waitFor(pipeline.execute()); assertEquals(asList(docE), pipelineSnapshotToValues(snapshot)); } From 59eb4ae1d0db1cc91f7799e5ecbb8358f4916b1f Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 23 May 2025 16:30:59 -0400 Subject: [PATCH 080/152] fix after merge --- .../main/java/com/google/firebase/firestore/Pipeline.kt | 8 ++++---- .../java/com/google/firebase/firestore/model/Values.kt | 9 ++++++--- .../google/firebase/firestore/pipeline/expressions.kt | 6 ++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index c3562491781..309721199c4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -131,7 +131,7 @@ class Pipeline private constructor( firestore: FirebaseFirestore, userDataReader: UserDataReader, - stages: FluentIterable> + stages: FluentIterable> ) : AbstractPipeline(firestore, userDataReader, stages) { internal constructor( firestore: FirebaseFirestore, @@ -760,15 +760,15 @@ class RealtimePipeline internal constructor( firestore: FirebaseFirestore, userDataReader: UserDataReader, - stages: FluentIterable> + stages: FluentIterable> ) : AbstractPipeline(firestore, userDataReader, stages) { internal constructor( firestore: FirebaseFirestore, userDataReader: UserDataReader, - stage: Stage<*> + stage: BaseStage<*> ) : this(firestore, userDataReader, FluentIterable.of(stage)) - private fun append(stage: Stage<*>): RealtimePipeline { + private fun append(stage: BaseStage<*>): RealtimePipeline { return RealtimePipeline(firestore, userDataReader, stages.append(stage)) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 82ca0386202..7b78ee4780a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -663,14 +663,17 @@ internal object Values { ) } + @JvmStatic + fun encodeValue(value: Timestamp): Value = Value.newBuilder().setTimestampValue(value).build() + @JvmField - val TRUE: Value = Value.newBuilder().setBooleanValue(true).build() + val TRUE_VALUE: Value = Value.newBuilder().setBooleanValue(true).build() @JvmField - val FALSE: Value = Value.newBuilder().setBooleanValue(false).build() + val FALSE_VALUE: Value = Value.newBuilder().setBooleanValue(false).build() @JvmStatic - fun encodeValue(value: Boolean): Value = if (value) TRUE else FALSE + fun encodeValue(value: Boolean): Value = if (value) TRUE_VALUE else FALSE_VALUE @JvmStatic fun encodeValue(geoPoint: GeoPoint): Value = 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 f17e7d7803f..7c10b9d4bca 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 @@ -2954,7 +2954,8 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the ifError operation. */ @JvmStatic - fun ifError(tryExpr: BooleanExpr, catchExpr: BooleanExpr): BooleanExpr = BooleanExpr("if_error", tryExpr, catchExpr) + fun ifError(tryExpr: BooleanExpr, catchExpr: BooleanExpr): BooleanExpr = + BooleanExpr("if_error", notImplemented, tryExpr, catchExpr) /** * Creates an expression that returns the [catchValue] argument if there is an error, else @@ -4217,9 +4218,10 @@ internal constructor(name: String, function: EvaluateFunction, params: Array Date: Tue, 27 May 2025 14:27:43 -0400 Subject: [PATCH 081/152] Implement offline evaluation of map --- .../firebase/firestore/pipeline/evaluation.kt | 20 +++++++++++++++++++ .../firestore/pipeline/expressions.kt | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index 0b6876659b5..aae36893ecf 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -1,3 +1,4 @@ +@file:JvmName("Evaluation") package com.google.firebase.firestore.pipeline import com.google.common.math.LongMath @@ -7,6 +8,7 @@ import com.google.common.math.LongMath.checkedSubtract import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values +import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.model.Values.isNanValue import com.google.firebase.firestore.util.Assert import com.google.firestore.v1.Value @@ -393,6 +395,24 @@ internal val evaluateUnixSecondsToTimestamp = unaryFunction { seconds: Long -> EvaluateResult.timestamp(seconds, 0) } +// === Map Functions === + +internal val evaluateMap: EvaluateFunction = { params -> + if (params.size % 2 != 0) + throw Assert.fail("Function should have even number of params, but %d were given.", params.size) + else block@{ input: MutableDocument -> + val map: MutableMap = HashMap(params.size / 2) + for (i in params.indices step 2) { + val k = params[i](input).value ?: return@block EvaluateResultError + if (!k.hasStringValue()) return@block EvaluateResultError + val v = params[i + 1](input).value ?: return@block EvaluateResultError + // It is against the API contract to include a key more than once. + if (map.put(k.stringValue, v) != null) return@block EvaluateResultError + } + EvaluateResultValue(encodeValue(map)) + } +} + // === Helper Functions === private inline fun catch(f: () -> EvaluateResult): EvaluateResult = 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 7c10b9d4bca..c507cce61d9 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 @@ -1786,7 +1786,7 @@ abstract class Expr internal constructor() { FunctionExpr("str_concat", evaluateStrConcat, fieldName, *otherStrings) internal fun map(elements: Array): Expr = - FunctionExpr("map", notImplemented, elements) + FunctionExpr("map", evaluateMap, elements) /** * Creates an expression that creates a Firestore map value from an input object. From 6a024dcb1cc5a08b1611a4f1fabe93ce73db6e93 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 27 May 2025 16:52:43 -0400 Subject: [PATCH 082/152] Add array --- .../google/firebase/firestore/model/Values.kt | 22 +++++++++---------- .../firestore/pipeline/EvaluateResult.kt | 1 + .../firebase/firestore/pipeline/evaluation.kt | 11 ++++++++++ .../firestore/pipeline/expressions.kt | 20 +++++++++++++++++ 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 7b78ee4780a..705a4aa596a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -652,16 +652,8 @@ internal object Values { @JvmStatic fun encodeValue(date: Date): Value = encodeValue(com.google.firebase.Timestamp((date))) @JvmStatic - fun encodeValue(timestamp: com.google.firebase.Timestamp): Value { - // Firestore backend truncates precision down to microseconds. To ensure offline mode works - // the same with regards to truncation, perform the truncation immediately without waiting for - // the backend to do that. - val truncatedNanoseconds: Int = timestamp.nanoseconds / 1000 * 1000 - - return encodeValue( - Timestamp.newBuilder().setSeconds(timestamp.seconds).setNanos(truncatedNanoseconds).build() - ) - } + fun encodeValue(timestamp: com.google.firebase.Timestamp): Value = + encodeValue(timestamp(timestamp.seconds, timestamp.nanoseconds)) @JvmStatic fun encodeValue(value: Timestamp): Value = Value.newBuilder().setTimestampValue(value).build() @@ -736,6 +728,12 @@ internal object Values { } @JvmStatic - fun timestamp(seconds: Long, nanos: Int): Timestamp = - Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build() + fun timestamp(seconds: Long, nanos: Int): Timestamp { + // Firestore backend truncates precision down to microseconds. To ensure offline mode works + // the same with regards to truncation, perform the truncation immediately without waiting for + // the backend to do that. + val truncatedNanoseconds: Int = nanos / 1000 * 1000 + + return Timestamp.newBuilder().setSeconds(seconds).setNanos(truncatedNanoseconds).build() + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt index 84ee187cfd9..f221a85214b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt @@ -17,6 +17,7 @@ internal sealed class EvaluateResult(val value: Value?) { fun long(long: Long) = EvaluateResultValue(encodeValue(long)) fun long(int: Int) = EvaluateResultValue(encodeValue(int.toLong())) fun string(string: String) = EvaluateResultValue(encodeValue(string)) + fun list(list: List) = EvaluateResultValue(encodeValue(list)) fun timestamp(timestamp: Timestamp): EvaluateResult = EvaluateResultValue(encodeValue(timestamp)) fun timestamp(seconds: Long, nanos: Int): EvaluateResult = diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index aae36893ecf..e03f689cdea 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -218,6 +218,8 @@ internal val evaluateSubtract = arithmeticPrimitive(Math::subtractExact, Double: // === Array Functions === +internal val evaluateArray = variadicNullableValueFunction(EvaluateResult.Companion::list) + internal val evaluateEqAny = notImplemented internal val evaluateNotEqAny = notImplemented @@ -632,6 +634,15 @@ private inline fun variadicFunction( } } +@JvmName("variadicNullableValueFunction") +private inline fun variadicNullableValueFunction( + crossinline function: (List) -> EvaluateResult +): EvaluateFunction = { params -> + block@{ input: MutableDocument -> + catch { function(params.map { p -> p(input).value ?: return@block EvaluateResultError }) } + } +} + @JvmName("variadicStringFunction") private inline fun variadicFunction( crossinline function: (List) -> EvaluateResult 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 c507cce61d9..fa5d4464601 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 @@ -2601,6 +2601,26 @@ abstract class Expr internal constructor() { fun lte(fieldName: String, value: Any): BooleanExpr = BooleanExpr("lte", evaluateLte, fieldName, value) + /** + * Creates an expression that creates a Firestore array value from an input array. + * + * @param elements The input array to evaluate in the expression. + * @return A new [Expr] representing the array function. + */ + @JvmStatic + fun array(vararg elements: Expr): Expr = + FunctionExpr("array", evaluateArray, elements) + + /** + * Creates an expression that creates a Firestore array value from an input array. + * + * @param elements The input array to evaluate in the expression. + * @return A new [Expr] representing the array function. + */ + @JvmStatic + fun array(elements: List): Expr = + FunctionExpr("array", evaluateArray, elements.toTypedArray()) + /** * Creates an expression that concatenates an array with other arrays. * From 13077e6e8bf7b372561524c732477bfdd00c5cea Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 28 May 2025 13:47:33 -0400 Subject: [PATCH 083/152] Remove broken test --- .../firebase/firestore/core/PipelineTests.kt | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt index 85d21574cf6..459e9c173d9 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt @@ -4,12 +4,8 @@ import com.google.common.truth.Truth.assertThat import com.google.firebase.firestore.RealtimePipelineSource import com.google.firebase.firestore.TestUtil import com.google.firebase.firestore.model.MutableDocument -import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.pipeline.Expr.Companion.field -import com.google.firebase.firestore.pipeline.minus -import com.google.firebase.firestore.pipeline.plus import com.google.firebase.firestore.testutil.TestUtilKtx.doc -import com.google.protobuf.Timestamp import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking @@ -30,33 +26,4 @@ internal class PipelineTests { assertThat(list).hasSize(1) } - - @Test - fun xxx(): Unit = runBlocking { - val zero: Timestamp = Values.timestamp(0, 0) - - assertThat(plus(zero, 0, 0)) - .isEqualTo(zero) - - assertThat(plus(Values.timestamp(1, 1), 1, 1)) - .isEqualTo(Values.timestamp(2, 2)) - - assertThat(plus(Values.timestamp(1, 1), 0, 1)) - .isEqualTo(Values.timestamp(1, 2)) - - assertThat(plus(Values.timestamp(1, 1), 1, 0)) - .isEqualTo(Values.timestamp(2, 1)) - - assertThat(minus(zero, 0, 0)) - .isEqualTo(zero) - - assertThat(minus(Values.timestamp(1, 1), 1, 1)) - .isEqualTo(zero) - - assertThat(minus(Values.timestamp(1, 1), 0, 1)) - .isEqualTo(Values.timestamp(1, 0)) - - assertThat(minus(Values.timestamp(1, 1), 1, 0)) - .isEqualTo(Values.timestamp(0, 1)) - } } From 742546d4b09e6b8b9b1e6e62141e0be46ce59b55 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 28 May 2025 16:34:15 -0400 Subject: [PATCH 084/152] Add Arithmetic tests --- .../firestore/pipeline/EvaluateResult.kt | 16 +- .../firebase/firestore/pipeline/evaluation.kt | 2 +- .../firestore/pipeline/ArithmeticTests.kt | 549 ++++++++++++++++++ .../firebase/firestore/pipeline/testUtil.kt | 15 + 4 files changed, 576 insertions(+), 6 deletions(-) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt index f221a85214b..801420e4653 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt @@ -6,6 +6,8 @@ import com.google.firestore.v1.Value import com.google.protobuf.Timestamp internal sealed class EvaluateResult(val value: Value?) { + abstract val isError: Boolean + companion object { val TRUE: EvaluateResultValue = EvaluateResultValue(Values.TRUE_VALUE) val FALSE: EvaluateResultValue = EvaluateResultValue(Values.FALSE_VALUE) @@ -24,12 +26,16 @@ internal sealed class EvaluateResult(val value: Value?) { if (seconds !in -62_135_596_800 until 253_402_300_800) EvaluateResultError else timestamp(Values.timestamp(seconds, nanos)) } - internal inline fun evaluateNonNull(f: (Value) -> EvaluateResult): EvaluateResult = - if (value?.hasNullValue() == true) f(value) else this } -internal object EvaluateResultError : EvaluateResult(null) +internal object EvaluateResultError : EvaluateResult(null) { + override val isError: Boolean = true +} -internal object EvaluateResultUnset : EvaluateResult(null) +internal object EvaluateResultUnset : EvaluateResult(null) { + override val isError: Boolean = false +} -internal class EvaluateResultValue(value: Value) : EvaluateResult(value) +internal class EvaluateResultValue(value: Value) : EvaluateResult(value) { + override val isError: Boolean = false +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index e03f689cdea..bfd714b7233 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -138,7 +138,7 @@ internal val evaluateDivide = arithmeticPrimitive(Long::div, Double::div) internal val evaluateFloor = arithmeticPrimitive({ it }, Math::floor) -internal val evaluateMod = arithmeticPrimitive(Long::mod, Double::mod) +internal val evaluateMod = arithmeticPrimitive(Long::rem, Double::rem) internal val evaluateMultiply: EvaluateFunction = arithmeticPrimitive(Math::multiplyExact, Double::times) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt new file mode 100644 index 00000000000..e3af3b049f7 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt @@ -0,0 +1,549 @@ +package com.google.firebase.firestore.pipeline + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.firestore.model.Values.encodeValue // Returns com.google.protobuf.Value +import com.google.firebase.firestore.pipeline.Expr.Companion.add +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.subtract +import com.google.firebase.firestore.pipeline.Expr.Companion.multiply +import com.google.firebase.firestore.pipeline.Expr.Companion.divide +import com.google.firebase.firestore.pipeline.Expr.Companion.mod +import org.junit.Test + +internal class ArithmeticTests { + + @Test + fun addFunctionTestWithBasicNumerics() { + assertThat(evaluate(add(constant(1L), constant(2L))).value) + .isEqualTo(encodeValue(3L)) + assertThat(evaluate(add(constant(1L), constant(2.5))).value) + .isEqualTo(encodeValue(3.5)) + assertThat(evaluate(add(constant(1.0), constant(2L))).value) + .isEqualTo(encodeValue(3.0)) + assertThat(evaluate(add(constant(1.0), constant(2.0))).value) + .isEqualTo(encodeValue(3.0)) + } + + @Test + fun addFunctionTestWithBasicNonNumerics() { + assertThat(evaluate(add(constant(1L), constant("1"))).isError).isTrue() + assertThat(evaluate(add(constant("1"), constant(1.0))).isError).isTrue() + assertThat(evaluate(add(constant("1"), constant("1"))).isError).isTrue() + } + + @Test + fun addFunctionTestWithDoubleLongAdditionOverflow() { + val longMaxAsDoublePlusOne = Long.MAX_VALUE.toDouble() + 1.0 + assertThat(evaluate(add(constant(Long.MAX_VALUE), constant(1.0))).value) + .isEqualTo(encodeValue(longMaxAsDoublePlusOne)) + + val intermediate = longMaxAsDoublePlusOne + assertThat(evaluate(add(constant(intermediate), constant(100L))).value) + .isEqualTo(encodeValue(intermediate + 100.0)) + } + + @Test + fun addFunctionTestWithDoubleAdditionOverflow() { + assertThat(evaluate(add(constant(Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(add(constant(-Double.MAX_VALUE), constant(-Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun addFunctionTestWithSumPosAndNegInfinityReturnNaN() { + assertThat(evaluate(add(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.NaN)) + } + + @Test + fun addFunctionTestWithLongAdditionOverflow() { + assertThat(evaluate(add(constant(Long.MAX_VALUE), constant(1L))).isError).isTrue() + assertThat(evaluate(add(constant(Long.MIN_VALUE), constant(-1L))).isError).isTrue() + assertThat(evaluate(add(constant(1L), constant(Long.MAX_VALUE))).isError).isTrue() + } + + @Test + fun addFunctionTestWithNanNumberReturnNaN() { + val nanVal = Double.NaN + assertThat(evaluate(add(constant(1L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(1.0), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(-9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(Double.MAX_VALUE), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(-Double.MAX_VALUE), constant(nanVal))).value) // Corresponds to C++ std::numeric_limits::lowest() + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + } + + @Test + fun addFunctionTestWithNanNotNumberTypeReturnError() { + assertThat(evaluate(add(constant(Double.NaN), constant("hello world"))).isError).isTrue() + } + + @Test + fun addFunctionTestWithMultiArgument() { + assertThat(evaluate(add(add(constant(1L), constant(2L)), constant(3L))).value) + .isEqualTo(encodeValue(6L)) + assertThat(evaluate(add(add(constant(1.0), constant(2L)), constant(3L))).value) + .isEqualTo(encodeValue(6.0)) + } + + // --- Subtract Tests (Ported) --- + @Test + fun subtractFunctionTestWithBasicNumerics() { + assertThat(evaluate(subtract(constant(1L), constant(2L))).value) + .isEqualTo(encodeValue(-1L)) + assertThat(evaluate(subtract(constant(1L), constant(2.5))).value) + .isEqualTo(encodeValue(-1.5)) + assertThat(evaluate(subtract(constant(1.0), constant(2L))).value) + .isEqualTo(encodeValue(-1.0)) + assertThat(evaluate(subtract(constant(1.0), constant(2.0))).value) + .isEqualTo(encodeValue(-1.0)) + } + + @Test + fun subtractFunctionTestWithBasicNonNumerics() { + assertThat(evaluate(subtract(constant(1L), constant("1"))).isError).isTrue() + assertThat(evaluate(subtract(constant("1"), constant(1.0))).isError).isTrue() + assertThat(evaluate(subtract(constant("1"), constant("1"))).isError).isTrue() + } + + @Test + fun subtractFunctionTestWithDoubleSubtractionOverflow() { + assertThat(evaluate(subtract(constant(-Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat(evaluate(subtract(constant(Double.MAX_VALUE), constant(-Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + } + + @Test + fun subtractFunctionTestWithLongSubtractionOverflow() { + assertThat(evaluate(subtract(constant(Long.MIN_VALUE), constant(1L))).isError).isTrue() + assertThat(evaluate(subtract(constant(Long.MAX_VALUE), constant(-1L))).isError).isTrue() + } + + @Test + fun subtractFunctionTestWithNanNumberReturnNaN() { + val nanVal = Double.NaN + assertThat(evaluate(subtract(constant(1L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(1.0), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(-9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(Double.MAX_VALUE), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(-Double.MAX_VALUE), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + } + + @Test + fun subtractFunctionTestWithNanNotNumberTypeReturnError() { + assertThat(evaluate(subtract(constant(Double.NaN), constant("hello world"))).isError).isTrue() + } + + @Test + fun subtractFunctionTestWithPositiveInfinity() { + assertThat(evaluate(subtract(constant(Double.POSITIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(subtract(constant(1L), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun subtractFunctionTestWithNegativeInfinity() { + assertThat(evaluate(subtract(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat(evaluate(subtract(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + } + + @Test + fun subtractFunctionTestWithPositiveInfinityNegativeInfinity() { + assertThat(evaluate(subtract(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(subtract(constant(Double.NEGATIVE_INFINITY), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + // --- Multiply Tests (Ported) --- + @Test + fun multiplyFunctionTestWithBasicNumerics() { + assertThat(evaluate(multiply(constant(1L), constant(2L))).value) + .isEqualTo(encodeValue(2L)) + assertThat(evaluate(multiply(constant(3L), constant(2.5))).value) + .isEqualTo(encodeValue(7.5)) + assertThat(evaluate(multiply(constant(1.0), constant(2L))).value) + .isEqualTo(encodeValue(2.0)) + assertThat(evaluate(multiply(constant(1.32), constant(2.0))).value) + .isEqualTo(encodeValue(2.64)) + } + + @Test + fun multiplyFunctionTestWithBasicNonNumerics() { + assertThat(evaluate(multiply(constant(1L), constant("1"))).isError).isTrue() + assertThat(evaluate(multiply(constant("1"), constant(1.0))).isError).isTrue() + assertThat(evaluate(multiply(constant("1"), constant("1"))).isError).isTrue() + } + + @Test + fun multiplyFunctionTestWithDoubleLongMultiplicationOverflow() { + assertThat(evaluate(multiply(constant(Long.MAX_VALUE), constant(100.0))).value) + .isEqualTo(encodeValue(Long.MAX_VALUE.toDouble() * 100.0)) + assertThat(evaluate(multiply(constant(Long.MAX_VALUE), constant(100L))).isError).isTrue() + } + + @Test + fun multiplyFunctionTestWithDoubleMultiplicationOverflow() { + assertThat(evaluate(multiply(constant(Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(multiply(constant(-Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun multiplyFunctionTestWithLongMultiplicationOverflow() { + assertThat(evaluate(multiply(constant(Long.MAX_VALUE), constant(10L))).isError).isTrue() + assertThat(evaluate(multiply(constant(Long.MIN_VALUE), constant(10L))).isError).isTrue() + assertThat(evaluate(multiply(constant(-10L), constant(Long.MAX_VALUE))).isError).isTrue() + assertThat(evaluate(multiply(constant(-10L), constant(Long.MIN_VALUE))).isError).isTrue() + } + + @Test + fun multiplyFunctionTestWithNanNumberReturnNaN() { + val nanVal = Double.NaN + assertThat(evaluate(multiply(constant(1L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(1.0), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(-9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(Double.MAX_VALUE), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(-Double.MAX_VALUE), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + } + + @Test + fun multiplyFunctionTestWithNanNotNumberTypeReturnError() { + assertThat(evaluate(multiply(constant(Double.NaN), constant("hello world"))).isError).isTrue() + } + + @Test + fun multiplyFunctionTestWithPositiveInfinity() { + assertThat(evaluate(multiply(constant(Double.POSITIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(multiply(constant(1L), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + } + + @Test + fun multiplyFunctionTestWithNegativeInfinity() { + assertThat(evaluate(multiply(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat(evaluate(multiply(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun multiplyFunctionTestWithPositiveInfinityNegativeInfinityReturnsNegativeInfinity() { + assertThat(evaluate(multiply(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat(evaluate(multiply(constant(Double.NEGATIVE_INFINITY), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun multiplyFunctionTestWithMultiArgument() { + assertThat(evaluate(multiply(multiply(constant(1L), constant(2L)), constant(3L))).value) + .isEqualTo(encodeValue(6L)) + assertThat(evaluate(multiply(constant(1.0), multiply(constant(2L), constant(3L)))).value) + .isEqualTo(encodeValue(6.0)) + } + + // --- Divide Tests (Ported) --- + @Test + fun divideFunctionTestWithBasicNumerics() { + assertThat(evaluate(divide(constant(10L), constant(2L))).value) + .isEqualTo(encodeValue(5L)) + assertThat(evaluate(divide(constant(10L), constant(2.0))).value) + .isEqualTo(encodeValue(5.0)) + assertThat(evaluate(divide(constant(10.0), constant(3L))).value) + .isEqualTo(encodeValue(10.0 / 3.0)) + assertThat(evaluate(divide(constant(10.0), constant(7.0))).value) + .isEqualTo(encodeValue(10.0 / 7.0)) + } + + @Test + fun divideFunctionTestWithBasicNonNumerics() { + assertThat(evaluate(divide(constant(1L), constant("1"))).isError).isTrue() + assertThat(evaluate(divide(constant("1"), constant(1.0))).isError).isTrue() + assertThat(evaluate(divide(constant("1"), constant("1"))).isError).isTrue() + } + + @Test + fun divideFunctionTestWithLongDivision() { + assertThat(evaluate(divide(constant(10L), constant(3L))).value) + .isEqualTo(encodeValue(3L)) + assertThat(evaluate(divide(constant(-10L), constant(3L))).value) + .isEqualTo(encodeValue(-3L)) + assertThat(evaluate(divide(constant(10L), constant(-3L))).value) + .isEqualTo(encodeValue(-3L)) + assertThat(evaluate(divide(constant(-10L), constant(-3L))).value) + .isEqualTo(encodeValue(3L)) + } + + @Test + fun divideFunctionTestWithDoubleDivisionOverflow() { + assertThat(evaluate(divide(constant(Double.MAX_VALUE), constant(0.5))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(divide(constant(-Double.MAX_VALUE), constant(0.5))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun divideFunctionTestWithByZero() { + assertThat(evaluate(divide(constant(1L), constant(0L))).isError).isTrue() + assertThat(evaluate(divide(constant(1.1), constant(0.0))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(divide(constant(1.1), constant(-0.0))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat(evaluate(divide(constant(0.0), constant(0.0))).value) + .isEqualTo(encodeValue(Double.NaN)) + } + + @Test + fun divideFunctionTestWithNanNumberReturnNaN() { + val nanVal = Double.NaN + assertThat(evaluate(divide(constant(1L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(nanVal), constant(1L))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(1.0), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(nanVal), constant(1.0))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(nanVal), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(nanVal), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(nanVal)) + } + + @Test + fun divideFunctionTestWithNanNotNumberTypeReturnError() { + assertThat(evaluate(divide(constant(Double.NaN), constant("hello world"))).isError).isTrue() + } + + @Test + fun divideFunctionTestWithPositiveInfinity() { + assertThat(evaluate(divide(constant(Double.POSITIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(divide(constant(1L), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(0.0)) + } + + @Test + fun divideFunctionTestWithNegativeInfinity() { + assertThat(evaluate(divide(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat(evaluate(divide(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(-0.0)) + } + + @Test + fun divideFunctionTestWithPositiveInfinityNegativeInfinityReturnsNan() { + assertThat(evaluate(divide(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.NaN)) + assertThat(evaluate(divide(constant(Double.NEGATIVE_INFINITY), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.NaN)) + } + + // --- Mod Tests (Ported) --- + @Test + fun modFunctionTestWithDivisorZero() { + assertThat(evaluate(mod(constant(42L), constant(0L))).isError).isTrue() + assertThat(evaluate(mod(constant(42.0), constant(0.0))).value) + .isEqualTo(encodeValue(Double.NaN)) + assertThat(evaluate(mod(constant(42.0), constant(-0.0))).value) + .isEqualTo(encodeValue(Double.NaN)) + } + + @Test + fun modFunctionTestWithDividendZeroReturnsZero() { + assertThat(evaluate(mod(constant(0L), constant(42L))).value) + .isEqualTo(encodeValue(0L)) + assertThat(evaluate(mod(constant(0.0), constant(42.0))).value) + .isEqualTo(encodeValue(0.0)) + assertThat(evaluate(mod(constant(-0.0), constant(42.0))).value) + .isEqualTo(encodeValue(-0.0)) + } + + @Test + fun modFunctionTestWithLongPositivePositive() { + assertThat(evaluate(mod(constant(10L), constant(3L))).value) + .isEqualTo(encodeValue(1L)) + } + + @Test + fun modFunctionTestWithLongNegativeNegative() { + assertThat(evaluate(mod(constant(-10L), constant(-3L))).value) + .isEqualTo(encodeValue(-1L)) + } + + @Test + fun modFunctionTestWithLongPositiveNegative() { + assertThat(evaluate(mod(constant(10L), constant(-3L))).value) + .isEqualTo(encodeValue(1L)) + } + + @Test + fun modFunctionTestWithLongNegativePositive() { + assertThat(evaluate(mod(constant(-10L), constant(3L))).value) + .isEqualTo(encodeValue(-1L)) + } + + @Test + fun modFunctionTestWithDoublePositivePositive() { + // 10.5 % 3.0 is exactly 1.5 + assertThat(evaluate(mod(constant(10.5), constant(3.0))).value) + .isEqualTo(encodeValue(1.5)) + } + + @Test + fun modFunctionTestWithDoubleNegativeNegative() { + val resultValue = evaluate(mod(constant(-7.3), constant(-1.8))).value + assertThat(resultValue?.doubleValue).isWithin(1e-9).of(-0.1) + } + + @Test + fun modFunctionTestWithDoublePositiveNegative() { + val resultValue = evaluate(mod(constant(9.8), constant(-2.5))).value + assertThat(resultValue?.doubleValue).isWithin(1e-9).of(2.3) + } + + @Test + fun modFunctionTestWithDoubleNegativePositive() { + val resultValue = evaluate(mod(constant(-7.5), constant(2.3))).value + assertThat(resultValue?.doubleValue).isWithin(1e-9).of(-0.6) + } + + @Test + fun modFunctionTestWithLongPerfectlyDivisible() { + assertThat(evaluate(mod(constant(10L), constant(5L))).value) + .isEqualTo(encodeValue(0L)) + assertThat(evaluate(mod(constant(-10L), constant(5L))).value) + .isEqualTo(encodeValue(0L)) + assertThat(evaluate(mod(constant(10L), constant(-5L))).value) + .isEqualTo(encodeValue(0L)) + assertThat(evaluate(mod(constant(-10L), constant(-5L))).value) + .isEqualTo(encodeValue(0L)) + } + + @Test + fun modFunctionTestWithDoublePerfectlyDivisible() { + assertThat(evaluate(mod(constant(10.0), constant(2.5))).value) + .isEqualTo(encodeValue(0.0)) + assertThat(evaluate(mod(constant(10.0), constant(-2.5))).value) + .isEqualTo(encodeValue(0.0)) + assertThat(evaluate(mod(constant(-10.0), constant(2.5))).value) + .isEqualTo(encodeValue(-0.0)) + assertThat(evaluate(mod(constant(-10.0), constant(-2.5))).value) + .isEqualTo(encodeValue(-0.0)) + } + + @Test + fun modFunctionTestWithNonNumericsReturnError() { + assertThat(evaluate(mod(constant(10L), constant("1"))).isError).isTrue() + assertThat(evaluate(mod(constant("1"), constant(10L))).isError).isTrue() + assertThat(evaluate(mod(constant("1"), constant("1"))).isError).isTrue() + } + + @Test + fun modFunctionTestWithNanNumberReturnNaN() { + val nanVal = Double.NaN + assertThat(evaluate(mod(constant(1L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(mod(constant(1.0), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + } + + @Test + fun modFunctionTestWithNanNotNumberTypeReturnError() { + assertThat(evaluate(mod(constant(Double.NaN), constant("hello world"))).isError).isTrue() + } + + @Test + fun modFunctionTestWithNumberPosInfinityReturnSelf() { + assertThat(evaluate(mod(constant(1L), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(1.0)) + assertThat(evaluate(mod(constant(42.123), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(42.123)) + assertThat(evaluate(mod(constant(-99.9), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(-99.9)) + } + + @Test + fun modFunctionTestWithPosInfinityNumberReturnNaN() { + assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.NaN)) + assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(42.123))).value) + .isEqualTo(encodeValue(Double.NaN)) + assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(-99.9))).value) + .isEqualTo(encodeValue(Double.NaN)) + } + + @Test + fun modFunctionTestWithNumberNegInfinityReturnSelf() { + assertThat(evaluate(mod(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(1.0)) + assertThat(evaluate(mod(constant(42.123), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(42.123)) + assertThat(evaluate(mod(constant(-99.9), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(-99.9)) + } + + @Test + fun modFunctionTestWithNegInfinityNumberReturnNaN() { + assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.NaN)) + assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(42.123))).value) + .isEqualTo(encodeValue(Double.NaN)) + assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(-99.9))).value) + .isEqualTo(encodeValue(Double.NaN)) + } + + @Test + fun modFunctionTestWithPosAndNegInfinityReturnNaN() { + assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.NaN)) + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt new file mode 100644 index 00000000000..f293b3e49a2 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt @@ -0,0 +1,15 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firebase.firestore.UserDataReader +import com.google.firebase.firestore.model.DatabaseId +import com.google.firebase.firestore.model.MutableDocument +import com.google.firebase.firestore.testutil.TestUtilKtx.doc + +val DATABASE_ID = UserDataReader(DatabaseId.forDatabase("projectId", "databaseId")) +val EMPTY_DOC: MutableDocument = doc("foo/1", 0, mapOf()) +internal val EVALUATION_CONTEXT = EvaluationContext(DATABASE_ID) + +internal fun evaluate(expr: Expr): EvaluateResult { + val function = expr.evaluateContext(EVALUATION_CONTEXT) + return function(EMPTY_DOC) +} \ No newline at end of file From 5eff0fa68fd9b4800ad34de572c08710b935837f Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 28 May 2025 23:10:36 -0400 Subject: [PATCH 085/152] Add comparison tests --- .../google/firebase/firestore/model/Values.kt | 87 +- .../firestore/pipeline/EvaluateResult.kt | 5 + .../firebase/firestore/pipeline/evaluation.kt | 126 +- .../firestore/pipeline/expressions.kt | 41 +- .../firestore/pipeline/ArithmeticTests.kt | 1058 ++++++++--------- .../firebase/firestore/pipeline/testUtil.kt | 37 +- 6 files changed, 707 insertions(+), 647 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 705a4aa596a..79a4363fe23 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -107,7 +107,8 @@ internal object Values { } } - fun strictEquals(left: Value, right: Value): Boolean { + fun strictEquals(left: Value, right: Value): Boolean? { + if (left.hasNullValue() || right.hasNullValue()) return null val leftType = typeOrder(left) val rightType = typeOrder(right) if (leftType != rightType) { @@ -115,7 +116,7 @@ internal object Values { } return when (leftType) { - TYPE_ORDER_NULL -> false + TYPE_ORDER_NULL -> null TYPE_ORDER_NUMBER -> strictNumberEquals(left, right) TYPE_ORDER_ARRAY -> strictArrayEquals(left, right) TYPE_ORDER_VECTOR, @@ -127,6 +128,15 @@ internal object Values { } } + fun strictCompare(left: Value, right: Value): Int? { + val leftType = typeOrder(left) + val rightType = typeOrder(right) + if (leftType != rightType) { + return null + } + return compareInternal(leftType, left, right) + } + @JvmStatic fun equals(left: Value?, right: Value?): Boolean { if (left === right) { @@ -156,29 +166,33 @@ internal object Values { } private fun strictNumberEquals(left: Value, right: Value): Boolean { - if (left.valueTypeCase != right.valueTypeCase) { - return false - } - return when (left.valueTypeCase) { - ValueTypeCase.INTEGER_VALUE -> left.integerValue == right.integerValue - ValueTypeCase.DOUBLE_VALUE -> left.doubleValue == right.doubleValue - else -> false - } + if (left.doubleValue.isNaN() || right.doubleValue.isNaN()) return false + return numberEquals(left, right) } - private fun numberEquals(left: Value, right: Value): Boolean { - if (left.valueTypeCase != right.valueTypeCase) { - return false - } - return when (left.valueTypeCase) { - ValueTypeCase.INTEGER_VALUE -> left.integerValue == right.integerValue + private fun numberEquals(left: Value, right: Value): Boolean = + when (left.valueTypeCase) { + ValueTypeCase.INTEGER_VALUE -> + when (right.valueTypeCase) { + ValueTypeCase.INTEGER_VALUE -> left.integerValue == right.integerValue + ValueTypeCase.DOUBLE_VALUE -> right.doubleValue.compareTo(left.integerValue) == 0 + else -> false + } ValueTypeCase.DOUBLE_VALUE -> - doubleToLongBits(left.doubleValue) == doubleToLongBits(right.doubleValue) + when (right.valueTypeCase) { + ValueTypeCase.INTEGER_VALUE -> + compareDoubleWithLong(left.doubleValue, right.integerValue) == 0 + ValueTypeCase.DOUBLE_VALUE -> + doubleToLongBits(left.doubleValue) == doubleToLongBits(right.doubleValue) + else -> false + } else -> false } - } - private fun strictArrayEquals(left: Value, right: Value): Boolean { + private fun compareDoubleWithLong(double: Double, long: Long): Int = + if (double.isNaN()) -1 else double.compareTo(long) + + private fun strictArrayEquals(left: Value, right: Value): Boolean? { val leftArray = left.arrayValue val rightArray = right.arrayValue @@ -186,13 +200,16 @@ internal object Values { return false } + var foundNull = false for (i in 0 until leftArray.valuesCount) { - if (!strictEquals(leftArray.getValues(i), rightArray.getValues(i))) { + val equals = strictEquals(leftArray.getValues(i), rightArray.getValues(i)) + if (equals === null) { + foundNull = true + } else if (!equals) { return false } } - - return true + return if (foundNull) null else true } private fun arrayEquals(left: Value, right: Value): Boolean { @@ -212,7 +229,7 @@ internal object Values { return true } - private fun strictObjectEquals(left: Value, right: Value): Boolean { + private fun strictObjectEquals(left: Value, right: Value): Boolean? { val leftMap = left.mapValue val rightMap = right.mapValue @@ -220,14 +237,18 @@ internal object Values { return false } + var foundNull = false for ((key, value) in leftMap.fieldsMap) { val otherEntry = rightMap.fieldsMap[key] ?: return false - if (!strictEquals(value, otherEntry)) { + val equals = strictEquals(value, otherEntry) + if (equals === null) { + foundNull = true + } else if (!equals) { return false } } - return true + return if (foundNull) null else true } private fun objectEquals(left: Value, right: Value): Boolean { @@ -268,7 +289,11 @@ internal object Values { return Util.compareIntegers(leftType, rightType) } - return when (leftType) { + return compareInternal(leftType, left, right) + } + + private fun compareInternal(leftType: Int, left: Value, right: Value): Int = + when (leftType) { TYPE_ORDER_NULL, TYPE_ORDER_MAX_VALUE -> 0 TYPE_ORDER_BOOLEAN -> Util.compareBooleans(left.booleanValue, right.booleanValue) @@ -288,7 +313,6 @@ internal object Values { TYPE_ORDER_VECTOR -> compareVectors(left.mapValue, right.mapValue) else -> throw Assert.fail("Invalid value type: $leftType") } - } @JvmStatic fun lowerBoundCompare( @@ -658,14 +682,11 @@ internal object Values { @JvmStatic fun encodeValue(value: Timestamp): Value = Value.newBuilder().setTimestampValue(value).build() - @JvmField - val TRUE_VALUE: Value = Value.newBuilder().setBooleanValue(true).build() + @JvmField val TRUE_VALUE: Value = Value.newBuilder().setBooleanValue(true).build() - @JvmField - val FALSE_VALUE: Value = Value.newBuilder().setBooleanValue(false).build() + @JvmField val FALSE_VALUE: Value = Value.newBuilder().setBooleanValue(false).build() - @JvmStatic - fun encodeValue(value: Boolean): Value = if (value) TRUE_VALUE else FALSE_VALUE + @JvmStatic fun encodeValue(value: Boolean): Value = if (value) TRUE_VALUE else FALSE_VALUE @JvmStatic fun encodeValue(geoPoint: GeoPoint): Value = diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt index 801420e4653..ecd3c5d0e99 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt @@ -7,6 +7,10 @@ import com.google.protobuf.Timestamp internal sealed class EvaluateResult(val value: Value?) { abstract val isError: Boolean + val isSuccess: Boolean + get() = this is EvaluateResultValue + val isUnset: Boolean + get() = this is EvaluateResultUnset companion object { val TRUE: EvaluateResultValue = EvaluateResultValue(Values.TRUE_VALUE) @@ -14,6 +18,7 @@ internal sealed class EvaluateResult(val value: Value?) { val NULL: EvaluateResultValue = EvaluateResultValue(Values.NULL_VALUE) val DOUBLE_ZERO: EvaluateResultValue = double(0.0) val LONG_ZERO: EvaluateResultValue = long(0) + fun boolean(boolean: Boolean?) = if (boolean === null) NULL else boolean(boolean) fun boolean(boolean: Boolean) = if (boolean) TRUE else FALSE fun double(double: Double) = EvaluateResultValue(encodeValue(double)) fun long(long: Long) = EvaluateResultValue(encodeValue(long)) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index bfd714b7233..1adbbc23f8a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -1,4 +1,5 @@ @file:JvmName("Evaluation") + package com.google.firebase.firestore.pipeline import com.google.common.math.LongMath @@ -10,6 +11,8 @@ import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.model.Values.isNanValue +import com.google.firebase.firestore.model.Values.strictCompare +import com.google.firebase.firestore.model.Values.strictEquals import com.google.firebase.firestore.util.Assert import com.google.firestore.v1.Value import com.google.protobuf.ByteString @@ -76,17 +79,37 @@ internal val evaluateXor: EvaluateFunction = variadicFunction { values: BooleanA // === Comparison Functions === -internal val evaluateEq: EvaluateFunction = comparison(Values::strictEquals) +internal val evaluateEq: EvaluateFunction = binaryFunction { p1: Value, p2: Value -> + EvaluateResult.boolean(strictEquals(p1, p2)) +} -internal val evaluateNeq: EvaluateFunction = comparison { v1, v2 -> !Values.strictEquals(v1, v2) } +internal val evaluateNeq: EvaluateFunction = binaryFunction { p1: Value, p2: Value -> + EvaluateResult.boolean(strictEquals(p1, p2)?.not()) +} -internal val evaluateGt: EvaluateFunction = comparison { v1, v2 -> Values.compare(v1, v2) > 0 } +internal val evaluateGt: EvaluateFunction = comparison { v1, v2 -> + (strictCompare(v1, v2) ?: return@comparison false) > 0 +} -internal val evaluateGte: EvaluateFunction = comparison { v1, v2 -> Values.compare(v1, v2) >= 0 } +internal val evaluateGte: EvaluateFunction = comparison { v1, v2 -> + when (strictEquals(v1, v2)) { + true -> true + false -> (strictCompare(v1, v2) ?: return@comparison false) > 0 + null -> null + } +} -internal val evaluateLt: EvaluateFunction = comparison { v1, v2 -> Values.compare(v1, v2) < 0 } +internal val evaluateLt: EvaluateFunction = comparison { v1, v2 -> + (strictCompare(v1, v2) ?: return@comparison false) < 0 +} -internal val evaluateLte: EvaluateFunction = comparison { v1, v2 -> Values.compare(v1, v2) <= 0 } +internal val evaluateLte: EvaluateFunction = comparison { v1, v2 -> + when (strictEquals(v1, v2)) { + true -> true + false -> (strictCompare(v1, v2) ?: return@comparison false) < 0 + null -> null + } +} internal val evaluateNot: EvaluateFunction = unaryFunction { b: Boolean -> EvaluateResult.boolean(b.not()) @@ -297,52 +320,48 @@ internal fun plus(t: Timestamp, seconds: Long, nanos: Long): Timestamp = } private fun plus(t: Timestamp, seconds: Long): Timestamp = - if (seconds == 0L) t - else Values.timestamp(checkedAdd(t.seconds, seconds), t.nanos) + if (seconds == 0L) t else Values.timestamp(checkedAdd(t.seconds, seconds), t.nanos) internal fun minus(t: Timestamp, seconds: Long, nanos: Long): Timestamp = if (nanos == 0L) { minus(t, seconds) } else { val nanoSum = t.nanos - nanos // Overflow not possible since nanos is 0 to 1 000 000. - val secondsSum: Long = checkedSubtract(t.seconds, checkedSubtract(seconds, nanoSum / L_NANOS_PER_SECOND)) + val secondsSum: Long = + checkedSubtract(t.seconds, checkedSubtract(seconds, nanoSum / L_NANOS_PER_SECOND)) Values.timestamp(secondsSum, (nanoSum % I_NANOS_PER_SECOND).toInt()) } private fun minus(t: Timestamp, seconds: Long): Timestamp = - if (seconds == 0L) t - else Values.timestamp(checkedSubtract(t.seconds, seconds), t.nanos) - - -internal val evaluateTimestampAdd = - ternaryTimestampFunction { t: Timestamp, u: String, n: Long -> - EvaluateResult.timestamp( - when (u) { - "microsecond" -> plus(t, n / L_MICROS_PER_SECOND, (n % L_MICROS_PER_SECOND) * 1000) - "millisecond" -> plus(t, n / L_MILLIS_PER_SECOND, (n % L_MILLIS_PER_SECOND) * 1000_000) - "second" -> plus(t, n) - "minute" -> plus(t, checkedMultiply(n, 60)) - "hour" -> plus(t, checkedMultiply(n, 3600)) - "day" -> plus(t, checkedMultiply(n, 86400)) - else -> return@ternaryTimestampFunction EvaluateResultError - } - ) - } + if (seconds == 0L) t else Values.timestamp(checkedSubtract(t.seconds, seconds), t.nanos) -internal val evaluateTimestampSub = - ternaryTimestampFunction { t: Timestamp, u: String, n: Long -> - EvaluateResult.timestamp( - when (u) { - "microsecond" -> minus(t, n / L_MICROS_PER_SECOND, (n % L_MICROS_PER_SECOND) * 1000) - "millisecond" -> minus(t, n / L_MILLIS_PER_SECOND, (n % L_MILLIS_PER_SECOND) * 1000_000) - "second" -> minus(t, n) - "minute" -> minus(t, checkedMultiply(n, 60)) - "hour" -> minus(t, checkedMultiply(n, 3600)) - "day" -> minus(t, checkedMultiply(n, 86400)) - else -> return@ternaryTimestampFunction EvaluateResultError - } - ) - } +internal val evaluateTimestampAdd = ternaryTimestampFunction { t: Timestamp, u: String, n: Long -> + EvaluateResult.timestamp( + when (u) { + "microsecond" -> plus(t, n / L_MICROS_PER_SECOND, (n % L_MICROS_PER_SECOND) * 1000) + "millisecond" -> plus(t, n / L_MILLIS_PER_SECOND, (n % L_MILLIS_PER_SECOND) * 1000_000) + "second" -> plus(t, n) + "minute" -> plus(t, checkedMultiply(n, 60)) + "hour" -> plus(t, checkedMultiply(n, 3600)) + "day" -> plus(t, checkedMultiply(n, 86400)) + else -> return@ternaryTimestampFunction EvaluateResultError + } + ) +} + +internal val evaluateTimestampSub = ternaryTimestampFunction { t: Timestamp, u: String, n: Long -> + EvaluateResult.timestamp( + when (u) { + "microsecond" -> minus(t, n / L_MICROS_PER_SECOND, (n % L_MICROS_PER_SECOND) * 1000) + "millisecond" -> minus(t, n / L_MILLIS_PER_SECOND, (n % L_MILLIS_PER_SECOND) * 1000_000) + "second" -> minus(t, n) + "minute" -> minus(t, checkedMultiply(n, 60)) + "hour" -> minus(t, checkedMultiply(n, 3600)) + "day" -> minus(t, checkedMultiply(n, 86400)) + else -> return@ternaryTimestampFunction EvaluateResultError + } + ) +} internal val evaluateTimestampTrunc = notImplemented // TODO: Does not exist in expressions.kt yet. @@ -402,17 +421,18 @@ internal val evaluateUnixSecondsToTimestamp = unaryFunction { seconds: Long -> internal val evaluateMap: EvaluateFunction = { params -> if (params.size % 2 != 0) throw Assert.fail("Function should have even number of params, but %d were given.", params.size) - else block@{ input: MutableDocument -> - val map: MutableMap = HashMap(params.size / 2) - for (i in params.indices step 2) { - val k = params[i](input).value ?: return@block EvaluateResultError - if (!k.hasStringValue()) return@block EvaluateResultError - val v = params[i + 1](input).value ?: return@block EvaluateResultError - // It is against the API contract to include a key more than once. - if (map.put(k.stringValue, v) != null) return@block EvaluateResultError + else + block@{ input: MutableDocument -> + val map: MutableMap = HashMap(params.size / 2) + for (i in params.indices step 2) { + val k = params[i](input).value ?: return@block EvaluateResultError + if (!k.hasStringValue()) return@block EvaluateResultError + val v = params[i + 1](input).value ?: return@block EvaluateResultError + // It is against the API contract to include a key more than once. + if (map.put(k.stringValue, v) != null) return@block EvaluateResultError + } + EvaluateResultValue(encodeValue(map)) } - EvaluateResultValue(encodeValue(map)) - } } // === Helper Functions === @@ -688,10 +708,10 @@ private inline fun variadicFunction( } } -private inline fun comparison(crossinline predicate: (Value, Value) -> Boolean): EvaluateFunction = +private inline fun comparison(crossinline f: (Value, Value) -> Boolean?): EvaluateFunction = binaryFunction { p1: Value, p2: Value -> if (isNanValue(p1) or isNanValue(p2)) EvaluateResult.FALSE - else catch { EvaluateResult.boolean(predicate(p1, p2)) } + else EvaluateResult.boolean(f(p1, p2)) } private inline fun arithmeticPrimitive( 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 fa5d4464601..3c3bafd6297 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 @@ -48,11 +48,14 @@ import java.util.Date */ abstract class Expr internal constructor() { - private class ValueConstant(val value: Value) : Expr() { + private class Constant(val value: Value) : Expr() { override fun toProto(userDataReader: UserDataReader): Value = value override fun evaluateContext(context: EvaluationContext) = { _: MutableDocument -> EvaluateResultValue(value) } + override fun toString(): String { + return "Constant(value=$value)" + } } companion object { @@ -78,7 +81,7 @@ abstract class Expr internal constructor() { is DocumentReference -> constant(value) is ByteArray -> constant(value) is VectorValue -> constant(value) - is Value -> ValueConstant(value) + is Value -> Constant(value) is Map<*, *> -> map( value @@ -100,7 +103,7 @@ abstract class Expr internal constructor() { internal fun toArrayOfExprOrConstant(others: Array): Array = others.map(::toExprOrConstant).toTypedArray() - private val NULL: Expr = ValueConstant(Values.NULL_VALUE) + private val NULL: Expr = Constant(Values.NULL_VALUE) /** * Create a constant for a [String] value. @@ -110,7 +113,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: String): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -121,7 +124,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: Number): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -132,7 +135,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: Date): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -143,7 +146,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: Timestamp): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -178,8 +181,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] constant instance. */ @JvmStatic - fun constant(value: GeoPoint): Expr { - return ValueConstant(encodeValue(value)) + fun constant(value: GeoPoint): Expr { // Ensure this overload exists or is correctly placed + return Constant(encodeValue(value)) } /** @@ -190,7 +193,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: ByteArray): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -201,7 +204,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: Blob): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -235,7 +238,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: VectorValue): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -256,7 +259,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun vector(vector: DoubleArray): Expr { - return ValueConstant(Values.encodeVectorValue(vector)) + return Constant(Values.encodeVectorValue(vector)) } /** @@ -267,7 +270,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun vector(vector: VectorValue): Expr { - return ValueConstant(encodeValue(vector)) + return Constant(encodeValue(vector)) } /** @@ -1785,8 +1788,7 @@ abstract class Expr internal constructor() { fun strConcat(fieldName: String, vararg otherStrings: Any): Expr = FunctionExpr("str_concat", evaluateStrConcat, fieldName, *otherStrings) - internal fun map(elements: Array): Expr = - FunctionExpr("map", evaluateMap, elements) + internal fun map(elements: Array): Expr = FunctionExpr("map", evaluateMap, elements) /** * Creates an expression that creates a Firestore map value from an input object. @@ -2608,8 +2610,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the array function. */ @JvmStatic - fun array(vararg elements: Expr): Expr = - FunctionExpr("array", evaluateArray, elements) + fun array(vararg elements: Expr): Expr = FunctionExpr("array", evaluateArray, elements) /** * Creates an expression that creates a Firestore array value from an input array. @@ -2969,8 +2970,8 @@ abstract class Expr internal constructor() { * This overload will return [BooleanExpr] when both parameters are also [BooleanExpr]. * * @param tryExpr The try boolean expression. - * @param catchExpr The catch boolean expression that will be evaluated and returned if the [tryExpr] - * produces an error. + * @param catchExpr The catch boolean expression that will be evaluated and returned if the + * [tryExpr] produces an error. * @return A new [BooleanExpr] representing the ifError operation. */ @JvmStatic diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt index e3af3b049f7..0aad32d6c86 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt @@ -4,546 +4,532 @@ import com.google.common.truth.Truth.assertThat import com.google.firebase.firestore.model.Values.encodeValue // Returns com.google.protobuf.Value import com.google.firebase.firestore.pipeline.Expr.Companion.add import com.google.firebase.firestore.pipeline.Expr.Companion.constant -import com.google.firebase.firestore.pipeline.Expr.Companion.subtract -import com.google.firebase.firestore.pipeline.Expr.Companion.multiply import com.google.firebase.firestore.pipeline.Expr.Companion.divide import com.google.firebase.firestore.pipeline.Expr.Companion.mod +import com.google.firebase.firestore.pipeline.Expr.Companion.multiply +import com.google.firebase.firestore.pipeline.Expr.Companion.subtract import org.junit.Test internal class ArithmeticTests { - @Test - fun addFunctionTestWithBasicNumerics() { - assertThat(evaluate(add(constant(1L), constant(2L))).value) - .isEqualTo(encodeValue(3L)) - assertThat(evaluate(add(constant(1L), constant(2.5))).value) - .isEqualTo(encodeValue(3.5)) - assertThat(evaluate(add(constant(1.0), constant(2L))).value) - .isEqualTo(encodeValue(3.0)) - assertThat(evaluate(add(constant(1.0), constant(2.0))).value) - .isEqualTo(encodeValue(3.0)) - } - - @Test - fun addFunctionTestWithBasicNonNumerics() { - assertThat(evaluate(add(constant(1L), constant("1"))).isError).isTrue() - assertThat(evaluate(add(constant("1"), constant(1.0))).isError).isTrue() - assertThat(evaluate(add(constant("1"), constant("1"))).isError).isTrue() - } - - @Test - fun addFunctionTestWithDoubleLongAdditionOverflow() { - val longMaxAsDoublePlusOne = Long.MAX_VALUE.toDouble() + 1.0 - assertThat(evaluate(add(constant(Long.MAX_VALUE), constant(1.0))).value) - .isEqualTo(encodeValue(longMaxAsDoublePlusOne)) - - val intermediate = longMaxAsDoublePlusOne - assertThat(evaluate(add(constant(intermediate), constant(100L))).value) - .isEqualTo(encodeValue(intermediate + 100.0)) - } - - @Test - fun addFunctionTestWithDoubleAdditionOverflow() { - assertThat(evaluate(add(constant(Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) - .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) - assertThat(evaluate(add(constant(-Double.MAX_VALUE), constant(-Double.MAX_VALUE))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - } - - @Test - fun addFunctionTestWithSumPosAndNegInfinityReturnNaN() { - assertThat(evaluate(add(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.NaN)) - } - - @Test - fun addFunctionTestWithLongAdditionOverflow() { - assertThat(evaluate(add(constant(Long.MAX_VALUE), constant(1L))).isError).isTrue() - assertThat(evaluate(add(constant(Long.MIN_VALUE), constant(-1L))).isError).isTrue() - assertThat(evaluate(add(constant(1L), constant(Long.MAX_VALUE))).isError).isTrue() - } - - @Test - fun addFunctionTestWithNanNumberReturnNaN() { - val nanVal = Double.NaN - assertThat(evaluate(add(constant(1L), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(add(constant(1.0), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(add(constant(9007199254740991L), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(add(constant(-9007199254740991L), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(add(constant(Double.MAX_VALUE), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(add(constant(-Double.MAX_VALUE), constant(nanVal))).value) // Corresponds to C++ std::numeric_limits::lowest() - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(add(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(add(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - } - - @Test - fun addFunctionTestWithNanNotNumberTypeReturnError() { - assertThat(evaluate(add(constant(Double.NaN), constant("hello world"))).isError).isTrue() - } - - @Test - fun addFunctionTestWithMultiArgument() { - assertThat(evaluate(add(add(constant(1L), constant(2L)), constant(3L))).value) - .isEqualTo(encodeValue(6L)) - assertThat(evaluate(add(add(constant(1.0), constant(2L)), constant(3L))).value) - .isEqualTo(encodeValue(6.0)) - } - - // --- Subtract Tests (Ported) --- - @Test - fun subtractFunctionTestWithBasicNumerics() { - assertThat(evaluate(subtract(constant(1L), constant(2L))).value) - .isEqualTo(encodeValue(-1L)) - assertThat(evaluate(subtract(constant(1L), constant(2.5))).value) - .isEqualTo(encodeValue(-1.5)) - assertThat(evaluate(subtract(constant(1.0), constant(2L))).value) - .isEqualTo(encodeValue(-1.0)) - assertThat(evaluate(subtract(constant(1.0), constant(2.0))).value) - .isEqualTo(encodeValue(-1.0)) - } - - @Test - fun subtractFunctionTestWithBasicNonNumerics() { - assertThat(evaluate(subtract(constant(1L), constant("1"))).isError).isTrue() - assertThat(evaluate(subtract(constant("1"), constant(1.0))).isError).isTrue() - assertThat(evaluate(subtract(constant("1"), constant("1"))).isError).isTrue() - } - - @Test - fun subtractFunctionTestWithDoubleSubtractionOverflow() { - assertThat(evaluate(subtract(constant(-Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - assertThat(evaluate(subtract(constant(Double.MAX_VALUE), constant(-Double.MAX_VALUE))).value) - .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) - } - - @Test - fun subtractFunctionTestWithLongSubtractionOverflow() { - assertThat(evaluate(subtract(constant(Long.MIN_VALUE), constant(1L))).isError).isTrue() - assertThat(evaluate(subtract(constant(Long.MAX_VALUE), constant(-1L))).isError).isTrue() - } - - @Test - fun subtractFunctionTestWithNanNumberReturnNaN() { - val nanVal = Double.NaN - assertThat(evaluate(subtract(constant(1L), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(subtract(constant(1.0), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(subtract(constant(9007199254740991L), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(subtract(constant(-9007199254740991L), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(subtract(constant(Double.MAX_VALUE), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(subtract(constant(-Double.MAX_VALUE), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(subtract(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(subtract(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - } - - @Test - fun subtractFunctionTestWithNanNotNumberTypeReturnError() { - assertThat(evaluate(subtract(constant(Double.NaN), constant("hello world"))).isError).isTrue() - } - - @Test - fun subtractFunctionTestWithPositiveInfinity() { - assertThat(evaluate(subtract(constant(Double.POSITIVE_INFINITY), constant(1L))).value) - .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) - assertThat(evaluate(subtract(constant(1L), constant(Double.POSITIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - } - - @Test - fun subtractFunctionTestWithNegativeInfinity() { - assertThat(evaluate(subtract(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - assertThat(evaluate(subtract(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) - } - - @Test - fun subtractFunctionTestWithPositiveInfinityNegativeInfinity() { - assertThat(evaluate(subtract(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) - assertThat(evaluate(subtract(constant(Double.NEGATIVE_INFINITY), constant(Double.POSITIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - } - - // --- Multiply Tests (Ported) --- - @Test - fun multiplyFunctionTestWithBasicNumerics() { - assertThat(evaluate(multiply(constant(1L), constant(2L))).value) - .isEqualTo(encodeValue(2L)) - assertThat(evaluate(multiply(constant(3L), constant(2.5))).value) - .isEqualTo(encodeValue(7.5)) - assertThat(evaluate(multiply(constant(1.0), constant(2L))).value) - .isEqualTo(encodeValue(2.0)) - assertThat(evaluate(multiply(constant(1.32), constant(2.0))).value) - .isEqualTo(encodeValue(2.64)) - } - - @Test - fun multiplyFunctionTestWithBasicNonNumerics() { - assertThat(evaluate(multiply(constant(1L), constant("1"))).isError).isTrue() - assertThat(evaluate(multiply(constant("1"), constant(1.0))).isError).isTrue() - assertThat(evaluate(multiply(constant("1"), constant("1"))).isError).isTrue() - } - - @Test - fun multiplyFunctionTestWithDoubleLongMultiplicationOverflow() { - assertThat(evaluate(multiply(constant(Long.MAX_VALUE), constant(100.0))).value) - .isEqualTo(encodeValue(Long.MAX_VALUE.toDouble() * 100.0)) - assertThat(evaluate(multiply(constant(Long.MAX_VALUE), constant(100L))).isError).isTrue() - } - - @Test - fun multiplyFunctionTestWithDoubleMultiplicationOverflow() { - assertThat(evaluate(multiply(constant(Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) - .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) - assertThat(evaluate(multiply(constant(-Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - } - - @Test - fun multiplyFunctionTestWithLongMultiplicationOverflow() { - assertThat(evaluate(multiply(constant(Long.MAX_VALUE), constant(10L))).isError).isTrue() - assertThat(evaluate(multiply(constant(Long.MIN_VALUE), constant(10L))).isError).isTrue() - assertThat(evaluate(multiply(constant(-10L), constant(Long.MAX_VALUE))).isError).isTrue() - assertThat(evaluate(multiply(constant(-10L), constant(Long.MIN_VALUE))).isError).isTrue() - } - - @Test - fun multiplyFunctionTestWithNanNumberReturnNaN() { - val nanVal = Double.NaN - assertThat(evaluate(multiply(constant(1L), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(multiply(constant(1.0), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(multiply(constant(9007199254740991L), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(multiply(constant(-9007199254740991L), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(multiply(constant(Double.MAX_VALUE), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(multiply(constant(-Double.MAX_VALUE), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(multiply(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(multiply(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - } - - @Test - fun multiplyFunctionTestWithNanNotNumberTypeReturnError() { - assertThat(evaluate(multiply(constant(Double.NaN), constant("hello world"))).isError).isTrue() - } - - @Test - fun multiplyFunctionTestWithPositiveInfinity() { - assertThat(evaluate(multiply(constant(Double.POSITIVE_INFINITY), constant(1L))).value) - .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) - assertThat(evaluate(multiply(constant(1L), constant(Double.POSITIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) - } - - @Test - fun multiplyFunctionTestWithNegativeInfinity() { - assertThat(evaluate(multiply(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - assertThat(evaluate(multiply(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - } - - @Test - fun multiplyFunctionTestWithPositiveInfinityNegativeInfinityReturnsNegativeInfinity() { - assertThat(evaluate(multiply(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - assertThat(evaluate(multiply(constant(Double.NEGATIVE_INFINITY), constant(Double.POSITIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - } - - @Test - fun multiplyFunctionTestWithMultiArgument() { - assertThat(evaluate(multiply(multiply(constant(1L), constant(2L)), constant(3L))).value) - .isEqualTo(encodeValue(6L)) - assertThat(evaluate(multiply(constant(1.0), multiply(constant(2L), constant(3L)))).value) - .isEqualTo(encodeValue(6.0)) - } - - // --- Divide Tests (Ported) --- - @Test - fun divideFunctionTestWithBasicNumerics() { - assertThat(evaluate(divide(constant(10L), constant(2L))).value) - .isEqualTo(encodeValue(5L)) - assertThat(evaluate(divide(constant(10L), constant(2.0))).value) - .isEqualTo(encodeValue(5.0)) - assertThat(evaluate(divide(constant(10.0), constant(3L))).value) - .isEqualTo(encodeValue(10.0 / 3.0)) - assertThat(evaluate(divide(constant(10.0), constant(7.0))).value) - .isEqualTo(encodeValue(10.0 / 7.0)) - } - - @Test - fun divideFunctionTestWithBasicNonNumerics() { - assertThat(evaluate(divide(constant(1L), constant("1"))).isError).isTrue() - assertThat(evaluate(divide(constant("1"), constant(1.0))).isError).isTrue() - assertThat(evaluate(divide(constant("1"), constant("1"))).isError).isTrue() - } - - @Test - fun divideFunctionTestWithLongDivision() { - assertThat(evaluate(divide(constant(10L), constant(3L))).value) - .isEqualTo(encodeValue(3L)) - assertThat(evaluate(divide(constant(-10L), constant(3L))).value) - .isEqualTo(encodeValue(-3L)) - assertThat(evaluate(divide(constant(10L), constant(-3L))).value) - .isEqualTo(encodeValue(-3L)) - assertThat(evaluate(divide(constant(-10L), constant(-3L))).value) - .isEqualTo(encodeValue(3L)) - } - - @Test - fun divideFunctionTestWithDoubleDivisionOverflow() { - assertThat(evaluate(divide(constant(Double.MAX_VALUE), constant(0.5))).value) - .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) - assertThat(evaluate(divide(constant(-Double.MAX_VALUE), constant(0.5))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - } - - @Test - fun divideFunctionTestWithByZero() { - assertThat(evaluate(divide(constant(1L), constant(0L))).isError).isTrue() - assertThat(evaluate(divide(constant(1.1), constant(0.0))).value) - .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) - assertThat(evaluate(divide(constant(1.1), constant(-0.0))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - assertThat(evaluate(divide(constant(0.0), constant(0.0))).value) - .isEqualTo(encodeValue(Double.NaN)) - } - - @Test - fun divideFunctionTestWithNanNumberReturnNaN() { - val nanVal = Double.NaN - assertThat(evaluate(divide(constant(1L), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(divide(constant(nanVal), constant(1L))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(divide(constant(1.0), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(divide(constant(nanVal), constant(1.0))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(divide(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(divide(constant(nanVal), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(divide(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(divide(constant(nanVal), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(nanVal)) - } - - @Test - fun divideFunctionTestWithNanNotNumberTypeReturnError() { - assertThat(evaluate(divide(constant(Double.NaN), constant("hello world"))).isError).isTrue() - } - - @Test - fun divideFunctionTestWithPositiveInfinity() { - assertThat(evaluate(divide(constant(Double.POSITIVE_INFINITY), constant(1L))).value) - .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) - assertThat(evaluate(divide(constant(1L), constant(Double.POSITIVE_INFINITY))).value) - .isEqualTo(encodeValue(0.0)) - } - - @Test - fun divideFunctionTestWithNegativeInfinity() { - assertThat(evaluate(divide(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) - .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) - assertThat(evaluate(divide(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(-0.0)) - } - - @Test - fun divideFunctionTestWithPositiveInfinityNegativeInfinityReturnsNan() { - assertThat(evaluate(divide(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.NaN)) - assertThat(evaluate(divide(constant(Double.NEGATIVE_INFINITY), constant(Double.POSITIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.NaN)) - } - - // --- Mod Tests (Ported) --- - @Test - fun modFunctionTestWithDivisorZero() { - assertThat(evaluate(mod(constant(42L), constant(0L))).isError).isTrue() - assertThat(evaluate(mod(constant(42.0), constant(0.0))).value) - .isEqualTo(encodeValue(Double.NaN)) - assertThat(evaluate(mod(constant(42.0), constant(-0.0))).value) - .isEqualTo(encodeValue(Double.NaN)) - } - - @Test - fun modFunctionTestWithDividendZeroReturnsZero() { - assertThat(evaluate(mod(constant(0L), constant(42L))).value) - .isEqualTo(encodeValue(0L)) - assertThat(evaluate(mod(constant(0.0), constant(42.0))).value) - .isEqualTo(encodeValue(0.0)) - assertThat(evaluate(mod(constant(-0.0), constant(42.0))).value) - .isEqualTo(encodeValue(-0.0)) - } - - @Test - fun modFunctionTestWithLongPositivePositive() { - assertThat(evaluate(mod(constant(10L), constant(3L))).value) - .isEqualTo(encodeValue(1L)) - } - - @Test - fun modFunctionTestWithLongNegativeNegative() { - assertThat(evaluate(mod(constant(-10L), constant(-3L))).value) - .isEqualTo(encodeValue(-1L)) - } - - @Test - fun modFunctionTestWithLongPositiveNegative() { - assertThat(evaluate(mod(constant(10L), constant(-3L))).value) - .isEqualTo(encodeValue(1L)) - } - - @Test - fun modFunctionTestWithLongNegativePositive() { - assertThat(evaluate(mod(constant(-10L), constant(3L))).value) - .isEqualTo(encodeValue(-1L)) - } - - @Test - fun modFunctionTestWithDoublePositivePositive() { - // 10.5 % 3.0 is exactly 1.5 - assertThat(evaluate(mod(constant(10.5), constant(3.0))).value) - .isEqualTo(encodeValue(1.5)) - } - - @Test - fun modFunctionTestWithDoubleNegativeNegative() { - val resultValue = evaluate(mod(constant(-7.3), constant(-1.8))).value - assertThat(resultValue?.doubleValue).isWithin(1e-9).of(-0.1) - } - - @Test - fun modFunctionTestWithDoublePositiveNegative() { - val resultValue = evaluate(mod(constant(9.8), constant(-2.5))).value - assertThat(resultValue?.doubleValue).isWithin(1e-9).of(2.3) - } - - @Test - fun modFunctionTestWithDoubleNegativePositive() { - val resultValue = evaluate(mod(constant(-7.5), constant(2.3))).value - assertThat(resultValue?.doubleValue).isWithin(1e-9).of(-0.6) - } - - @Test - fun modFunctionTestWithLongPerfectlyDivisible() { - assertThat(evaluate(mod(constant(10L), constant(5L))).value) - .isEqualTo(encodeValue(0L)) - assertThat(evaluate(mod(constant(-10L), constant(5L))).value) - .isEqualTo(encodeValue(0L)) - assertThat(evaluate(mod(constant(10L), constant(-5L))).value) - .isEqualTo(encodeValue(0L)) - assertThat(evaluate(mod(constant(-10L), constant(-5L))).value) - .isEqualTo(encodeValue(0L)) - } - - @Test - fun modFunctionTestWithDoublePerfectlyDivisible() { - assertThat(evaluate(mod(constant(10.0), constant(2.5))).value) - .isEqualTo(encodeValue(0.0)) - assertThat(evaluate(mod(constant(10.0), constant(-2.5))).value) - .isEqualTo(encodeValue(0.0)) - assertThat(evaluate(mod(constant(-10.0), constant(2.5))).value) - .isEqualTo(encodeValue(-0.0)) - assertThat(evaluate(mod(constant(-10.0), constant(-2.5))).value) - .isEqualTo(encodeValue(-0.0)) - } - - @Test - fun modFunctionTestWithNonNumericsReturnError() { - assertThat(evaluate(mod(constant(10L), constant("1"))).isError).isTrue() - assertThat(evaluate(mod(constant("1"), constant(10L))).isError).isTrue() - assertThat(evaluate(mod(constant("1"), constant("1"))).isError).isTrue() - } - - @Test - fun modFunctionTestWithNanNumberReturnNaN() { - val nanVal = Double.NaN - assertThat(evaluate(mod(constant(1L), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(mod(constant(1.0), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) - .isEqualTo(encodeValue(nanVal)) - } - - @Test - fun modFunctionTestWithNanNotNumberTypeReturnError() { - assertThat(evaluate(mod(constant(Double.NaN), constant("hello world"))).isError).isTrue() - } - - @Test - fun modFunctionTestWithNumberPosInfinityReturnSelf() { - assertThat(evaluate(mod(constant(1L), constant(Double.POSITIVE_INFINITY))).value) - .isEqualTo(encodeValue(1.0)) - assertThat(evaluate(mod(constant(42.123), constant(Double.POSITIVE_INFINITY))).value) - .isEqualTo(encodeValue(42.123)) - assertThat(evaluate(mod(constant(-99.9), constant(Double.POSITIVE_INFINITY))).value) - .isEqualTo(encodeValue(-99.9)) - } - - @Test - fun modFunctionTestWithPosInfinityNumberReturnNaN() { - assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(1L))).value) - .isEqualTo(encodeValue(Double.NaN)) - assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(42.123))).value) - .isEqualTo(encodeValue(Double.NaN)) - assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(-99.9))).value) - .isEqualTo(encodeValue(Double.NaN)) - } - - @Test - fun modFunctionTestWithNumberNegInfinityReturnSelf() { - assertThat(evaluate(mod(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(1.0)) - assertThat(evaluate(mod(constant(42.123), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(42.123)) - assertThat(evaluate(mod(constant(-99.9), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(-99.9)) - } - - @Test - fun modFunctionTestWithNegInfinityNumberReturnNaN() { - assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) - .isEqualTo(encodeValue(Double.NaN)) - assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(42.123))).value) - .isEqualTo(encodeValue(Double.NaN)) - assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(-99.9))).value) - .isEqualTo(encodeValue(Double.NaN)) - } - - @Test - fun modFunctionTestWithPosAndNegInfinityReturnNaN() { - assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value) - .isEqualTo(encodeValue(Double.NaN)) - } + @Test + fun addFunctionTestWithBasicNumerics() { + assertThat(evaluate(add(constant(1L), constant(2L))).value).isEqualTo(encodeValue(3L)) + assertThat(evaluate(add(constant(1L), constant(2.5))).value).isEqualTo(encodeValue(3.5)) + assertThat(evaluate(add(constant(1.0), constant(2L))).value).isEqualTo(encodeValue(3.0)) + assertThat(evaluate(add(constant(1.0), constant(2.0))).value).isEqualTo(encodeValue(3.0)) + } + + @Test + fun addFunctionTestWithBasicNonNumerics() { + assertThat(evaluate(add(constant(1L), constant("1"))).isError).isTrue() + assertThat(evaluate(add(constant("1"), constant(1.0))).isError).isTrue() + assertThat(evaluate(add(constant("1"), constant("1"))).isError).isTrue() + } + + @Test + fun addFunctionTestWithDoubleLongAdditionOverflow() { + val longMaxAsDoublePlusOne = Long.MAX_VALUE.toDouble() + 1.0 + assertThat(evaluate(add(constant(Long.MAX_VALUE), constant(1.0))).value) + .isEqualTo(encodeValue(longMaxAsDoublePlusOne)) + + val intermediate = longMaxAsDoublePlusOne + assertThat(evaluate(add(constant(intermediate), constant(100L))).value) + .isEqualTo(encodeValue(intermediate + 100.0)) + } + + @Test + fun addFunctionTestWithDoubleAdditionOverflow() { + assertThat(evaluate(add(constant(Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(add(constant(-Double.MAX_VALUE), constant(-Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun addFunctionTestWithSumPosAndNegInfinityReturnNaN() { + assertThat( + evaluate(add(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value + ) + .isEqualTo(encodeValue(Double.NaN)) + } + + @Test + fun addFunctionTestWithLongAdditionOverflow() { + assertThat(evaluate(add(constant(Long.MAX_VALUE), constant(1L))).isError).isTrue() + assertThat(evaluate(add(constant(Long.MIN_VALUE), constant(-1L))).isError).isTrue() + assertThat(evaluate(add(constant(1L), constant(Long.MAX_VALUE))).isError).isTrue() + } + + @Test + fun addFunctionTestWithNanNumberReturnNaN() { + val nanVal = Double.NaN + assertThat(evaluate(add(constant(1L), constant(nanVal))).value).isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(1.0), constant(nanVal))).value).isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(-9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(Double.MAX_VALUE), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat( + evaluate(add(constant(-Double.MAX_VALUE), constant(nanVal))).value + ) // Corresponds to C++ std::numeric_limits::lowest() + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(add(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + } + + @Test + fun addFunctionTestWithNanNotNumberTypeReturnError() { + assertThat(evaluate(add(constant(Double.NaN), constant("hello world"))).isError).isTrue() + } + + @Test + fun addFunctionTestWithMultiArgument() { + assertThat(evaluate(add(add(constant(1L), constant(2L)), constant(3L))).value) + .isEqualTo(encodeValue(6L)) + assertThat(evaluate(add(add(constant(1.0), constant(2L)), constant(3L))).value) + .isEqualTo(encodeValue(6.0)) + } + + // --- Subtract Tests (Ported) --- + @Test + fun subtractFunctionTestWithBasicNumerics() { + assertThat(evaluate(subtract(constant(1L), constant(2L))).value).isEqualTo(encodeValue(-1L)) + assertThat(evaluate(subtract(constant(1L), constant(2.5))).value).isEqualTo(encodeValue(-1.5)) + assertThat(evaluate(subtract(constant(1.0), constant(2L))).value).isEqualTo(encodeValue(-1.0)) + assertThat(evaluate(subtract(constant(1.0), constant(2.0))).value).isEqualTo(encodeValue(-1.0)) + } + + @Test + fun subtractFunctionTestWithBasicNonNumerics() { + assertThat(evaluate(subtract(constant(1L), constant("1"))).isError).isTrue() + assertThat(evaluate(subtract(constant("1"), constant(1.0))).isError).isTrue() + assertThat(evaluate(subtract(constant("1"), constant("1"))).isError).isTrue() + } + + @Test + fun subtractFunctionTestWithDoubleSubtractionOverflow() { + assertThat(evaluate(subtract(constant(-Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat(evaluate(subtract(constant(Double.MAX_VALUE), constant(-Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + } + + @Test + fun subtractFunctionTestWithLongSubtractionOverflow() { + assertThat(evaluate(subtract(constant(Long.MIN_VALUE), constant(1L))).isError).isTrue() + assertThat(evaluate(subtract(constant(Long.MAX_VALUE), constant(-1L))).isError).isTrue() + } + + @Test + fun subtractFunctionTestWithNanNumberReturnNaN() { + val nanVal = Double.NaN + assertThat(evaluate(subtract(constant(1L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(1.0), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(-9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(Double.MAX_VALUE), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(-Double.MAX_VALUE), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(subtract(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + } + + @Test + fun subtractFunctionTestWithNanNotNumberTypeReturnError() { + assertThat(evaluate(subtract(constant(Double.NaN), constant("hello world"))).isError).isTrue() + } + + @Test + fun subtractFunctionTestWithPositiveInfinity() { + assertThat(evaluate(subtract(constant(Double.POSITIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(subtract(constant(1L), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun subtractFunctionTestWithNegativeInfinity() { + assertThat(evaluate(subtract(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat(evaluate(subtract(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + } + + @Test + fun subtractFunctionTestWithPositiveInfinityNegativeInfinity() { + assertThat( + evaluate(subtract(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))) + .value + ) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat( + evaluate(subtract(constant(Double.NEGATIVE_INFINITY), constant(Double.POSITIVE_INFINITY))) + .value + ) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + // --- Multiply Tests (Ported) --- + @Test + fun multiplyFunctionTestWithBasicNumerics() { + assertThat(evaluate(multiply(constant(1L), constant(2L))).value).isEqualTo(encodeValue(2L)) + assertThat(evaluate(multiply(constant(3L), constant(2.5))).value).isEqualTo(encodeValue(7.5)) + assertThat(evaluate(multiply(constant(1.0), constant(2L))).value).isEqualTo(encodeValue(2.0)) + assertThat(evaluate(multiply(constant(1.32), constant(2.0))).value).isEqualTo(encodeValue(2.64)) + } + + @Test + fun multiplyFunctionTestWithBasicNonNumerics() { + assertThat(evaluate(multiply(constant(1L), constant("1"))).isError).isTrue() + assertThat(evaluate(multiply(constant("1"), constant(1.0))).isError).isTrue() + assertThat(evaluate(multiply(constant("1"), constant("1"))).isError).isTrue() + } + + @Test + fun multiplyFunctionTestWithDoubleLongMultiplicationOverflow() { + assertThat(evaluate(multiply(constant(Long.MAX_VALUE), constant(100.0))).value) + .isEqualTo(encodeValue(Long.MAX_VALUE.toDouble() * 100.0)) + assertThat(evaluate(multiply(constant(Long.MAX_VALUE), constant(100L))).isError).isTrue() + } + + @Test + fun multiplyFunctionTestWithDoubleMultiplicationOverflow() { + assertThat(evaluate(multiply(constant(Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(multiply(constant(-Double.MAX_VALUE), constant(Double.MAX_VALUE))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun multiplyFunctionTestWithLongMultiplicationOverflow() { + assertThat(evaluate(multiply(constant(Long.MAX_VALUE), constant(10L))).isError).isTrue() + assertThat(evaluate(multiply(constant(Long.MIN_VALUE), constant(10L))).isError).isTrue() + assertThat(evaluate(multiply(constant(-10L), constant(Long.MAX_VALUE))).isError).isTrue() + assertThat(evaluate(multiply(constant(-10L), constant(Long.MIN_VALUE))).isError).isTrue() + } + + @Test + fun multiplyFunctionTestWithNanNumberReturnNaN() { + val nanVal = Double.NaN + assertThat(evaluate(multiply(constant(1L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(1.0), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(-9007199254740991L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(Double.MAX_VALUE), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(-Double.MAX_VALUE), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(multiply(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + } + + @Test + fun multiplyFunctionTestWithNanNotNumberTypeReturnError() { + assertThat(evaluate(multiply(constant(Double.NaN), constant("hello world"))).isError).isTrue() + } + + @Test + fun multiplyFunctionTestWithPositiveInfinity() { + assertThat(evaluate(multiply(constant(Double.POSITIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(multiply(constant(1L), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + } + + @Test + fun multiplyFunctionTestWithNegativeInfinity() { + assertThat(evaluate(multiply(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat(evaluate(multiply(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun multiplyFunctionTestWithPositiveInfinityNegativeInfinityReturnsNegativeInfinity() { + assertThat( + evaluate(multiply(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))) + .value + ) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat( + evaluate(multiply(constant(Double.NEGATIVE_INFINITY), constant(Double.POSITIVE_INFINITY))) + .value + ) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun multiplyFunctionTestWithMultiArgument() { + assertThat(evaluate(multiply(multiply(constant(1L), constant(2L)), constant(3L))).value) + .isEqualTo(encodeValue(6L)) + assertThat(evaluate(multiply(constant(1.0), multiply(constant(2L), constant(3L)))).value) + .isEqualTo(encodeValue(6.0)) + } + + // --- Divide Tests (Ported) --- + @Test + fun divideFunctionTestWithBasicNumerics() { + assertThat(evaluate(divide(constant(10L), constant(2L))).value).isEqualTo(encodeValue(5L)) + assertThat(evaluate(divide(constant(10L), constant(2.0))).value).isEqualTo(encodeValue(5.0)) + assertThat(evaluate(divide(constant(10.0), constant(3L))).value) + .isEqualTo(encodeValue(10.0 / 3.0)) + assertThat(evaluate(divide(constant(10.0), constant(7.0))).value) + .isEqualTo(encodeValue(10.0 / 7.0)) + } + + @Test + fun divideFunctionTestWithBasicNonNumerics() { + assertThat(evaluate(divide(constant(1L), constant("1"))).isError).isTrue() + assertThat(evaluate(divide(constant("1"), constant(1.0))).isError).isTrue() + assertThat(evaluate(divide(constant("1"), constant("1"))).isError).isTrue() + } + + @Test + fun divideFunctionTestWithLongDivision() { + assertThat(evaluate(divide(constant(10L), constant(3L))).value).isEqualTo(encodeValue(3L)) + assertThat(evaluate(divide(constant(-10L), constant(3L))).value).isEqualTo(encodeValue(-3L)) + assertThat(evaluate(divide(constant(10L), constant(-3L))).value).isEqualTo(encodeValue(-3L)) + assertThat(evaluate(divide(constant(-10L), constant(-3L))).value).isEqualTo(encodeValue(3L)) + } + + @Test + fun divideFunctionTestWithDoubleDivisionOverflow() { + assertThat(evaluate(divide(constant(Double.MAX_VALUE), constant(0.5))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(divide(constant(-Double.MAX_VALUE), constant(0.5))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + } + + @Test + fun divideFunctionTestWithByZero() { + assertThat(evaluate(divide(constant(1L), constant(0L))).isError).isTrue() + assertThat(evaluate(divide(constant(1.1), constant(0.0))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(divide(constant(1.1), constant(-0.0))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat(evaluate(divide(constant(0.0), constant(0.0))).value) + .isEqualTo(encodeValue(Double.NaN)) + } + + @Test + fun divideFunctionTestWithNanNumberReturnNaN() { + val nanVal = Double.NaN + assertThat(evaluate(divide(constant(1L), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(nanVal), constant(1L))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(1.0), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(nanVal), constant(1.0))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(nanVal), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(divide(constant(nanVal), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(nanVal)) + } + + @Test + fun divideFunctionTestWithNanNotNumberTypeReturnError() { + assertThat(evaluate(divide(constant(Double.NaN), constant("hello world"))).isError).isTrue() + } + + @Test + fun divideFunctionTestWithPositiveInfinity() { + assertThat(evaluate(divide(constant(Double.POSITIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.POSITIVE_INFINITY)) + assertThat(evaluate(divide(constant(1L), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(0.0)) + } + + @Test + fun divideFunctionTestWithNegativeInfinity() { + assertThat(evaluate(divide(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.NEGATIVE_INFINITY)) + assertThat(evaluate(divide(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(-0.0)) + } + + @Test + fun divideFunctionTestWithPositiveInfinityNegativeInfinityReturnsNan() { + assertThat( + evaluate(divide(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))) + .value + ) + .isEqualTo(encodeValue(Double.NaN)) + assertThat( + evaluate(divide(constant(Double.NEGATIVE_INFINITY), constant(Double.POSITIVE_INFINITY))) + .value + ) + .isEqualTo(encodeValue(Double.NaN)) + } + + // --- Mod Tests (Ported) --- + @Test + fun modFunctionTestWithDivisorZero() { + assertThat(evaluate(mod(constant(42L), constant(0L))).isError).isTrue() + assertThat(evaluate(mod(constant(42.0), constant(0.0))).value) + .isEqualTo(encodeValue(Double.NaN)) + assertThat(evaluate(mod(constant(42.0), constant(-0.0))).value) + .isEqualTo(encodeValue(Double.NaN)) + } + + @Test + fun modFunctionTestWithDividendZeroReturnsZero() { + assertThat(evaluate(mod(constant(0L), constant(42L))).value).isEqualTo(encodeValue(0L)) + assertThat(evaluate(mod(constant(0.0), constant(42.0))).value).isEqualTo(encodeValue(0.0)) + assertThat(evaluate(mod(constant(-0.0), constant(42.0))).value).isEqualTo(encodeValue(-0.0)) + } + + @Test + fun modFunctionTestWithLongPositivePositive() { + assertThat(evaluate(mod(constant(10L), constant(3L))).value).isEqualTo(encodeValue(1L)) + } + + @Test + fun modFunctionTestWithLongNegativeNegative() { + assertThat(evaluate(mod(constant(-10L), constant(-3L))).value).isEqualTo(encodeValue(-1L)) + } + + @Test + fun modFunctionTestWithLongPositiveNegative() { + assertThat(evaluate(mod(constant(10L), constant(-3L))).value).isEqualTo(encodeValue(1L)) + } + + @Test + fun modFunctionTestWithLongNegativePositive() { + assertThat(evaluate(mod(constant(-10L), constant(3L))).value).isEqualTo(encodeValue(-1L)) + } + + @Test + fun modFunctionTestWithDoublePositivePositive() { + // 10.5 % 3.0 is exactly 1.5 + assertThat(evaluate(mod(constant(10.5), constant(3.0))).value).isEqualTo(encodeValue(1.5)) + } + + @Test + fun modFunctionTestWithDoubleNegativeNegative() { + val resultValue = evaluate(mod(constant(-7.3), constant(-1.8))).value + assertThat(resultValue?.doubleValue).isWithin(1e-9).of(-0.1) + } + + @Test + fun modFunctionTestWithDoublePositiveNegative() { + val resultValue = evaluate(mod(constant(9.8), constant(-2.5))).value + assertThat(resultValue?.doubleValue).isWithin(1e-9).of(2.3) + } + + @Test + fun modFunctionTestWithDoubleNegativePositive() { + val resultValue = evaluate(mod(constant(-7.5), constant(2.3))).value + assertThat(resultValue?.doubleValue).isWithin(1e-9).of(-0.6) + } + + @Test + fun modFunctionTestWithLongPerfectlyDivisible() { + assertThat(evaluate(mod(constant(10L), constant(5L))).value).isEqualTo(encodeValue(0L)) + assertThat(evaluate(mod(constant(-10L), constant(5L))).value).isEqualTo(encodeValue(0L)) + assertThat(evaluate(mod(constant(10L), constant(-5L))).value).isEqualTo(encodeValue(0L)) + assertThat(evaluate(mod(constant(-10L), constant(-5L))).value).isEqualTo(encodeValue(0L)) + } + + @Test + fun modFunctionTestWithDoublePerfectlyDivisible() { + assertThat(evaluate(mod(constant(10.0), constant(2.5))).value).isEqualTo(encodeValue(0.0)) + assertThat(evaluate(mod(constant(10.0), constant(-2.5))).value).isEqualTo(encodeValue(0.0)) + assertThat(evaluate(mod(constant(-10.0), constant(2.5))).value).isEqualTo(encodeValue(-0.0)) + assertThat(evaluate(mod(constant(-10.0), constant(-2.5))).value).isEqualTo(encodeValue(-0.0)) + } + + @Test + fun modFunctionTestWithNonNumericsReturnError() { + assertThat(evaluate(mod(constant(10L), constant("1"))).isError).isTrue() + assertThat(evaluate(mod(constant("1"), constant(10L))).isError).isTrue() + assertThat(evaluate(mod(constant("1"), constant("1"))).isError).isTrue() + } + + @Test + fun modFunctionTestWithNanNumberReturnNaN() { + val nanVal = Double.NaN + assertThat(evaluate(mod(constant(1L), constant(nanVal))).value).isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(mod(constant(1.0), constant(nanVal))).value).isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(nanVal))).value) + .isEqualTo(encodeValue(nanVal)) + } + + @Test + fun modFunctionTestWithNanNotNumberTypeReturnError() { + assertThat(evaluate(mod(constant(Double.NaN), constant("hello world"))).isError).isTrue() + } + + @Test + fun modFunctionTestWithNumberPosInfinityReturnSelf() { + assertThat(evaluate(mod(constant(1L), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(1.0)) + assertThat(evaluate(mod(constant(42.123), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(42.123)) + assertThat(evaluate(mod(constant(-99.9), constant(Double.POSITIVE_INFINITY))).value) + .isEqualTo(encodeValue(-99.9)) + } + + @Test + fun modFunctionTestWithPosInfinityNumberReturnNaN() { + assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.NaN)) + assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(42.123))).value) + .isEqualTo(encodeValue(Double.NaN)) + assertThat(evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(-99.9))).value) + .isEqualTo(encodeValue(Double.NaN)) + } + + @Test + fun modFunctionTestWithNumberNegInfinityReturnSelf() { + assertThat(evaluate(mod(constant(1L), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(1.0)) + assertThat(evaluate(mod(constant(42.123), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(42.123)) + assertThat(evaluate(mod(constant(-99.9), constant(Double.NEGATIVE_INFINITY))).value) + .isEqualTo(encodeValue(-99.9)) + } + + @Test + fun modFunctionTestWithNegInfinityNumberReturnNaN() { + assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(1L))).value) + .isEqualTo(encodeValue(Double.NaN)) + assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(42.123))).value) + .isEqualTo(encodeValue(Double.NaN)) + assertThat(evaluate(mod(constant(Double.NEGATIVE_INFINITY), constant(-99.9))).value) + .isEqualTo(encodeValue(Double.NaN)) + } + + @Test + fun modFunctionTestWithPosAndNegInfinityReturnNaN() { + assertThat( + evaluate(mod(constant(Double.POSITIVE_INFINITY), constant(Double.NEGATIVE_INFINITY))).value + ) + .isEqualTo(encodeValue(Double.NaN)) + } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt index f293b3e49a2..99de633cac5 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt @@ -1,15 +1,42 @@ package com.google.firebase.firestore.pipeline +import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.model.DatabaseId import com.google.firebase.firestore.model.MutableDocument +import com.google.firebase.firestore.model.Values.NULL_VALUE +import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.testutil.TestUtilKtx.doc -val DATABASE_ID = UserDataReader(DatabaseId.forDatabase("projectId", "databaseId")) +val DATABASE_ID = UserDataReader(DatabaseId.forDatabase("project", "(default)")) val EMPTY_DOC: MutableDocument = doc("foo/1", 0, mapOf()) internal val EVALUATION_CONTEXT = EvaluationContext(DATABASE_ID) -internal fun evaluate(expr: Expr): EvaluateResult { - val function = expr.evaluateContext(EVALUATION_CONTEXT) - return function(EMPTY_DOC) -} \ No newline at end of file +internal fun evaluate(expr: Expr): EvaluateResult = evaluate(expr, EMPTY_DOC) + +internal fun evaluate(expr: Expr, doc: MutableDocument): EvaluateResult { + val function = expr.evaluateContext(EVALUATION_CONTEXT) + return function(doc) +} + +// Helper to check for successful evaluation to a boolean value +internal fun assertEvaluatesTo(result: EvaluateResult, expected: Boolean, message: () -> String) { + assertWithMessage(message()).that(result.isSuccess).isTrue() + assertWithMessage(message()).that(result.value).isEqualTo(encodeValue(expected)) +} + +// Helper to check for evaluation resulting in NULL +internal fun assertEvaluatesToNull(result: EvaluateResult, message: () -> String) { + assertWithMessage(message()).that(result.isSuccess).isTrue() // Null is a successful evaluation + assertWithMessage(message()).that(result.value).isEqualTo(NULL_VALUE) +} + +// Helper to check for evaluation resulting in UNSET (e.g. field not found) +internal fun assertEvaluatesToUnset(result: EvaluateResult, message: () -> String) { + assertWithMessage(message()).that(result).isSameInstanceAs(EvaluateResultUnset) +} + +// Helper to check for evaluation resulting in an error +internal fun assertEvaluatesToError(result: EvaluateResult, message: () -> String) { + assertWithMessage(message()).that(result).isSameInstanceAs(EvaluateResultError) +} From e875d1a24150fbb5fa71d239f2c1e68718085100 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 28 May 2025 23:11:16 -0400 Subject: [PATCH 086/152] Add comparison tests --- .../firestore/pipeline/ComparisonTests.kt | 949 ++++++++++++++++++ 1 file changed, 949 insertions(+) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt new file mode 100644 index 00000000000..16190aee636 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt @@ -0,0 +1,949 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firebase.Timestamp // For creating Timestamp instances +import com.google.firebase.firestore.GeoPoint // For creating GeoPoint instances +import com.google.firebase.firestore.model.Values.NULL_VALUE +import com.google.firebase.firestore.pipeline.Expr.Companion.array +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.eq +import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.pipeline.Expr.Companion.gt +import com.google.firebase.firestore.pipeline.Expr.Companion.gte +import com.google.firebase.firestore.pipeline.Expr.Companion.lt +import com.google.firebase.firestore.pipeline.Expr.Companion.lte +import com.google.firebase.firestore.pipeline.Expr.Companion.map +import com.google.firebase.firestore.pipeline.Expr.Companion.neq +import com.google.firebase.firestore.pipeline.Expr.Companion.nullValue +import com.google.firebase.firestore.testutil.TestUtil // For test helpers like map, array, etc. +import com.google.firebase.firestore.testutil.TestUtilKtx.doc // For creating MutableDocument +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +// Helper data similar to C++ ComparisonValueTestData +internal object ComparisonTestData { + private const val MAX_LONG_EXACTLY_REPRESENTABLE_AS_DOUBLE = 1L shl 53 + + private val BOOLEAN_VALUES: List = listOf(constant(false), constant(true)) + + private val NUMERIC_VALUES: List = + listOf( + constant(Double.NEGATIVE_INFINITY), + constant(-Double.MAX_VALUE), + constant(Long.MIN_VALUE), + constant(-MAX_LONG_EXACTLY_REPRESENTABLE_AS_DOUBLE), + constant(-1L), + constant(-0.5), + constant(-Double.MIN_VALUE), // Smallest positive normal, negated + constant(0.0), // Represents both +0.0 and -0.0 for ordering + constant(Double.MIN_VALUE), // Smallest positive normal + constant(0.5), + constant(1L), + constant(42L), + constant(MAX_LONG_EXACTLY_REPRESENTABLE_AS_DOUBLE), + constant(Long.MAX_VALUE), + constant(Double.MAX_VALUE), + constant(Double.POSITIVE_INFINITY), + // doubleNaN is handled separately due to its comparison properties + ) + + val doubleNaN = constant(Double.NaN) + + private val TIMESTAMP_VALUES: List = + listOf( + constant(Timestamp(-42, 0)), + constant(Timestamp(-42, 42000000)), + constant(Timestamp(0, 0)), + constant(Timestamp(0, 42000000)), + constant(Timestamp(42, 0)), + constant(Timestamp(42, 42000000)) + ) + + private val STRING_VALUES: List = + listOf( + constant(""), + constant("a"), + constant("abcdefgh"), + constant("santé"), + constant("santé et bonheur"), + constant("z") + ) + + private val BLOB_VALUES: List = + listOf( + constant(TestUtil.blob()), // Empty + constant(TestUtil.blob(0, 2, 56, 42)), + constant(TestUtil.blob(2, 26)), + constant(TestUtil.blob(2, 26, 31)) + ) + + // Note: TestUtil.ref uses a default project "project" and default database "(default)" + // So TestUtil.ref("foo/bar") becomes "projects/project/databases/(default)/documents/foo/bar" + private val REF_VALUES: List = + listOf( + constant(TestUtil.ref("foo/bar")), + constant(TestUtil.ref("foo/bar/qux/a")), + constant(TestUtil.ref("foo/bar/qux/bleh")), + constant(TestUtil.ref("foo/bar/qux/hi")), + constant(TestUtil.ref("foo/bar/tonk/a")), + constant(TestUtil.ref("foo/baz")) + ) + + private val GEO_POINT_VALUES: List = + listOf( + constant(GeoPoint(-87.0, -92.0)), + constant(GeoPoint(-87.0, 0.0)), + constant(GeoPoint(-87.0, 42.0)), + constant(GeoPoint(0.0, -92.0)), + constant(GeoPoint(0.0, 0.0)), + constant(GeoPoint(0.0, 42.0)), + constant(GeoPoint(42.0, -92.0)), + constant(GeoPoint(42.0, 0.0)), + constant(GeoPoint(42.0, 42.0)) + ) + + private val ARRAY_VALUES: List = + listOf( + array(), + array(constant(true), constant(15L)), + array(constant(1L), constant(2L)), + array(constant(Timestamp(12, 0))), + array(constant("foo")), + array(constant("foo"), constant("bar")), + array(constant(GeoPoint(0.0, 0.0))), + array(map(emptyMap())) + ) + + private val MAP_VALUES: List = + listOf( + map(emptyMap()), + map(mapOf("ABA" to "qux")), + map(mapOf("aba" to "hello")), + map(mapOf("aba" to "hello", "foo" to true)), + map(mapOf("aba" to "qux")), + map(mapOf("foo" to "aaa")) + ) + + // Combine all comparable, non-NaN, non-Null values from the categorized lists + // This is useful for testing against Null or NaN. + val allSupportedComparableValues: List = + BOOLEAN_VALUES + + NUMERIC_VALUES + // numericValuesForNanTest already excludes NaN + TIMESTAMP_VALUES + + STRING_VALUES + + BLOB_VALUES + + REF_VALUES + + GEO_POINT_VALUES + + ARRAY_VALUES + + MAP_VALUES + + // For tests specifically about numeric comparisons against NaN + val numericValuesForNanTest: List = NUMERIC_VALUES // This list already excludes NaN + + // --- Dynamically generated comparison pairs based on Firestore type ordering --- + // Type Order: Null < Boolean < Number < Timestamp < String < Blob < Reference < GeoPoint < Array + // < Map + + private val allValueCategories: List> = + listOf( + listOf(nullValue()), // Null first + BOOLEAN_VALUES, + NUMERIC_VALUES, // NaN is not in this list + TIMESTAMP_VALUES, + STRING_VALUES, + BLOB_VALUES, + REF_VALUES, + GEO_POINT_VALUES, + ARRAY_VALUES, + MAP_VALUES + ) + + val equivalentValues: List> = buildList { + // Self-equality for all defined values (except NaN, which is special) + allSupportedComparableValues.forEach { add(it to it) } + + // Specific numeric equivalences + add(constant(0L) to constant(0.0)) + add(constant(1L) to constant(1.0)) + add(constant(-5L) to constant(-5.0)) + add( + constant(MAX_LONG_EXACTLY_REPRESENTABLE_AS_DOUBLE) to + constant(MAX_LONG_EXACTLY_REPRESENTABLE_AS_DOUBLE.toDouble()) + ) + + // Map key order doesn't matter for equality + add(map(mapOf("a" to 1L, "b" to 2L)) to map(mapOf("b" to 2L, "a" to 1L))) + } + + val lessThanValues: List> = buildList { + // Intra-type comparisons + for (category in allValueCategories) { + for (i in 0 until category.size - 1) { + for (j in i + 1 until category.size) { + add(category[i] to category[j]) + } + } + } + } + + val mixedTypeValues: List> = buildList { + val categories = allValueCategories.filter { it.isNotEmpty() } + for (i in categories.indices) { + for (j in i + 1 until categories.size) { + // Only add pairs if they are not already covered by lessThan (inter-type) + // This list is for types that are strictly non-comparable by value for <, >, <=, >= (should + // yield false) + // or where one is null (should yield null for <, >, <=, >=) + val val1 = categories[i].first() + val val2 = categories[j].first() + + // If one is null, it's a null-operand case, handled elsewhere for <, >, etc. + // For eq/neq, null vs non-null is false/true (or null if other is also null). + // Here, we are interested in pairs that, if not null, would typically result in 'false' for + // relational ops. + if (val1 != nullValue() && val2 != nullValue()) { + add(val1 to val2) + } + } + } + // Add some specific tricky mixed types not covered by systematic generation + add(constant(true) to constant(0L)) + add(constant(Timestamp(0, 0)) to constant("abc")) + add(array(constant(1L)) to map(mapOf("a" to 1L))) + } +} + +// Using RobolectricTestRunner if any Android-specific classes are indirectly used by model classes. +// Firestore model classes might depend on Android context for certain initializations. +@RunWith(RobolectricTestRunner::class) +internal class ComparisonTests { + + // --- Eq (==) Tests --- + + @Test + fun eq_equivalentValues_returnTrue() { + ComparisonTestData.equivalentValues.forEach { (v1, v2) -> + val result = evaluate(eq(v1, v2)) + assertEvaluatesTo(result, true) { "eq($v1, $v2)" } + } + } + + @Test + fun eq_lessThanValues_returnFalse() { + ComparisonTestData.lessThanValues.forEach { (v1, v2) -> + // eq(v1, v2) + val result1 = evaluate(eq(v1, v2)) + assertEvaluatesTo(result1, false) { "eq($v1, $v2)" } + // eq(v2, v1) + val result2 = evaluate(eq(v2, v1)) + assertEvaluatesTo(result2, false) { "eq($v2, $v1)" } + } + } + + // GreaterThanValues can be derived from LessThanValues by swapping pairs + @Test + fun eq_greaterThanValues_returnFalse() { + ComparisonTestData.lessThanValues.forEach { (less, greater) -> + // eq(greater, less) + val result = evaluate(eq(greater, less)) + assertEvaluatesTo(result, false) { "eq($greater, $less)" } + } + } + + @Test + fun eq_mixedTypeValues_returnFalse() { + ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> + val result1 = evaluate(eq(v1, v2)) + assertEvaluatesTo(result1, false) { "eq($v1, $v2)" } + val result2 = evaluate(eq(v2, v1)) + assertEvaluatesTo(result2, false) { "eq($v2, $v1)" } + } + } + + @Test + fun eq_nullEqualsNull_returnsNull() { + // In SQL-like semantics, NULL == NULL is NULL, not TRUE. + // Firestore's behavior for direct comparison of two NULL constants: + val v1 = nullValue() + val v2 = nullValue() + val result = evaluate(eq(v1, v2)) + assertEvaluatesToNull(result) { "eq($v1, $v2)" } + } + + @Test + fun eq_nullOperand_returnsNullOrError() { + ComparisonTestData.allSupportedComparableValues.forEach { value -> + val nullVal = nullValue() + // eq(null, value) + assertEvaluatesToNull(evaluate(eq(nullVal, value))) { "eq($nullVal, $value)" } + // eq(value, null) + assertEvaluatesToNull(evaluate(eq(value, nullVal))) { "eq($value, $nullVal)" } + } + // eq(null, nonExistentField) + val nullVal = nullValue() + val missingField = field("nonexistent") + assertEvaluatesToError(evaluate(eq(nullVal, missingField))) { "eq($nullVal, $missingField)" } + } + + @Test + fun eq_nanComparisons_returnFalse() { + val nanExpr = ComparisonTestData.doubleNaN + + // NaN == NaN is false + assertEvaluatesTo(evaluate(eq(nanExpr, nanExpr)), false) { "eq($nanExpr, $nanExpr)" } + + ComparisonTestData.numericValuesForNanTest.forEach { numVal -> + assertEvaluatesTo(evaluate(eq(nanExpr, numVal)), false) { "eq($nanExpr, $numVal)" } + assertEvaluatesTo(evaluate(eq(numVal, nanExpr)), false) { "eq($numVal, $nanExpr)" } + } + + // Compare NaN with non-numeric types + (ComparisonTestData.allSupportedComparableValues - + ComparisonTestData.numericValuesForNanTest.toSet() - + nanExpr) + .forEach { otherVal -> + if (otherVal != nanExpr) { // Ensure we are not re-testing NaN vs NaN or NaN vs Numeric + assertEvaluatesTo(evaluate(eq(nanExpr, otherVal)), false) { "eq($nanExpr, $otherVal)" } + assertEvaluatesTo(evaluate(eq(otherVal, nanExpr)), false) { "eq($otherVal, $nanExpr)" } + } + } + + // NaN in array + val arrayWithNaN1 = array(constant(Double.NaN)) + val arrayWithNaN2 = array(constant(Double.NaN)) + assertEvaluatesTo(evaluate(eq(arrayWithNaN1, arrayWithNaN2)), false) { + "eq($arrayWithNaN1, $arrayWithNaN2)" + } + + // NaN in map + val mapWithNaN1 = map(mapOf("foo" to Double.NaN)) + val mapWithNaN2 = map(mapOf("foo" to Double.NaN)) + assertEvaluatesTo(evaluate(eq(mapWithNaN1, mapWithNaN2)), false) { + "eq($mapWithNaN1, $mapWithNaN2)" + } + } + + @Test + fun eq_nullContainerEquality_various() { + val nullArray = array(nullValue()) // Array containing a Firestore Null + + assertEvaluatesTo(evaluate(eq(nullArray, constant(1L))), false) { "eq($nullArray, 1L)" } + assertEvaluatesTo(evaluate(eq(nullArray, constant("1"))), false) { "eq($nullArray, \"1\")" } + assertEvaluatesToNull(evaluate(eq(nullArray, nullValue()))) { "eq($nullArray, ${nullValue()})" } + assertEvaluatesTo(evaluate(eq(nullArray, ComparisonTestData.doubleNaN)), false) { + "eq($nullArray, ${ComparisonTestData.doubleNaN})" + } + assertEvaluatesTo(evaluate(eq(nullArray, array())), false) { "eq($nullArray, [])" } + + val nanArray = array(constant(Double.NaN)) + assertEvaluatesToNull(evaluate(eq(nullArray, nanArray))) { "eq($nullArray, $nanArray)" } + + val anotherNullArray = array(nullValue()) + assertEvaluatesToNull(evaluate(eq(nullArray, anotherNullArray))) { + "eq($nullArray, $anotherNullArray)" + } + + val nullMap = map(mapOf("foo" to NULL_VALUE)) // Map containing a Firestore Null + val anotherNullMap = map(mapOf("foo" to NULL_VALUE)) + assertEvaluatesToNull(evaluate(eq(nullMap, anotherNullMap))) { "eq($nullMap, $anotherNullMap)" } + assertEvaluatesTo(evaluate(eq(nullMap, map(emptyMap()))), false) { "eq($nullMap, {})" } + } + + @Test + fun eq_errorHandling_returnsError() { + val errorExpr = + field("a.b") // Accessing a nested field that might not exist or be of wrong type + val testDoc = doc("test/eqError", 0, mapOf("a" to 123)) + + ComparisonTestData.allSupportedComparableValues.forEach { value -> + assertEvaluatesToError(evaluate(eq(errorExpr, value), testDoc)) { "eq($errorExpr, $value)" } + assertEvaluatesToError(evaluate(eq(value, errorExpr), testDoc)) { "eq($value, $errorExpr)" } + } + assertEvaluatesToError(evaluate(eq(errorExpr, errorExpr), testDoc)) { + "eq($errorExpr, $errorExpr)" + } + assertEvaluatesToError(evaluate(eq(errorExpr, nullValue()), testDoc)) { + "eq($errorExpr, ${nullValue()})" + } + } + + @Test + fun eq_missingField_returnsError() { + val missingField = field("nonexistent") + val presentValue = constant(1L) + val testDoc = doc("test/eqMissing", 0, mapOf("exists" to 10L)) + + assertEvaluatesToError(evaluate(eq(missingField, presentValue), testDoc)) { + "eq($missingField, $presentValue)" + } + assertEvaluatesToError(evaluate(eq(presentValue, missingField), testDoc)) { + "eq($presentValue, $missingField)" + } + } + + // --- Neq (!=) Tests --- + + @Test + fun neq_equivalentValues_returnFalse() { + ComparisonTestData.equivalentValues.forEach { (v1, v2) -> + val result = evaluate(neq(v1, v2)) + if (v1 == nullValue() && v2 == nullValue()) { + assertEvaluatesToNull(result) { "neq($v1, $v2)" } + } else { + assertEvaluatesTo(result, false) { "neq($v1, $v2)" } + } + } + } + + @Test + fun neq_lessThanValues_returnTrue() { + ComparisonTestData.lessThanValues.forEach { (v1, v2) -> + assertEvaluatesTo(evaluate(neq(v1, v2)), true) { "neq($v1, $v2)" } + assertEvaluatesTo(evaluate(neq(v2, v1)), true) { "neq($v2, $v1)" } + } + } + + @Test + fun neq_greaterThanValues_returnTrue() { + ComparisonTestData.lessThanValues.forEach { (less, greater) -> + assertEvaluatesTo(evaluate(neq(greater, less)), true) { "neq($greater, $less)" } + } + } + + @Test + fun neq_mixedTypeValues_returnTrue() { + ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> + if (v1 == nullValue() || v2 == nullValue()) { + assertEvaluatesToNull(evaluate(neq(v1, v2))) { "neq($v1, $v2)" } + assertEvaluatesToNull(evaluate(neq(v2, v1))) { "neq($v2, $v1)" } + } else { + assertEvaluatesTo(evaluate(neq(v1, v2)), true) { "neq($v1, $v2)" } + assertEvaluatesTo(evaluate(neq(v2, v1)), true) { "neq($v2, $v1)" } + } + } + } + + @Test + fun neq_nullNotEqualsNull_returnsNull() { + val v1 = nullValue() + val v2 = nullValue() + val result = evaluate(neq(v1, v2)) + assertEvaluatesToNull(result) { "neq($v1, $v2)" } + } + + @Test + fun neq_nullOperand_returnsNullOrError() { + ComparisonTestData.allSupportedComparableValues.forEach { value -> + val nullVal = nullValue() + assertEvaluatesToNull(evaluate(neq(nullVal, value))) { "neq($nullVal, $value)" } + assertEvaluatesToNull(evaluate(neq(value, nullVal))) { "neq($value, $nullVal)" } + } + val nullVal = nullValue() + val missingField = field("nonexistent") + assertEvaluatesToError(evaluate(neq(nullVal, missingField))) { "neq($nullVal, $missingField)" } + } + + @Test + fun neq_nanComparisons_returnTrue() { + val nanExpr = ComparisonTestData.doubleNaN + assertEvaluatesTo(evaluate(neq(nanExpr, nanExpr)), true) { "neq($nanExpr, $nanExpr)" } + + ComparisonTestData.numericValuesForNanTest.forEach { numVal -> + assertEvaluatesTo(evaluate(neq(nanExpr, numVal)), true) { "neq($nanExpr, $numVal)" } + assertEvaluatesTo(evaluate(neq(numVal, nanExpr)), true) { "neq($numVal, $nanExpr)" } + } + + (ComparisonTestData.allSupportedComparableValues - + ComparisonTestData.numericValuesForNanTest.toSet() - + nanExpr) + .forEach { otherVal -> + if (otherVal != nanExpr) { + assertEvaluatesTo(evaluate(neq(nanExpr, otherVal)), true) { "neq($nanExpr, $otherVal)" } + assertEvaluatesTo(evaluate(neq(otherVal, nanExpr)), true) { "neq($otherVal, $nanExpr)" } + } + } + + val arrayWithNaN1 = array(constant(Double.NaN)) + val arrayWithNaN2 = array(constant(Double.NaN)) + assertEvaluatesTo(evaluate(neq(arrayWithNaN1, arrayWithNaN2)), true) { + "neq($arrayWithNaN1, $arrayWithNaN2)" + } + + val mapWithNaN1 = map(mapOf("foo" to Double.NaN)) + val mapWithNaN2 = map(mapOf("foo" to Double.NaN)) + assertEvaluatesTo(evaluate(neq(mapWithNaN1, mapWithNaN2)), true) { + "neq($mapWithNaN1, $mapWithNaN2)" + } + } + + @Test + fun neq_errorHandling_returnsError() { + val errorExpr = field("a.b") + val testDoc = doc("test/neqError", 0, mapOf("a" to 123)) + + ComparisonTestData.allSupportedComparableValues.forEach { value -> + assertEvaluatesToError(evaluate(neq(errorExpr, value), testDoc)) { "neq($errorExpr, $value)" } + assertEvaluatesToError(evaluate(neq(value, errorExpr), testDoc)) { "neq($value, $errorExpr)" } + } + assertEvaluatesToError(evaluate(neq(errorExpr, errorExpr), testDoc)) { + "neq($errorExpr, $errorExpr)" + } + assertEvaluatesToError(evaluate(neq(errorExpr, nullValue()), testDoc)) { + "neq($errorExpr, ${nullValue()})" + } + } + + @Test + fun neq_missingField_returnsError() { + val missingField = field("nonexistent") + val presentValue = constant(1L) + val testDoc = doc("test/neqMissing", 0, mapOf("exists" to 10L)) + + assertEvaluatesToError(evaluate(neq(missingField, presentValue), testDoc)) { + "neq($missingField, $presentValue)" + } + assertEvaluatesToError(evaluate(neq(presentValue, missingField), testDoc)) { + "neq($presentValue, $missingField)" + } + } + + // --- Lt (<) Tests --- + + @Test + fun lt_equivalentValues_returnFalse() { + ComparisonTestData.equivalentValues.forEach { (v1, v2) -> + if (v1 == nullValue() && v2 == nullValue()) { + assertEvaluatesToNull(evaluate(lt(v1, v2))) { "lt($v1, $v2)" } + } else { + assertEvaluatesTo(evaluate(lt(v1, v2)), false) { "lt($v1, $v2)" } + } + } + } + + @Test + fun lt_lessThanValues_returnTrue() { + ComparisonTestData.lessThanValues.forEach { (v1, v2) -> + val result = evaluate(lt(v1, v2)) + if (result.value?.booleanValue == false) { + return + } + assertEvaluatesTo(result, true) { "lt($v1, $v2)" } + } + } + + @Test + fun lt_greaterThanValues_returnFalse() { + ComparisonTestData.lessThanValues.forEach { (less, greater) -> + assertEvaluatesTo(evaluate(lt(greater, less)), false) { "lt($greater, $less)" } + } + } + + @Test + fun lt_mixedTypeValues_returnFalse() { + ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> + if (v1 == nullValue() || v2 == nullValue()) { + assertEvaluatesToNull(evaluate(lt(v1, v2))) { "lt($v1, $v2)" } + assertEvaluatesToNull(evaluate(lt(v2, v1))) { "lt($v2, $v1)" } + } else { + assertEvaluatesTo(evaluate(lt(v1, v2)), false) { "lt($v1, $v2)" } + assertEvaluatesTo(evaluate(lt(v2, v1)), false) { "lt($v2, $v1)" } + } + } + } + + @Test + fun lt_nullOperand_returnsNullOrError() { + ComparisonTestData.allSupportedComparableValues.forEach { value -> + val nullVal = nullValue() + assertEvaluatesToNull(evaluate(lt(nullVal, value))) { "lt($nullVal, $value)" } + assertEvaluatesToNull(evaluate(lt(value, nullVal))) { "lt($value, $nullVal)" } + } + val nullVal = nullValue() + assertEvaluatesToNull(evaluate(lt(nullVal, nullVal))) { "lt($nullVal, $nullVal)" } + val missingField = field("nonexistent") + assertEvaluatesToError(evaluate(lt(nullVal, missingField))) { "lt($nullVal, $missingField)" } + } + + @Test + fun lt_nanComparisons_returnFalse() { + val nanExpr = ComparisonTestData.doubleNaN + assertEvaluatesTo(evaluate(lt(nanExpr, nanExpr)), false) { "lt($nanExpr, $nanExpr)" } + + ComparisonTestData.numericValuesForNanTest.forEach { numVal -> + assertEvaluatesTo(evaluate(lt(nanExpr, numVal)), false) { "lt($nanExpr, $numVal)" } + assertEvaluatesTo(evaluate(lt(numVal, nanExpr)), false) { "lt($numVal, $nanExpr)" } + } + (ComparisonTestData.allSupportedComparableValues - + ComparisonTestData.numericValuesForNanTest.toSet() - + nanExpr) + .forEach { otherVal -> + if (otherVal != nanExpr) { + assertEvaluatesTo(evaluate(lt(nanExpr, otherVal)), false) { "lt($nanExpr, $otherVal)" } + assertEvaluatesTo(evaluate(lt(otherVal, nanExpr)), false) { "lt($otherVal, $nanExpr)" } + } + } + val arrayWithNaN1 = array(constant(Double.NaN)) + val arrayWithNaN2 = array(constant(Double.NaN)) + assertEvaluatesTo(evaluate(lt(arrayWithNaN1, arrayWithNaN2)), false) { + "lt($arrayWithNaN1, $arrayWithNaN2)" + } + } + + @Test + fun lt_errorHandling_returnsError() { + val errorExpr = field("a.b") + val testDoc = doc("test/ltError", 0, mapOf("a" to 123)) + + ComparisonTestData.allSupportedComparableValues.forEach { value -> + assertEvaluatesToError(evaluate(lt(errorExpr, value), testDoc)) { "lt($errorExpr, $value)" } + assertEvaluatesToError(evaluate(lt(value, errorExpr), testDoc)) { "lt($value, $errorExpr)" } + } + assertEvaluatesToError(evaluate(lt(errorExpr, errorExpr), testDoc)) { + "lt($errorExpr, $errorExpr)" + } + assertEvaluatesToError(evaluate(lt(errorExpr, nullValue()), testDoc)) { + "lt($errorExpr, ${nullValue()})" + } + } + + @Test + fun lt_missingField_returnsError() { + val missingField = field("nonexistent") + val presentValue = constant(1L) + val testDoc = doc("test/ltMissing", 0, mapOf("exists" to 10L)) + + assertEvaluatesToError(evaluate(lt(missingField, presentValue), testDoc)) { + "lt($missingField, $presentValue)" + } + assertEvaluatesToError(evaluate(lt(presentValue, missingField), testDoc)) { + "lt($presentValue, $missingField)" + } + } + + // --- Lte (<=) Tests --- + + @Test + fun lte_equivalentValues_returnTrue() { + ComparisonTestData.equivalentValues.forEach { (v1, v2) -> + if (v1 == nullValue() && v2 == nullValue()) { + assertEvaluatesToNull(evaluate(lte(v1, v2))) { "lte($v1, $v2)" } + } else { + assertEvaluatesTo(evaluate(lte(v1, v2)), true) { "lte($v1, $v2)" } + } + } + } + + @Test + fun lte_lessThanValues_returnTrue() { + ComparisonTestData.lessThanValues.forEach { (v1, v2) -> + assertEvaluatesTo(evaluate(lte(v1, v2)), true) { "lte($v1, $v2)" } + } + } + + @Test + fun lte_greaterThanValues_returnFalse() { + ComparisonTestData.lessThanValues.forEach { (less, greater) -> + assertEvaluatesTo(evaluate(lte(greater, less)), false) { "lte($greater, $less)" } + } + } + + @Test + fun lte_mixedTypeValues_returnFalse() { + ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> + if (v1 == nullValue() || v2 == nullValue()) { + assertEvaluatesToNull(evaluate(lte(v1, v2))) { "lte($v1, $v2)" } + assertEvaluatesToNull(evaluate(lte(v2, v1))) { "lte($v2, $v1)" } + } else { + assertEvaluatesTo(evaluate(lte(v1, v2)), false) { "lte($v1, $v2)" } + assertEvaluatesTo(evaluate(lte(v2, v1)), false) { "lte($v2, $v1)" } + } + } + } + + @Test + fun lte_nullOperand_returnsNullOrError() { + ComparisonTestData.allSupportedComparableValues.forEach { value -> + val nullVal = nullValue() + assertEvaluatesToNull(evaluate(lte(nullVal, value))) { "lte($nullVal, $value)" } + assertEvaluatesToNull(evaluate(lte(value, nullVal))) { "lte($value, $nullVal)" } + } + val nullVal = nullValue() + assertEvaluatesToNull(evaluate(lte(nullVal, nullVal))) { "lte($nullVal, $nullVal)" } + val missingField = field("nonexistent") + assertEvaluatesToError(evaluate(lte(nullVal, missingField))) { "lte($nullVal, $missingField)" } + } + + @Test + fun lte_nanComparisons_returnFalse() { + val nanExpr = ComparisonTestData.doubleNaN + assertEvaluatesTo(evaluate(lte(nanExpr, nanExpr)), false) { "lte($nanExpr, $nanExpr)" } + + ComparisonTestData.numericValuesForNanTest.forEach { numVal -> + assertEvaluatesTo(evaluate(lte(nanExpr, numVal)), false) { "lte($nanExpr, $numVal)" } + assertEvaluatesTo(evaluate(lte(numVal, nanExpr)), false) { "lte($numVal, $nanExpr)" } + } + (ComparisonTestData.allSupportedComparableValues - + ComparisonTestData.numericValuesForNanTest.toSet() - + nanExpr) + .forEach { otherVal -> + if (otherVal != nanExpr) { + assertEvaluatesTo(evaluate(lte(nanExpr, otherVal)), false) { "lte($nanExpr, $otherVal)" } + assertEvaluatesTo(evaluate(lte(otherVal, nanExpr)), false) { "lte($otherVal, $nanExpr)" } + } + } + val arrayWithNaN1 = array(constant(Double.NaN)) + val arrayWithNaN2 = array(constant(Double.NaN)) + assertEvaluatesTo(evaluate(lte(arrayWithNaN1, arrayWithNaN2)), false) { + "lte($arrayWithNaN1, $arrayWithNaN2)" + } + } + + @Test + fun lte_errorHandling_returnsError() { + val errorExpr = field("a.b") + val testDoc = doc("test/lteError", 0, mapOf("a" to 123)) + + ComparisonTestData.allSupportedComparableValues.forEach { value -> + assertEvaluatesToError(evaluate(lte(errorExpr, value), testDoc)) { "lte($errorExpr, $value)" } + assertEvaluatesToError(evaluate(lte(value, errorExpr), testDoc)) { "lte($value, $errorExpr)" } + } + assertEvaluatesToError(evaluate(lte(errorExpr, errorExpr), testDoc)) { + "lte($errorExpr, $errorExpr)" + } + assertEvaluatesToError(evaluate(lte(errorExpr, nullValue()), testDoc)) { + "lte($errorExpr, ${nullValue()})" + } + } + + @Test + fun lte_missingField_returnsError() { + val missingField = field("nonexistent") + val presentValue = constant(1L) + val testDoc = doc("test/lteMissing", 0, mapOf("exists" to 10L)) + + assertEvaluatesToError(evaluate(lte(missingField, presentValue), testDoc)) { + "lte($missingField, $presentValue)" + } + assertEvaluatesToError(evaluate(lte(presentValue, missingField), testDoc)) { + "lte($presentValue, $missingField)" + } + } + + // --- Gt (>) Tests --- + + @Test + fun gt_equivalentValues_returnFalse() { + ComparisonTestData.equivalentValues.forEach { (v1, v2) -> + if (v1 == nullValue() && v2 == nullValue()) { + assertEvaluatesToNull(evaluate(gt(v1, v2))) { "gt($v1, $v2)" } + } else { + assertEvaluatesTo(evaluate(gt(v1, v2)), false) { "gt($v1, $v2)" } + } + } + } + + @Test + fun gt_lessThanValues_returnFalse() { + ComparisonTestData.lessThanValues.forEach { (v1, v2) -> + assertEvaluatesTo(evaluate(gt(v1, v2)), false) { "gt($v1, $v2)" } + } + } + + @Test + fun gt_greaterThanValues_returnTrue() { + ComparisonTestData.lessThanValues.forEach { (less, greater) -> + assertEvaluatesTo(evaluate(gt(greater, less)), true) { "gt($greater, $less)" } + } + } + + @Test + fun gt_mixedTypeValues_returnFalse() { + ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> + if (v1 == nullValue() || v2 == nullValue()) { + assertEvaluatesToNull(evaluate(gt(v1, v2))) { "gt($v1, $v2)" } + assertEvaluatesToNull(evaluate(gt(v2, v1))) { "gt($v2, $v1)" } + } else { + assertEvaluatesTo(evaluate(gt(v1, v2)), false) { "gt($v1, $v2)" } + assertEvaluatesTo(evaluate(gt(v2, v1)), false) { "gt($v2, $v1)" } + } + } + } + + @Test + fun gt_nullOperand_returnsNullOrError() { + ComparisonTestData.allSupportedComparableValues.forEach { value -> + val nullVal = nullValue() + assertEvaluatesToNull(evaluate(gt(nullVal, value))) { "gt($nullVal, $value)" } + assertEvaluatesToNull(evaluate(gt(value, nullVal))) { "gt($value, $nullVal)" } + } + val nullVal = nullValue() + assertEvaluatesToNull(evaluate(gt(nullVal, nullVal))) { "gt($nullVal, $nullVal)" } + val missingField = field("nonexistent") + assertEvaluatesToError(evaluate(gt(nullVal, missingField))) { "gt($nullVal, $missingField)" } + } + + @Test + fun gt_nanComparisons_returnFalse() { + val nanExpr = ComparisonTestData.doubleNaN + assertEvaluatesTo(evaluate(gt(nanExpr, nanExpr)), false) { "gt($nanExpr, $nanExpr)" } + + ComparisonTestData.numericValuesForNanTest.forEach { numVal -> + assertEvaluatesTo(evaluate(gt(nanExpr, numVal)), false) { "gt($nanExpr, $numVal)" } + assertEvaluatesTo(evaluate(gt(numVal, nanExpr)), false) { "gt($numVal, $nanExpr)" } + } + (ComparisonTestData.allSupportedComparableValues - + ComparisonTestData.numericValuesForNanTest.toSet() - + nanExpr) + .forEach { otherVal -> + if (otherVal != nanExpr) { + assertEvaluatesTo(evaluate(gt(nanExpr, otherVal)), false) { "gt($nanExpr, $otherVal)" } + assertEvaluatesTo(evaluate(gt(otherVal, nanExpr)), false) { "gt($otherVal, $nanExpr)" } + } + } + val arrayWithNaN1 = array(constant(Double.NaN)) + val arrayWithNaN2 = array(constant(Double.NaN)) + assertEvaluatesTo(evaluate(gt(arrayWithNaN1, arrayWithNaN2)), false) { + "gt($arrayWithNaN1, $arrayWithNaN2)" + } + } + + @Test + fun gt_errorHandling_returnsError() { + val errorExpr = field("a.b") + val testDoc = doc("test/gtError", 0, mapOf("a" to 123)) + + ComparisonTestData.allSupportedComparableValues.forEach { value -> + assertEvaluatesToError(evaluate(gt(errorExpr, value), testDoc)) { "gt($errorExpr, $value)" } + assertEvaluatesToError(evaluate(gt(value, errorExpr), testDoc)) { "gt($value, $errorExpr)" } + } + assertEvaluatesToError(evaluate(gt(errorExpr, errorExpr), testDoc)) { + "gt($errorExpr, $errorExpr)" + } + assertEvaluatesToError(evaluate(gt(errorExpr, nullValue()), testDoc)) { + "gt($errorExpr, ${nullValue()})" + } + } + + @Test + fun gt_missingField_returnsError() { + val missingField = field("nonexistent") + val presentValue = constant(1L) + val testDoc = doc("test/gtMissing", 0, mapOf("exists" to 10L)) + + assertEvaluatesToError(evaluate(gt(missingField, presentValue), testDoc)) { + "gt($missingField, $presentValue)" + } + assertEvaluatesToError(evaluate(gt(presentValue, missingField), testDoc)) { + "gt($presentValue, $missingField)" + } + } + + // --- Gte (>=) Tests --- + + @Test + fun gte_equivalentValues_returnTrue() { + ComparisonTestData.equivalentValues.forEach { (v1, v2) -> + if (v1 == nullValue() && v2 == nullValue()) { + assertEvaluatesToNull(evaluate(gte(v1, v2))) { "gte($v1, $v2)" } + } else { + assertEvaluatesTo(evaluate(gte(v1, v2)), true) { "gte($v1, $v2)" } + } + } + } + + @Test + fun gte_lessThanValues_returnFalse() { + ComparisonTestData.lessThanValues.forEach { (v1, v2) -> + assertEvaluatesTo(evaluate(gte(v1, v2)), false) { "gte($v1, $v2)" } + } + } + + @Test + fun gte_greaterThanValues_returnTrue() { + ComparisonTestData.lessThanValues.forEach { (less, greater) -> + assertEvaluatesTo(evaluate(gte(greater, less)), true) { "gte($greater, $less)" } + } + } + + @Test + fun gte_mixedTypeValues_returnFalse() { + ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> + if (v1 == nullValue() || v2 == nullValue()) { + assertEvaluatesToNull(evaluate(gte(v1, v2))) { "gte($v1, $v2)" } + assertEvaluatesToNull(evaluate(gte(v2, v1))) { "gte($v2, $v1)" } + } else { + assertEvaluatesTo(evaluate(gte(v1, v2)), false) { "gte($v1, $v2)" } + assertEvaluatesTo(evaluate(gte(v2, v1)), false) { "gte($v2, $v1)" } + } + } + } + + @Test + fun gte_nullOperand_returnsNullOrError() { + ComparisonTestData.allSupportedComparableValues.forEach { value -> + val nullVal = nullValue() + assertEvaluatesToNull(evaluate(gte(nullVal, value))) { "gte($nullVal, $value)" } + assertEvaluatesToNull(evaluate(gte(value, nullVal))) { "gte($value, $nullVal)" } + } + val nullVal = nullValue() + assertEvaluatesToNull(evaluate(gte(nullVal, nullVal))) { "gte($nullVal, $nullVal)" } + val missingField = field("nonexistent") + assertEvaluatesToError(evaluate(gte(nullVal, missingField))) { "gte($nullVal, $missingField)" } + } + + @Test + fun gte_nanComparisons_returnFalse() { + val nanExpr = ComparisonTestData.doubleNaN + assertEvaluatesTo(evaluate(gte(nanExpr, nanExpr)), false) { "gte($nanExpr, $nanExpr)" } + + ComparisonTestData.numericValuesForNanTest.forEach { numVal -> + assertEvaluatesTo(evaluate(gte(nanExpr, numVal)), false) { "gte($nanExpr, $numVal)" } + assertEvaluatesTo(evaluate(gte(numVal, nanExpr)), false) { "gte($numVal, $nanExpr)" } + } + (ComparisonTestData.allSupportedComparableValues - + ComparisonTestData.numericValuesForNanTest.toSet() - + nanExpr) + .forEach { otherVal -> + if (otherVal != nanExpr) { + assertEvaluatesTo(evaluate(gte(nanExpr, otherVal)), false) { "gte($nanExpr, $otherVal)" } + assertEvaluatesTo(evaluate(gte(otherVal, nanExpr)), false) { "gte($otherVal, $nanExpr)" } + } + } + val arrayWithNaN1 = array(constant(Double.NaN)) + val arrayWithNaN2 = array(constant(Double.NaN)) + assertEvaluatesTo(evaluate(gte(arrayWithNaN1, arrayWithNaN2)), false) { + "gte($arrayWithNaN1, $arrayWithNaN2)" + } + } + + @Test + fun gte_errorHandling_returnsError() { + val errorExpr = field("a.b") + val testDoc = doc("test/gteError", 0, mapOf("a" to 123)) + + ComparisonTestData.allSupportedComparableValues.forEach { value -> + assertEvaluatesToError(evaluate(gte(errorExpr, value), testDoc)) { "gte($errorExpr, $value)" } + assertEvaluatesToError(evaluate(gte(value, errorExpr), testDoc)) { "gte($value, $errorExpr)" } + } + assertEvaluatesToError(evaluate(gte(errorExpr, errorExpr), testDoc)) { + "gte($errorExpr, $errorExpr)" + } + assertEvaluatesToError(evaluate(gte(errorExpr, nullValue()), testDoc)) { + "gte($errorExpr, ${nullValue()})" + } + } + + @Test + fun gte_missingField_returnsError() { + val missingField = field("nonexistent") + val presentValue = constant(1L) + val testDoc = doc("test/gteMissing", 0, mapOf("exists" to 10L)) + + assertEvaluatesToError(evaluate(gte(missingField, presentValue), testDoc)) { + "gte($missingField, $presentValue)" + } + assertEvaluatesToError(evaluate(gte(presentValue, missingField), testDoc)) { + "gte($presentValue, $missingField)" + } + } +} From 4e7d6db42029aaa20d4fc3946920737be77e7acb Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 28 May 2025 23:12:42 -0400 Subject: [PATCH 087/152] Add comparison tests --- .../com/google/firebase/firestore/pipeline/ComparisonTests.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt index 16190aee636..2f09fc8a3b1 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt @@ -524,9 +524,6 @@ internal class ComparisonTests { fun lt_lessThanValues_returnTrue() { ComparisonTestData.lessThanValues.forEach { (v1, v2) -> val result = evaluate(lt(v1, v2)) - if (result.value?.booleanValue == false) { - return - } assertEvaluatesTo(result, true) { "lt($v1, $v2)" } } } From 00211cc1e24d8391eb007272232d9137c1dee176 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 29 May 2025 11:17:50 -0400 Subject: [PATCH 088/152] Refactor --- .../firestore/pipeline/ComparisonTests.kt | 676 ++++++++++++------ .../firebase/firestore/pipeline/testUtil.kt | 27 +- 2 files changed, 471 insertions(+), 232 deletions(-) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt index 2f09fc8a3b1..9185275c3a6 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt @@ -224,7 +224,7 @@ internal class ComparisonTests { fun eq_equivalentValues_returnTrue() { ComparisonTestData.equivalentValues.forEach { (v1, v2) -> val result = evaluate(eq(v1, v2)) - assertEvaluatesTo(result, true) { "eq($v1, $v2)" } + assertEvaluatesTo(result, true, "eq(%s, %s)", v1, v2) } } @@ -233,10 +233,10 @@ internal class ComparisonTests { ComparisonTestData.lessThanValues.forEach { (v1, v2) -> // eq(v1, v2) val result1 = evaluate(eq(v1, v2)) - assertEvaluatesTo(result1, false) { "eq($v1, $v2)" } + assertEvaluatesTo(result1, false, "eq(%s, %s)", v1, v2) // eq(v2, v1) val result2 = evaluate(eq(v2, v1)) - assertEvaluatesTo(result2, false) { "eq($v2, $v1)" } + assertEvaluatesTo(result2, false, "eq(%s, %s)", v2, v1) } } @@ -246,7 +246,7 @@ internal class ComparisonTests { ComparisonTestData.lessThanValues.forEach { (less, greater) -> // eq(greater, less) val result = evaluate(eq(greater, less)) - assertEvaluatesTo(result, false) { "eq($greater, $less)" } + assertEvaluatesTo(result, false, "eq(%s, %s)", greater, less) } } @@ -254,9 +254,9 @@ internal class ComparisonTests { fun eq_mixedTypeValues_returnFalse() { ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> val result1 = evaluate(eq(v1, v2)) - assertEvaluatesTo(result1, false) { "eq($v1, $v2)" } + assertEvaluatesTo(result1, false, "eq(%s, %s)", v1, v2) val result2 = evaluate(eq(v2, v1)) - assertEvaluatesTo(result2, false) { "eq($v2, $v1)" } + assertEvaluatesTo(result2, false, "eq(%s, %s)", v2, v1) } } @@ -267,7 +267,7 @@ internal class ComparisonTests { val v1 = nullValue() val v2 = nullValue() val result = evaluate(eq(v1, v2)) - assertEvaluatesToNull(result) { "eq($v1, $v2)" } + assertEvaluatesToNull(result, "eq(%s, %s)", v1, v2) } @Test @@ -275,14 +275,14 @@ internal class ComparisonTests { ComparisonTestData.allSupportedComparableValues.forEach { value -> val nullVal = nullValue() // eq(null, value) - assertEvaluatesToNull(evaluate(eq(nullVal, value))) { "eq($nullVal, $value)" } + assertEvaluatesToNull(evaluate(eq(nullVal, value)), "eq(%s, %s)", nullVal, value) // eq(value, null) - assertEvaluatesToNull(evaluate(eq(value, nullVal))) { "eq($value, $nullVal)" } + assertEvaluatesToNull(evaluate(eq(value, nullVal)), "eq(%s, %s)", value, nullVal) } // eq(null, nonExistentField) val nullVal = nullValue() val missingField = field("nonexistent") - assertEvaluatesToError(evaluate(eq(nullVal, missingField))) { "eq($nullVal, $missingField)" } + assertEvaluatesToError(evaluate(eq(nullVal, missingField)), "eq(%s, %s)", nullVal, missingField) } @Test @@ -290,11 +290,11 @@ internal class ComparisonTests { val nanExpr = ComparisonTestData.doubleNaN // NaN == NaN is false - assertEvaluatesTo(evaluate(eq(nanExpr, nanExpr)), false) { "eq($nanExpr, $nanExpr)" } + assertEvaluatesTo(evaluate(eq(nanExpr, nanExpr)), false, "eq(%s, %s)", nanExpr, nanExpr) ComparisonTestData.numericValuesForNanTest.forEach { numVal -> - assertEvaluatesTo(evaluate(eq(nanExpr, numVal)), false) { "eq($nanExpr, $numVal)" } - assertEvaluatesTo(evaluate(eq(numVal, nanExpr)), false) { "eq($numVal, $nanExpr)" } + assertEvaluatesTo(evaluate(eq(nanExpr, numVal)), false, "eq(%s, %s)", nanExpr, numVal) + assertEvaluatesTo(evaluate(eq(numVal, nanExpr)), false, "eq(%s, %s)", numVal, nanExpr) } // Compare NaN with non-numeric types @@ -303,50 +303,75 @@ internal class ComparisonTests { nanExpr) .forEach { otherVal -> if (otherVal != nanExpr) { // Ensure we are not re-testing NaN vs NaN or NaN vs Numeric - assertEvaluatesTo(evaluate(eq(nanExpr, otherVal)), false) { "eq($nanExpr, $otherVal)" } - assertEvaluatesTo(evaluate(eq(otherVal, nanExpr)), false) { "eq($otherVal, $nanExpr)" } + assertEvaluatesTo(evaluate(eq(nanExpr, otherVal)), false, "eq(%s, %s)", nanExpr, otherVal) + assertEvaluatesTo(evaluate(eq(otherVal, nanExpr)), false, "eq(%s, %s)", otherVal, nanExpr) } } // NaN in array val arrayWithNaN1 = array(constant(Double.NaN)) val arrayWithNaN2 = array(constant(Double.NaN)) - assertEvaluatesTo(evaluate(eq(arrayWithNaN1, arrayWithNaN2)), false) { - "eq($arrayWithNaN1, $arrayWithNaN2)" - } + assertEvaluatesTo( + evaluate(eq(arrayWithNaN1, arrayWithNaN2)), + false, + "eq(%s, %s)", + arrayWithNaN1, + arrayWithNaN2 + ) // NaN in map val mapWithNaN1 = map(mapOf("foo" to Double.NaN)) val mapWithNaN2 = map(mapOf("foo" to Double.NaN)) - assertEvaluatesTo(evaluate(eq(mapWithNaN1, mapWithNaN2)), false) { - "eq($mapWithNaN1, $mapWithNaN2)" - } + assertEvaluatesTo( + evaluate(eq(mapWithNaN1, mapWithNaN2)), + false, + "eq(%s, %s)", + mapWithNaN1, + mapWithNaN2 + ) } @Test fun eq_nullContainerEquality_various() { val nullArray = array(nullValue()) // Array containing a Firestore Null - assertEvaluatesTo(evaluate(eq(nullArray, constant(1L))), false) { "eq($nullArray, 1L)" } - assertEvaluatesTo(evaluate(eq(nullArray, constant("1"))), false) { "eq($nullArray, \"1\")" } - assertEvaluatesToNull(evaluate(eq(nullArray, nullValue()))) { "eq($nullArray, ${nullValue()})" } - assertEvaluatesTo(evaluate(eq(nullArray, ComparisonTestData.doubleNaN)), false) { - "eq($nullArray, ${ComparisonTestData.doubleNaN})" - } - assertEvaluatesTo(evaluate(eq(nullArray, array())), false) { "eq($nullArray, [])" } + assertEvaluatesTo(evaluate(eq(nullArray, constant(1L))), false, "eq(%s, 1L)", nullArray) + assertEvaluatesTo(evaluate(eq(nullArray, constant("1"))), false, "eq(%s, \\\"1\\\")", nullArray) + assertEvaluatesToNull( + evaluate(eq(nullArray, nullValue())), + "eq(%s, %s)", + nullArray, + nullValue() + ) + assertEvaluatesTo( + evaluate(eq(nullArray, ComparisonTestData.doubleNaN)), + false, + "eq(%s, %s)", + nullArray, + ComparisonTestData.doubleNaN + ) + assertEvaluatesTo(evaluate(eq(nullArray, array())), false, "eq(%s, [])", nullArray) val nanArray = array(constant(Double.NaN)) - assertEvaluatesToNull(evaluate(eq(nullArray, nanArray))) { "eq($nullArray, $nanArray)" } + assertEvaluatesToNull(evaluate(eq(nullArray, nanArray)), "eq(%s, %s)", nullArray, nanArray) val anotherNullArray = array(nullValue()) - assertEvaluatesToNull(evaluate(eq(nullArray, anotherNullArray))) { - "eq($nullArray, $anotherNullArray)" - } + assertEvaluatesToNull( + evaluate(eq(nullArray, anotherNullArray)), + "eq(%s, %s)", + nullArray, + anotherNullArray + ) val nullMap = map(mapOf("foo" to NULL_VALUE)) // Map containing a Firestore Null val anotherNullMap = map(mapOf("foo" to NULL_VALUE)) - assertEvaluatesToNull(evaluate(eq(nullMap, anotherNullMap))) { "eq($nullMap, $anotherNullMap)" } - assertEvaluatesTo(evaluate(eq(nullMap, map(emptyMap()))), false) { "eq($nullMap, {})" } + assertEvaluatesToNull( + evaluate(eq(nullMap, anotherNullMap)), + "eq(%s, %s)", + nullMap, + anotherNullMap + ) + assertEvaluatesTo(evaluate(eq(nullMap, map(emptyMap()))), false, "eq(%s, {})", nullMap) } @Test @@ -356,15 +381,31 @@ internal class ComparisonTests { val testDoc = doc("test/eqError", 0, mapOf("a" to 123)) ComparisonTestData.allSupportedComparableValues.forEach { value -> - assertEvaluatesToError(evaluate(eq(errorExpr, value), testDoc)) { "eq($errorExpr, $value)" } - assertEvaluatesToError(evaluate(eq(value, errorExpr), testDoc)) { "eq($value, $errorExpr)" } - } - assertEvaluatesToError(evaluate(eq(errorExpr, errorExpr), testDoc)) { - "eq($errorExpr, $errorExpr)" - } - assertEvaluatesToError(evaluate(eq(errorExpr, nullValue()), testDoc)) { - "eq($errorExpr, ${nullValue()})" + assertEvaluatesToError( + evaluate(eq(errorExpr, value), testDoc), + "eq(%s, %s)", + errorExpr, + value + ) + assertEvaluatesToError( + evaluate(eq(value, errorExpr), testDoc), + "eq(%s, %s)", + value, + errorExpr + ) } + assertEvaluatesToError( + evaluate(eq(errorExpr, errorExpr), testDoc), + "eq(%s, %s)", + errorExpr, + errorExpr + ) + assertEvaluatesToError( + evaluate(eq(errorExpr, nullValue()), testDoc), + "eq(%s, %s)", + errorExpr, + nullValue() + ) } @Test @@ -373,12 +414,18 @@ internal class ComparisonTests { val presentValue = constant(1L) val testDoc = doc("test/eqMissing", 0, mapOf("exists" to 10L)) - assertEvaluatesToError(evaluate(eq(missingField, presentValue), testDoc)) { - "eq($missingField, $presentValue)" - } - assertEvaluatesToError(evaluate(eq(presentValue, missingField), testDoc)) { - "eq($presentValue, $missingField)" - } + assertEvaluatesToError( + evaluate(eq(missingField, presentValue), testDoc), + "eq(%s, %s)", + missingField, + presentValue + ) + assertEvaluatesToError( + evaluate(eq(presentValue, missingField), testDoc), + "eq(%s, %s)", + presentValue, + missingField + ) } // --- Neq (!=) Tests --- @@ -388,9 +435,9 @@ internal class ComparisonTests { ComparisonTestData.equivalentValues.forEach { (v1, v2) -> val result = evaluate(neq(v1, v2)) if (v1 == nullValue() && v2 == nullValue()) { - assertEvaluatesToNull(result) { "neq($v1, $v2)" } + assertEvaluatesToNull(result, "neq(%s, %s)", v1, v2) } else { - assertEvaluatesTo(result, false) { "neq($v1, $v2)" } + assertEvaluatesTo(result, false, "neq(%s, %s)", v1, v2) } } } @@ -398,15 +445,15 @@ internal class ComparisonTests { @Test fun neq_lessThanValues_returnTrue() { ComparisonTestData.lessThanValues.forEach { (v1, v2) -> - assertEvaluatesTo(evaluate(neq(v1, v2)), true) { "neq($v1, $v2)" } - assertEvaluatesTo(evaluate(neq(v2, v1)), true) { "neq($v2, $v1)" } + assertEvaluatesTo(evaluate(neq(v1, v2)), true, "neq(%s, %s)", v1, v2) + assertEvaluatesTo(evaluate(neq(v2, v1)), true, "neq(%s, %s)", v2, v1) } } @Test fun neq_greaterThanValues_returnTrue() { ComparisonTestData.lessThanValues.forEach { (less, greater) -> - assertEvaluatesTo(evaluate(neq(greater, less)), true) { "neq($greater, $less)" } + assertEvaluatesTo(evaluate(neq(greater, less)), true, "neq(%s, %s)", greater, less) } } @@ -414,11 +461,11 @@ internal class ComparisonTests { fun neq_mixedTypeValues_returnTrue() { ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> if (v1 == nullValue() || v2 == nullValue()) { - assertEvaluatesToNull(evaluate(neq(v1, v2))) { "neq($v1, $v2)" } - assertEvaluatesToNull(evaluate(neq(v2, v1))) { "neq($v2, $v1)" } + assertEvaluatesToNull(evaluate(neq(v1, v2)), "neq(%s, %s)", v1, v2) + assertEvaluatesToNull(evaluate(neq(v2, v1)), "neq(%s, %s)", v2, v1) } else { - assertEvaluatesTo(evaluate(neq(v1, v2)), true) { "neq($v1, $v2)" } - assertEvaluatesTo(evaluate(neq(v2, v1)), true) { "neq($v2, $v1)" } + assertEvaluatesTo(evaluate(neq(v1, v2)), true, "neq(%s, %s)", v1, v2) + assertEvaluatesTo(evaluate(neq(v2, v1)), true, "neq(%s, %s)", v2, v1) } } } @@ -428,29 +475,34 @@ internal class ComparisonTests { val v1 = nullValue() val v2 = nullValue() val result = evaluate(neq(v1, v2)) - assertEvaluatesToNull(result) { "neq($v1, $v2)" } + assertEvaluatesToNull(result, "neq(%s, %s)", v1, v2) } @Test fun neq_nullOperand_returnsNullOrError() { ComparisonTestData.allSupportedComparableValues.forEach { value -> val nullVal = nullValue() - assertEvaluatesToNull(evaluate(neq(nullVal, value))) { "neq($nullVal, $value)" } - assertEvaluatesToNull(evaluate(neq(value, nullVal))) { "neq($value, $nullVal)" } + assertEvaluatesToNull(evaluate(neq(nullVal, value)), "neq(%s, %s)", nullVal, value) + assertEvaluatesToNull(evaluate(neq(value, nullVal)), "neq(%s, %s)", value, nullVal) } val nullVal = nullValue() val missingField = field("nonexistent") - assertEvaluatesToError(evaluate(neq(nullVal, missingField))) { "neq($nullVal, $missingField)" } + assertEvaluatesToError( + evaluate(neq(nullVal, missingField)), + "neq(%s, %s)", + nullVal, + missingField + ) } @Test fun neq_nanComparisons_returnTrue() { val nanExpr = ComparisonTestData.doubleNaN - assertEvaluatesTo(evaluate(neq(nanExpr, nanExpr)), true) { "neq($nanExpr, $nanExpr)" } + assertEvaluatesTo(evaluate(neq(nanExpr, nanExpr)), true, "neq(%s, %s)", nanExpr, nanExpr) ComparisonTestData.numericValuesForNanTest.forEach { numVal -> - assertEvaluatesTo(evaluate(neq(nanExpr, numVal)), true) { "neq($nanExpr, $numVal)" } - assertEvaluatesTo(evaluate(neq(numVal, nanExpr)), true) { "neq($numVal, $nanExpr)" } + assertEvaluatesTo(evaluate(neq(nanExpr, numVal)), true, "neq(%s, %s)", nanExpr, numVal) + assertEvaluatesTo(evaluate(neq(numVal, nanExpr)), true, "neq(%s, %s)", numVal, nanExpr) } (ComparisonTestData.allSupportedComparableValues - @@ -458,22 +510,42 @@ internal class ComparisonTests { nanExpr) .forEach { otherVal -> if (otherVal != nanExpr) { - assertEvaluatesTo(evaluate(neq(nanExpr, otherVal)), true) { "neq($nanExpr, $otherVal)" } - assertEvaluatesTo(evaluate(neq(otherVal, nanExpr)), true) { "neq($otherVal, $nanExpr)" } + assertEvaluatesTo( + evaluate(neq(nanExpr, otherVal)), + true, + "neq(%s, %s)", + nanExpr, + otherVal + ) + assertEvaluatesTo( + evaluate(neq(otherVal, nanExpr)), + true, + "neq(%s, %s)", + otherVal, + nanExpr + ) } } val arrayWithNaN1 = array(constant(Double.NaN)) val arrayWithNaN2 = array(constant(Double.NaN)) - assertEvaluatesTo(evaluate(neq(arrayWithNaN1, arrayWithNaN2)), true) { - "neq($arrayWithNaN1, $arrayWithNaN2)" - } + assertEvaluatesTo( + evaluate(neq(arrayWithNaN1, arrayWithNaN2)), + true, + "neq(%s, %s)", + arrayWithNaN1, + arrayWithNaN2 + ) val mapWithNaN1 = map(mapOf("foo" to Double.NaN)) val mapWithNaN2 = map(mapOf("foo" to Double.NaN)) - assertEvaluatesTo(evaluate(neq(mapWithNaN1, mapWithNaN2)), true) { - "neq($mapWithNaN1, $mapWithNaN2)" - } + assertEvaluatesTo( + evaluate(neq(mapWithNaN1, mapWithNaN2)), + true, + "neq(%s, %s)", + mapWithNaN1, + mapWithNaN2 + ) } @Test @@ -482,15 +554,31 @@ internal class ComparisonTests { val testDoc = doc("test/neqError", 0, mapOf("a" to 123)) ComparisonTestData.allSupportedComparableValues.forEach { value -> - assertEvaluatesToError(evaluate(neq(errorExpr, value), testDoc)) { "neq($errorExpr, $value)" } - assertEvaluatesToError(evaluate(neq(value, errorExpr), testDoc)) { "neq($value, $errorExpr)" } - } - assertEvaluatesToError(evaluate(neq(errorExpr, errorExpr), testDoc)) { - "neq($errorExpr, $errorExpr)" - } - assertEvaluatesToError(evaluate(neq(errorExpr, nullValue()), testDoc)) { - "neq($errorExpr, ${nullValue()})" + assertEvaluatesToError( + evaluate(neq(errorExpr, value), testDoc), + "neq(%s, %s)", + errorExpr, + value + ) + assertEvaluatesToError( + evaluate(neq(value, errorExpr), testDoc), + "neq(%s, %s)", + value, + errorExpr + ) } + assertEvaluatesToError( + evaluate(neq(errorExpr, errorExpr), testDoc), + "neq(%s, %s)", + errorExpr, + errorExpr + ) + assertEvaluatesToError( + evaluate(neq(errorExpr, nullValue()), testDoc), + "neq(%s, %s)", + errorExpr, + nullValue() + ) } @Test @@ -499,12 +587,18 @@ internal class ComparisonTests { val presentValue = constant(1L) val testDoc = doc("test/neqMissing", 0, mapOf("exists" to 10L)) - assertEvaluatesToError(evaluate(neq(missingField, presentValue), testDoc)) { - "neq($missingField, $presentValue)" - } - assertEvaluatesToError(evaluate(neq(presentValue, missingField), testDoc)) { - "neq($presentValue, $missingField)" - } + assertEvaluatesToError( + evaluate(neq(missingField, presentValue), testDoc), + "neq(%s, %s)", + missingField, + presentValue + ) + assertEvaluatesToError( + evaluate(neq(presentValue, missingField), testDoc), + "neq(%s, %s)", + presentValue, + missingField + ) } // --- Lt (<) Tests --- @@ -513,9 +607,9 @@ internal class ComparisonTests { fun lt_equivalentValues_returnFalse() { ComparisonTestData.equivalentValues.forEach { (v1, v2) -> if (v1 == nullValue() && v2 == nullValue()) { - assertEvaluatesToNull(evaluate(lt(v1, v2))) { "lt($v1, $v2)" } + assertEvaluatesToNull(evaluate(lt(v1, v2)), "lt(%s, %s)", v1, v2) } else { - assertEvaluatesTo(evaluate(lt(v1, v2)), false) { "lt($v1, $v2)" } + assertEvaluatesTo(evaluate(lt(v1, v2)), false, "lt(%s, %s)", v1, v2) } } } @@ -524,14 +618,14 @@ internal class ComparisonTests { fun lt_lessThanValues_returnTrue() { ComparisonTestData.lessThanValues.forEach { (v1, v2) -> val result = evaluate(lt(v1, v2)) - assertEvaluatesTo(result, true) { "lt($v1, $v2)" } + assertEvaluatesTo(result, true, "lt(%s, %s)", v1, v2) } } @Test fun lt_greaterThanValues_returnFalse() { ComparisonTestData.lessThanValues.forEach { (less, greater) -> - assertEvaluatesTo(evaluate(lt(greater, less)), false) { "lt($greater, $less)" } + assertEvaluatesTo(evaluate(lt(greater, less)), false, "lt(%s, %s)", greater, less) } } @@ -539,11 +633,11 @@ internal class ComparisonTests { fun lt_mixedTypeValues_returnFalse() { ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> if (v1 == nullValue() || v2 == nullValue()) { - assertEvaluatesToNull(evaluate(lt(v1, v2))) { "lt($v1, $v2)" } - assertEvaluatesToNull(evaluate(lt(v2, v1))) { "lt($v2, $v1)" } + assertEvaluatesToNull(evaluate(lt(v1, v2)), "lt(%s, %s)", v1, v2) + assertEvaluatesToNull(evaluate(lt(v2, v1)), "lt(%s, %s)", v2, v1) } else { - assertEvaluatesTo(evaluate(lt(v1, v2)), false) { "lt($v1, $v2)" } - assertEvaluatesTo(evaluate(lt(v2, v1)), false) { "lt($v2, $v1)" } + assertEvaluatesTo(evaluate(lt(v1, v2)), false, "lt(%s, %s)", v1, v2) + assertEvaluatesTo(evaluate(lt(v2, v1)), false, "lt(%s, %s)", v2, v1) } } } @@ -552,38 +646,42 @@ internal class ComparisonTests { fun lt_nullOperand_returnsNullOrError() { ComparisonTestData.allSupportedComparableValues.forEach { value -> val nullVal = nullValue() - assertEvaluatesToNull(evaluate(lt(nullVal, value))) { "lt($nullVal, $value)" } - assertEvaluatesToNull(evaluate(lt(value, nullVal))) { "lt($value, $nullVal)" } + assertEvaluatesToNull(evaluate(lt(nullVal, value)), "lt(%s, %s)", nullVal, value) + assertEvaluatesToNull(evaluate(lt(value, nullVal)), "lt(%s, %s)", value, nullVal) } val nullVal = nullValue() - assertEvaluatesToNull(evaluate(lt(nullVal, nullVal))) { "lt($nullVal, $nullVal)" } + assertEvaluatesToNull(evaluate(lt(nullVal, nullVal)), "lt(%s, %s)", nullVal, nullVal) val missingField = field("nonexistent") - assertEvaluatesToError(evaluate(lt(nullVal, missingField))) { "lt($nullVal, $missingField)" } + assertEvaluatesToError(evaluate(lt(nullVal, missingField)), "lt(%s, %s)", nullVal, missingField) } @Test fun lt_nanComparisons_returnFalse() { val nanExpr = ComparisonTestData.doubleNaN - assertEvaluatesTo(evaluate(lt(nanExpr, nanExpr)), false) { "lt($nanExpr, $nanExpr)" } + assertEvaluatesTo(evaluate(lt(nanExpr, nanExpr)), false, "lt(%s, %s)", nanExpr, nanExpr) ComparisonTestData.numericValuesForNanTest.forEach { numVal -> - assertEvaluatesTo(evaluate(lt(nanExpr, numVal)), false) { "lt($nanExpr, $numVal)" } - assertEvaluatesTo(evaluate(lt(numVal, nanExpr)), false) { "lt($numVal, $nanExpr)" } + assertEvaluatesTo(evaluate(lt(nanExpr, numVal)), false, "lt(%s, %s)", nanExpr, numVal) + assertEvaluatesTo(evaluate(lt(numVal, nanExpr)), false, "lt(%s, %s)", numVal, nanExpr) } (ComparisonTestData.allSupportedComparableValues - ComparisonTestData.numericValuesForNanTest.toSet() - nanExpr) .forEach { otherVal -> if (otherVal != nanExpr) { - assertEvaluatesTo(evaluate(lt(nanExpr, otherVal)), false) { "lt($nanExpr, $otherVal)" } - assertEvaluatesTo(evaluate(lt(otherVal, nanExpr)), false) { "lt($otherVal, $nanExpr)" } + assertEvaluatesTo(evaluate(lt(nanExpr, otherVal)), false, "lt(%s, %s)", nanExpr, otherVal) + assertEvaluatesTo(evaluate(lt(otherVal, nanExpr)), false, "lt(%s, %s)", otherVal, nanExpr) } } val arrayWithNaN1 = array(constant(Double.NaN)) val arrayWithNaN2 = array(constant(Double.NaN)) - assertEvaluatesTo(evaluate(lt(arrayWithNaN1, arrayWithNaN2)), false) { - "lt($arrayWithNaN1, $arrayWithNaN2)" - } + assertEvaluatesTo( + evaluate(lt(arrayWithNaN1, arrayWithNaN2)), + false, + "lt(%s, %s)", + arrayWithNaN1, + arrayWithNaN2 + ) } @Test @@ -592,15 +690,31 @@ internal class ComparisonTests { val testDoc = doc("test/ltError", 0, mapOf("a" to 123)) ComparisonTestData.allSupportedComparableValues.forEach { value -> - assertEvaluatesToError(evaluate(lt(errorExpr, value), testDoc)) { "lt($errorExpr, $value)" } - assertEvaluatesToError(evaluate(lt(value, errorExpr), testDoc)) { "lt($value, $errorExpr)" } - } - assertEvaluatesToError(evaluate(lt(errorExpr, errorExpr), testDoc)) { - "lt($errorExpr, $errorExpr)" - } - assertEvaluatesToError(evaluate(lt(errorExpr, nullValue()), testDoc)) { - "lt($errorExpr, ${nullValue()})" + assertEvaluatesToError( + evaluate(lt(errorExpr, value), testDoc), + "lt(%s, %s)", + errorExpr, + value + ) + assertEvaluatesToError( + evaluate(lt(value, errorExpr), testDoc), + "lt(%s, %s)", + value, + errorExpr + ) } + assertEvaluatesToError( + evaluate(lt(errorExpr, errorExpr), testDoc), + "lt(%s, %s)", + errorExpr, + errorExpr + ) + assertEvaluatesToError( + evaluate(lt(errorExpr, nullValue()), testDoc), + "lt(%s, %s)", + errorExpr, + nullValue() + ) } @Test @@ -609,12 +723,18 @@ internal class ComparisonTests { val presentValue = constant(1L) val testDoc = doc("test/ltMissing", 0, mapOf("exists" to 10L)) - assertEvaluatesToError(evaluate(lt(missingField, presentValue), testDoc)) { - "lt($missingField, $presentValue)" - } - assertEvaluatesToError(evaluate(lt(presentValue, missingField), testDoc)) { - "lt($presentValue, $missingField)" - } + assertEvaluatesToError( + evaluate(lt(missingField, presentValue), testDoc), + "lt(%s, %s)", + missingField, + presentValue + ) + assertEvaluatesToError( + evaluate(lt(presentValue, missingField), testDoc), + "lt(%s, %s)", + presentValue, + missingField + ) } // --- Lte (<=) Tests --- @@ -623,9 +743,9 @@ internal class ComparisonTests { fun lte_equivalentValues_returnTrue() { ComparisonTestData.equivalentValues.forEach { (v1, v2) -> if (v1 == nullValue() && v2 == nullValue()) { - assertEvaluatesToNull(evaluate(lte(v1, v2))) { "lte($v1, $v2)" } + assertEvaluatesToNull(evaluate(lte(v1, v2)), "lte(%s, %s)", v1, v2) } else { - assertEvaluatesTo(evaluate(lte(v1, v2)), true) { "lte($v1, $v2)" } + assertEvaluatesTo(evaluate(lte(v1, v2)), true, "lte(%s, %s)", v1, v2) } } } @@ -633,14 +753,14 @@ internal class ComparisonTests { @Test fun lte_lessThanValues_returnTrue() { ComparisonTestData.lessThanValues.forEach { (v1, v2) -> - assertEvaluatesTo(evaluate(lte(v1, v2)), true) { "lte($v1, $v2)" } + assertEvaluatesTo(evaluate(lte(v1, v2)), true, "lte(%s, %s)", v1, v2) } } @Test fun lte_greaterThanValues_returnFalse() { ComparisonTestData.lessThanValues.forEach { (less, greater) -> - assertEvaluatesTo(evaluate(lte(greater, less)), false) { "lte($greater, $less)" } + assertEvaluatesTo(evaluate(lte(greater, less)), false, "lte(%s, %s)", greater, less) } } @@ -648,11 +768,11 @@ internal class ComparisonTests { fun lte_mixedTypeValues_returnFalse() { ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> if (v1 == nullValue() || v2 == nullValue()) { - assertEvaluatesToNull(evaluate(lte(v1, v2))) { "lte($v1, $v2)" } - assertEvaluatesToNull(evaluate(lte(v2, v1))) { "lte($v2, $v1)" } + assertEvaluatesToNull(evaluate(lte(v1, v2)), "lte(%s, %s)", v1, v2) + assertEvaluatesToNull(evaluate(lte(v2, v1)), "lte(%s, %s)", v2, v1) } else { - assertEvaluatesTo(evaluate(lte(v1, v2)), false) { "lte($v1, $v2)" } - assertEvaluatesTo(evaluate(lte(v2, v1)), false) { "lte($v2, $v1)" } + assertEvaluatesTo(evaluate(lte(v1, v2)), false, "lte(%s, %s)", v1, v2) + assertEvaluatesTo(evaluate(lte(v2, v1)), false, "lte(%s, %s)", v2, v1) } } } @@ -661,38 +781,59 @@ internal class ComparisonTests { fun lte_nullOperand_returnsNullOrError() { ComparisonTestData.allSupportedComparableValues.forEach { value -> val nullVal = nullValue() - assertEvaluatesToNull(evaluate(lte(nullVal, value))) { "lte($nullVal, $value)" } - assertEvaluatesToNull(evaluate(lte(value, nullVal))) { "lte($value, $nullVal)" } + assertEvaluatesToNull(evaluate(lte(nullVal, value)), "lte(%s, %s)", nullVal, value) + assertEvaluatesToNull(evaluate(lte(value, nullVal)), "lte(%s, %s)", value, nullVal) } val nullVal = nullValue() - assertEvaluatesToNull(evaluate(lte(nullVal, nullVal))) { "lte($nullVal, $nullVal)" } + assertEvaluatesToNull(evaluate(lte(nullVal, nullVal)), "lte(%s, %s)", nullVal, nullVal) val missingField = field("nonexistent") - assertEvaluatesToError(evaluate(lte(nullVal, missingField))) { "lte($nullVal, $missingField)" } + assertEvaluatesToError( + evaluate(lte(nullVal, missingField)), + "lte(%s, %s)", + nullVal, + missingField + ) } @Test fun lte_nanComparisons_returnFalse() { val nanExpr = ComparisonTestData.doubleNaN - assertEvaluatesTo(evaluate(lte(nanExpr, nanExpr)), false) { "lte($nanExpr, $nanExpr)" } + assertEvaluatesTo(evaluate(lte(nanExpr, nanExpr)), false, "lte(%s, %s)", nanExpr, nanExpr) ComparisonTestData.numericValuesForNanTest.forEach { numVal -> - assertEvaluatesTo(evaluate(lte(nanExpr, numVal)), false) { "lte($nanExpr, $numVal)" } - assertEvaluatesTo(evaluate(lte(numVal, nanExpr)), false) { "lte($numVal, $nanExpr)" } + assertEvaluatesTo(evaluate(lte(nanExpr, numVal)), false, "lte(%s, %s)", nanExpr, numVal) + assertEvaluatesTo(evaluate(lte(numVal, nanExpr)), false, "lte(%s, %s)", numVal, nanExpr) } (ComparisonTestData.allSupportedComparableValues - ComparisonTestData.numericValuesForNanTest.toSet() - nanExpr) .forEach { otherVal -> if (otherVal != nanExpr) { - assertEvaluatesTo(evaluate(lte(nanExpr, otherVal)), false) { "lte($nanExpr, $otherVal)" } - assertEvaluatesTo(evaluate(lte(otherVal, nanExpr)), false) { "lte($otherVal, $nanExpr)" } + assertEvaluatesTo( + evaluate(lte(nanExpr, otherVal)), + false, + "lte(%s, %s)", + nanExpr, + otherVal + ) + assertEvaluatesTo( + evaluate(lte(otherVal, nanExpr)), + false, + "lte(%s, %s)", + otherVal, + nanExpr + ) } } val arrayWithNaN1 = array(constant(Double.NaN)) val arrayWithNaN2 = array(constant(Double.NaN)) - assertEvaluatesTo(evaluate(lte(arrayWithNaN1, arrayWithNaN2)), false) { - "lte($arrayWithNaN1, $arrayWithNaN2)" - } + assertEvaluatesTo( + evaluate(lte(arrayWithNaN1, arrayWithNaN2)), + false, + "lte(%s, %s)", + arrayWithNaN1, + arrayWithNaN2 + ) } @Test @@ -701,15 +842,31 @@ internal class ComparisonTests { val testDoc = doc("test/lteError", 0, mapOf("a" to 123)) ComparisonTestData.allSupportedComparableValues.forEach { value -> - assertEvaluatesToError(evaluate(lte(errorExpr, value), testDoc)) { "lte($errorExpr, $value)" } - assertEvaluatesToError(evaluate(lte(value, errorExpr), testDoc)) { "lte($value, $errorExpr)" } - } - assertEvaluatesToError(evaluate(lte(errorExpr, errorExpr), testDoc)) { - "lte($errorExpr, $errorExpr)" - } - assertEvaluatesToError(evaluate(lte(errorExpr, nullValue()), testDoc)) { - "lte($errorExpr, ${nullValue()})" + assertEvaluatesToError( + evaluate(lte(errorExpr, value), testDoc), + "lte(%s, %s)", + errorExpr, + value + ) + assertEvaluatesToError( + evaluate(lte(value, errorExpr), testDoc), + "lte(%s, %s)", + value, + errorExpr + ) } + assertEvaluatesToError( + evaluate(lte(errorExpr, errorExpr), testDoc), + "lte(%s, %s)", + errorExpr, + errorExpr + ) + assertEvaluatesToError( + evaluate(lte(errorExpr, nullValue()), testDoc), + "lte(%s, %s)", + errorExpr, + nullValue() + ) } @Test @@ -718,12 +875,18 @@ internal class ComparisonTests { val presentValue = constant(1L) val testDoc = doc("test/lteMissing", 0, mapOf("exists" to 10L)) - assertEvaluatesToError(evaluate(lte(missingField, presentValue), testDoc)) { - "lte($missingField, $presentValue)" - } - assertEvaluatesToError(evaluate(lte(presentValue, missingField), testDoc)) { - "lte($presentValue, $missingField)" - } + assertEvaluatesToError( + evaluate(lte(missingField, presentValue), testDoc), + "lte(%s, %s)", + missingField, + presentValue + ) + assertEvaluatesToError( + evaluate(lte(presentValue, missingField), testDoc), + "lte(%s, %s)", + presentValue, + missingField + ) } // --- Gt (>) Tests --- @@ -732,9 +895,9 @@ internal class ComparisonTests { fun gt_equivalentValues_returnFalse() { ComparisonTestData.equivalentValues.forEach { (v1, v2) -> if (v1 == nullValue() && v2 == nullValue()) { - assertEvaluatesToNull(evaluate(gt(v1, v2))) { "gt($v1, $v2)" } + assertEvaluatesToNull(evaluate(gt(v1, v2)), "gt(%s, %s)", v1, v2) } else { - assertEvaluatesTo(evaluate(gt(v1, v2)), false) { "gt($v1, $v2)" } + assertEvaluatesTo(evaluate(gt(v1, v2)), false, "gt(%s, %s)", v1, v2) } } } @@ -742,14 +905,14 @@ internal class ComparisonTests { @Test fun gt_lessThanValues_returnFalse() { ComparisonTestData.lessThanValues.forEach { (v1, v2) -> - assertEvaluatesTo(evaluate(gt(v1, v2)), false) { "gt($v1, $v2)" } + assertEvaluatesTo(evaluate(gt(v1, v2)), false, "gt(%s, %s)", v1, v2) } } @Test fun gt_greaterThanValues_returnTrue() { ComparisonTestData.lessThanValues.forEach { (less, greater) -> - assertEvaluatesTo(evaluate(gt(greater, less)), true) { "gt($greater, $less)" } + assertEvaluatesTo(evaluate(gt(greater, less)), true, "gt(%s, %s)", greater, less) } } @@ -757,11 +920,11 @@ internal class ComparisonTests { fun gt_mixedTypeValues_returnFalse() { ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> if (v1 == nullValue() || v2 == nullValue()) { - assertEvaluatesToNull(evaluate(gt(v1, v2))) { "gt($v1, $v2)" } - assertEvaluatesToNull(evaluate(gt(v2, v1))) { "gt($v2, $v1)" } + assertEvaluatesToNull(evaluate(gt(v1, v2)), "gt(%s, %s)", v1, v2) + assertEvaluatesToNull(evaluate(gt(v2, v1)), "gt(%s, %s)", v2, v1) } else { - assertEvaluatesTo(evaluate(gt(v1, v2)), false) { "gt($v1, $v2)" } - assertEvaluatesTo(evaluate(gt(v2, v1)), false) { "gt($v2, $v1)" } + assertEvaluatesTo(evaluate(gt(v1, v2)), false, "gt(%s, %s)", v1, v2) + assertEvaluatesTo(evaluate(gt(v2, v1)), false, "gt(%s, %s)", v2, v1) } } } @@ -770,38 +933,42 @@ internal class ComparisonTests { fun gt_nullOperand_returnsNullOrError() { ComparisonTestData.allSupportedComparableValues.forEach { value -> val nullVal = nullValue() - assertEvaluatesToNull(evaluate(gt(nullVal, value))) { "gt($nullVal, $value)" } - assertEvaluatesToNull(evaluate(gt(value, nullVal))) { "gt($value, $nullVal)" } + assertEvaluatesToNull(evaluate(gt(nullVal, value)), "gt(%s, %s)", nullVal, value) + assertEvaluatesToNull(evaluate(gt(value, nullVal)), "gt(%s, %s)", value, nullVal) } val nullVal = nullValue() - assertEvaluatesToNull(evaluate(gt(nullVal, nullVal))) { "gt($nullVal, $nullVal)" } + assertEvaluatesToNull(evaluate(gt(nullVal, nullVal)), "gt(%s, %s)", nullVal, nullVal) val missingField = field("nonexistent") - assertEvaluatesToError(evaluate(gt(nullVal, missingField))) { "gt($nullVal, $missingField)" } + assertEvaluatesToError(evaluate(gt(nullVal, missingField)), "gt(%s, %s)", nullVal, missingField) } @Test fun gt_nanComparisons_returnFalse() { val nanExpr = ComparisonTestData.doubleNaN - assertEvaluatesTo(evaluate(gt(nanExpr, nanExpr)), false) { "gt($nanExpr, $nanExpr)" } + assertEvaluatesTo(evaluate(gt(nanExpr, nanExpr)), false, "gt(%s, %s)", nanExpr, nanExpr) ComparisonTestData.numericValuesForNanTest.forEach { numVal -> - assertEvaluatesTo(evaluate(gt(nanExpr, numVal)), false) { "gt($nanExpr, $numVal)" } - assertEvaluatesTo(evaluate(gt(numVal, nanExpr)), false) { "gt($numVal, $nanExpr)" } + assertEvaluatesTo(evaluate(gt(nanExpr, numVal)), false, "gt(%s, %s)", nanExpr, numVal) + assertEvaluatesTo(evaluate(gt(numVal, nanExpr)), false, "gt(%s, %s)", numVal, nanExpr) } (ComparisonTestData.allSupportedComparableValues - ComparisonTestData.numericValuesForNanTest.toSet() - nanExpr) .forEach { otherVal -> if (otherVal != nanExpr) { - assertEvaluatesTo(evaluate(gt(nanExpr, otherVal)), false) { "gt($nanExpr, $otherVal)" } - assertEvaluatesTo(evaluate(gt(otherVal, nanExpr)), false) { "gt($otherVal, $nanExpr)" } + assertEvaluatesTo(evaluate(gt(nanExpr, otherVal)), false, "gt(%s, %s)", nanExpr, otherVal) + assertEvaluatesTo(evaluate(gt(otherVal, nanExpr)), false, "gt(%s, %s)", otherVal, nanExpr) } } val arrayWithNaN1 = array(constant(Double.NaN)) val arrayWithNaN2 = array(constant(Double.NaN)) - assertEvaluatesTo(evaluate(gt(arrayWithNaN1, arrayWithNaN2)), false) { - "gt($arrayWithNaN1, $arrayWithNaN2)" - } + assertEvaluatesTo( + evaluate(gt(arrayWithNaN1, arrayWithNaN2)), + false, + "gt(%s, %s)", + arrayWithNaN1, + arrayWithNaN2 + ) } @Test @@ -810,15 +977,31 @@ internal class ComparisonTests { val testDoc = doc("test/gtError", 0, mapOf("a" to 123)) ComparisonTestData.allSupportedComparableValues.forEach { value -> - assertEvaluatesToError(evaluate(gt(errorExpr, value), testDoc)) { "gt($errorExpr, $value)" } - assertEvaluatesToError(evaluate(gt(value, errorExpr), testDoc)) { "gt($value, $errorExpr)" } - } - assertEvaluatesToError(evaluate(gt(errorExpr, errorExpr), testDoc)) { - "gt($errorExpr, $errorExpr)" - } - assertEvaluatesToError(evaluate(gt(errorExpr, nullValue()), testDoc)) { - "gt($errorExpr, ${nullValue()})" + assertEvaluatesToError( + evaluate(gt(errorExpr, value), testDoc), + "gt(%s, %s)", + errorExpr, + value + ) + assertEvaluatesToError( + evaluate(gt(value, errorExpr), testDoc), + "gt(%s, %s)", + value, + errorExpr + ) } + assertEvaluatesToError( + evaluate(gt(errorExpr, errorExpr), testDoc), + "gt(%s, %s)", + errorExpr, + errorExpr + ) + assertEvaluatesToError( + evaluate(gt(errorExpr, nullValue()), testDoc), + "gt(%s, %s)", + errorExpr, + nullValue() + ) } @Test @@ -827,12 +1010,18 @@ internal class ComparisonTests { val presentValue = constant(1L) val testDoc = doc("test/gtMissing", 0, mapOf("exists" to 10L)) - assertEvaluatesToError(evaluate(gt(missingField, presentValue), testDoc)) { - "gt($missingField, $presentValue)" - } - assertEvaluatesToError(evaluate(gt(presentValue, missingField), testDoc)) { - "gt($presentValue, $missingField)" - } + assertEvaluatesToError( + evaluate(gt(missingField, presentValue), testDoc), + "gt(%s, %s)", + missingField, + presentValue + ) + assertEvaluatesToError( + evaluate(gt(presentValue, missingField), testDoc), + "gt(%s, %s)", + presentValue, + missingField + ) } // --- Gte (>=) Tests --- @@ -841,9 +1030,9 @@ internal class ComparisonTests { fun gte_equivalentValues_returnTrue() { ComparisonTestData.equivalentValues.forEach { (v1, v2) -> if (v1 == nullValue() && v2 == nullValue()) { - assertEvaluatesToNull(evaluate(gte(v1, v2))) { "gte($v1, $v2)" } + assertEvaluatesToNull(evaluate(gte(v1, v2)), "gte(%s, %s)", v1, v2) } else { - assertEvaluatesTo(evaluate(gte(v1, v2)), true) { "gte($v1, $v2)" } + assertEvaluatesTo(evaluate(gte(v1, v2)), true, "gte(%s, %s)", v1, v2) } } } @@ -851,14 +1040,14 @@ internal class ComparisonTests { @Test fun gte_lessThanValues_returnFalse() { ComparisonTestData.lessThanValues.forEach { (v1, v2) -> - assertEvaluatesTo(evaluate(gte(v1, v2)), false) { "gte($v1, $v2)" } + assertEvaluatesTo(evaluate(gte(v1, v2)), false, "gte(%s, %s)", v1, v2) } } @Test fun gte_greaterThanValues_returnTrue() { ComparisonTestData.lessThanValues.forEach { (less, greater) -> - assertEvaluatesTo(evaluate(gte(greater, less)), true) { "gte($greater, $less)" } + assertEvaluatesTo(evaluate(gte(greater, less)), true, "gte(%s, %s)", greater, less) } } @@ -866,11 +1055,11 @@ internal class ComparisonTests { fun gte_mixedTypeValues_returnFalse() { ComparisonTestData.mixedTypeValues.forEach { (v1, v2) -> if (v1 == nullValue() || v2 == nullValue()) { - assertEvaluatesToNull(evaluate(gte(v1, v2))) { "gte($v1, $v2)" } - assertEvaluatesToNull(evaluate(gte(v2, v1))) { "gte($v2, $v1)" } + assertEvaluatesToNull(evaluate(gte(v1, v2)), "gte(%s, %s)", v1, v2) + assertEvaluatesToNull(evaluate(gte(v2, v1)), "gte(%s, %s)", v2, v1) } else { - assertEvaluatesTo(evaluate(gte(v1, v2)), false) { "gte($v1, $v2)" } - assertEvaluatesTo(evaluate(gte(v2, v1)), false) { "gte($v2, $v1)" } + assertEvaluatesTo(evaluate(gte(v1, v2)), false, "gte(%s, %s)", v1, v2) + assertEvaluatesTo(evaluate(gte(v2, v1)), false, "gte(%s, %s)", v2, v1) } } } @@ -879,38 +1068,59 @@ internal class ComparisonTests { fun gte_nullOperand_returnsNullOrError() { ComparisonTestData.allSupportedComparableValues.forEach { value -> val nullVal = nullValue() - assertEvaluatesToNull(evaluate(gte(nullVal, value))) { "gte($nullVal, $value)" } - assertEvaluatesToNull(evaluate(gte(value, nullVal))) { "gte($value, $nullVal)" } + assertEvaluatesToNull(evaluate(gte(nullVal, value)), "gte(%s, %s)", nullVal, value) + assertEvaluatesToNull(evaluate(gte(value, nullVal)), "gte(%s, %s)", value, nullVal) } val nullVal = nullValue() - assertEvaluatesToNull(evaluate(gte(nullVal, nullVal))) { "gte($nullVal, $nullVal)" } + assertEvaluatesToNull(evaluate(gte(nullVal, nullVal)), "gte(%s, %s)", nullVal, nullVal) val missingField = field("nonexistent") - assertEvaluatesToError(evaluate(gte(nullVal, missingField))) { "gte($nullVal, $missingField)" } + assertEvaluatesToError( + evaluate(gte(nullVal, missingField)), + "gte(%s, %s)", + nullVal, + missingField + ) } @Test fun gte_nanComparisons_returnFalse() { val nanExpr = ComparisonTestData.doubleNaN - assertEvaluatesTo(evaluate(gte(nanExpr, nanExpr)), false) { "gte($nanExpr, $nanExpr)" } + assertEvaluatesTo(evaluate(gte(nanExpr, nanExpr)), false, "gte(%s, %s)", nanExpr, nanExpr) ComparisonTestData.numericValuesForNanTest.forEach { numVal -> - assertEvaluatesTo(evaluate(gte(nanExpr, numVal)), false) { "gte($nanExpr, $numVal)" } - assertEvaluatesTo(evaluate(gte(numVal, nanExpr)), false) { "gte($numVal, $nanExpr)" } + assertEvaluatesTo(evaluate(gte(nanExpr, numVal)), false, "gte(%s, %s)", nanExpr, numVal) + assertEvaluatesTo(evaluate(gte(numVal, nanExpr)), false, "gte(%s, %s)", numVal, nanExpr) } (ComparisonTestData.allSupportedComparableValues - ComparisonTestData.numericValuesForNanTest.toSet() - nanExpr) .forEach { otherVal -> if (otherVal != nanExpr) { - assertEvaluatesTo(evaluate(gte(nanExpr, otherVal)), false) { "gte($nanExpr, $otherVal)" } - assertEvaluatesTo(evaluate(gte(otherVal, nanExpr)), false) { "gte($otherVal, $nanExpr)" } + assertEvaluatesTo( + evaluate(gte(nanExpr, otherVal)), + false, + "gte(%s, %s)", + nanExpr, + otherVal + ) + assertEvaluatesTo( + evaluate(gte(otherVal, nanExpr)), + false, + "gte(%s, %s)", + otherVal, + nanExpr + ) } } val arrayWithNaN1 = array(constant(Double.NaN)) val arrayWithNaN2 = array(constant(Double.NaN)) - assertEvaluatesTo(evaluate(gte(arrayWithNaN1, arrayWithNaN2)), false) { - "gte($arrayWithNaN1, $arrayWithNaN2)" - } + assertEvaluatesTo( + evaluate(gte(arrayWithNaN1, arrayWithNaN2)), + false, + "gte(%s, %s)", + arrayWithNaN1, + arrayWithNaN2 + ) } @Test @@ -919,15 +1129,31 @@ internal class ComparisonTests { val testDoc = doc("test/gteError", 0, mapOf("a" to 123)) ComparisonTestData.allSupportedComparableValues.forEach { value -> - assertEvaluatesToError(evaluate(gte(errorExpr, value), testDoc)) { "gte($errorExpr, $value)" } - assertEvaluatesToError(evaluate(gte(value, errorExpr), testDoc)) { "gte($value, $errorExpr)" } - } - assertEvaluatesToError(evaluate(gte(errorExpr, errorExpr), testDoc)) { - "gte($errorExpr, $errorExpr)" - } - assertEvaluatesToError(evaluate(gte(errorExpr, nullValue()), testDoc)) { - "gte($errorExpr, ${nullValue()})" + assertEvaluatesToError( + evaluate(gte(errorExpr, value), testDoc), + "gte(%s, %s)", + errorExpr, + value + ) + assertEvaluatesToError( + evaluate(gte(value, errorExpr), testDoc), + "gte(%s, %s)", + value, + errorExpr + ) } + assertEvaluatesToError( + evaluate(gte(errorExpr, errorExpr), testDoc), + "gte(%s, %s)", + errorExpr, + errorExpr + ) + assertEvaluatesToError( + evaluate(gte(errorExpr, nullValue()), testDoc), + "gte(%s, %s)", + errorExpr, + nullValue() + ) } @Test @@ -936,11 +1162,17 @@ internal class ComparisonTests { val presentValue = constant(1L) val testDoc = doc("test/gteMissing", 0, mapOf("exists" to 10L)) - assertEvaluatesToError(evaluate(gte(missingField, presentValue), testDoc)) { - "gte($missingField, $presentValue)" - } - assertEvaluatesToError(evaluate(gte(presentValue, missingField), testDoc)) { - "gte($presentValue, $missingField)" - } + assertEvaluatesToError( + evaluate(gte(missingField, presentValue), testDoc), + "gte(%s, %s)", + missingField, + presentValue + ) + assertEvaluatesToError( + evaluate(gte(presentValue, missingField), testDoc), + "gte(%s, %s)", + presentValue, + missingField + ) } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt index 99de633cac5..02c1325f506 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt @@ -20,23 +20,30 @@ internal fun evaluate(expr: Expr, doc: MutableDocument): EvaluateResult { } // Helper to check for successful evaluation to a boolean value -internal fun assertEvaluatesTo(result: EvaluateResult, expected: Boolean, message: () -> String) { - assertWithMessage(message()).that(result.isSuccess).isTrue() - assertWithMessage(message()).that(result.value).isEqualTo(encodeValue(expected)) +internal fun assertEvaluatesTo( + result: EvaluateResult, + expected: Boolean, + format: String, + vararg args: Any? +) { + assertWithMessage(format, *args).that(result.isSuccess).isTrue() + assertWithMessage(format, *args).that(result.value).isEqualTo(encodeValue(expected)) } // Helper to check for evaluation resulting in NULL -internal fun assertEvaluatesToNull(result: EvaluateResult, message: () -> String) { - assertWithMessage(message()).that(result.isSuccess).isTrue() // Null is a successful evaluation - assertWithMessage(message()).that(result.value).isEqualTo(NULL_VALUE) +internal fun assertEvaluatesToNull(result: EvaluateResult, format: String, vararg args: Any?) { + assertWithMessage(format, *args) + .that(result.isSuccess) + .isTrue() // Null is a successful evaluation + assertWithMessage(format, *args).that(result.value).isEqualTo(NULL_VALUE) } // Helper to check for evaluation resulting in UNSET (e.g. field not found) -internal fun assertEvaluatesToUnset(result: EvaluateResult, message: () -> String) { - assertWithMessage(message()).that(result).isSameInstanceAs(EvaluateResultUnset) +internal fun assertEvaluatesToUnset(result: EvaluateResult, format: String, vararg args: Any?) { + assertWithMessage(format, *args).that(result).isSameInstanceAs(EvaluateResultUnset) } // Helper to check for evaluation resulting in an error -internal fun assertEvaluatesToError(result: EvaluateResult, message: () -> String) { - assertWithMessage(message()).that(result).isSameInstanceAs(EvaluateResultError) +internal fun assertEvaluatesToError(result: EvaluateResult, format: String, vararg args: Any?) { + assertWithMessage(format, *args).that(result).isSameInstanceAs(EvaluateResultError) } From 8c516fd029d9dd79081073931a298c4e61d33fd0 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 29 May 2025 22:31:12 -0400 Subject: [PATCH 089/152] Implement and test realtime array functions --- .../firebase/firestore/pipeline/evaluation.kt | 90 +++++- .../firestore/pipeline/expressions.kt | 13 +- .../firebase/firestore/pipeline/ArrayTests.kt | 291 ++++++++++++++++++ 3 files changed, 384 insertions(+), 10 deletions(-) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArrayTests.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index 1adbbc23f8a..d8ed275a741 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -243,15 +243,63 @@ internal val evaluateSubtract = arithmeticPrimitive(Math::subtractExact, Double: internal val evaluateArray = variadicNullableValueFunction(EvaluateResult.Companion::list) -internal val evaluateEqAny = notImplemented +internal val evaluateEqAny = binaryFunction { list: List, value: Value -> + eqAny(value, list) +} internal val evaluateNotEqAny = notImplemented -internal val evaluateArrayContains = notImplemented +internal val evaluateArrayContains = binaryFunction { array: Value, value: Value -> + if (array.hasArrayValue()) eqAny(value, array.arrayValue.valuesList) else EvaluateResultError +} -internal val evaluateArrayContainsAny = notImplemented +internal val evaluateArrayContainsAny = + binaryFunction { array: List, searchValues: List -> + var foundNull = false + for (value in array) for (search in searchValues) when (strictEquals(value, search)) { + true -> return@binaryFunction EvaluateResult.TRUE + false -> {} + null -> foundNull = true + } + return@binaryFunction if (foundNull) EvaluateResult.NULL else EvaluateResult.FALSE + } -internal val evaluateArrayLength = notImplemented +internal val evaluateArrayContainsAll = + binaryFunction { array: List, searchValues: List -> + var foundNullAtLeastOnce = false + for (search in searchValues) { + var found = false + var foundNull = false + for (value in array) when (strictEquals(value, search)) { + true -> { + found = true + break + } + false -> {} + null -> foundNull = true + } + if (foundNull) { + foundNullAtLeastOnce = true + } else if (!found) { + return@binaryFunction EvaluateResult.FALSE + } + } + return@binaryFunction if (foundNullAtLeastOnce) EvaluateResult.NULL else EvaluateResult.TRUE + } + +internal val evaluateArrayLength = unaryFunction { array: List -> + EvaluateResult.long(array.size) +} + +private fun eqAny(value: Value, list: List): EvaluateResult { + var foundNull = false + for (element in list) when (strictEquals(value, element)) { + true -> return EvaluateResult.TRUE + false -> {} + null -> foundNull = true + } + return if (foundNull) EvaluateResult.NULL else EvaluateResult.FALSE +} // === String Functions === @@ -490,6 +538,14 @@ private inline fun unaryFunction(crossinline timestampOp: (Timestamp) -> Evaluat timestampOp, ) +@JvmName("unaryArrayFunction") +private inline fun unaryFunction(crossinline longOp: (List) -> EvaluateResult) = + unaryFunctionType( + Value.ValueTypeCase.ARRAY_VALUE, + { it.arrayValue.valuesList }, + longOp, + ) + private inline fun unaryFunction( crossinline byteOp: (ByteString) -> EvaluateResult, crossinline stringOp: (String) -> EvaluateResult @@ -559,6 +615,20 @@ private inline fun binaryFunction( } } +@JvmName("binaryValueArrayFunction") +private inline fun binaryFunction( + crossinline function: (Value, List) -> EvaluateResult +): EvaluateFunction = binaryFunction { v1: Value, v2: Value -> + if (v2.hasArrayValue()) function(v1, v2.arrayValue.valuesList) else EvaluateResultError +} + +@JvmName("binaryArrayValueFunction") +private inline fun binaryFunction( + crossinline function: (List, Value) -> EvaluateResult +): EvaluateFunction = binaryFunction { v1: Value, v2: Value -> + if (v1.hasArrayValue()) function(v1.arrayValue.valuesList, v2) else EvaluateResultError +} + @JvmName("binaryStringStringFunction") private inline fun binaryFunction(crossinline function: (String, String) -> EvaluateResult) = binaryFunctionType( @@ -569,6 +639,18 @@ private inline fun binaryFunction(crossinline function: (String, String) -> Eval function ) +@JvmName("binaryArrayArrayFunction") +private inline fun binaryFunction( + crossinline function: (List, List) -> EvaluateResult +) = + binaryFunctionType( + Value.ValueTypeCase.ARRAY_VALUE, + { it.arrayValue.valuesList }, + Value.ValueTypeCase.ARRAY_VALUE, + { it.arrayValue.valuesList }, + function + ) + private inline fun ternaryTimestampFunction( crossinline function: (Timestamp, String, Long) -> EvaluateResult ): EvaluateFunction = ternaryNullableValueFunction { timestamp: Value, unit: Value, number: Value -> 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 3c3bafd6297..28351465f74 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 @@ -2610,7 +2610,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the array function. */ @JvmStatic - fun array(vararg elements: Expr): Expr = FunctionExpr("array", evaluateArray, elements) + fun array(vararg elements: Any?): Expr = + FunctionExpr("array", evaluateArray, elements.map(::toExprOrConstant).toTypedArray()) /** * Creates an expression that creates a Firestore array value from an input array. @@ -2619,8 +2620,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the array function. */ @JvmStatic - fun array(elements: List): Expr = - FunctionExpr("array", evaluateArray, elements.toTypedArray()) + fun array(elements: List): Expr = + FunctionExpr("array", evaluateArray, elements.map(::toExprOrConstant).toTypedArray()) /** * Creates an expression that concatenates an array with other arrays. @@ -2753,7 +2754,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAll(array: Expr, arrayExpression: Expr) = - BooleanExpr("array_contains_all", notImplemented, array, arrayExpression) + BooleanExpr("array_contains_all", evaluateArrayContainsAll, array, arrayExpression) /** * Creates an expression that checks if array field contains all the specified [values]. @@ -2766,7 +2767,7 @@ abstract class Expr internal constructor() { fun arrayContainsAll(arrayFieldName: String, values: List) = BooleanExpr( "array_contains_all", - notImplemented, + evaluateArrayContainsAll, arrayFieldName, ListOfExprs(toArrayOfExprOrConstant(values)) ) @@ -2780,7 +2781,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAll(arrayFieldName: String, arrayExpression: Expr) = - BooleanExpr("array_contains_all", notImplemented, arrayFieldName, arrayExpression) + BooleanExpr("array_contains_all", evaluateArrayContainsAll, arrayFieldName, arrayExpression) /** * Creates an expression that checks if [array] contains any of the specified [values]. diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArrayTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArrayTests.kt new file mode 100644 index 00000000000..4c4b94e2468 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArrayTests.kt @@ -0,0 +1,291 @@ +package com.google.firebase.firestore.pipeline + +import com.google.common.truth.Truth.assertWithMessage +import com.google.firebase.firestore.model.Values.encodeValue +import com.google.firebase.firestore.pipeline.Expr.Companion.array // For the helper & direct use +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContains +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContainsAll +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContainsAny +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayLength +import com.google.firebase.firestore.pipeline.Expr.Companion.constant // For the helper +import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.pipeline.Expr.Companion.map // For map literals +import com.google.firebase.firestore.pipeline.Expr.Companion.nullValue // For the helper +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ArrayTests { + // --- ArrayContainsAll Tests --- + @Test + fun `arrayContainsAll - contains all`() { + val arrayToSearch = array("1", 42L, true, "additional", "values", "in", "array") + val valuesToFind = array("1", 42L, true) + val expr = arrayContainsAll(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), true, "arrayContainsAll basic true case") + } + + @Test + fun `arrayContainsAll - does not contain all`() { + val arrayToSearch = array("1", 42L, true) + val valuesToFind = array("1", 99L) + val expr = arrayContainsAll(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), false, "arrayContainsAll basic false case") + } + + @Test + fun `arrayContainsAll - equivalent numerics`() { + val arrayToSearch = array(42L, true, "additional", "values", "in", "array") + val valuesToFind = array(42.0, true) + val expr = arrayContainsAll(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), true, "arrayContainsAll equivalent numerics") + } + + @Test + fun `arrayContainsAll - array to search is empty`() { + val arrayToSearch = array() + val valuesToFind = array(42.0, true) + val expr = arrayContainsAll(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), false, "arrayContainsAll empty array to search") + } + + @Test + fun `arrayContainsAll - search value is empty`() { + val arrayToSearch = array(42.0, true) + val valuesToFind = array() + val expr = arrayContainsAll(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), true, "arrayContainsAll empty search values") + } + + @Test + fun `arrayContainsAll - search value is NaN`() { + val arrayToSearch = array(Double.NaN, 42.0) + val valuesToFind = array(Double.NaN) + // Firestore/backend behavior: NaN comparisons are always false. + // arrayContainsAll uses standard equality which means NaN == NaN is false. + // If arrayToSearch contains NaN and valuesToFind contains NaN, it won't find it. + val expr = arrayContainsAll(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), false, "arrayContainsAll with NaN in search values") + } + + @Test + fun `arrayContainsAll - search value has duplicates`() { + val arrayToSearch = array(true, "hi") + val valuesToFind = array(true, true, true) + val expr = arrayContainsAll(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), true, "arrayContainsAll with duplicate search values") + } + + @Test + fun `arrayContainsAll - array to search is empty and search value is empty`() { + val arrayToSearch = array() + val valuesToFind = array() + val expr = arrayContainsAll(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), true, "arrayContainsAll both empty") + } + + @Test + fun `arrayContainsAll - large number of elements`() { + val elements = (1..500).map { it.toLong() } + // Use the statically imported 'array' directly here as it takes List + // The elements.map { constant(it) } is correct as Expr.array(List) expects Expr elements + val arrayToSearch = array(elements.map { constant(it) }) + val valuesToFind = array(elements.map { constant(it) }) + val expr = arrayContainsAll(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), true, "arrayContainsAll large number of elements") + } + + // --- ArrayContainsAny Tests --- + @Test + fun `arrayContainsAny - value found in array`() { + val arrayToSearch = array(42L, "matang", true) + val valuesToFind = array("matang", false) + val expr = arrayContainsAny(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), true, "arrayContainsAny value found") + } + + @Test + fun `arrayContainsAny - equivalent numerics`() { + val arrayToSearch = array(42L, "matang", true) + val valuesToFind = array(42.0, 2L) + val expr = arrayContainsAny(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), true, "arrayContainsAny equivalent numerics") + } + + @Test + fun `arrayContainsAny - values not found in array`() { + val arrayToSearch = array(42L, "matang", true) + val valuesToFind = array(99L, "false") + val expr = arrayContainsAny(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), false, "arrayContainsAny values not found") + } + + @Test + fun `arrayContainsAny - both input type is array`() { + val arrayToSearch = array(array(1L, 2L, 3L), array(4L, 5L, 6L), array(7L, 8L, 9L)) + val valuesToFind = array(array(1L, 2L, 3L), array(4L, 5L, 6L)) + val expr = arrayContainsAny(arrayToSearch, valuesToFind) + assertEvaluatesTo(evaluate(expr), true, "arrayContainsAny nested arrays") + } + + @Test + fun `arrayContainsAny - search is null returns null`() { + val arrayToSearch = array(null, 1L, "matang", true) + val valuesToFind = array(nullValue()) // Searching for a null + val expr = arrayContainsAny(arrayToSearch, valuesToFind) + // Firestore/backend behavior: null comparisons return null. + assertEvaluatesToNull(evaluate(expr), "arrayContainsAny search for null") + } + + @Test + fun `arrayContainsAny - array is not array type returns error`() { + val expr = arrayContainsAny(constant("matang"), array("matang", false)) + assertEvaluatesToError(evaluate(expr), "arrayContainsAny first arg not array") + } + + @Test + fun `arrayContainsAny - search is not array type returns error`() { + val expr = arrayContainsAny(array("matang", false), constant("matang")) + assertEvaluatesToError(evaluate(expr), "arrayContainsAny second arg not array") + } + + @Test + fun `arrayContainsAny - array not found returns error`() { + val expr = arrayContainsAny(field("not-exist"), array("matang", false)) + // Accessing a non-existent field results in UNSET, which then causes an error in + // arrayContainsAny + assertEvaluatesToError(evaluate(expr), "arrayContainsAny field not-exist for array") + } + + @Test + fun `arrayContainsAny - search not found returns error`() { + val arrayToSearch = array(42L, "matang", true) + val expr = arrayContainsAny(arrayToSearch, field("not-exist")) + // Accessing a non-existent field results in UNSET, which then causes an error in + // arrayContainsAny + assertEvaluatesToError(evaluate(expr), "arrayContainsAny field not-exist for search values") + } + + // --- ArrayContains Tests --- + @Test + fun `arrayContains - value found in array`() { + val expr = arrayContains(array("hello", "world"), constant("hello")) + assertEvaluatesTo(evaluate(expr), true, "arrayContains value found") + } + + @Test + fun `arrayContains - value not found in array`() { + val arrayToSearch = array(42L, "matang", true) + val expr = arrayContains(arrayToSearch, constant(4L)) + assertEvaluatesTo(evaluate(expr), false, "arrayContains value not found") + } + + @Test + fun `arrayContains - equivalent numerics`() { + val arrayToSearch = array(42L, "matang", true) + val expr = arrayContains(arrayToSearch, constant(42.0)) + assertEvaluatesTo(evaluate(expr), true, "arrayContains equivalent numerics") + } + + @Test + fun `arrayContains - both input type is array`() { + val arrayToSearch = array(array(1L, 2L, 3L), array(4L, 5L, 6L), array(7L, 8L, 9L)) + val valueToFind = array(1L, 2L, 3L) + val expr = arrayContains(arrayToSearch, valueToFind) + assertEvaluatesTo(evaluate(expr), true, "arrayContains nested arrays") + } + + @Test + fun `arrayContains - search value is null returns null`() { + val arrayToSearch = array(null, 1L, "matang", true) + val expr = arrayContains(arrayToSearch, nullValue()) + // Firestore/backend behavior: null comparisons return null. + assertEvaluatesToNull(evaluate(expr), "arrayContains search for null") + } + + @Test + fun `arrayContains - search value is null empty values array returns null`() { + val expr = arrayContains(array(), nullValue()) + // Firestore/backend behavior: null comparisons return null. + assertEvaluatesToNull(evaluate(expr), "arrayContains search for null in empty array") + } + + @Test + fun `arrayContains - search value is map`() { + val arrayToSearch = array(123L, mapOf("foo" to 123L), mapOf("bar" to 42L), mapOf("foo" to 42L)) + val valueToFind = map(mapOf("foo" to 42L)) // Use Expr.map directly + val expr = arrayContains(arrayToSearch, valueToFind) + assertEvaluatesTo(evaluate(expr), true, "arrayContains search for map") + } + + @Test + fun `arrayContains - search value is NaN`() { + val arrayToSearch = array(Double.NaN, "foo") + val valueToFind = constant(Double.NaN) + // Firestore/backend behavior: NaN comparisons are always false. + val expr = arrayContains(arrayToSearch, valueToFind) + assertEvaluatesTo(evaluate(expr), false, "arrayContains search for NaN") + } + + @Test + fun `arrayContains - array to search is not array type returns error`() { + val expr = arrayContains(constant("matang"), constant("values")) + assertEvaluatesToError(evaluate(expr), "arrayContains first arg not array") + } + + @Test + fun `arrayContains - array to search not found returns error`() { + val expr = arrayContains(field("not-exist"), constant("matang")) + // Accessing a non-existent field results in UNSET, which then causes an error in arrayContains + assertEvaluatesToError(evaluate(expr), "arrayContains field not-exist for array") + } + + @Test + fun `arrayContains - array to search is empty returns false`() { + val expr = arrayContains(array(), constant("matang")) + assertEvaluatesTo(evaluate(expr), false, "arrayContains empty array") + } + + @Test + fun `arrayContains - search value reference not found returns error`() { + val arrayToSearch = array(42L, "matang", true) + val expr = arrayContains(arrayToSearch, field("not-exist")) + // Accessing a non-existent field for the search value results in UNSET. + // arrayContains then attempts to compare with UNSET, which is an error. + assertEvaluatesToError(evaluate(expr), "arrayContains field not-exist for search value") + } + + // --- ArrayLength Tests --- + @Test + fun `arrayLength - length`() { + val expr = arrayLength(array("1", 42L, true)) + val result = evaluate(expr) + assertWithMessage("arrayLength basic").that(result.isSuccess).isTrue() + assertWithMessage("arrayLength basic value").that(result.value).isEqualTo(encodeValue(3L)) + } + + @Test + fun `arrayLength - empty array`() { + val expr = arrayLength(array()) + val result = evaluate(expr) + assertWithMessage("arrayLength empty").that(result.isSuccess).isTrue() + assertWithMessage("arrayLength empty value").that(result.value).isEqualTo(encodeValue(0L)) + } + + @Test + fun `arrayLength - array with duplicate elements`() { + val expr = arrayLength(array(true, true)) + val result = evaluate(expr) + assertWithMessage("arrayLength duplicates").that(result.isSuccess).isTrue() + assertWithMessage("arrayLength duplicates value").that(result.value).isEqualTo(encodeValue(2L)) + } + + @Test + fun `arrayLength - not array type returns error`() { + assertEvaluatesToError(evaluate(arrayLength(constant("notAnArray"))), "arrayLength string") + assertEvaluatesToError(evaluate(arrayLength(constant(123L))), "arrayLength long") + assertEvaluatesToError(evaluate(arrayLength(constant(true))), "arrayLength boolean") + assertEvaluatesToError(evaluate(arrayLength(map(mapOf("a" to 1)))), "arrayLength map") + } +} From 37e25e7facfba2d98ee1f36aae4e844d5b9df2e9 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 30 May 2025 11:41:47 -0400 Subject: [PATCH 090/152] Implement and test realtime debug functions --- .../firestore/pipeline/EvaluateResult.kt | 19 +-- .../firebase/firestore/pipeline/evaluation.kt | 49 +++++--- .../firestore/pipeline/expressions.kt | 30 ++++- .../firebase/firestore/pipeline/DebugTests.kt | 112 ++++++++++++++++++ 4 files changed, 183 insertions(+), 27 deletions(-) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/DebugTests.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt index ecd3c5d0e99..3ddbfc7ad73 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt @@ -7,10 +7,8 @@ import com.google.protobuf.Timestamp internal sealed class EvaluateResult(val value: Value?) { abstract val isError: Boolean - val isSuccess: Boolean - get() = this is EvaluateResultValue - val isUnset: Boolean - get() = this is EvaluateResultUnset + abstract val isSuccess: Boolean + abstract val isUnset: Boolean companion object { val TRUE: EvaluateResultValue = EvaluateResultValue(Values.TRUE_VALUE) @@ -33,14 +31,21 @@ internal sealed class EvaluateResult(val value: Value?) { } } +internal class EvaluateResultValue(value: Value) : EvaluateResult(value) { + override val isSuccess: Boolean = true + override val isError: Boolean = false + override val isUnset: Boolean = false +} + internal object EvaluateResultError : EvaluateResult(null) { + override val isSuccess: Boolean = false override val isError: Boolean = true + override val isUnset: Boolean = false } internal object EvaluateResultUnset : EvaluateResult(null) { + override val isSuccess: Boolean = false override val isError: Boolean = false + override val isUnset: Boolean = true } -internal class EvaluateResultValue(value: Value) : EvaluateResult(value) { - override val isError: Boolean = false -} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index d8ed275a741..839cfbfa37f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -33,9 +33,21 @@ internal typealias EvaluateFunction = (params: List) -> Evalua internal val notImplemented: EvaluateFunction = { _ -> throw NotImplementedError() } +// === Debug Functions === + +internal val evaluateIsError: EvaluateFunction = unaryFunction { r: EvaluateResult -> + EvaluateResult.boolean(r.isError) +} + // === Logical Functions === -internal val evaluateExists: EvaluateFunction = notImplemented +internal val evaluateExists: EvaluateFunction = unaryFunction { r: EvaluateResult -> + when (r) { + EvaluateResultError -> r + EvaluateResultUnset -> EvaluateResult.FALSE + is EvaluateResultValue -> EvaluateResult.TRUE + } +} internal val evaluateAnd: EvaluateFunction = { params -> fun(input: MutableDocument): EvaluateResult { @@ -492,18 +504,22 @@ private inline fun catch(f: () -> EvaluateResult): EvaluateResult = EvaluateResultError } -@JvmName("unaryValueFunction") private inline fun unaryFunction( - crossinline function: (Value) -> EvaluateResult + crossinline function: (EvaluateResult) -> EvaluateResult ): EvaluateFunction = { params -> if (params.size != 1) throw Assert.fail("Function should have exactly 1 params, but %d were given.", params.size) val p = params[0] - block@{ input: MutableDocument -> - val v = p(input).value ?: return@block EvaluateResultError - if (v.hasNullValue()) return@block EvaluateResult.NULL - catch { function(v) } - } + { input: MutableDocument -> catch { function(p(input)) } } +} + +@JvmName("unaryValueFunction") +private inline fun unaryFunction( + crossinline function: (Value) -> EvaluateResult +): EvaluateFunction = unaryFunction { r: EvaluateResult -> + val v = r.value + if (v === null) EvaluateResultError + else if (v.hasNullValue()) EvaluateResult.NULL else function(v) } @JvmName("unaryBooleanFunction") @@ -563,17 +579,12 @@ private inline fun unaryFunctionType( valueTypeCase: Value.ValueTypeCase, crossinline valueExtractor: (Value) -> T, crossinline function: (T) -> EvaluateResult -): EvaluateFunction = { params -> - if (params.size != 1) - throw Assert.fail("Function should have exactly 1 params, but %d were given.", params.size) - val p = params[0] - block@{ input: MutableDocument -> - val v = p(input).value ?: return@block EvaluateResultError - when (v.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL - valueTypeCase -> catch { function(valueExtractor(v)) } - else -> EvaluateResultError - } +): EvaluateFunction = unaryFunction { r: EvaluateResult -> + val v = r.value + if (v === null) EvaluateResultError else when (v.valueTypeCase) { + Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL + valueTypeCase -> catch { function(valueExtractor(v)) } + else -> EvaluateResultError } } 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 28351465f74..30df0b2c7e7 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 @@ -27,6 +27,7 @@ import com.google.firebase.firestore.model.FieldPath as ModelFieldPath import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue +import com.google.firebase.firestore.pipeline.Expr.Companion import com.google.firebase.firestore.pipeline.Expr.Companion.field import com.google.firebase.firestore.util.CustomClassMapper import com.google.firestore.v1.MapValue @@ -2979,6 +2980,14 @@ abstract class Expr internal constructor() { fun ifError(tryExpr: BooleanExpr, catchExpr: BooleanExpr): BooleanExpr = BooleanExpr("if_error", notImplemented, tryExpr, catchExpr) + /** + * Creates an expression that checks if a given expression produces an error. + * + * @param expr The expression to check. + * @return A new [BooleanExpr] representing the `isError` check. + */ + @JvmStatic fun isError(expr: Expr): BooleanExpr = BooleanExpr("is_error", evaluateIsError, expr) + /** * Creates an expression that returns the [catchValue] argument if there is an error, else * return the result of the [tryExpr] argument evaluation. @@ -4082,6 +4091,13 @@ abstract class Expr internal constructor() { */ fun ifError(catchValue: Any): Expr = Companion.ifError(this, catchValue) + /** + * Creates an expression that checks if this expression produces an error. + * + * @return A new [BooleanExpr] representing the `isError` check. + */ + fun isError(): BooleanExpr = Companion.isError(this) + internal abstract fun toProto(userDataReader: UserDataReader): Value internal abstract fun evaluateContext(context: EvaluationContext): EvaluateDocument @@ -4145,7 +4161,7 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select private fun evaluateInternal(input: MutableDocument): EvaluateResult { val value: Value? = input.getField(fieldPath) - return if (value == null) EvaluateResultUnset else EvaluateResultValue(value) + return if (value === null) EvaluateResultUnset else EvaluateResultValue(value) } } @@ -4311,6 +4327,18 @@ internal constructor(name: String, function: EvaluateFunction, params: Array + assertEvaluatesTo(evaluate(exists(valueExpr)), true, "exists(%s)", valueExpr) + } + } + + @Test + fun `null returns true for exists`() { + assertEvaluatesTo(evaluate(exists(nullValue())), true, "exists(null)") + } + + @Test + fun `error returns error for exists`() { + val errorProducingExpr = arrayLength(constant("notAnArray")) + assertEvaluatesToError(evaluate(exists(errorProducingExpr)), "exists(error_expr)") + } + + @Test + fun `unset with not exists returns true`() { + val unsetExpr = field("non-existent-field") + val existsExpr = exists(unsetExpr) + assertEvaluatesTo(evaluate(not(existsExpr)), true, "not(exists(non-existent-field))") + } + + @Test + fun `unset returns false for exists`() { + val unsetExpr = field("non-existent-field") + assertEvaluatesTo(evaluate(exists(unsetExpr)), false, "exists(non-existent-field)") + } + + @Test + fun `empty array returns true for exists`() { + assertEvaluatesTo(evaluate(exists(array())), true, "exists([])") + } + + @Test + fun `empty map returns true for exists`() { + // Expr.map() creates an empty map expression + assertEvaluatesTo(evaluate(exists(map(emptyMap()))), true, "exists({})") + } + + // --- IsError Tests --- + + @Test + fun `isError error returns true`() { + val errorProducingExpr = arrayLength(constant("notAnArray")) + assertEvaluatesTo(evaluate(isError(errorProducingExpr)), true, "isError(error_expr)") + } + + @Test + fun `isError field missing returns false`() { + // Evaluating a missing field results in UNSET. isError(UNSET) should be false. + val fieldExpr = field("target") + assertEvaluatesTo(evaluate(isError(fieldExpr)), false, "isError(missing_field)") + } + + @Test + fun `isError non-error returns false`() { + assertEvaluatesTo(evaluate(isError(constant(42L))), false, "isError(42L)") + } + + @Test + fun `isError explicit null returns false`() { + assertEvaluatesTo(evaluate(isError(nullValue())), false, "isError(null)") + } + + @Test + fun `isError unset returns false`() { + // Evaluating a non-existent field results in UNSET. isError(UNSET) should be false. + val unsetExpr = field("non-existent-field") + assertEvaluatesTo(evaluate(isError(unsetExpr)), false, "isError(non-existent-field)") + } + + @Test + fun `isError anything but error returns false`() { + ComparisonTestData.allSupportedComparableValues.forEach { valueExpr -> + assertEvaluatesTo(evaluate(isError(valueExpr)), false, "isError(%s)", valueExpr) + } + assertEvaluatesTo(evaluate(isError(nullValue())), false, "isError(null)") + assertEvaluatesTo(evaluate(isError(constant(0L))), false, "isError(0L)") + } +} From 2cb2f4534303a8b70c57a2234feefec63c881b2a Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 30 May 2025 15:18:55 -0400 Subject: [PATCH 091/152] Implement and test realtime field and logical functions --- .../firestore/pipeline/EvaluateResult.kt | 1 - .../firebase/firestore/pipeline/evaluation.kt | 260 ++-- .../firestore/pipeline/expressions.kt | 12 +- .../firebase/firestore/pipeline/DebugTests.kt | 1 - .../firebase/firestore/pipeline/FieldTests.kt | 42 + .../firestore/pipeline/LogicalTests.kt | 1250 +++++++++++++++++ .../firebase/firestore/pipeline/testUtil.kt | 11 +- 7 files changed, 1459 insertions(+), 118 deletions(-) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/FieldTests.kt create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LogicalTests.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt index 3ddbfc7ad73..51dd546eefc 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt @@ -48,4 +48,3 @@ internal object EvaluateResultUnset : EvaluateResult(null) { override val isError: Boolean = false override val isUnset: Boolean = true } - diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index 839cfbfa37f..bb8ec68a228 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -15,6 +15,7 @@ import com.google.firebase.firestore.model.Values.strictCompare import com.google.firebase.firestore.model.Values.strictEquals import com.google.firebase.firestore.util.Assert import com.google.firestore.v1.Value +import com.google.firestore.v1.Value.ValueTypeCase import com.google.protobuf.ByteString import com.google.protobuf.Timestamp import java.math.BigDecimal @@ -51,37 +52,43 @@ internal val evaluateExists: EvaluateFunction = unaryFunction { r: EvaluateResul internal val evaluateAnd: EvaluateFunction = { params -> fun(input: MutableDocument): EvaluateResult { - // We only propagate NULL if all no FALSE parameters exist. - var result: EvaluateResult = EvaluateResult.TRUE + var isError = false + var isNull = false for (param in params) { - val value = param(input).value ?: return EvaluateResultError - when (value.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> result = EvaluateResult.NULL - Value.ValueTypeCase.BOOLEAN_VALUE -> { - if (!value.booleanValue) return EvaluateResult.FALSE + val value = param(input).value + if (value === null) isError = true + else + when (value.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> isNull = true + ValueTypeCase.BOOLEAN_VALUE -> { + if (!value.booleanValue) return EvaluateResult.FALSE + } + else -> return EvaluateResultError } - else -> return EvaluateResultError - } } - return result + return if (isError) EvaluateResultError + else if (isNull) EvaluateResult.NULL else EvaluateResult.TRUE } } internal val evaluateOr: EvaluateFunction = { params -> fun(input: MutableDocument): EvaluateResult { - // We only propagate NULL if all no TRUE parameters exist. - var result: EvaluateResult = EvaluateResult.FALSE + var isError = false + var isNull = false for (param in params) { - val value = param(input).value ?: return EvaluateResultError - when (value.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> result = EvaluateResult.NULL - Value.ValueTypeCase.BOOLEAN_VALUE -> { - if (value.booleanValue) return EvaluateResult.TRUE + val value = param(input).value + if (value === null) isError = true + else + when (value.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> isNull = true + ValueTypeCase.BOOLEAN_VALUE -> { + if (value.booleanValue) return EvaluateResult.TRUE + } + else -> return EvaluateResultError } - else -> return EvaluateResultError - } } - return result + return if (isError) EvaluateResultError + else if (isNull) EvaluateResult.NULL else EvaluateResult.FALSE } } @@ -89,6 +96,33 @@ internal val evaluateXor: EvaluateFunction = variadicFunction { values: BooleanA EvaluateResult.boolean(values.fold(false, Boolean::xor)) } +internal val evaluateCond: EvaluateFunction = ternaryLazyFunction { p1, p2, p3 -> + val v1 = p1().value ?: return@ternaryLazyFunction EvaluateResultError + when (v1.valueTypeCase) { + ValueTypeCase.BOOLEAN_VALUE -> if (v1.booleanValue) p2() else p3() + ValueTypeCase.NULL_VALUE -> p3() + else -> EvaluateResultError + } +} + +internal val evaluateLogicalMaximum: EvaluateFunction = + variadicResultFunction { l: List -> + val value = + l.mapNotNull(EvaluateResult::value) + .filterNot(Value::hasNullValue) + .maxWithOrNull(Values::compare) + if (value === null) EvaluateResult.NULL else EvaluateResultValue(value) + } + +internal val evaluateLogicalMinimum: EvaluateFunction = + variadicResultFunction { l: List -> + val value = + l.mapNotNull(EvaluateResult::value) + .filterNot(Value::hasNullValue) + .minWithOrNull(Values::compare) + if (value === null) EvaluateResult.NULL else EvaluateResultValue(value) + } + // === Comparison Functions === internal val evaluateEq: EvaluateFunction = binaryFunction { p1: Value, p2: Value -> @@ -129,13 +163,17 @@ internal val evaluateNot: EvaluateFunction = unaryFunction { b: Boolean -> // === Type Functions === -internal val evaluateIsNaN: EvaluateFunction = unaryFunction { v: Value -> - EvaluateResult.boolean(isNanValue(v)) -} +internal val evaluateIsNaN: EvaluateFunction = + arithmetic( + { _: Long -> EvaluateResult.FALSE }, + { v: Double -> EvaluateResult.boolean(v.isNaN()) } + ) -internal val evaluateIsNotNaN: EvaluateFunction = unaryFunction { v: Value -> - EvaluateResult.boolean(!isNanValue(v)) -} +internal val evaluateIsNotNaN: EvaluateFunction = + arithmetic( + { _: Long -> EvaluateResult.TRUE }, + { v: Double -> EvaluateResult.boolean(!v.isNaN()) } + ) internal val evaluateIsNull: EvaluateFunction = { params -> if (params.size != 1) @@ -255,15 +293,11 @@ internal val evaluateSubtract = arithmeticPrimitive(Math::subtractExact, Double: internal val evaluateArray = variadicNullableValueFunction(EvaluateResult.Companion::list) -internal val evaluateEqAny = binaryFunction { list: List, value: Value -> - eqAny(value, list) -} +internal val evaluateEqAny = binaryFunction(::eqAny) -internal val evaluateNotEqAny = notImplemented +internal val evaluateNotEqAny = binaryFunction(::notEqAny) -internal val evaluateArrayContains = binaryFunction { array: Value, value: Value -> - if (array.hasArrayValue()) eqAny(value, array.arrayValue.valuesList) else EvaluateResultError -} +internal val evaluateArrayContains = binaryFunction { l: List, v: Value -> eqAny(v, l) } internal val evaluateArrayContainsAny = binaryFunction { array: List, searchValues: List -> @@ -313,6 +347,16 @@ private fun eqAny(value: Value, list: List): EvaluateResult { return if (foundNull) EvaluateResult.NULL else EvaluateResult.FALSE } +private fun notEqAny(value: Value, list: List): EvaluateResult { + var foundNull = false + for (element in list) when (strictEquals(value, element)) { + true -> return EvaluateResult.FALSE + false -> {} + null -> foundNull = true + } + return if (foundNull) EvaluateResult.NULL else EvaluateResult.TRUE +} + // === String Functions === internal val evaluateStrConcat = variadicFunction { strings: List -> @@ -525,7 +569,7 @@ private inline fun unaryFunction( @JvmName("unaryBooleanFunction") private inline fun unaryFunction(crossinline stringOp: (Boolean) -> EvaluateResult) = unaryFunctionType( - Value.ValueTypeCase.BOOLEAN_VALUE, + ValueTypeCase.BOOLEAN_VALUE, Value::getBooleanValue, stringOp, ) @@ -533,7 +577,7 @@ private inline fun unaryFunction(crossinline stringOp: (Boolean) -> EvaluateResu @JvmName("unaryStringFunction") private inline fun unaryFunction(crossinline stringOp: (String) -> EvaluateResult) = unaryFunctionType( - Value.ValueTypeCase.STRING_VALUE, + ValueTypeCase.STRING_VALUE, Value::getStringValue, stringOp, ) @@ -541,7 +585,7 @@ private inline fun unaryFunction(crossinline stringOp: (String) -> EvaluateResul @JvmName("unaryLongFunction") private inline fun unaryFunction(crossinline longOp: (Long) -> EvaluateResult) = unaryFunctionType( - Value.ValueTypeCase.INTEGER_VALUE, + ValueTypeCase.INTEGER_VALUE, Value::getIntegerValue, longOp, ) @@ -549,7 +593,7 @@ private inline fun unaryFunction(crossinline longOp: (Long) -> EvaluateResult) = @JvmName("unaryTimestampFunction") private inline fun unaryFunction(crossinline timestampOp: (Timestamp) -> EvaluateResult) = unaryFunctionType( - Value.ValueTypeCase.TIMESTAMP_VALUE, + ValueTypeCase.TIMESTAMP_VALUE, Value::getTimestampValue, timestampOp, ) @@ -557,7 +601,7 @@ private inline fun unaryFunction(crossinline timestampOp: (Timestamp) -> Evaluat @JvmName("unaryArrayFunction") private inline fun unaryFunction(crossinline longOp: (List) -> EvaluateResult) = unaryFunctionType( - Value.ValueTypeCase.ARRAY_VALUE, + ValueTypeCase.ARRAY_VALUE, { it.arrayValue.valuesList }, longOp, ) @@ -567,32 +611,34 @@ private inline fun unaryFunction( crossinline stringOp: (String) -> EvaluateResult ) = unaryFunctionType( - Value.ValueTypeCase.BYTES_VALUE, + ValueTypeCase.BYTES_VALUE, Value::getBytesValue, byteOp, - Value.ValueTypeCase.STRING_VALUE, + ValueTypeCase.STRING_VALUE, Value::getStringValue, stringOp, ) private inline fun unaryFunctionType( - valueTypeCase: Value.ValueTypeCase, + valueTypeCase: ValueTypeCase, crossinline valueExtractor: (Value) -> T, crossinline function: (T) -> EvaluateResult ): EvaluateFunction = unaryFunction { r: EvaluateResult -> val v = r.value - if (v === null) EvaluateResultError else when (v.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL - valueTypeCase -> catch { function(valueExtractor(v)) } - else -> EvaluateResultError - } + if (v === null) EvaluateResultError + else + when (v.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL + valueTypeCase -> catch { function(valueExtractor(v)) } + else -> EvaluateResultError + } } private inline fun unaryFunctionType( - valueTypeCase1: Value.ValueTypeCase, + valueTypeCase1: ValueTypeCase, crossinline valueExtractor1: (Value) -> T1, crossinline function1: (T1) -> EvaluateResult, - valueTypeCase2: Value.ValueTypeCase, + valueTypeCase2: ValueTypeCase, crossinline valueExtractor2: (Value) -> T2, crossinline function2: (T2) -> EvaluateResult ): EvaluateFunction = { params -> @@ -602,7 +648,7 @@ private inline fun unaryFunctionType( block@{ input: MutableDocument -> val v = p(input).value ?: return@block EvaluateResultError when (v.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL + ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL valueTypeCase1 -> catch { function1(valueExtractor1(v)) } valueTypeCase2 -> catch { function2(valueExtractor2(v)) } else -> EvaluateResultError @@ -643,9 +689,9 @@ private inline fun binaryFunction( @JvmName("binaryStringStringFunction") private inline fun binaryFunction(crossinline function: (String, String) -> EvaluateResult) = binaryFunctionType( - Value.ValueTypeCase.STRING_VALUE, + ValueTypeCase.STRING_VALUE, Value::getStringValue, - Value.ValueTypeCase.STRING_VALUE, + ValueTypeCase.STRING_VALUE, Value::getStringValue, function ) @@ -655,20 +701,32 @@ private inline fun binaryFunction( crossinline function: (List, List) -> EvaluateResult ) = binaryFunctionType( - Value.ValueTypeCase.ARRAY_VALUE, + ValueTypeCase.ARRAY_VALUE, { it.arrayValue.valuesList }, - Value.ValueTypeCase.ARRAY_VALUE, + ValueTypeCase.ARRAY_VALUE, { it.arrayValue.valuesList }, function ) +private inline fun ternaryLazyFunction( + crossinline function: + (() -> EvaluateResult, () -> EvaluateResult, () -> EvaluateResult) -> EvaluateResult +): EvaluateFunction = { params -> + if (params.size != 3) + throw Assert.fail("Function should have exactly 3 params, but %d were given.", params.size) + val p1 = params[0] + val p2 = params[1] + val p3 = params[2] + { input: MutableDocument -> catch { function({ p1(input) }, { p2(input) }, { p3(input) }) } } +} + private inline fun ternaryTimestampFunction( crossinline function: (Timestamp, String, Long) -> EvaluateResult ): EvaluateFunction = ternaryNullableValueFunction { timestamp: Value, unit: Value, number: Value -> val t: Timestamp = when (timestamp.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL - Value.ValueTypeCase.TIMESTAMP_VALUE -> timestamp.timestampValue + ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL + ValueTypeCase.TIMESTAMP_VALUE -> timestamp.timestampValue else -> return@ternaryNullableValueFunction EvaluateResultError } val u: String = @@ -676,8 +734,8 @@ private inline fun ternaryTimestampFunction( else return@ternaryNullableValueFunction EvaluateResultError val n: Long = when (number.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL - Value.ValueTypeCase.INTEGER_VALUE -> number.integerValue + ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL + ValueTypeCase.INTEGER_VALUE -> number.integerValue else -> return@ternaryNullableValueFunction EvaluateResultError } function(t, u, n) @@ -685,24 +743,17 @@ private inline fun ternaryTimestampFunction( private inline fun ternaryNullableValueFunction( crossinline function: (Value, Value, Value) -> EvaluateResult -): EvaluateFunction = { params -> - if (params.size != 3) - throw Assert.fail("Function should have exactly 3 params, but %d were given.", params.size) - val p1 = params[0] - val p2 = params[1] - val p3 = params[2] - block@{ input: MutableDocument -> - val v1 = p1(input).value ?: return@block EvaluateResultError - val v2 = p2(input).value ?: return@block EvaluateResultError - val v3 = p3(input).value ?: return@block EvaluateResultError - catch { function(v1, v2, v3) } - } +): EvaluateFunction = ternaryLazyFunction { p1, p2, p3 -> + val v1 = p1().value ?: return@ternaryLazyFunction EvaluateResultError + val v2 = p2().value ?: return@ternaryLazyFunction EvaluateResultError + val v3 = p3().value ?: return@ternaryLazyFunction EvaluateResultError + function(v1, v2, v3) } private inline fun binaryFunctionType( - valueTypeCase1: Value.ValueTypeCase, + valueTypeCase1: ValueTypeCase, crossinline valueExtractor1: (Value) -> T1, - valueTypeCase2: Value.ValueTypeCase, + valueTypeCase2: ValueTypeCase, crossinline valueExtractor2: (Value) -> T2, crossinline function: (T1, T2) -> EvaluateResult ): EvaluateFunction = { params -> @@ -714,15 +765,15 @@ private inline fun binaryFunctionType( val v1 = p1(input).value ?: return@block EvaluateResultError val v2 = p2(input).value ?: return@block EvaluateResultError when (v1.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> + ValueTypeCase.NULL_VALUE -> when (v2.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL + ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL valueTypeCase2 -> EvaluateResult.NULL else -> EvaluateResultError } valueTypeCase1 -> when (v2.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL + ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL valueTypeCase2 -> catch { function(valueExtractor1(v1), valueExtractor2(v2)) } else -> EvaluateResultError } @@ -731,39 +782,30 @@ private inline fun binaryFunctionType( } } -@JvmName("variadicValueFunction") -private inline fun variadicFunction( - crossinline function: (List) -> EvaluateResult +private inline fun variadicResultFunction( + crossinline function: (List) -> EvaluateResult ): EvaluateFunction = { params -> - block@{ input: MutableDocument -> - val values = ArrayList(params.size) - var nullFound = false - for (param in params) { - val v = param(input).value ?: return@block EvaluateResultError - if (v.hasNullValue()) nullFound = true - values.add(v) - } - if (nullFound) EvaluateResult.NULL else catch { function(values) } + { input: MutableDocument -> + val results = params.map { it(input) } + catch { function(results) } } } @JvmName("variadicNullableValueFunction") private inline fun variadicNullableValueFunction( crossinline function: (List) -> EvaluateResult -): EvaluateFunction = { params -> - block@{ input: MutableDocument -> - catch { function(params.map { p -> p(input).value ?: return@block EvaluateResultError }) } - } +): EvaluateFunction = variadicResultFunction { l: List -> + function(l.map { it.value ?: return@variadicResultFunction EvaluateResultError }) } @JvmName("variadicStringFunction") private inline fun variadicFunction( crossinline function: (List) -> EvaluateResult ): EvaluateFunction = - variadicFunctionType(Value.ValueTypeCase.STRING_VALUE, Value::getStringValue, function) + variadicFunctionType(ValueTypeCase.STRING_VALUE, Value::getStringValue, function) private inline fun variadicFunctionType( - valueTypeCase: Value.ValueTypeCase, + valueTypeCase: ValueTypeCase, crossinline valueExtractor: (Value) -> T, crossinline function: (List) -> EvaluateResult, ): EvaluateFunction = { params -> @@ -773,7 +815,7 @@ private inline fun variadicFunctionType( for (param in params) { val v = param(input).value ?: return@block EvaluateResultError when (v.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> nullFound = true + ValueTypeCase.NULL_VALUE -> nullFound = true valueTypeCase -> values.add(valueExtractor(v)) else -> return@block EvaluateResultError } @@ -792,8 +834,8 @@ private inline fun variadicFunction( params.forEachIndexed { i, param -> val v = param(input).value ?: return@block EvaluateResultError when (v.valueTypeCase) { - Value.ValueTypeCase.NULL_VALUE -> nullFound = true - Value.ValueTypeCase.BOOLEAN_VALUE -> values[i] = v.booleanValue + ValueTypeCase.NULL_VALUE -> nullFound = true + ValueTypeCase.BOOLEAN_VALUE -> values[i] = v.booleanValue else -> return@block EvaluateResultError } } @@ -837,10 +879,10 @@ private inline fun arithmetic( crossinline doubleOp: (Double) -> EvaluateResult ): EvaluateFunction = unaryFunctionType( - Value.ValueTypeCase.INTEGER_VALUE, + ValueTypeCase.INTEGER_VALUE, Value::getIntegerValue, intOp, - Value.ValueTypeCase.DOUBLE_VALUE, + ValueTypeCase.DOUBLE_VALUE, Value::getDoubleValue, doubleOp, ) @@ -852,8 +894,8 @@ private inline fun arithmetic( ): EvaluateFunction = binaryFunction { p1: Value, p2: Value -> if (p2.hasIntegerValue()) when (p1.valueTypeCase) { - Value.ValueTypeCase.INTEGER_VALUE -> intOp(p1.integerValue, p2.integerValue) - Value.ValueTypeCase.DOUBLE_VALUE -> doubleOp(p1.doubleValue, p2.integerValue) + ValueTypeCase.INTEGER_VALUE -> intOp(p1.integerValue, p2.integerValue) + ValueTypeCase.DOUBLE_VALUE -> doubleOp(p1.doubleValue, p2.integerValue) else -> EvaluateResultError } else EvaluateResultError @@ -864,16 +906,16 @@ private inline fun arithmetic( crossinline doubleOp: (Double, Double) -> EvaluateResult ): EvaluateFunction = binaryFunction { p1: Value, p2: Value -> when (p1.valueTypeCase) { - Value.ValueTypeCase.INTEGER_VALUE -> + ValueTypeCase.INTEGER_VALUE -> when (p2.valueTypeCase) { - Value.ValueTypeCase.INTEGER_VALUE -> intOp(p1.integerValue, p2.integerValue) - Value.ValueTypeCase.DOUBLE_VALUE -> doubleOp(p1.integerValue.toDouble(), p2.doubleValue) + ValueTypeCase.INTEGER_VALUE -> intOp(p1.integerValue, p2.integerValue) + ValueTypeCase.DOUBLE_VALUE -> doubleOp(p1.integerValue.toDouble(), p2.doubleValue) else -> EvaluateResultError } - Value.ValueTypeCase.DOUBLE_VALUE -> + ValueTypeCase.DOUBLE_VALUE -> when (p2.valueTypeCase) { - Value.ValueTypeCase.INTEGER_VALUE -> doubleOp(p1.doubleValue, p2.integerValue.toDouble()) - Value.ValueTypeCase.DOUBLE_VALUE -> doubleOp(p1.doubleValue, p2.doubleValue) + ValueTypeCase.INTEGER_VALUE -> doubleOp(p1.doubleValue, p2.integerValue.toDouble()) + ValueTypeCase.DOUBLE_VALUE -> doubleOp(p1.doubleValue, p2.doubleValue) else -> EvaluateResultError } else -> EvaluateResultError @@ -885,14 +927,14 @@ private inline fun arithmetic( ): EvaluateFunction = binaryFunction { p1: Value, p2: Value -> val v1: Double = when (p1.valueTypeCase) { - Value.ValueTypeCase.INTEGER_VALUE -> p1.integerValue.toDouble() - Value.ValueTypeCase.DOUBLE_VALUE -> p1.doubleValue + ValueTypeCase.INTEGER_VALUE -> p1.integerValue.toDouble() + ValueTypeCase.DOUBLE_VALUE -> p1.doubleValue else -> return@binaryFunction EvaluateResultError } val v2: Double = when (p2.valueTypeCase) { - Value.ValueTypeCase.INTEGER_VALUE -> p2.integerValue.toDouble() - Value.ValueTypeCase.DOUBLE_VALUE -> p2.doubleValue + ValueTypeCase.INTEGER_VALUE -> p2.integerValue.toDouble() + ValueTypeCase.DOUBLE_VALUE -> p2.doubleValue else -> return@binaryFunction EvaluateResultError } op(v1, v2) 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 30df0b2c7e7..4d1cf3507eb 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 @@ -1497,7 +1497,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMaximum(expr: Expr, vararg others: Any): Expr = - FunctionExpr("logical_max", notImplemented, expr, *others) + FunctionExpr("logical_max", evaluateLogicalMaximum, expr, *others) /** * Creates an expression that returns the largest value between multiple input expressions or @@ -1509,7 +1509,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMaximum(fieldName: String, vararg others: Any): Expr = - FunctionExpr("logical_max", notImplemented, fieldName, *others) + FunctionExpr("logical_max", evaluateLogicalMaximum, fieldName, *others) /** * Creates an expression that returns the smallest value between multiple input expressions or @@ -1521,7 +1521,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMinimum(expr: Expr, vararg others: Any): Expr = - FunctionExpr("logical_min", notImplemented, expr, *others) + FunctionExpr("logical_min", evaluateLogicalMinimum, expr, *others) /** * Creates an expression that returns the smallest value between multiple input expressions or @@ -1533,7 +1533,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun logicalMinimum(fieldName: String, vararg others: Any): Expr = - FunctionExpr("logical_min", notImplemented, fieldName, *others) + FunctionExpr("logical_min", evaluateLogicalMinimum, fieldName, *others) /** * Creates an expression that reverses a string. @@ -2920,7 +2920,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cond(condition: BooleanExpr, thenExpr: Expr, elseExpr: Expr): Expr = - FunctionExpr("cond", notImplemented, condition, thenExpr, elseExpr) + FunctionExpr("cond", evaluateCond, condition, thenExpr, elseExpr) /** * Creates a conditional expression that evaluates to a [thenValue] if a condition is true or an @@ -2933,7 +2933,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun cond(condition: BooleanExpr, thenValue: Any, elseValue: Any): Expr = - FunctionExpr("cond", notImplemented, condition, thenValue, elseValue) + FunctionExpr("cond", evaluateCond, condition, thenValue, elseValue) /** * Creates an expression that checks if a field exists. diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/DebugTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/DebugTests.kt index 2685ee3a7a0..2ddd9a34d63 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/DebugTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/DebugTests.kt @@ -9,7 +9,6 @@ import com.google.firebase.firestore.pipeline.Expr.Companion.isError import com.google.firebase.firestore.pipeline.Expr.Companion.map import com.google.firebase.firestore.pipeline.Expr.Companion.not import com.google.firebase.firestore.pipeline.Expr.Companion.nullValue -import com.google.firebase.firestore.testutil.TestUtil import com.google.firebase.firestore.testutil.TestUtil.doc import org.junit.Test import org.junit.runner.RunWith diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/FieldTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/FieldTests.kt new file mode 100644 index 00000000000..d213d6584a7 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/FieldTests.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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.firebase.firestore.pipeline + +import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class FieldTests { + + @Test + fun `can get field`() { + val docWithField = doc("coll/doc1", 1, mapOf("exists" to true)) + val fieldExpr = Expr.field("exists") + val result = evaluate(fieldExpr, docWithField) // Using evaluate from pipeline.testUtil + assertEvaluatesTo(result, true, "Expected field 'exists' to evaluate to true") + } + + @Test + fun `returns unset if not found`() { + val doc = doc("coll/doc1", 1, emptyMap()) + val fieldExpr = Expr.field("not-exists") + val result = evaluate(fieldExpr, doc) // Using evaluate from pipeline.testUtil + assertEvaluatesToUnset(result, "Expected non-existent field to evaluate to UNSET") + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LogicalTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LogicalTests.kt new file mode 100644 index 00000000000..8e398939302 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LogicalTests.kt @@ -0,0 +1,1250 @@ +/* + * Copyright 2025 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.firebase.firestore.pipeline + +import com.google.firebase.firestore.model.Values.encodeValue +import com.google.firebase.firestore.pipeline.Expr.Companion.add +import com.google.firebase.firestore.pipeline.Expr.Companion.and +import com.google.firebase.firestore.pipeline.Expr.Companion.array +import com.google.firebase.firestore.pipeline.Expr.Companion.cond +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.eqAny +import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.pipeline.Expr.Companion.isNan +import com.google.firebase.firestore.pipeline.Expr.Companion.isNotNan +import com.google.firebase.firestore.pipeline.Expr.Companion.isNotNull +import com.google.firebase.firestore.pipeline.Expr.Companion.isNull +import com.google.firebase.firestore.pipeline.Expr.Companion.logicalMaximum +import com.google.firebase.firestore.pipeline.Expr.Companion.logicalMinimum +import com.google.firebase.firestore.pipeline.Expr.Companion.map +import com.google.firebase.firestore.pipeline.Expr.Companion.not +import com.google.firebase.firestore.pipeline.Expr.Companion.notEqAny +import com.google.firebase.firestore.pipeline.Expr.Companion.nullValue +import com.google.firebase.firestore.pipeline.Expr.Companion.or +import com.google.firebase.firestore.pipeline.Expr.Companion.xor +import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class LogicalTests { + + private val trueExpr = constant(true) + private val falseExpr = constant(false) + private val nullExpr = nullValue() // Changed + private val nanExpr = constant(Double.NaN) + private val errorExpr = field("error.field").eq(constant("random")) + + // Corrected document creation using doc() from TestUtilKtx + private val testDocWithNan = + doc("coll/docNan", 1, mapOf("nanValue" to Double.NaN, "field" to "value")) + private val errorDoc = + doc("coll/docError", 1, mapOf("error" to 123)) // "error.field" will be UNSET + private val emptyDoc = doc("coll/docEmpty", 1, emptyMap()) + + // --- And (&&) Tests --- + // 2 Operands + @Test + fun `and - false, false is false`() { + val expr = and(falseExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "AND(false, false)") + } + + @Test + fun `and - false, error is false`() { + val expr = and(falseExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(false, error)") + } + + @Test + fun `and - false, true is false`() { + val expr = and(falseExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "AND(false, true)") + } + + @Test + fun `and - error, false is false`() { + val expr = and(errorExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(error, false)") + } + + @Test + fun `and - error, error is error`() { + val expr = and(errorExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "AND(error, error)") + } + + @Test + fun `and - error, true is error`() { + val expr = and(errorExpr, trueExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "AND(error, true)") + } + + @Test + fun `and - true, false is false`() { + val expr = and(trueExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "AND(true, false)") + } + + @Test + fun `and - true, error is error`() { + val expr = and(trueExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "AND(true, error)") + } + + @Test + fun `and - true, true is true`() { + val expr = and(trueExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "AND(true, true)") + } + + // 3 Operands + @Test + fun `and - false, false, false is false`() { + val expr = and(falseExpr, falseExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "AND(F,F,F)") + } + + @Test + fun `and - false, false, error is false`() { + val expr = and(falseExpr, falseExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(F,F,E)") + } + + @Test + fun `and - false, false, true is false`() { + val expr = and(falseExpr, falseExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "AND(F,F,T)") + } + + @Test + fun `and - false, error, false is false`() { + val expr = and(falseExpr, errorExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(F,E,F)") + } + + @Test + fun `and - false, error, error is false`() { + val expr = and(falseExpr, errorExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(F,E,E)") + } + + @Test + fun `and - false, error, true is false`() { + val expr = and(falseExpr, errorExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(F,E,T)") + } + + @Test + fun `and - false, true, false is false`() { + val expr = and(falseExpr, trueExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "AND(F,T,F)") + } + + @Test + fun `and - false, true, error is false`() { + val expr = and(falseExpr, trueExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(F,T,E)") + } + + @Test + fun `and - false, true, true is false`() { + val expr = and(falseExpr, trueExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "AND(F,T,T)") + } + + @Test + fun `and - error, false, false is false`() { + val expr = and(errorExpr, falseExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(E,F,F)") + } + + @Test + fun `and - error, false, error is false`() { + val expr = and(errorExpr, falseExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(E,F,E)") + } + + @Test + fun `and - error, false, true is false`() { + val expr = and(errorExpr, falseExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(E,F,T)") + } + + @Test + fun `and - error, error, false is false`() { + val expr = and(errorExpr, errorExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(E,E,F)") + } + + @Test + fun `and - error, error, error is error`() { + val expr = and(errorExpr, errorExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "AND(E,E,E)") + } + + @Test + fun `and - error, error, true is error`() { + val expr = and(errorExpr, errorExpr, trueExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "AND(E,E,T)") + } + + @Test + fun `and - error, true, false is false`() { + val expr = and(errorExpr, trueExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(E,T,F)") + } + + @Test + fun `and - error, true, error is error`() { + val expr = and(errorExpr, trueExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "AND(E,T,E)") + } + + @Test + fun `and - error, true, true is error`() { + val expr = and(errorExpr, trueExpr, trueExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "AND(E,T,T)") + } + + @Test + fun `and - true, false, false is false`() { + val expr = and(trueExpr, falseExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "AND(T,F,F)") + } + + @Test + fun `and - true, false, error is false`() { + val expr = and(trueExpr, falseExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(T,F,E)") + } + + @Test + fun `and - true, false, true is false`() { + val expr = and(trueExpr, falseExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "AND(T,F,T)") + } + + @Test + fun `and - true, error, false is false`() { + val expr = and(trueExpr, errorExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), false, "AND(T,E,F)") + } + + @Test + fun `and - true, error, error is error`() { + val expr = and(trueExpr, errorExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "AND(T,E,E)") + } + + @Test + fun `and - true, error, true is error`() { + val expr = and(trueExpr, errorExpr, trueExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "AND(T,E,T)") + } + + @Test + fun `and - true, true, false is false`() { + val expr = and(trueExpr, trueExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "AND(T,T,F)") + } + + @Test + fun `and - true, true, error is error`() { + val expr = and(trueExpr, trueExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "AND(T,T,E)") + } + + @Test + fun `and - true, true, true is true`() { + val expr = and(trueExpr, trueExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "AND(T,T,T)") + } + + // Nested + @Test + fun `and - nested and`() { + val child = and(trueExpr, falseExpr) // false + val expr = and(child, trueExpr) // false AND true -> false + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "Nested AND failed") + } + + // Multiple Arguments (already covered by 3-operand tests) + @Test + fun `and - multiple arguments`() { + val expr = and(trueExpr, trueExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "Multiple args AND failed") + } + + // --- Cond (? :) Tests --- + @Test + fun `cond - true condition returns true case`() { + val expr = cond(trueExpr, constant("true case"), errorExpr) + val result = evaluate(expr, emptyDoc) + assertEvaluatesTo(result, encodeValue("true case"), "cond(true, 'true case', error)") + } + + @Test + fun `cond - false condition returns false case`() { + val expr = cond(falseExpr, errorExpr, constant("false case")) + val result = evaluate(expr, emptyDoc) + assertEvaluatesTo(result, encodeValue("false case"), "cond(false, error, 'false case')") + } + + @Test + fun `cond - error condition returns error`() { + val expr = cond(errorExpr, constant("true case"), constant("false case")) + assertEvaluatesToError(evaluate(expr, errorDoc), "Cond with error condition") + } + + @Test + fun `cond - true condition but true case is error returns error`() { + val expr = cond(trueExpr, errorExpr, constant("false case")) + assertEvaluatesToError(evaluate(expr, errorDoc), "Cond with error true-case") + } + + @Test + fun `cond - false condition but false case is error returns error`() { + val expr = cond(falseExpr, constant("true case"), errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "Cond with error false-case") + } + + // --- Or (||) Tests --- + // 2 Operands + @Test + fun `or - false, false is false`() { + val expr = or(falseExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "OR(F,F)") + } + + @Test + fun `or - false, error is error`() { + val expr = or(falseExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "OR(F,E)") + } + + @Test + fun `or - false, true is true`() { + val expr = or(falseExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "OR(F,T)") + } + + @Test + fun `or - error, false is error`() { + val expr = or(errorExpr, falseExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "OR(E,F)") + } + + @Test + fun `or - error, error is error`() { + val expr = or(errorExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "OR(E,E)") + } + + @Test + fun `or - error, true is true`() { + val expr = or(errorExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(E,T)") + } + + @Test + fun `or - true, false is true`() { + val expr = or(trueExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "OR(T,F)") + } + + @Test + fun `or - true, error is true`() { + val expr = or(trueExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(T,E)") + } + + @Test + fun `or - true, true is true`() { + val expr = or(trueExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "OR(T,T)") + } + + // 3 Operands + @Test + fun `or - false, false, false is false`() { + val expr = or(falseExpr, falseExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "OR(F,F,F)") + } + + @Test + fun `or - false, false, error is error`() { + val expr = or(falseExpr, falseExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "OR(F,F,E)") + } + + @Test + fun `or - false, false, true is true`() { + val expr = or(falseExpr, falseExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "OR(F,F,T)") + } + + @Test + fun `or - false, error, false is error`() { + val expr = or(falseExpr, errorExpr, falseExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "OR(F,E,F)") + } + + @Test + fun `or - false, error, error is error`() { + val expr = or(falseExpr, errorExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "OR(F,E,E)") + } + + @Test + fun `or - false, error, true is true`() { + val expr = or(falseExpr, errorExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(F,E,T)") + } + + @Test + fun `or - false, true, false is true`() { + val expr = or(falseExpr, trueExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "OR(F,T,F)") + } + + @Test + fun `or - false, true, error is true`() { + val expr = or(falseExpr, trueExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(F,T,E)") + } + + @Test + fun `or - false, true, true is true`() { + val expr = or(falseExpr, trueExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "OR(F,T,T)") + } + + @Test + fun `or - error, false, false is error`() { + val expr = or(errorExpr, falseExpr, falseExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "OR(E,F,F)") + } + + @Test + fun `or - error, false, error is error`() { + val expr = or(errorExpr, falseExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "OR(E,F,E)") + } + + @Test + fun `or - error, false, true is true`() { + val expr = or(errorExpr, falseExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(E,F,T)") + } + + @Test + fun `or - error, error, false is error`() { + val expr = or(errorExpr, errorExpr, falseExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "OR(E,E,F)") + } + + @Test + fun `or - error, error, error is error`() { + val expr = or(errorExpr, errorExpr, errorExpr) + assertEvaluatesToError(evaluate(expr, errorDoc), "OR(E,E,E)") + } + + @Test + fun `or - error, error, true is true`() { + val expr = or(errorExpr, errorExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(E,E,T)") + } + + @Test + fun `or - error, true, false is true`() { + val expr = or(errorExpr, trueExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(E,T,F)") + } + + @Test + fun `or - error, true, error is true`() { + val expr = or(errorExpr, trueExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(E,T,E)") + } + + @Test + fun `or - error, true, true is true`() { + val expr = or(errorExpr, trueExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(E,T,T)") + } + + @Test + fun `or - true, false, false is true`() { + val expr = or(trueExpr, falseExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "OR(T,F,F)") + } + + @Test + fun `or - true, false, error is true`() { + val expr = or(trueExpr, falseExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(T,F,E)") + } + + @Test + fun `or - true, false, true is true`() { + val expr = or(trueExpr, falseExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "OR(T,F,T)") + } + + @Test + fun `or - true, error, false is true`() { + val expr = or(trueExpr, errorExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(T,E,F)") + } + + @Test + fun `or - true, error, error is true`() { + val expr = or(trueExpr, errorExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(T,E,E)") + } + + @Test + fun `or - true, error, true is true`() { + val expr = or(trueExpr, errorExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(T,E,T)") + } + + @Test + fun `or - true, true, false is true`() { + val expr = or(trueExpr, trueExpr, falseExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "OR(T,T,F)") + } + + @Test + fun `or - true, true, error is true`() { + val expr = or(trueExpr, trueExpr, errorExpr) + assertEvaluatesTo(evaluate(expr, errorDoc), true, "OR(T,T,E)") + } + + @Test + fun `or - true, true, true is true`() { + val expr = or(trueExpr, trueExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "OR(T,T,T)") + } + + // Nested + @Test + fun `or - nested or`() { + val child = or(trueExpr, falseExpr) // true + val expr = or(child, falseExpr) // true OR false -> true + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "Nested OR") + } + + // Multiple Arguments (already covered by 3-operand tests) + @Test + fun `or - multiple arguments`() { + val expr = or(trueExpr, falseExpr, trueExpr) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "Multiple args OR") + } + + // --- Not (!) Tests --- + @Test + fun `not - true to false`() { + val expr = not(trueExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "NOT(true)") + } + + @Test + fun `not - false to true`() { + val expr = not(falseExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "NOT(false)") + } + + @Test + fun `not - error is error`() { + val expr = not(errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "NOT(error)") + } + + // --- Xor Tests --- + // 2 Operands + @Test + fun `xor - false, false is false`() { + val expr = xor(falseExpr, falseExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "XOR(F,F)") + } + + @Test + fun `xor - false, error is error`() { + val expr = xor(falseExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(F,E)") + } + + @Test + fun `xor - false, true is true`() { + val expr = xor(falseExpr, trueExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "XOR(F,T)") + } + + @Test + fun `xor - error, false is error`() { + val expr = xor(errorExpr as BooleanExpr, falseExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,F)") + } + + @Test + fun `xor - error, error is error`() { + val expr = xor(errorExpr as BooleanExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,E)") + } + + @Test + fun `xor - error, true is error`() { + val expr = xor(errorExpr as BooleanExpr, trueExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,T)") + } + + @Test + fun `xor - true, false is true`() { + val expr = xor(trueExpr, falseExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "XOR(T,F)") + } + + @Test + fun `xor - true, error is error`() { + val expr = xor(trueExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(T,E)") + } + + @Test + fun `xor - true, true is false`() { + val expr = xor(trueExpr, trueExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "XOR(T,T)") + } + + // 3 Operands (XOR is true if an odd number of inputs are true) + @Test + fun `xor - false, false, false is false`() { + val expr = xor(falseExpr, falseExpr, falseExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "XOR(F,F,F)") + } + + @Test + fun `xor - false, false, error is error`() { + val expr = xor(falseExpr, falseExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(F,F,E)") + } + + @Test + fun `xor - false, false, true is true`() { + val expr = xor(falseExpr, falseExpr, trueExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "XOR(F,F,T)") + } + + @Test + fun `xor - false, error, false is error`() { + val expr = xor(falseExpr, errorExpr as BooleanExpr, falseExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(F,E,F)") + } + + @Test + fun `xor - false, error, error is error`() { + val expr = xor(falseExpr, errorExpr as BooleanExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(F,E,E)") + } + + @Test + fun `xor - false, error, true is error`() { + val expr = xor(falseExpr, errorExpr as BooleanExpr, trueExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(F,E,T)") + } + + @Test + fun `xor - false, true, false is true`() { + val expr = xor(falseExpr, trueExpr, falseExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "XOR(F,T,F)") + } + + @Test + fun `xor - false, true, error is error`() { + val expr = xor(falseExpr, trueExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(F,T,E)") + } + + @Test + fun `xor - false, true, true is false`() { + val expr = xor(falseExpr, trueExpr, trueExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "XOR(F,T,T)") + } + + @Test + fun `xor - error, false, false is error`() { + val expr = xor(errorExpr as BooleanExpr, falseExpr, falseExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,F,F)") + } + + @Test + fun `xor - error, false, error is error`() { + val expr = xor(errorExpr as BooleanExpr, falseExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,F,E)") + } + + @Test + fun `xor - error, false, true is error`() { + val expr = xor(errorExpr as BooleanExpr, falseExpr, trueExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,F,T)") + } + + @Test + fun `xor - error, error, false is error`() { + val expr = xor(errorExpr as BooleanExpr, errorExpr as BooleanExpr, falseExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,E,F)") + } + + @Test + fun `xor - error, error, error is error`() { + val expr = + xor(errorExpr as BooleanExpr, errorExpr as BooleanExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,E,E)") + } + + @Test + fun `xor - error, error, true is error`() { + val expr = xor(errorExpr as BooleanExpr, errorExpr as BooleanExpr, trueExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,E,T)") + } + + @Test + fun `xor - error, true, false is error`() { + val expr = xor(errorExpr as BooleanExpr, trueExpr, falseExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,T,F)") + } + + @Test + fun `xor - error, true, error is error`() { + val expr = xor(errorExpr as BooleanExpr, trueExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,T,E)") + } + + @Test + fun `xor - error, true, true is error`() { + val expr = xor(errorExpr as BooleanExpr, trueExpr, trueExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(E,T,T)") + } + + @Test + fun `xor - true, false, false is true`() { + val expr = xor(trueExpr, falseExpr, falseExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "XOR(T,F,F)") + } + + @Test + fun `xor - true, false, error is error`() { + val expr = xor(trueExpr, falseExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(T,F,E)") + } + + @Test + fun `xor - true, false, true is false`() { + val expr = xor(trueExpr, falseExpr, trueExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "XOR(T,F,T)") + } + + @Test + fun `xor - true, error, false is error`() { + val expr = xor(trueExpr, errorExpr as BooleanExpr, falseExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(T,E,F)") + } + + @Test + fun `xor - true, error, error is error`() { + val expr = xor(trueExpr, errorExpr as BooleanExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(T,E,E)") + } + + @Test + fun `xor - true, error, true is error`() { + val expr = xor(trueExpr, errorExpr as BooleanExpr, trueExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(T,E,T)") + } + + @Test + fun `xor - true, true, false is false`() { + val expr = xor(trueExpr, trueExpr, falseExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "XOR(T,T,F)") + } + + @Test + fun `xor - true, true, error is error`() { + val expr = xor(trueExpr, trueExpr, errorExpr as BooleanExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "XOR(T,T,E)") + } + + @Test + fun `xor - true, true, true is true`() { + val expr = xor(trueExpr, trueExpr, trueExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "XOR(T,T,T)") + } + + // Nested + @Test + fun `xor - nested xor`() { + val child = xor(trueExpr, falseExpr) // Changed + val expr = xor(child, trueExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "Nested XOR") + } + + // Multiple Arguments (already covered by 3-operand tests) + @Test + fun `xor - multiple arguments`() { + val expr = xor(trueExpr, falseExpr, trueExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "Multiple args XOR") + } + + // --- IsNull Tests --- + @Test + fun `isNull - null returns true`() { + val expr = isNull(nullExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "isNull(null)") + } + + @Test + fun `isNull - error returns error`() { + val expr = isNull(errorExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "isNull(error)") + } + + @Test + fun `isNull - unset field returns error`() { + val expr = isNull(field("non-existent-field")) // Changed + assertEvaluatesToError(evaluate(expr, emptyDoc), "isNull(unset)") + } + + @Test + fun `isNull - anything but null returns false`() { + val values = + listOf( + constant(true), + constant(false), + constant(0), + constant(1.0), + constant("abc"), + constant(Double.NaN), + array(constant(1)), + map(mapOf("a" to 1)) + ) + for (valueExpr in values) { + val expr = isNull(valueExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "isNull(${valueExpr})") + } + } + + // --- IsNotNull Tests --- + @Test + fun `isNotNull - null returns false`() { + val expr = isNotNull(nullExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "isNotNull(null)") + } + + @Test + fun `isNotNull - error returns error`() { + val expr = isNotNull(errorExpr) // Changed + assertEvaluatesToError(evaluate(expr, errorDoc), "isNotNull(error)") + } + + @Test + fun `isNotNull - unset field returns error`() { + val expr = isNotNull(field("non-existent-field")) // Changed + assertEvaluatesToError(evaluate(expr, emptyDoc), "isNotNull(unset)") + } + + @Test + fun `isNotNull - anything but null returns true`() { + val values = + listOf( + constant(true), + constant(false), + constant(0), + constant(1.0), + constant("abc"), + constant(Double.NaN), + array(constant(1)), + map(mapOf("a" to 1)) + ) + for (valueExpr in values) { + val expr = isNotNull(valueExpr) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "isNotNull(${valueExpr})") + } + } + + // --- IsNan / IsNotNan Tests --- + @Test + fun `isNan - nan returns true`() { + assertEvaluatesTo(evaluate(isNan(nanExpr), emptyDoc), true, "isNan(NaN)") // Changed + assertEvaluatesTo( + evaluate(isNan(field("nanValue")), testDocWithNan), + true, + "isNan(field(nanValue))" + ) // Changed + } + + @Test + fun `isNan - not nan returns false`() { + assertEvaluatesTo(evaluate(isNan(constant(42.0)), emptyDoc), false, "isNan(42.0)") // Changed + assertEvaluatesTo(evaluate(isNan(constant(42L)), emptyDoc), false, "isNan(42L)") // Changed + } + + @Test + fun `isNotNan - not nan returns true`() { + assertEvaluatesTo( + evaluate(isNotNan(constant(42.0)), emptyDoc), + true, + "isNotNan(42.0)" + ) // Changed + assertEvaluatesTo(evaluate(isNotNan(constant(42L)), emptyDoc), true, "isNotNan(42L)") // Changed + } + + @Test + fun `isNotNan - nan returns false`() { + assertEvaluatesTo(evaluate(isNotNan(nanExpr), emptyDoc), false, "isNotNan(NaN)") // Changed + assertEvaluatesTo( + evaluate(isNotNan(field("nanValue")), testDocWithNan), + false, + "isNotNan(field(nanValue))" + ) // Changed + } + + @Test + fun `isNan - other nan representations returns true`() { + val nanPlusOne = add(nanExpr, constant(1L)) // Changed + assertEvaluatesTo(evaluate(isNan(nanPlusOne), emptyDoc), true, "isNan(NaN + 1)") // Changed + } + + @Test + fun `isNan - non numeric returns error`() { + assertEvaluatesToError( + evaluate(isNan(constant(true)), emptyDoc), + "isNan(true) should be error" + ) // Changed + assertEvaluatesToError( + evaluate(isNan(constant("abc")), emptyDoc), + "isNan(abc) should be error" + ) // Changed + assertEvaluatesToError( + evaluate(isNan(array()), emptyDoc), + "isNan([]) should be error" + ) // Changed + assertEvaluatesToError( + evaluate(isNan(map(emptyMap())), emptyDoc), + "isNan({}) should be error" + ) // Changed + } + + @Test + fun `isNan - null returns null`() { + assertEvaluatesToNull( + evaluate(isNan(nullExpr), emptyDoc), + "isNan(null) should be null" + ) // Changed + } + + // --- EqAny Tests --- + @Test + fun `eqAny - value found in array`() { + val expr = eqAny(constant("hello"), array(constant("hello"), constant("world"))) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "eqAny(hello, [hello, world])") + } + + @Test + fun `eqAny - value not found in array`() { + val expr = eqAny(constant(4L), array(constant(42L), constant("matang"), constant(true))) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "eqAny(4, [42, matang, true])") + } + + @Test + fun `notEqAny - value not found in array`() { + val expr = + notEqAny(constant(4L), array(constant(42L), constant("matang"), constant(true))) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "notEqAny(4, [42, matang, true])") + } + + @Test + fun `notEqAny - value found in array`() { + val expr = notEqAny(constant("hello"), array(constant("hello"), constant("world"))) // Changed + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "notEqAny(hello, [hello, world])") + } + + @Test + fun `eqAny - equivalent numerics`() { + assertEvaluatesTo( + evaluate( + eqAny(constant(42L), array(constant(42.0), constant("matang"), constant(true))), + emptyDoc + ), + true, + "eqAny(42L, [42.0,...])" + ) + assertEvaluatesTo( + evaluate( + eqAny(constant(42.0), array(constant(42L), constant("matang"), constant(true))), + emptyDoc + ), + true, + "eqAny(42.0, [42L,...])" + ) + } + + @Test + fun `eqAny - both input type is array`() { + val searchArray = array(constant(1L), constant(2L), constant(3L)) + val valuesArray = + array( + array(constant(1L), constant(2L), constant(3L)), + array(constant(4L), constant(5L), constant(6L)) + ) + assertEvaluatesTo( + evaluate(eqAny(searchArray, valuesArray), emptyDoc), + true, + "eqAny([1,2,3], [[1,2,3],...])" + ) + } + + @Test + fun `eqAny - array not found returns error`() { + val expr = eqAny(constant("matang"), field("non-existent-field")) + assertEvaluatesToError(evaluate(expr, emptyDoc), "eqAny(matang, non-existent-field)") + } + + @Test + fun `eqAny - array is empty returns false`() { + val expr = eqAny(constant(42L), array()) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "eqAny(42L, [])") + } + + @Test + fun `eqAny - search reference not found returns error`() { + val expr = eqAny(field("non-existent-field"), array(constant(42L))) + assertEvaluatesToError(evaluate(expr, emptyDoc), "eqAny(non-existent-field, [42L])") + } + + @Test + fun `eqAny - search is null`() { + val expr = eqAny(nullExpr, array(nullExpr, constant(1L), constant("matang"))) + assertEvaluatesToNull(evaluate(expr, emptyDoc), "eqAny(null, [null,1,matang])") + } + + @Test + fun `eqAny - search is null empty values array returns null`() { + val expr = eqAny(nullExpr, array()) + assertEvaluatesToNull(evaluate(expr, emptyDoc), "eqAny(null, [])") + } + + @Test + fun `eqAny - search is nan`() { + val expr = eqAny(nanExpr, array(nanExpr, constant(42L), constant(3.14))) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "eqAny(NaN, [NaN,42,3.14])") + } + + @Test + fun `eqAny - search is empty array is empty`() { + val expr = eqAny(array(), array()) + assertEvaluatesTo(evaluate(expr, emptyDoc), false, "eqAny([], [])") + } + + @Test + fun `eqAny - search is empty array contains empty array returns true`() { + val expr = eqAny(array(), array(array())) + assertEvaluatesTo(evaluate(expr, emptyDoc), true, "eqAny([], [[]])") + } + + @Test + fun `eqAny - search is map`() { + val searchMap = map(mapOf("foo" to constant(42L))) + val valuesArray = + array( + array(constant(123L)), + map(mapOf("bar" to constant(42L))), + map(mapOf("foo" to constant(42L))) + ) + assertEvaluatesTo( + evaluate(eqAny(searchMap, valuesArray), emptyDoc), + true, + "eqAny(map, [...,map])" + ) + } + + // --- LogicalMaximum Tests --- + // Note: logicalMaximum is notImplemented in expressions.kt. + // Tests will fail if NotImplementedError is thrown, which is the desired behavior + // until the function is implemented. Assertions check for correctness once implemented. + @Test + fun `logicalMaximum - numeric type`() { + val expr = logicalMaximum(constant(1L), logicalMaximum(constant(2.0), constant(3L))) + val result = evaluate(expr, emptyDoc) + assertEvaluatesTo(result, encodeValue(3L), "Max(1L, Max(2.0, 3L)) should be 3L") + } + + @Test + fun `logicalMaximum - string type`() { + val expr = logicalMaximum(logicalMaximum(constant("a"), constant("b")), constant("c")) + val result = evaluate(expr, emptyDoc) + assertEvaluatesTo(result, encodeValue("c"), "Max(Max('a', 'b'), 'c') should be 'c'") + } + + @Test + fun `logicalMaximum - mixed type`() { + val expr = logicalMaximum(constant(1L), logicalMaximum(constant("1"), constant(0L))) + val result = evaluate(expr, emptyDoc) + assertEvaluatesTo(result, encodeValue("1"), "Max(1L, Max('1', 0L)) should be '1'") + } + + @Test + fun `logicalMaximum - only null and error returns null`() { + val expr = logicalMaximum(nullExpr, errorExpr) + val result = evaluate(expr, errorDoc) + assertEvaluatesToNull(result, "Max(Null, Error) should be Null") + } + + @Test + fun `logicalMaximum - nan and numbers`() { + val expr1 = logicalMaximum(nanExpr, constant(0L)) + assertEvaluatesTo(evaluate(expr1, emptyDoc), encodeValue(0L), "Max(NaN, 0L) should be 0L") + + val expr2 = logicalMaximum(constant(0L), nanExpr) + assertEvaluatesTo(evaluate(expr2, emptyDoc), encodeValue(0L), "Max(0L, NaN) should be 0L") + + val expr3 = logicalMaximum(nanExpr, nullExpr, errorExpr) + assertEvaluatesTo( + evaluate(expr3, errorDoc), + encodeValue(Double.NaN), + "Max(NaN, Null, Error) should be NaN" + ) + + val expr4 = logicalMaximum(nanExpr, errorExpr) + assertEvaluatesTo( + evaluate(expr4, errorDoc), + encodeValue(Double.NaN), + "Max(NaN, Error) should be NaN" + ) + } + + @Test + fun `logicalMaximum - error input skip`() { + val expr = logicalMaximum(errorExpr, constant(1L)) + val result = evaluate(expr, errorDoc) + assertEvaluatesTo(result, encodeValue(1L), "Max(Error, 1L) should be 1L") + } + + @Test + fun `logicalMaximum - null input skip`() { + val expr = logicalMaximum(nullExpr, constant(1L)) + val result = evaluate(expr, emptyDoc) + assertEvaluatesTo(result, encodeValue(1L), "Max(Null, 1L) should be 1L") + } + + @Test + fun `logicalMaximum - equivalent numerics`() { + val expr = logicalMaximum(constant(1L), constant(1.0)) + val result = evaluate(expr, emptyDoc) + // Firestore considers 1L and 1.0 equivalent for comparison. Max could return either. + // C++ test implies it might return based on the first type if equivalent, or a preferred type. + // Let's assert it's numerically 1. The exact Value proto might differ. + // A more robust check might be needed if the exact proto type matters and varies. + // For now, assuming it might return the integer form if an integer is dominant or first. + assertEvaluatesTo(result, encodeValue(1L), "Max(1L, 1.0) should be numerically 1") + } + + // --- LogicalMinimum Tests --- + + @Test + fun `logicalMinimum - numeric type`() { + val expr = logicalMinimum(constant(1L), logicalMinimum(constant(2.0), constant(3L))) + val result = evaluate(expr, emptyDoc) + assertEvaluatesTo(result, encodeValue(1L), "Min(1L, Min(2.0, 3L)) should be 1L") + } + + @Test + fun `logicalMinimum - string type`() { + val expr = logicalMinimum(logicalMinimum(constant("a"), constant("b")), constant("c")) + val result = evaluate(expr, emptyDoc) + assertEvaluatesTo(result, encodeValue("a"), "Min(Min('a', 'b'), 'c') should be 'a'") + } + + @Test + fun `logicalMinimum - mixed type`() { + val expr = logicalMinimum(constant(1L), logicalMinimum(constant("1"), constant(0L))) + val result = evaluate(expr, emptyDoc) + assertEvaluatesTo(result, encodeValue(0L), "Min(1L, Min('1', 0L)) should be 0L") + } + + @Test + fun `logicalMinimum - only null and error returns null`() { + val expr = logicalMinimum(nullExpr, errorExpr) + val result = evaluate(expr, errorDoc) + assertEvaluatesToNull(result, "Min(Null, Error) should be Null") + } + + @Test + fun `logicalMinimum - nan and numbers`() { + val expr1 = logicalMinimum(nanExpr, constant(0L)) + assertEvaluatesTo( + evaluate(expr1, emptyDoc), + encodeValue(Double.NaN), + "Min(NaN, 0L) should be NaN" + ) + + val expr2 = logicalMinimum(constant(0L), nanExpr) + assertEvaluatesTo( + evaluate(expr2, emptyDoc), + encodeValue(Double.NaN), + "Min(0L, NaN) should be NaN" + ) + + val expr3 = logicalMinimum(nanExpr, nullExpr, errorExpr) + assertEvaluatesTo( + evaluate(expr3, errorDoc), + encodeValue(Double.NaN), + "Min(NaN, Null, Error) should be NaN" + ) + + val expr4 = logicalMinimum(nanExpr, errorExpr) + assertEvaluatesTo( + evaluate(expr4, errorDoc), + encodeValue(Double.NaN), + "Min(NaN, Error) should be NaN" + ) + } + + @Test + fun `logicalMinimum - error input skip`() { + val expr = logicalMinimum(errorExpr, constant(1L)) + val result = evaluate(expr, errorDoc) + assertEvaluatesTo(result, encodeValue(1L), "Min(Error, 1L) should be 1L") + } + + @Test + fun `logicalMinimum - null input skip`() { + val expr = logicalMinimum(nullExpr, constant(1L)) + val result = evaluate(expr, emptyDoc) + assertEvaluatesTo(result, encodeValue(1L), "Min(Null, 1L) should be 1L") + } + + @Test + fun `logicalMinimum - equivalent numerics`() { + val expr = logicalMinimum(constant(1L), constant(1.0)) + val result = evaluate(expr, emptyDoc) + // Similar to Max, asserting against integer form. + assertEvaluatesTo(result, encodeValue(1L), "Min(1L, 1.0) should be numerically 1") + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt index 02c1325f506..2d4e4e0e9be 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt @@ -7,6 +7,7 @@ import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values.NULL_VALUE import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import com.google.firestore.v1.Value val DATABASE_ID = UserDataReader(DatabaseId.forDatabase("project", "(default)")) val EMPTY_DOC: MutableDocument = doc("foo/1", 0, mapOf()) @@ -25,9 +26,17 @@ internal fun assertEvaluatesTo( expected: Boolean, format: String, vararg args: Any? +) = assertEvaluatesTo(result, encodeValue(expected), format, *args) + +// Helper to check for successful evaluation to a value +internal fun assertEvaluatesTo( + result: EvaluateResult, + expected: Value, + format: String, + vararg args: Any? ) { assertWithMessage(format, *args).that(result.isSuccess).isTrue() - assertWithMessage(format, *args).that(result.value).isEqualTo(encodeValue(expected)) + assertWithMessage(format, *args).that(result.value).isEqualTo(expected) } // Helper to check for evaluation resulting in NULL From 2e68d65dfcf3dff051792e6cf8154c06be0cfdc9 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 2 Jun 2025 16:19:50 -0400 Subject: [PATCH 092/152] Implement and test realtime string functions --- .../firebase/firestore/pipeline/evaluation.kt | 157 ++- .../firestore/pipeline/expressions.kt | 59 +- .../firestore/pipeline/StringTests.kt | 911 ++++++++++++++++++ 3 files changed, 1096 insertions(+), 31 deletions(-) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StringTests.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index bb8ec68a228..5537222fe2e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -18,6 +18,8 @@ import com.google.firestore.v1.Value import com.google.firestore.v1.Value.ValueTypeCase import com.google.protobuf.ByteString import com.google.protobuf.Timestamp +import com.google.re2j.Pattern +import com.google.re2j.PatternSyntaxException import java.math.BigDecimal import java.math.RoundingMode import kotlin.math.absoluteValue @@ -363,6 +365,10 @@ internal val evaluateStrConcat = variadicFunction { strings: List -> EvaluateResult.string(buildString { strings.forEach(::append) }) } +internal val evaluateStrContains = binaryFunction { value: String, substring: String -> + EvaluateResult.boolean(value.contains(substring)) +} + internal val evaluateStartsWith = binaryFunction { value: String, prefix: String -> EvaluateResult.boolean(value.startsWith(prefix)) } @@ -385,17 +391,17 @@ internal val evaluateCharLength = unaryFunction { s: String -> EvaluateResult.long(s.codePointCount(0, s.length)) } -internal val evaluateToLowercase = notImplemented +internal val evaluateToLowercase = unaryFunctionPrimitive(String::lowercase) -internal val evaluateToUppercase = notImplemented +internal val evaluateToUppercase = unaryFunctionPrimitive(String::uppercase) -internal val evaluateReverse = notImplemented +internal val evaluateReverse = unaryFunctionPrimitive(String::reversed) internal val evaluateSplit = notImplemented // TODO: Does not exist in expressions.kt yet. internal val evaluateSubstring = notImplemented // TODO: Does not exist in expressions.kt yet. -internal val evaluateTrim = notImplemented +internal val evaluateTrim = unaryFunctionPrimitive(String::trim) internal val evaluateLTrim = notImplemented // TODO: Does not exist in expressions.kt yet. @@ -403,6 +409,63 @@ internal val evaluateRTrim = notImplemented // TODO: Does not exist in expressio internal val evaluateStrJoin = notImplemented // TODO: Does not exist in expressions.kt yet. +internal val evaluateReplaceAll = notImplemented // TODO: Does not exist in backend yet. + +internal val evaluateReplaceFirst = notImplemented // TODO: Does not exist in backend yet. + +internal val evaluateRegexContains = binaryPatternFunction { pattern: Pattern, value: String -> + pattern.matcher(value).find() +} + +internal val evaluateRegexMatch = binaryPatternFunction(Pattern::matches) + +internal val evaluateLike = + binaryPatternConstructorFunction( + { likeString: String -> + try { + Pattern.compile(likeToRegex(likeString)) + } catch (e: Exception) { + null + } + }, + Pattern::matches + ) + +private fun likeToRegex(like: String): String = buildString { + var escape = false + for (c in like) { + if (escape) { + escape = false + when (c) { + '\\' -> append("\\\\") + else -> append(c) + } + } else + when (c) { + '\\' -> escape = true + '_' -> append('.') + '%' -> append(".*") + '.' -> append("\\.") + '*' -> append("\\*") + '?' -> append("\\?") + '+' -> append("\\+") + '^' -> append("\\^") + '$' -> append("\\$") + '|' -> append("\\|") + '(' -> append("\\(") + ')' -> append("\\)") + '[' -> append("\\[") + ']' -> append("\\]") + '{' -> append("\\{") + '}' -> append("\\}") + else -> append(c) + } + } + if (escape) { + throw Exception("LIKE pattern ends in backslash") + } +} + // === Date / Timestamp Functions === private const val L_NANOS_PER_SECOND: Long = 1000_000_000 @@ -574,6 +637,12 @@ private inline fun unaryFunction(crossinline stringOp: (Boolean) -> EvaluateResu stringOp, ) +@JvmName("unaryStringFunctionPrimitive") +private inline fun unaryFunctionPrimitive(crossinline stringOp: (String) -> String) = + unaryFunction { s: String -> + EvaluateResult.string(stringOp(s)) + } + @JvmName("unaryStringFunction") private inline fun unaryFunction(crossinline stringOp: (String) -> EvaluateResult) = unaryFunctionType( @@ -696,6 +765,49 @@ private inline fun binaryFunction(crossinline function: (String, String) -> Eval function ) +@JvmName("binaryStringPatternConstructorFunction") +private inline fun binaryPatternConstructorFunction( + crossinline patternConstructor: (String) -> Pattern?, + crossinline function: (Pattern, String) -> Boolean +) = + binaryFunctionConstructorType( + ValueTypeCase.STRING_VALUE, + Value::getStringValue, + ValueTypeCase.STRING_VALUE, + Value::getStringValue + ) { + val cache = cache(patternConstructor) + ({ value: String, regex: String -> + val pattern = cache(regex) + if (pattern == null) EvaluateResultError else EvaluateResult.boolean(function(pattern, value)) + }) + } + +@JvmName("binaryStringPatternFunction") +private inline fun binaryPatternFunction(crossinline function: (Pattern, String) -> Boolean) = + binaryPatternConstructorFunction( + { s: String -> + try { + Pattern.compile(s) + } catch (e: PatternSyntaxException) { + null + } + }, + function + ) + +private inline fun cache(crossinline ifAbsent: (String) -> T): (String) -> T? { + var cache: Pair = Pair(null, null) + return block@{ s: String -> + var (regex, pattern) = cache + if (regex != s) { + pattern = ifAbsent(s) + cache = Pair(s, pattern) + } + return@block pattern + } +} + @JvmName("binaryArrayArrayFunction") private inline fun binaryFunction( crossinline function: (List, List) -> EvaluateResult @@ -756,12 +868,43 @@ private inline fun binaryFunctionType( valueTypeCase2: ValueTypeCase, crossinline valueExtractor2: (Value) -> T2, crossinline function: (T1, T2) -> EvaluateResult +): EvaluateFunction = { params -> + if (params.size != 2) + throw Assert.fail("Function should have exactly 2 params, but %d were given.", params.size) + (block@{ input: MutableDocument -> + val v1 = params[0](input).value ?: return@block EvaluateResultError + val v2 = params[1](input).value ?: return@block EvaluateResultError + when (v1.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> + when (v2.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL + valueTypeCase2 -> EvaluateResult.NULL + else -> EvaluateResultError + } + valueTypeCase1 -> + when (v2.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL + valueTypeCase2 -> catch { function(valueExtractor1(v1), valueExtractor2(v2)) } + else -> EvaluateResultError + } + else -> EvaluateResultError + } + }) +} + +private inline fun binaryFunctionConstructorType( + valueTypeCase1: ValueTypeCase, + crossinline valueExtractor1: (Value) -> T1, + valueTypeCase2: ValueTypeCase, + crossinline valueExtractor2: (Value) -> T2, + crossinline functionConstructor: () -> (T1, T2) -> EvaluateResult ): EvaluateFunction = { params -> if (params.size != 2) throw Assert.fail("Function should have exactly 2 params, but %d were given.", params.size) val p1 = params[0] val p2 = params[1] - block@{ input: MutableDocument -> + val f = functionConstructor() + (block@{ input: MutableDocument -> val v1 = p1(input).value ?: return@block EvaluateResultError val v2 = p2(input).value ?: return@block EvaluateResultError when (v1.valueTypeCase) { @@ -774,12 +917,12 @@ private inline fun binaryFunctionType( valueTypeCase1 -> when (v2.valueTypeCase) { ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL - valueTypeCase2 -> catch { function(valueExtractor1(v1), valueExtractor2(v2)) } + valueTypeCase2 -> catch { f(valueExtractor1(v1), valueExtractor2(v2)) } else -> EvaluateResultError } else -> EvaluateResultError } - } + }) } private inline fun variadicResultFunction( 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 4d1cf3507eb..567ffac126a 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 @@ -242,6 +242,17 @@ abstract class Expr internal constructor() { return Constant(encodeValue(value)) } + /** + * Create a [Blob] constant from a [ByteArray]. + * + * @param bytes The [ByteArray] to convert to a Blob. + * @return A new [Expr] constant instance representing the Blob. + */ + @JvmStatic + fun blob(bytes: ByteArray): Expr { + return constant(Blob.fromBytes(bytes)) + } + /** * Constant for a null value. * @@ -1204,7 +1215,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(stringExpression: Expr, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_first", notImplemented, stringExpression, find, replace) + FunctionExpr("replace_first", evaluateReplaceFirst, stringExpression, find, replace) /** * Creates an expression that replaces the first occurrence of a substring within the @@ -1217,7 +1228,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(stringExpression: Expr, find: String, replace: String): Expr = - FunctionExpr("replace_first", notImplemented, stringExpression, find, replace) + FunctionExpr("replace_first", evaluateReplaceFirst, stringExpression, find, replace) /** * Creates an expression that replaces the first occurrence of a substring within the specified @@ -1232,7 +1243,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(fieldName: String, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_first", notImplemented, fieldName, find, replace) + FunctionExpr("replace_first", evaluateReplaceFirst, fieldName, find, replace) /** * Creates an expression that replaces the first occurrence of a substring within the specified @@ -1245,7 +1256,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceFirst(fieldName: String, find: String, replace: String): Expr = - FunctionExpr("replace_first", notImplemented, fieldName, find, replace) + FunctionExpr("replace_first", evaluateReplaceFirst, fieldName, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the @@ -1258,7 +1269,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(stringExpression: Expr, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_all", notImplemented, stringExpression, find, replace) + FunctionExpr("replace_all", evaluateReplaceAll, stringExpression, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the @@ -1271,7 +1282,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(stringExpression: Expr, find: String, replace: String): Expr = - FunctionExpr("replace_all", notImplemented, stringExpression, find, replace) + FunctionExpr("replace_all", evaluateReplaceAll, stringExpression, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the specified @@ -1286,7 +1297,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(fieldName: String, find: Expr, replace: Expr): Expr = - FunctionExpr("replace_all", notImplemented, fieldName, find, replace) + FunctionExpr("replace_all", evaluateReplaceAll, fieldName, find, replace) /** * Creates an expression that replaces all occurrences of a substring within the specified @@ -1299,7 +1310,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun replaceAll(fieldName: String, find: String, replace: String): Expr = - FunctionExpr("replace_all", notImplemented, fieldName, find, replace) + FunctionExpr("replace_all", evaluateReplaceAll, fieldName, find, replace) /** * Creates an expression that calculates the character length of a string expression in UTF8. @@ -1350,7 +1361,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(stringExpression: Expr, pattern: Expr): BooleanExpr = - BooleanExpr("like", notImplemented, stringExpression, pattern) + BooleanExpr("like", evaluateLike, stringExpression, pattern) /** * Creates an expression that performs a case-sensitive wildcard string comparison. @@ -1361,7 +1372,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(stringExpression: Expr, pattern: String): BooleanExpr = - BooleanExpr("like", notImplemented, stringExpression, pattern) + BooleanExpr("like", evaluateLike, stringExpression, pattern) /** * Creates an expression that performs a case-sensitive wildcard string comparison against a @@ -1373,7 +1384,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(fieldName: String, pattern: Expr): BooleanExpr = - BooleanExpr("like", notImplemented, fieldName, pattern) + BooleanExpr("like", evaluateLike, fieldName, pattern) /** * Creates an expression that performs a case-sensitive wildcard string comparison against a @@ -1385,7 +1396,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun like(fieldName: String, pattern: String): BooleanExpr = - BooleanExpr("like", notImplemented, fieldName, pattern) + BooleanExpr("like", evaluateLike, fieldName, pattern) /** * Creates an expression that return a pseudo-random number of type double in the range of [0, @@ -1405,7 +1416,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(stringExpression: Expr, pattern: Expr): BooleanExpr = - BooleanExpr("regex_contains", notImplemented, stringExpression, pattern) + BooleanExpr("regex_contains", evaluateRegexContains, stringExpression, pattern) /** * Creates an expression that checks if a string expression contains a specified regular @@ -1417,7 +1428,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(stringExpression: Expr, pattern: String): BooleanExpr = - BooleanExpr("regex_contains", notImplemented, stringExpression, pattern) + BooleanExpr("regex_contains", evaluateRegexContains, stringExpression, pattern) /** * Creates an expression that checks if a string field contains a specified regular expression @@ -1429,7 +1440,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(fieldName: String, pattern: Expr) = - BooleanExpr("regex_contains", notImplemented, fieldName, pattern) + BooleanExpr("regex_contains", evaluateRegexContains, fieldName, pattern) /** * Creates an expression that checks if a string field contains a specified regular expression @@ -1441,7 +1452,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexContains(fieldName: String, pattern: String) = - BooleanExpr("regex_contains", notImplemented, fieldName, pattern) + BooleanExpr("regex_contains", evaluateRegexContains, fieldName, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1452,7 +1463,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(stringExpression: Expr, pattern: Expr): BooleanExpr = - BooleanExpr("regex_match", notImplemented, stringExpression, pattern) + BooleanExpr("regex_match", evaluateRegexMatch, stringExpression, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1463,7 +1474,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(stringExpression: Expr, pattern: String): BooleanExpr = - BooleanExpr("regex_match", notImplemented, stringExpression, pattern) + BooleanExpr("regex_match", evaluateRegexMatch, stringExpression, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1474,7 +1485,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(fieldName: String, pattern: Expr) = - BooleanExpr("regex_match", notImplemented, fieldName, pattern) + BooleanExpr("regex_match", evaluateRegexMatch, fieldName, pattern) /** * Creates an expression that checks if a string field matches a specified regular expression. @@ -1485,7 +1496,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun regexMatch(fieldName: String, pattern: String) = - BooleanExpr("regex_match", notImplemented, fieldName, pattern) + BooleanExpr("regex_match", evaluateRegexMatch, fieldName, pattern) /** * Creates an expression that returns the largest value between multiple input expressions or @@ -1563,7 +1574,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(stringExpression: Expr, substring: Expr): BooleanExpr = - BooleanExpr("str_contains", notImplemented, stringExpression, substring) + BooleanExpr("str_contains", evaluateStrContains, stringExpression, substring) /** * Creates an expression that checks if a string expression contains a specified substring. @@ -1574,7 +1585,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(stringExpression: Expr, substring: String): BooleanExpr = - BooleanExpr("str_contains", notImplemented, stringExpression, substring) + BooleanExpr("str_contains", evaluateStrContains, stringExpression, substring) /** * Creates an expression that checks if a string field contains a specified substring. @@ -1585,7 +1596,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(fieldName: String, substring: Expr): BooleanExpr = - BooleanExpr("str_contains", notImplemented, fieldName, substring) + BooleanExpr("str_contains", evaluateStrContains, fieldName, substring) /** * Creates an expression that checks if a string field contains a specified substring. @@ -1596,7 +1607,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun strContains(fieldName: String, substring: String): BooleanExpr = - BooleanExpr("str_contains", notImplemented, fieldName, substring) + BooleanExpr("str_contains", evaluateStrContains, fieldName, substring) /** * Creates an expression that checks if a string expression starts with a given [prefix]. diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StringTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StringTests.kt new file mode 100644 index 00000000000..4e769057af7 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StringTests.kt @@ -0,0 +1,911 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firebase.firestore.model.Values.encodeValue +import com.google.firebase.firestore.pipeline.Expr.Companion.blob +import com.google.firebase.firestore.pipeline.Expr.Companion.byteLength +import com.google.firebase.firestore.pipeline.Expr.Companion.charLength +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.endsWith +import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.pipeline.Expr.Companion.like +import com.google.firebase.firestore.pipeline.Expr.Companion.nullValue +import com.google.firebase.firestore.pipeline.Expr.Companion.regexContains +import com.google.firebase.firestore.pipeline.Expr.Companion.regexMatch +import com.google.firebase.firestore.pipeline.Expr.Companion.reverse +import com.google.firebase.firestore.pipeline.Expr.Companion.startsWith +import com.google.firebase.firestore.pipeline.Expr.Companion.strConcat +import com.google.firebase.firestore.pipeline.Expr.Companion.strContains +import com.google.firebase.firestore.pipeline.Expr.Companion.toLower +import com.google.firebase.firestore.pipeline.Expr.Companion.toUpper +import com.google.firebase.firestore.pipeline.Expr.Companion.trim +import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class StringTests { + + // --- ByteLength Tests --- + @Test + fun byteLength_emptyString_returnsZero() { + val expr = byteLength(constant("")) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(0L), "byteLength(\"\")") + } + + @Test + fun byteLength_emptyByte_returnsZero() { + val expr = byteLength(blob(byteArrayOf())) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(0L), "byteLength(blob(byteArrayOf()))") + } + + @Test + fun byteLength_nonStringOrBytes_returnsErrorOrCorrectLength() { + // Test with non-string/byte types - should error + assertEvaluatesToError(evaluate(byteLength(constant(123L))), "byteLength(123L)") + assertEvaluatesToError(evaluate(byteLength(constant(true))), "byteLength(true)") + + // Test with a valid Blob + val bytesForBlob = byteArrayOf(0x01.toByte(), 0x02.toByte(), 0x03.toByte()) + val exprAsBlob = + byteLength(blob(bytesForBlob)) // Renamed exprBlob to avoid conflict if it was a var + val resultBlob = evaluate(exprAsBlob) + assertEvaluatesTo(resultBlob, encodeValue(3L), "byteLength(blob(1,2,3))") + + // Test with a valid ByteArray + val bytesArray = byteArrayOf(0x01.toByte(), 0x02.toByte(), 0x03.toByte(), 0x04.toByte()) + val exprByteArray = byteLength(constant(bytesArray)) + val resultByteArray = evaluate(exprByteArray) + assertEvaluatesTo(resultByteArray, encodeValue(4L), "byteLength(byteArrayOf(1,2,3,4))") + } + + @Test + fun byteLength_highSurrogateOnly_returnsError() { + // UTF-8 encoding of a lone high surrogate is invalid. + // U+D83C (high surrogate) incorrectly encoded as 3 bytes in ISO-8859-1 + // This test assumes the underlying string processing correctly identifies invalid UTF-8 + val expr = byteLength(constant("\uD83C")) // Java string with lone high surrogate + val result = evaluate(expr) + // Depending on implementation, this might error or give a byte length + // Based on C++ test, it should be an error if strict UTF-8 validation is done. + // The Kotlin `evaluateByteLength` uses `string.toByteArray(Charsets.UTF_8).size` + // which for a lone surrogate might throw an exception or produce replacement characters. + // Let's assume it should error if the input string is not valid UTF-8 representable. + // Java's toByteArray(UTF_8) replaces unpaired surrogates with '?', which is 1 byte. + // This behavior differs from the C++ test's expectation of an error. + // For now, let's match the likely Java behavior. '?' is one byte. + // UPDATE: The C++ test `\xED\xA0\xBC` is an invalid UTF-8 sequence for U+D83C. + // Java's `"\uD83C".toByteArray(StandardCharsets.UTF_8)` results in `[0x3f]` (the replacement + // char '?') + // So length is 1. The C++ test is more about the validity of the byte sequence itself. + // The current Kotlin `evaluateByteLength` directly converts string to UTF-8 bytes. + // If the string itself contains invalid sequences from a C++ perspective, + // the Java/Kotlin layer might "fix" it before byte conversion. + // The C++ test `SharedConstant(u"\xED\xA0\xBC")` passes an invalid byte sequence. + // We can't directly do that with `constant("string")` in Kotlin. + // We'd have to construct a Blob from invalid bytes if we wanted to test that. + // For `byteLength(constant("string"))`, if the string is representable, it will give a length. + // Let's assume the goal is to test the `byteLength` function with string inputs. + // A lone surrogate in a Java string is valid at the string level. + // Its UTF-8 representation is a replacement character. + assertEvaluatesTo(result, encodeValue(1L), "byteLength(\"\\uD83C\") - lone high surrogate") + } + + @Test + fun byteLength_lowSurrogateOnly_returnsError() { + // Similar to high surrogate, Java's toByteArray(UTF_8) replaces with '?' + val expr = byteLength(constant("\uDF53")) // Java string with lone low surrogate + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(1L), "byteLength(\"\\uDF53\") - lone low surrogate") + } + + @Test + fun byteLength_lowAndHighSurrogateSwapped_returnsError() { + // "\uDF53\uD83C" - two replacement characters '??' + val expr = byteLength(constant("\uDF53\uD83C")) + val result = evaluate(expr) + assertEvaluatesTo( + result, + encodeValue(2L), + "byteLength(\"\\uDF53\\uD83C\") - swapped surrogates" + ) + } + + @Test + fun byteLength_wrongContinuation_returnsError() { + // This C++ test checks specific invalid UTF-8 byte sequences. + // In Kotlin, `constant(String)` takes a valid Java String. + // If we want to test invalid byte sequences, we should use `constant(Blob)` or + // `constant(ByteArray)`. + // The `evaluateByteLength` for string input converts the Java string to UTF-8 bytes. + // If the Java string itself is valid (e.g. contains lone surrogates), it gets converted (often + // with replacement chars). + // The C++ tests like "Start \xFF End" are passing byte sequences that are not valid UTF-8. + // We cannot directly create `constant("Start \xFF End")` where \xFF is a literal byte. + // We will skip porting these specific invalid byte sequence tests for string inputs, + // as they test behavior not directly exposed by `byteLength(constant(String))` in the same way. + // The `byteLength` for `Blob` would be the place for such tests if needed. + // For now, we assume `byteLength(String)` expects a valid Java string. + } + + @Test + fun byteLength_ascii() { + assertEvaluatesTo(evaluate(byteLength(constant("abc"))), encodeValue(3L), "byteLength(\"abc\")") + assertEvaluatesTo( + evaluate(byteLength(constant("1234"))), + encodeValue(4L), + "byteLength(\"1234\")" + ) + assertEvaluatesTo( + evaluate(byteLength(constant("abc123!@"))), + encodeValue(8L), + "byteLength(\"abc123!@\")" + ) + } + + @Test + fun byteLength_largeString() { + val largeA = "a".repeat(1500) + val largeAbBuilder = StringBuilder(3000) + for (i in 0 until 1500) { + largeAbBuilder.append("ab") + } + val largeAb = largeAbBuilder.toString() + + assertEvaluatesTo( + evaluate(byteLength(constant(largeA))), + encodeValue(1500L), + "byteLength(largeA)" + ) + assertEvaluatesTo( + evaluate(byteLength(constant(largeAb))), + encodeValue(3000L), + "byteLength(largeAb)" + ) + } + + @Test + fun byteLength_twoBytesPerCharacter() { + // UTF-8: é=2, ç=2, ñ=2, ö=2, ü=2 => 10 bytes + val str = "éçñöü" // Each char is 2 bytes in UTF-8 + assertEvaluatesTo( + evaluate(byteLength(constant(str))), + encodeValue(10L), + "byteLength(\"éçñöü\")" + ) + + val bytesTwo = + byteArrayOf( + 0xc3.toByte(), + 0xa9.toByte(), + 0xc3.toByte(), + 0xa7.toByte(), + 0xc3.toByte(), + 0xb1.toByte(), + 0xc3.toByte(), + 0xb6.toByte(), + 0xc3.toByte(), + 0xbc.toByte() + ) + assertEvaluatesTo( + evaluate(byteLength(blob(bytesTwo))), + encodeValue(10L), + "byteLength(blob for \"éçñöü\")" + ) + } + + @Test + fun byteLength_threeBytesPerCharacter() { + // UTF-8: 你=3, 好=3, 世=3, 界=3 => 12 bytes + val str = "你好世界" // Each char is 3 bytes in UTF-8 + assertEvaluatesTo(evaluate(byteLength(constant(str))), encodeValue(12L), "byteLength(\"你好世界\")") + + val bytesThree = + byteArrayOf( + 0xe4.toByte(), + 0xbd.toByte(), + 0xa0.toByte(), + 0xe5.toByte(), + 0xa5.toByte(), + 0xbd.toByte(), + 0xe4.toByte(), + 0xb8.toByte(), + 0x96.toByte(), + 0xe7.toByte(), + 0x95.toByte(), + 0x8c.toByte() + ) + assertEvaluatesTo( + evaluate(byteLength(blob(bytesThree))), + encodeValue(12L), + "byteLength(blob for \"你好世界\")" + ) + } + + @Test + fun byteLength_fourBytesPerCharacter() { + // UTF-8: 🀘=4, 🂡=4 => 8 bytes (U+1F018, U+1F0A1) + val str = "🀘🂡" // Each char is 4 bytes in UTF-8 + assertEvaluatesTo(evaluate(byteLength(constant(str))), encodeValue(8L), "byteLength(\"🀘🂡\")") + val bytesFour = + byteArrayOf( + 0xF0.toByte(), + 0x9F.toByte(), + 0x80.toByte(), + 0x98.toByte(), + 0xF0.toByte(), + 0x9F.toByte(), + 0x82.toByte(), + 0xA1.toByte() + ) + assertEvaluatesTo( + evaluate(byteLength(blob(bytesFour))), + encodeValue(8L), + "byteLength(blob for \"🀘🂡\")" + ) + } + + @Test + fun byteLength_mixOfDifferentEncodedLengths() { + // a=1, é=2, 好=3, 🂡=4 => 10 bytes + val str = "aé好🂡" + assertEvaluatesTo( + evaluate(byteLength(constant(str))), + encodeValue(10L), + "byteLength(\"aé好🂡\")" + ) + val bytesMix = + byteArrayOf( + 0x61.toByte(), + 0xc3.toByte(), + 0xa9.toByte(), + 0xe5.toByte(), + 0xa5.toByte(), + 0xbd.toByte(), + 0xF0.toByte(), + 0x9F.toByte(), + 0x82.toByte(), + 0xA1.toByte() + ) + assertEvaluatesTo( + evaluate(byteLength(blob(bytesMix))), + encodeValue(10L), + "byteLength(blob for \"aé好🂡\")" + ) + } + + // --- CharLength Tests --- + @Test + fun charLength_emptyString_returnsZero() { + val expr = charLength(constant("")) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(0L), "charLength(\"\")") + } + + @Test + fun charLength_bytesType_returnsError() { + // charLength expects a string, not bytes/blob + val charBlobBytes = byteArrayOf('a'.code.toByte(), 'b'.code.toByte(), 'c'.code.toByte()) + val expr = charLength(blob(charBlobBytes)) + val result = evaluate(expr) + assertEvaluatesToError(result, "charLength(blob)") + } + + @Test + fun charLength_baseCaseBmp() { + assertEvaluatesTo(evaluate(charLength(constant("abc"))), encodeValue(3L), "charLength(\"abc\")") + assertEvaluatesTo( + evaluate(charLength(constant("1234"))), + encodeValue(4L), + "charLength(\"1234\")" + ) + assertEvaluatesTo( + evaluate(charLength(constant("abc123!@"))), + encodeValue(8L), + "charLength(\"abc123!@\")" + ) + assertEvaluatesTo( + evaluate(charLength(constant("你好世界"))), + encodeValue(4L), + "charLength(\"你好世界\")" + ) + assertEvaluatesTo( + evaluate(charLength(constant("cafétéria"))), + encodeValue(9L), + "charLength(\"cafétéria\")" + ) + assertEvaluatesTo( + evaluate(charLength(constant("абвгд"))), + encodeValue(5L), + "charLength(\"абвгд\")" + ) + assertEvaluatesTo( + evaluate(charLength(constant("¡Hola! ¿Cómo estás?"))), + encodeValue(19L), + "charLength(\"¡Hola! ¿Cómo estás?\")" + ) + assertEvaluatesTo( + evaluate(charLength(constant("☺"))), + encodeValue(1L), + "charLength(\"☺\")" + ) // U+263A + } + + @Test + fun charLength_spaces() { + assertEvaluatesTo(evaluate(charLength(constant(" "))), encodeValue(1L), "charLength(\" \")") + assertEvaluatesTo(evaluate(charLength(constant(" "))), encodeValue(2L), "charLength(\" \")") + assertEvaluatesTo(evaluate(charLength(constant("a b"))), encodeValue(3L), "charLength(\"a b\")") + } + + @Test + fun charLength_specialCharacters() { + assertEvaluatesTo(evaluate(charLength(constant("\n"))), encodeValue(1L), "charLength(\"\\n\")") + assertEvaluatesTo(evaluate(charLength(constant("\t"))), encodeValue(1L), "charLength(\"\\t\")") + assertEvaluatesTo(evaluate(charLength(constant("\\"))), encodeValue(1L), "charLength(\"\\\\\")") + } + + @Test + fun charLength_bmpSmpMix() { + // Hello = 5, Smiling Face Emoji (U+1F60A) = 1 code point => 6 + assertEvaluatesTo( + evaluate(charLength(constant("Hello😊"))), + encodeValue(6L), + "charLength(\"Hello😊\")" + ) + } + + @Test + fun charLength_smp() { + // Strawberry (U+1F353) = 1, Peach (U+1F351) = 1 => 2 code points + assertEvaluatesTo( + evaluate(charLength(constant("🍓🍑"))), + encodeValue(2L), + "charLength(\"🍓🍑\")" + ) + } + + @Test + fun charLength_highSurrogateOnly() { + // A lone high surrogate U+D83C is 1 code point in a Java String. + // The Kotlin `evaluateCharLength` uses `string.length` which counts UTF-16 code units. + // For a lone surrogate, this is 1. + // This differs from C++ test which expects an error for invalid UTF-8 sequence. + // The current Kotlin implementation of charLength is `value.stringValue.length` which is UTF-16 + // code units. + // This needs to be `value.stringValue.codePointCount(0, value.stringValue.length)` for correct + // char count. + // For now, I will write the test based on the current `expressions.kt` (which seems to be + // `stringValue.length`). + // If `charLength` is fixed to count code points, this test will need adjustment. + // Assuming current `evaluateCharLength` uses `s.length()`: + assertEvaluatesTo( + evaluate(charLength(constant("\uD83C"))), + encodeValue(1L), + "charLength(\"\\uD83C\") - lone high surrogate" + ) + } + + @Test + fun charLength_lowSurrogateOnly() { + // Similar to high surrogate. + assertEvaluatesTo( + evaluate(charLength(constant("\uDF53"))), + encodeValue(1L), + "charLength(\"\\uDF53\") - lone low surrogate" + ) + } + + @Test + fun charLength_lowAndHighSurrogateSwapped() { + // "\uDF53\uD83C" - two UTF-16 code units. + assertEvaluatesTo( + evaluate(charLength(constant("\uDF53\uD83C"))), + encodeValue(2L), + "charLength(\"\\uDF53\\uD83C\") - swapped surrogates" + ) + } + + @Test + fun charLength_largeString() { + val largeA = "a".repeat(1500) + val largeAbBuilder = StringBuilder(3000) + for (i in 0 until 1500) { + largeAbBuilder.append("ab") + } + val largeAb = largeAbBuilder.toString() + + assertEvaluatesTo( + evaluate(charLength(constant(largeA))), + encodeValue(1500L), + "charLength(largeA)" + ) + assertEvaluatesTo( + evaluate(charLength(constant(largeAb))), + encodeValue(3000L), + "charLength(largeAb)" + ) + } + + // --- StrConcat Tests --- + @Test + fun strConcat_multipleStringChildren_returnsCombination() { + val expr = strConcat(constant("foo"), constant(" "), constant("bar")) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue("foo bar"), "strConcat(\"foo\", \" \", \"bar\")") + } + + @Test + fun strConcat_multipleNonStringChildren_returnsError() { + // strConcat should only accept strings or expressions that evaluate to strings. + // The Kotlin `strConcat` vararg is `Any`, then converted via `toArrayOfExprOrConstant`. + // `evaluateStrConcat` checks if all resolved params are strings. + val expr = strConcat(constant("foo"), constant(42L), constant("bar")) + val result = evaluate(expr) + assertEvaluatesToError(result, "strConcat(\"foo\", 42L, \"bar\")") + } + + @Test + fun strConcat_multipleCalls() { + val expr = strConcat(constant("foo"), constant(" "), constant("bar")) + assertEvaluatesTo(evaluate(expr), encodeValue("foo bar"), "strConcat call 1") + assertEvaluatesTo(evaluate(expr), encodeValue("foo bar"), "strConcat call 2") + assertEvaluatesTo(evaluate(expr), encodeValue("foo bar"), "strConcat call 3") + } + + @Test + fun strConcat_largeNumberOfInputs() { + val argCount = 500 + val args = Array(argCount) { constant("a") } + val expectedResult = "a".repeat(argCount) + val expr = strConcat(args.first(), *args.drop(1).toTypedArray()) // Pass varargs correctly + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(expectedResult), "strConcat large number of inputs") + } + + @Test + fun strConcat_largeStrings() { + val a500 = "a".repeat(500) + val b500 = "b".repeat(500) + val c500 = "c".repeat(500) + val expr = strConcat(constant(a500), constant(b500), constant(c500)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(a500 + b500 + c500), "strConcat large strings") + } + + // --- EndsWith Tests --- + @Test + fun endsWith_getNonStringValue_isError() { + val expr = endsWith(constant(42L), constant("search")) + assertEvaluatesToError(evaluate(expr), "endsWith(42L, \"search\")") + } + + @Test + fun endsWith_getNonStringSuffix_isError() { + val expr = endsWith(constant("search"), constant(42L)) + assertEvaluatesToError(evaluate(expr), "endsWith(\"search\", 42L)") + } + + @Test + fun endsWith_emptyInputs_returnsTrue() { + val expr = endsWith(constant(""), constant("")) + assertEvaluatesTo(evaluate(expr), true, "endsWith(\"\", \"\")") + } + + @Test + fun endsWith_emptyValue_returnsFalse() { + val expr = endsWith(constant(""), constant("v")) + assertEvaluatesTo(evaluate(expr), false, "endsWith(\"\", \"v\")") + } + + @Test + fun endsWith_emptySuffix_returnsTrue() { + val expr = endsWith(constant("value"), constant("")) + assertEvaluatesTo(evaluate(expr), true, "endsWith(\"value\", \"\")") + } + + @Test + fun endsWith_returnsTrue() { + val expr = endsWith(constant("search"), constant("rch")) + assertEvaluatesTo(evaluate(expr), true, "endsWith(\"search\", \"rch\")") + } + + @Test + fun endsWith_returnsFalse() { + val expr = endsWith(constant("search"), constant("rcH")) // Case-sensitive + assertEvaluatesTo(evaluate(expr), false, "endsWith(\"search\", \"rcH\")") + } + + @Test + fun endsWith_largeSuffix_returnsFalse() { + val expr = endsWith(constant("val"), constant("a very long suffix")) + assertEvaluatesTo(evaluate(expr), false, "endsWith(\"val\", \"a very long suffix\")") + } + + // --- Like Tests --- (Expected to be failing/error due to notImplemented) + @Test + fun like_getNonStringLike_isError() { + val expr = like(constant(42L), constant("search")) + assertEvaluatesToError(evaluate(expr), "like(42L, \"search\")") + } + + @Test + fun like_getNonStringValue_isError() { + val expr = like(constant("ear"), constant(42L)) + assertEvaluatesToError(evaluate(expr), "like(\"ear\", 42L)") + } + + @Test + fun like_getStaticLike() { + val expr = like(constant("yummy food"), constant("%food")) + assertEvaluatesTo(evaluate(expr), true, "like(\"yummy food\", \"%food\")") + } + + @Test + fun like_getEmptySearchString() { + val expr = like(constant(""), constant("%hi%")) + assertEvaluatesTo(evaluate(expr), false, "like(\"\", \"%hi%\")") + } + + @Test + fun like_getEmptyLike() { + val expr = like(constant("yummy food"), constant("")) + assertEvaluatesTo(evaluate(expr), false, "like(\"yummy food\", \"\")") + } + + @Test + fun like_getEscapedLike() { + val expr = like(constant("yummy food??"), constant("%food??")) + assertEvaluatesTo(evaluate(expr), true, "like(\"yummy food??\", \"%food??\")") + } + + @Test + fun like_getDynamicLike() { + val expr = like(constant("yummy food"), field("regex")) + val doc1 = doc("coll/doc1", 0, mapOf("regex" to "yummy%")) + val doc2 = doc("coll/doc2", 0, mapOf("regex" to "food%")) + val doc3 = doc("coll/doc3", 0, mapOf("regex" to "yummy_food")) + + assertEvaluatesTo(evaluate(expr, doc1), true, "like dynamic doc1") + assertEvaluatesTo(evaluate(expr, doc2), false, "like dynamic doc2") + assertEvaluatesTo(evaluate(expr, doc3), true, "like dynamic doc3") + } + + // --- RegexContains Tests --- + @Test + fun regexContains_getNonStringRegex_isError() { + val expr = regexContains(constant(42L), constant("search")) + assertEvaluatesToError(evaluate(expr), "regexContains(42L, \"search\")") + } + + @Test + fun regexContains_getNonStringValue_isError() { + val expr = regexContains(constant("ear"), constant(42L)) + assertEvaluatesToError(evaluate(expr), "regexContains(\"ear\", 42L)") + } + + @Test + fun regexContains_getInvalidRegex_isError() { + val expr = regexContains(constant("abcabc"), constant("(abc)\\1")) + assertEvaluatesToError(evaluate(expr), "regexContains invalid regex") + } + + @Test + fun regexContains_getStaticRegex() { + val expr = regexContains(constant("yummy food"), constant(".*oo.*")) + assertEvaluatesTo(evaluate(expr), true, "regexContains static") + } + + @Test + fun regexContains_getSubStringLiteral() { + val expr = regexContains(constant("yummy good food"), constant("good")) + assertEvaluatesTo(evaluate(expr), true, "regexContains substring literal") + } + + @Test + fun regexContains_getSubStringRegex() { + val expr = regexContains(constant("yummy good food"), constant("go*d")) + assertEvaluatesTo(evaluate(expr), true, "regexContains substring regex") + } + + @Test + fun regexContains_getDynamicRegex() { + val expr = regexContains(constant("yummy food"), field("regex")) + val doc1 = doc("coll/doc1", 0, mapOf("regex" to "^yummy.*")) + val doc2 = doc("coll/doc2", 0, mapOf("regex" to "fooood$")) // This should be false for contains + val doc3 = doc("coll/doc3", 0, mapOf("regex" to ".*")) + + assertEvaluatesTo(evaluate(expr, doc1), true, "regexContains dynamic doc1") + assertEvaluatesTo(evaluate(expr, doc2), false, "regexContains dynamic doc2") + assertEvaluatesTo(evaluate(expr, doc3), true, "regexContains dynamic doc3") + } + + // --- RegexMatch Tests --- + @Test + fun regexMatch_getNonStringRegex_isError() { + val expr = regexMatch(constant(42L), constant("search")) + assertEvaluatesToError(evaluate(expr), "regexMatch(42L, \"search\")") + } + + @Test + fun regexMatch_getNonStringValue_isError() { + val expr = regexMatch(constant("ear"), constant(42L)) + assertEvaluatesToError(evaluate(expr), "regexMatch(\"ear\", 42L)") + } + + @Test + fun regexMatch_getInvalidRegex_isError() { + val expr = regexMatch(constant("abcabc"), constant("(abc)\\1")) + assertEvaluatesToError(evaluate(expr), "regexMatch invalid regex") + } + + @Test + fun regexMatch_getStaticRegex() { + val expr = regexMatch(constant("yummy food"), constant(".*oo.*")) + assertEvaluatesTo(evaluate(expr), true, "regexMatch static") + } + + @Test + fun regexMatch_getSubStringLiteral() { + val expr = regexMatch(constant("yummy good food"), constant("good")) + assertEvaluatesTo(evaluate(expr), false, "regexMatch substring literal (false)") + } + + @Test + fun regexMatch_getSubStringRegex() { + val expr = regexMatch(constant("yummy good food"), constant("go*d")) + assertEvaluatesTo(evaluate(expr), false, "regexMatch substring regex (false)") + } + + @Test + fun regexMatch_getDynamicRegex() { + val expr = regexMatch(constant("yummy food"), field("regex")) + val doc1 = doc("coll/doc1", 0, mapOf("regex" to "^yummy.*")) // Should be true + val doc2 = doc("coll/doc2", 0, mapOf("regex" to "fooood$")) + val doc3 = doc("coll/doc3", 0, mapOf("regex" to ".*")) + val doc4 = doc("coll/doc4", 0, mapOf("regex" to "yummy")) // Should be false + + assertEvaluatesTo(evaluate(expr, doc1), true, "regexMatch dynamic doc1") + assertEvaluatesTo(evaluate(expr, doc2), false, "regexMatch dynamic doc2") + assertEvaluatesTo(evaluate(expr, doc3), true, "regexMatch dynamic doc3") + assertEvaluatesTo(evaluate(expr, doc4), false, "regexMatch dynamic doc4") + } + + // --- StartsWith Tests --- + @Test + fun startsWith_getNonStringValue_isError() { + val expr = startsWith(constant(42L), constant("search")) + assertEvaluatesToError(evaluate(expr), "startsWith(42L, \"search\")") + } + + @Test + fun startsWith_getNonStringPrefix_isError() { + val expr = startsWith(constant("search"), constant(42L)) + assertEvaluatesToError(evaluate(expr), "startsWith(\"search\", 42L)") + } + + @Test + fun startsWith_emptyInputs_returnsTrue() { + val expr = startsWith(constant(""), constant("")) + assertEvaluatesTo(evaluate(expr), true, "startsWith(\"\", \"\")") + } + + @Test + fun startsWith_emptyValue_returnsFalse() { + val expr = startsWith(constant(""), constant("v")) + assertEvaluatesTo(evaluate(expr), false, "startsWith(\"\", \"v\")") + } + + @Test + fun startsWith_emptyPrefix_returnsTrue() { + val expr = startsWith(constant("value"), constant("")) + assertEvaluatesTo(evaluate(expr), true, "startsWith(\"value\", \"\")") + } + + @Test + fun startsWith_returnsTrue() { + val expr = startsWith(constant("search"), constant("sea")) + assertEvaluatesTo(evaluate(expr), true, "startsWith(\"search\", \"sea\")") + } + + @Test + fun startsWith_returnsFalse() { + val expr = startsWith(constant("search"), constant("Sea")) // Case-sensitive + assertEvaluatesTo(evaluate(expr), false, "startsWith(\"search\", \"Sea\")") + } + + @Test + fun startsWith_largePrefix_returnsFalse() { + val expr = startsWith(constant("val"), constant("a very long prefix")) + assertEvaluatesTo(evaluate(expr), false, "startsWith(\"val\", \"a very long prefix\")") + } + + // --- StrContains Tests --- + @Test + fun strContains_valueNonString_isError() { + val expr = strContains(constant(42L), constant("value")) + assertEvaluatesToError(evaluate(expr), "strContains(42L, \"value\")") + } + + @Test + fun strContains_subStringNonString_isError() { + val expr = strContains(constant("search space"), constant(42L)) + assertEvaluatesToError(evaluate(expr), "strContains(\"search space\", 42L)") + } + + @Test + fun strContains_executeTrue() { + assertEvaluatesTo( + evaluate(strContains(constant("abc"), constant("c"))), + true, + "strContains true 1" + ) + assertEvaluatesTo( + evaluate(strContains(constant("abc"), constant("bc"))), + true, + "strContains true 2" + ) + assertEvaluatesTo( + evaluate(strContains(constant("abc"), constant("abc"))), + true, + "strContains true 3" + ) + assertEvaluatesTo( + evaluate(strContains(constant("abc"), constant(""))), + true, + "strContains true 4" + ) // Empty string is a substring + assertEvaluatesTo( + evaluate(strContains(constant(""), constant(""))), + true, + "strContains true 5" + ) // Empty string in empty string + assertEvaluatesTo( + evaluate(strContains(constant("☃☃☃"), constant("☃"))), + true, + "strContains true 6" + ) + } + + @Test + fun strContains_executeFalse() { + assertEvaluatesTo( + evaluate(strContains(constant("abc"), constant("abcd"))), + false, + "strContains false 1" + ) + assertEvaluatesTo( + evaluate(strContains(constant("abc"), constant("d"))), + false, + "strContains false 2" + ) + assertEvaluatesTo( + evaluate(strContains(constant(""), constant("a"))), + false, + "strContains false 3" + ) + } + + // --- ToLower Tests --- + @Test + fun toLower_basic() { + val expr = toLower(constant("FOO Bar")) + assertEvaluatesTo(evaluate(expr), encodeValue("foo bar"), "toLower(\"FOO Bar\")") + } + + @Test + fun toLower_empty() { + val expr = toLower(constant("")) + assertEvaluatesTo(evaluate(expr), encodeValue(""), "toLower(\"\")") + } + + @Test + fun toLower_nonString() { + val expr = toLower(constant(123L)) + assertEvaluatesToError(evaluate(expr), "toLower(123L)") + } + + @Test + fun toLower_null() { + val expr = toLower(nullValue()) // Use Expr.nullValue() for Firestore null + assertEvaluatesToNull(evaluate(expr), "toLower(null)") + } + + // --- ToUpper Tests --- + @Test + fun toUpper_basic() { + val expr = toUpper(constant("foo Bar")) + assertEvaluatesTo(evaluate(expr), encodeValue("FOO BAR"), "toUpper(\"foo Bar\")") + } + + @Test + fun toUpper_empty() { + val expr = toUpper(constant("")) + assertEvaluatesTo(evaluate(expr), encodeValue(""), "toUpper(\"\")") + } + + @Test + fun toUpper_nonString() { + val expr = toUpper(constant(123L)) + assertEvaluatesToError(evaluate(expr), "toUpper(123L)") + } + + @Test + fun toUpper_null() { + val expr = toUpper(nullValue()) + assertEvaluatesToNull(evaluate(expr), "toUpper(null)") + } + + // --- Trim Tests --- + @Test + fun trim_basic() { + val expr = trim(constant(" foo bar ")) + assertEvaluatesTo(evaluate(expr), encodeValue("foo bar"), "trim(\" foo bar \")") + } + + @Test + fun trim_noTrimNeeded() { + val expr = trim(constant("foo bar")) + assertEvaluatesTo(evaluate(expr), encodeValue("foo bar"), "trim(\"foo bar\")") + } + + @Test + fun trim_onlyWhitespace() { + val expr = trim(constant(" \t\n ")) + assertEvaluatesTo(evaluate(expr), encodeValue(""), "trim(\" \\t\\n \")") + } + + @Test + fun trim_empty() { + val expr = trim(constant("")) + assertEvaluatesTo(evaluate(expr), encodeValue(""), "trim(\"\")") + } + + @Test + fun trim_nonString() { + val expr = trim(constant(123L)) + assertEvaluatesToError(evaluate(expr), "trim(123L)") + } + + @Test + fun trim_null() { + val expr = trim(nullValue()) + assertEvaluatesToNull(evaluate(expr), "trim(null)") + } + + // --- Reverse Tests --- + @Test + fun reverse_basic() { + val expr = reverse(constant("abc")) + assertEvaluatesTo(evaluate(expr), encodeValue("cba"), "reverse(\"abc\")") + } + + @Test + fun reverse_empty() { + val expr = reverse(constant("")) + assertEvaluatesTo(evaluate(expr), encodeValue(""), "reverse(\"\")") + } + + @Test + fun reverse_unicode() { + // a=1, é=2, 好=3, 🂡=4 + // Original: "aé好🂡" + // Reversed: "🂡好éa" + val expr = reverse(constant("aé好🂡")) + assertEvaluatesTo(evaluate(expr), encodeValue("🂡好éa"), "reverse(\"aé好🂡\")") + } + + @Test + fun reverse_nonString() { + val expr = reverse(constant(123L)) + assertEvaluatesToError(evaluate(expr), "reverse(123L)") + } + + @Test + fun reverse_null() { + val expr = reverse(nullValue()) + assertEvaluatesToNull(evaluate(expr), "reverse(null)") + } +} From 6332029e4766dd0785e4186b24664ba1b304b484 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 2 Jun 2025 17:11:22 -0400 Subject: [PATCH 093/152] Implement and test realtime timestamp functions --- .../google/firebase/firestore/model/Values.kt | 23 +- .../firestore/pipeline/EvaluateResult.kt | 7 +- .../firebase/firestore/pipeline/evaluation.kt | 4 +- .../firestore/pipeline/TimestampTests.kt | 713 ++++++++++++++++++ 4 files changed, 742 insertions(+), 5 deletions(-) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/TimestampTests.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 79a4363fe23..52fc539e6c1 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -750,11 +750,32 @@ internal object Values { @JvmStatic fun timestamp(seconds: Long, nanos: Int): Timestamp { + validateRange(seconds, nanos) + // Firestore backend truncates precision down to microseconds. To ensure offline mode works // the same with regards to truncation, perform the truncation immediately without waiting for // the backend to do that. val truncatedNanoseconds: Int = nanos / 1000 * 1000 - return Timestamp.newBuilder().setSeconds(seconds).setNanos(truncatedNanoseconds).build() } + + /** + * Ensures that the date and time are within what we consider valid ranges. + * + * More specifically, the nanoseconds need to be less than 1 billion- otherwise it would trip over + * into seconds, and need to be greater than zero. + * + * The seconds need to be after the date `1/1/1` and before the date `1/1/10000`. + * + * @throws IllegalArgumentException if the date and time are considered invalid + */ + private fun validateRange(seconds: Long, nanoseconds: Int) { + require(nanoseconds in 0 until 1_000_000_000) { + "Timestamp nanoseconds out of range: $nanoseconds" + } + + require(seconds in -62_135_596_800 until 253_402_300_800) { + "Timestamp seconds out of range: $seconds" + } + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt index 51dd546eefc..dc785d465ed 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt @@ -26,8 +26,11 @@ internal sealed class EvaluateResult(val value: Value?) { fun timestamp(timestamp: Timestamp): EvaluateResult = EvaluateResultValue(encodeValue(timestamp)) fun timestamp(seconds: Long, nanos: Int): EvaluateResult = - if (seconds !in -62_135_596_800 until 253_402_300_800) EvaluateResultError - else timestamp(Values.timestamp(seconds, nanos)) + try { + timestamp(Values.timestamp(seconds, nanos)) + } catch (e: IllegalArgumentException) { + EvaluateResultError + } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index 5537222fe2e..1aa91595236 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -568,14 +568,14 @@ internal val evaluateTimestampToUnixSeconds = unaryFunction { t: Timestamp -> internal val evaluateUnixMicrosToTimestamp = unaryFunction { micros: Long -> EvaluateResult.timestamp( Math.floorDiv(micros, L_MICROS_PER_SECOND), - Math.floorMod(micros, I_MICROS_PER_SECOND) + Math.floorMod(micros, I_MICROS_PER_SECOND) * 1000 ) } internal val evaluateUnixMillisToTimestamp = unaryFunction { millis: Long -> EvaluateResult.timestamp( Math.floorDiv(millis, L_MILLIS_PER_SECOND), - Math.floorMod(millis, I_MILLIS_PER_SECOND) + Math.floorMod(millis, I_MILLIS_PER_SECOND) * 1000_000 ) } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/TimestampTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/TimestampTests.kt new file mode 100644 index 00000000000..b2ae0a9ba80 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/TimestampTests.kt @@ -0,0 +1,713 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firebase.Timestamp +import com.google.firebase.firestore.model.Values.encodeValue +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.nullValue // For null constant +import com.google.firebase.firestore.pipeline.Expr.Companion.timestampAdd +import com.google.firebase.firestore.pipeline.Expr.Companion.timestampToUnixMicros +import com.google.firebase.firestore.pipeline.Expr.Companion.timestampToUnixMillis +import com.google.firebase.firestore.pipeline.Expr.Companion.timestampToUnixSeconds +import com.google.firebase.firestore.pipeline.Expr.Companion.unixMicrosToTimestamp +import com.google.firebase.firestore.pipeline.Expr.Companion.unixMillisToTimestamp +import com.google.firebase.firestore.pipeline.Expr.Companion.unixSecondsToTimestamp +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class TimestampTests { + + // --- UnixMicrosToTimestamp Tests --- + + @Test + fun unixMicrosToTimestamp_stringType_returnsError() { + val expr = unixMicrosToTimestamp(constant("abc")) + val result = evaluate(expr) + assertEvaluatesToError(result, "unixMicrosToTimestamp(\"abc\")") + } + + @Test + fun unixMicrosToTimestamp_zeroValue_returnsTimestampEpoch() { + val expr = unixMicrosToTimestamp(constant(0L)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(Timestamp(0, 0)), "unixMicrosToTimestamp(0L)") + } + + @Test + fun unixMicrosToTimestamp_intType_returnsTimestamp() { + // C++ test uses 1000000LL, which is 1 second + val expr = unixMicrosToTimestamp(constant(1000000L)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(Timestamp(1, 0)), "unixMicrosToTimestamp(1000000L)") + } + + @Test + fun unixMicrosToTimestamp_longType_returnsTimestamp() { + // C++ test uses 9876543210LL micros + // 9876543210 / 1,000,000 = 9876 seconds + // 9876543210 % 1,000,000 = 543210 micros = 543210000 nanos + val expr = unixMicrosToTimestamp(constant(9876543210L)) + val result = evaluate(expr) + assertEvaluatesTo( + result, + encodeValue(Timestamp(9876, 543210000)), + "unixMicrosToTimestamp(9876543210L)" + ) + } + + @Test + fun unixMicrosToTimestamp_longTypeNegative_returnsTimestamp() { + // -10000 micros = -0.01 seconds + // seconds = -1 (floor of -0.01) + // remaining_micros = -10000 - (-1 * 1,000,000) = -10000 + 1,000,000 = 990,000 micros + // nanos = 990,000 * 1000 = 990,000,000 nanos + val expr = unixMicrosToTimestamp(constant(-10000L)) + val result = evaluate(expr) + assertEvaluatesTo( + result, + encodeValue(Timestamp(-1, 990000000)), + "unixMicrosToTimestamp(-10000L)" + ) + } + + @Test + fun unixMicrosToTimestamp_longTypeNegativeOverflow_returnsError() { + // Min representable timestamp: seconds=-62135596800, nanos=0 + // Corresponds to micros: -62135596800 * 1,000,000 = -62135596800000000 + val minMicros = -62135596800000000L + + // Test the boundary value + val boundaryExpr = unixMicrosToTimestamp(constant(minMicros)) + val boundaryResult = evaluate(boundaryExpr) + assertEvaluatesTo( + boundaryResult, + encodeValue(Timestamp(-62135596800L, 0)), + "unixMicrosToTimestamp(minMicros)" + ) + + // Test value just below the boundary (minMicros - 1) + // The C++ test uses SubtractExpr for this, we can do it directly. + val belowMinExpr = unixMicrosToTimestamp(constant(minMicros - 1)) + val belowMinResult = evaluate(belowMinExpr) + assertEvaluatesToError(belowMinResult, "unixMicrosToTimestamp(minMicros - 1)") + } + + @Test + fun unixMicrosToTimestamp_longTypePositiveOverflow_returnsError() { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + // Corresponds to micros: 253402300799 * 1,000,000 + 999999 (since nanos are truncated to + // micros) + // = 253402300799000000 + 999999 = 253402300799999999 + val maxMicros = 253402300799999999L + + // Test the boundary value + // Nanos are 999999000 because 999999 micros * 1000 = 999999000 nanos + val boundaryExpr = unixMicrosToTimestamp(constant(maxMicros)) + val boundaryResult = evaluate(boundaryExpr) + assertEvaluatesTo( + boundaryResult, + encodeValue(Timestamp(253402300799L, 999999000)), // Nanos from 999999 micros + "unixMicrosToTimestamp(maxMicros)" + ) + + // Test value just above the boundary (maxMicros + 1) + val aboveMaxExpr = unixMicrosToTimestamp(constant(maxMicros + 1)) + val aboveMaxResult = evaluate(aboveMaxExpr) + assertEvaluatesToError(aboveMaxResult, "unixMicrosToTimestamp(maxMicros + 1)") + } + + // --- UnixMillisToTimestamp Tests --- + + @Test + fun unixMillisToTimestamp_stringType_returnsError() { + val expr = unixMillisToTimestamp(constant("abc")) + val result = evaluate(expr) + assertEvaluatesToError(result, "unixMillisToTimestamp(\"abc\")") + } + + @Test + fun unixMillisToTimestamp_zeroValue_returnsTimestampEpoch() { + val expr = unixMillisToTimestamp(constant(0L)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(Timestamp(0, 0)), "unixMillisToTimestamp(0L)") + } + + @Test + fun unixMillisToTimestamp_intType_returnsTimestamp() { + // C++ test uses 1000LL, which is 1 second + val expr = unixMillisToTimestamp(constant(1000L)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(Timestamp(1, 0)), "unixMillisToTimestamp(1000L)") + } + + @Test + fun unixMillisToTimestamp_longType_returnsTimestamp() { + // C++ test uses 9876543210LL millis + // 9876543210 / 1000 = 9876543 seconds + // 9876543210 % 1000 = 210 millis = 210000000 nanos + val expr = unixMillisToTimestamp(constant(9876543210L)) + val result = evaluate(expr) + assertEvaluatesTo( + result, + encodeValue(Timestamp(9876543, 210000000)), + "unixMillisToTimestamp(9876543210L)" + ) + } + + @Test + fun unixMillisToTimestamp_longTypeNegative_returnsTimestamp() { + // -10000 millis = -10 seconds + val expr = unixMillisToTimestamp(constant(-10000L)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(Timestamp(-10, 0)), "unixMillisToTimestamp(-10000L)") + } + + @Test + fun unixMillisToTimestamp_longTypeNegativeOverflow_returnsError() { + // Min representable timestamp: seconds=-62135596800, nanos=0 + // Corresponds to millis: -62135596800 * 1000 = -62135596800000 + val minMillis = -62135596800000L + + // Test the boundary value + val boundaryExpr = unixMillisToTimestamp(constant(minMillis)) + val boundaryResult = evaluate(boundaryExpr) + assertEvaluatesTo( + boundaryResult, + encodeValue(Timestamp(-62135596800L, 0)), + "unixMillisToTimestamp(minMillis)" + ) + + // Test value just below the boundary (minMillis - 1) + val belowMinExpr = unixMillisToTimestamp(constant(minMillis - 1)) + val belowMinResult = evaluate(belowMinExpr) + assertEvaluatesToError(belowMinResult, "unixMillisToTimestamp(minMillis - 1)") + } + + @Test + fun unixMillisToTimestamp_longTypePositiveOverflow_returnsError() { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + // Corresponds to millis: 253402300799 * 1000 + 999 (since nanos are truncated to millis) + // = 253402300799000 + 999 = 253402300799999 + val maxMillis = 253402300799999L + + // Test the boundary value + // Nanos are 999000000 because 999 millis * 1,000,000 = 999,000,000 nanos + val boundaryExpr = unixMillisToTimestamp(constant(maxMillis)) + val boundaryResult = evaluate(boundaryExpr) + assertEvaluatesTo( + boundaryResult, + encodeValue(Timestamp(253402300799L, 999000000)), // Nanos from 999 millis + "unixMillisToTimestamp(maxMillis)" + ) + + // Test value just above the boundary (maxMillis + 1) + val aboveMaxExpr = unixMillisToTimestamp(constant(maxMillis + 1)) + val aboveMaxResult = evaluate(aboveMaxExpr) + assertEvaluatesToError(aboveMaxResult, "unixMillisToTimestamp(maxMillis + 1)") + } + + // --- UnixSecondsToTimestamp Tests --- + + @Test + fun unixSecondsToTimestamp_stringType_returnsError() { + val expr = unixSecondsToTimestamp(constant("abc")) + val result = evaluate(expr) + assertEvaluatesToError(result, "unixSecondsToTimestamp(\"abc\")") + } + + @Test + fun unixSecondsToTimestamp_zeroValue_returnsTimestampEpoch() { + val expr = unixSecondsToTimestamp(constant(0L)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(Timestamp(0, 0)), "unixSecondsToTimestamp(0L)") + } + + @Test + fun unixSecondsToTimestamp_intType_returnsTimestamp() { + val expr = unixSecondsToTimestamp(constant(1L)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(Timestamp(1, 0)), "unixSecondsToTimestamp(1L)") + } + + @Test + fun unixSecondsToTimestamp_longType_returnsTimestamp() { + val expr = unixSecondsToTimestamp(constant(9876543210L)) + val result = evaluate(expr) + assertEvaluatesTo( + result, + encodeValue(Timestamp(9876543210L, 0)), + "unixSecondsToTimestamp(9876543210L)" + ) + } + + @Test + fun unixSecondsToTimestamp_longTypeNegative_returnsTimestamp() { + val expr = unixSecondsToTimestamp(constant(-10000L)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(Timestamp(-10000L, 0)), "unixSecondsToTimestamp(-10000L)") + } + + @Test + fun unixSecondsToTimestamp_longTypeNegativeOverflow_returnsError() { + // Min representable timestamp: seconds=-62135596800, nanos=0 + val minSeconds = -62135596800L + + // Test the boundary value + val boundaryExpr = unixSecondsToTimestamp(constant(minSeconds)) + val boundaryResult = evaluate(boundaryExpr) + assertEvaluatesTo( + boundaryResult, + encodeValue(Timestamp(minSeconds, 0)), + "unixSecondsToTimestamp(minSeconds)" + ) + + // Test value just below the boundary (minSeconds - 1) + val belowMinExpr = unixSecondsToTimestamp(constant(minSeconds - 1)) + val belowMinResult = evaluate(belowMinExpr) + assertEvaluatesToError(belowMinResult, "unixSecondsToTimestamp(minSeconds - 1)") + } + + @Test + fun unixSecondsToTimestamp_longTypePositiveOverflow_returnsError() { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + // For UnixSecondsToTimestamp, we only care about the seconds part for overflow. + val maxSeconds = 253402300799L + + // Test the boundary value + val boundaryExpr = unixSecondsToTimestamp(constant(maxSeconds)) + val boundaryResult = evaluate(boundaryExpr) + assertEvaluatesTo( + boundaryResult, + encodeValue(Timestamp(maxSeconds, 0)), + "unixSecondsToTimestamp(maxSeconds)" + ) + + // Test value just above the boundary (maxSeconds + 1) + val aboveMaxExpr = unixSecondsToTimestamp(constant(maxSeconds + 1)) + val aboveMaxResult = evaluate(aboveMaxExpr) + assertEvaluatesToError(aboveMaxResult, "unixSecondsToTimestamp(maxSeconds + 1)") + } + + // --- TimestampToUnixMicros Tests --- + + @Test + fun timestampToUnixMicros_nonTimestampType_returnsError() { + val expr = timestampToUnixMicros(constant(123L)) + val result = evaluate(expr) + assertEvaluatesToError(result, "timestampToUnixMicros(123L)") + } + + @Test + fun timestampToUnixMicros_timestamp_returnsMicros() { + val ts = Timestamp(347068800, 0) // March 1, 1981 00:00:00 UTC + val expr = timestampToUnixMicros(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo( + result, + encodeValue(347068800000000L), + "timestampToUnixMicros(Timestamp(347068800, 0))" + ) + } + + @Test + fun timestampToUnixMicros_epochTimestamp_returnsMicros() { + val ts = Timestamp(0, 0) + val expr = timestampToUnixMicros(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(0L), "timestampToUnixMicros(Timestamp(0, 0))") + } + + @Test + fun timestampToUnixMicros_currentTimestamp_returnsMicros() { + // Example: March 15, 2023 12:00:00.123456 UTC + val ts = Timestamp(1678886400, 123456000) + val expectedMicros = 1678886400L * 1000000L + 123456L + val expr = timestampToUnixMicros(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo( + result, + encodeValue(expectedMicros), + "timestampToUnixMicros(Timestamp(1678886400, 123456000))" + ) + } + + @Test + fun timestampToUnixMicros_maxTimestamp_returnsMicros() { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + val maxTs = Timestamp(253402300799L, 999999999) + // Expected micros: 253402300799 * 1,000,000 + 999999 (nanos truncated to micros) + val expectedMicros = 253402300799L * 1000000L + 999999L + val expr = timestampToUnixMicros(constant(maxTs)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(expectedMicros), "timestampToUnixMicros(maxTimestamp)") + } + + @Test + fun timestampToUnixMicros_minTimestamp_returnsMicros() { + // Min representable timestamp: seconds=-62135596800, nanos=0 + val minTs = Timestamp(-62135596800L, 0) + // Expected micros: -62135596800 * 1,000,000 = -62135596800000000 + val expectedMicros = -62135596800L * 1000000L + val expr = timestampToUnixMicros(constant(minTs)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(expectedMicros), "timestampToUnixMicros(minTimestamp)") + } + + @Test + fun timestampToUnixMicros_timestampTruncatesToMicros() { + // Timestamp: seconds=-1, nanos=999999999 (which is 999999.999 micros) + // Expected Micros: -1 * 1,000,000 + 999999 = -1 + val ts = Timestamp(-1, 999999999) + val expr = timestampToUnixMicros(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(-1L), "timestampToUnixMicros(Timestamp(-1, 999999999))") + } + + // --- TimestampToUnixMillis Tests --- + + @Test + fun timestampToUnixMillis_nonTimestampType_returnsError() { + val expr = timestampToUnixMillis(constant(123L)) + val result = evaluate(expr) + assertEvaluatesToError(result, "timestampToUnixMillis(123L)") + } + + @Test + fun timestampToUnixMillis_timestamp_returnsMillis() { + val ts = Timestamp(347068800, 0) // March 1, 1981 00:00:00 UTC + val expr = timestampToUnixMillis(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo( + result, + encodeValue(347068800000L), + "timestampToUnixMillis(Timestamp(347068800, 0))" + ) + } + + @Test + fun timestampToUnixMillis_epochTimestamp_returnsMillis() { + val ts = Timestamp(0, 0) + val expr = timestampToUnixMillis(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(0L), "timestampToUnixMillis(Timestamp(0, 0))") + } + + @Test + fun timestampToUnixMillis_currentTimestamp_returnsMillis() { + // Example: March 15, 2023 12:00:00.123 UTC + val ts = Timestamp(1678886400, 123000000) + val expectedMillis = 1678886400L * 1000L + 123L + val expr = timestampToUnixMillis(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo( + result, + encodeValue(expectedMillis), + "timestampToUnixMillis(Timestamp(1678886400, 123000000))" + ) + } + + @Test + fun timestampToUnixMillis_maxTimestamp_returnsMillis() { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + // Millis calculation truncates nanos part: 999999999 / 1,000,000 = 999 + val maxTs = Timestamp(253402300799L, 999000000) // Nanos for 999ms + val expectedMillis = 253402300799L * 1000L + 999L + val expr = timestampToUnixMillis(constant(maxTs)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(expectedMillis), "timestampToUnixMillis(maxTimestamp)") + } + + @Test + fun timestampToUnixMillis_minTimestamp_returnsMillis() { + // Min representable timestamp: seconds=-62135596800, nanos=0 + val minTs = Timestamp(-62135596800L, 0) + val expectedMillis = -62135596800L * 1000L + val expr = timestampToUnixMillis(constant(minTs)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(expectedMillis), "timestampToUnixMillis(minTimestamp)") + } + + @Test + fun timestampToUnixMillis_timestampTruncatesToMillis() { + // Timestamp: seconds=-1, nanos=999999999 (which is 999.999999 ms) + // Expected Millis: -1 * 1000 + 999 = -1 + val ts = Timestamp(-1, 999999999) + val expr = timestampToUnixMillis(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(-1L), "timestampToUnixMillis(Timestamp(-1, 999999999))") + } + + // --- TimestampToUnixSeconds Tests --- + + @Test + fun timestampToUnixSeconds_nonTimestampType_returnsError() { + val expr = timestampToUnixSeconds(constant(123L)) + val result = evaluate(expr) + assertEvaluatesToError(result, "timestampToUnixSeconds(123L)") + } + + @Test + fun timestampToUnixSeconds_timestamp_returnsSeconds() { + val ts = Timestamp(347068800, 0) // March 1, 1981 00:00:00 UTC + val expr = timestampToUnixSeconds(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo( + result, + encodeValue(347068800L), + "timestampToUnixSeconds(Timestamp(347068800, 0))" + ) + } + + @Test + fun timestampToUnixSeconds_epochTimestamp_returnsSeconds() { + val ts = Timestamp(0, 0) + val expr = timestampToUnixSeconds(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(0L), "timestampToUnixSeconds(Timestamp(0, 0))") + } + + @Test + fun timestampToUnixSeconds_currentTimestamp_returnsSeconds() { + // Example: March 15, 2023 12:00:00.123456789 UTC + val ts = Timestamp(1678886400, 123456789) + val expectedSeconds = 1678886400L // Nanos are truncated + val expr = timestampToUnixSeconds(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo( + result, + encodeValue(expectedSeconds), + "timestampToUnixSeconds(Timestamp(1678886400, 123456789))" + ) + } + + @Test + fun timestampToUnixSeconds_maxTimestamp_returnsSeconds() { + // Max representable timestamp: seconds=253402300799, nanos=999999999 + val maxTs = Timestamp(253402300799L, 999999999) + val expectedSeconds = 253402300799L + val expr = timestampToUnixSeconds(constant(maxTs)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(expectedSeconds), "timestampToUnixSeconds(maxTimestamp)") + } + + @Test + fun timestampToUnixSeconds_minTimestamp_returnsSeconds() { + // Min representable timestamp: seconds=-62135596800, nanos=0 + val minTs = Timestamp(-62135596800L, 0) + val expectedSeconds = -62135596800L + val expr = timestampToUnixSeconds(constant(minTs)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(expectedSeconds), "timestampToUnixSeconds(minTimestamp)") + } + + @Test + fun timestampToUnixSeconds_timestampTruncatesToSeconds() { + // Timestamp: seconds=-1, nanos=999999999 + // Expected Seconds: -1 + val ts = Timestamp(-1, 999999999) + val expr = timestampToUnixSeconds(constant(ts)) + val result = evaluate(expr) + assertEvaluatesTo(result, encodeValue(-1L), "timestampToUnixSeconds(Timestamp(-1, 999999999))") + } + + // --- TimestampAdd Tests --- + // Note: The C++ tests use SharedConstant(nullptr) for null values. + // In Kotlin, we'll use `nullValue()` or `constant(null)` where appropriate, + // and `assertEvaluatesToNull` for checking null results. + + @Test + fun timestampAdd_timestampAddStringType_returnsError() { + val expr = timestampAdd(constant("abc"), constant("second"), constant(1L)) + assertEvaluatesToError(evaluate(expr), "timestampAdd(string, \"second\", 1L)") + } + + @Test + fun timestampAdd_zeroValue_returnsTimestampEpoch() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), constant("second"), constant(0L)) + assertEvaluatesTo(evaluate(expr), encodeValue(epoch), "timestampAdd(epoch, \"second\", 0L)") + } + + @Test + fun timestampAdd_intType_returnsTimestamp() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), constant("second"), constant(1L)) + assertEvaluatesTo( + evaluate(expr), + encodeValue(Timestamp(1, 0)), + "timestampAdd(epoch, \"second\", 1L)" + ) + } + + @Test + fun timestampAdd_longType_returnsTimestamp() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), constant("second"), constant(9876543210L)) + assertEvaluatesTo( + evaluate(expr), + encodeValue(Timestamp(9876543210L, 0)), + "timestampAdd(epoch, \"second\", 9876543210L)" + ) + } + + @Test + fun timestampAdd_longTypeNegative_returnsTimestamp() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), constant("second"), constant(-10000L)) + assertEvaluatesTo( + evaluate(expr), + encodeValue(Timestamp(-10000L, 0)), + "timestampAdd(epoch, \"second\", -10000L)" + ) + } + + @Test + fun timestampAdd_longTypeNegativeOverflow_returnsError() { + val minTs = Timestamp(-62135596800L, 0) // Min Firestore seconds + + // Test adding 0 (boundary) + val exprBoundary = timestampAdd(constant(minTs), constant("second"), constant(0L)) + assertEvaluatesTo( + evaluate(exprBoundary), + encodeValue(minTs), + "timestampAdd(minTs, \"second\", 0L)" + ) + + // Test adding -1 second (overflow) + val exprOverflow = timestampAdd(constant(minTs), constant("second"), constant(-1L)) + assertEvaluatesToError(evaluate(exprOverflow), "timestampAdd(minTs, \"second\", -1L)") + } + + @Test + fun timestampAdd_longTypePositiveOverflow_returnsError() { + // Max Firestore timestamp: seconds=253402300799, nanos=999999999 + // Use nanos that are multiple of 1000 for microsecond precision test + val maxTs = Timestamp(253402300799L, 999999000) + + // Test adding 0 microsecond (boundary) + val exprBoundary = timestampAdd(constant(maxTs), constant("microsecond"), constant(0L)) + assertEvaluatesTo( + evaluate(exprBoundary), + encodeValue(maxTs), + "timestampAdd(maxTs, \"microsecond\", 0L)" + ) + + // Test adding 1 microsecond (should overflow because maxTs.nanos + 1000 > 999999999) + // Max nanos is 999,999,999. maxTs has 999,999,000. Adding 1 micro (1000 nanos) + // would result in 1,000,999,000 nanos, which should carry over to seconds and overflow. + val exprOverflowMicro = timestampAdd(constant(maxTs), constant("microsecond"), constant(1L)) + assertEvaluatesToError(evaluate(exprOverflowMicro), "timestampAdd(maxTs, \"microsecond\", 1L)") + + // Test adding 1 second to a timestamp at max seconds but zero nanos + val nearMaxSecTs = Timestamp(253402300799L, 0) + val exprNearMaxBoundary = timestampAdd(constant(nearMaxSecTs), constant("second"), constant(0L)) + assertEvaluatesTo( + evaluate(exprNearMaxBoundary), + encodeValue(nearMaxSecTs), + "timestampAdd(nearMaxSecTs, \"second\", 0L)" + ) + + val exprNearMaxOverflow = timestampAdd(constant(nearMaxSecTs), constant("second"), constant(1L)) + assertEvaluatesToError( + evaluate(exprNearMaxOverflow), + "timestampAdd(nearMaxSecTs, \"second\", 1L)" + ) + } + + @Test + fun timestampAdd_longTypeMinute_returnsTimestamp() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), constant("minute"), constant(1L)) + assertEvaluatesTo( + evaluate(expr), + encodeValue(Timestamp(60, 0)), + "timestampAdd(epoch, \"minute\", 1L)" + ) + } + + @Test + fun timestampAdd_longTypeHour_returnsTimestamp() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), constant("hour"), constant(1L)) + assertEvaluatesTo( + evaluate(expr), + encodeValue(Timestamp(3600, 0)), + "timestampAdd(epoch, \"hour\", 1L)" + ) + } + + @Test + fun timestampAdd_longTypeDay_returnsTimestamp() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), constant("day"), constant(1L)) + assertEvaluatesTo( + evaluate(expr), + encodeValue(Timestamp(86400, 0)), + "timestampAdd(epoch, \"day\", 1L)" + ) + } + + @Test + fun timestampAdd_longTypeMillisecond_returnsTimestamp() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), constant("millisecond"), constant(1L)) + assertEvaluatesTo( + evaluate(expr), + encodeValue(Timestamp(0, 1000000)), + "timestampAdd(epoch, \"millisecond\", 1L)" + ) + } + + @Test + fun timestampAdd_longTypeMicrosecond_returnsTimestamp() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), constant("microsecond"), constant(1L)) + assertEvaluatesTo( + evaluate(expr), + encodeValue(Timestamp(0, 1000)), + "timestampAdd(epoch, \"microsecond\", 1L)" + ) + } + + @Test + fun timestampAdd_invalidTimeUnit_returnsError() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), constant("abc"), constant(1L)) + assertEvaluatesToError(evaluate(expr), "timestampAdd(epoch, \"abc\", 1L)") + } + + @Test + fun timestampAdd_invalidAmount_returnsError() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), constant("second"), constant("abc")) + assertEvaluatesToError(evaluate(expr), "timestampAdd(epoch, \"second\", \"abc\")") + } + + @Test + fun timestampAdd_nullAmount_returnsNull() { + val epoch = Timestamp(0, 0) + // C++ uses SharedConstant(nullptr). In Kotlin, this translates to `nullValue()` for an + // expression + // or `constant(null)` if the constant itself is null. + // `evaluateTimestampAdd` expects the amount to be a number. If it's null, it should error. + // However, if the *expression* for amount evaluates to null (e.g. field that is null), + // then the C++ test `ReturnsNull()` implies the operation results in SQL NULL. + // Let's assume `constant(nullValue())` represents a SQL NULL value. + val expr = timestampAdd(constant(epoch), constant("second"), nullValue()) + assertEvaluatesToNull(evaluate(expr), "timestampAdd(epoch, \"second\", nullValue())") + } + + @Test + fun timestampAdd_nullTimeUnit_returnsError() { + val epoch = Timestamp(0, 0) + val expr = timestampAdd(constant(epoch), nullValue(), constant(1L)) + assertEvaluatesToError(evaluate(expr), "timestampAdd(epoch, nullValue(), 1L)") + } + + @Test + fun timestampAdd_nullTimestamp_returnsNull() { + val expr = timestampAdd(nullValue(), constant("second"), constant(1L)) + assertEvaluatesToNull(evaluate(expr), "timestampAdd(nullValue(), \"second\", 1L)") + } +} From f5fdcf67c92e60f13c50074b07cd1835f9035b0e Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 2 Jun 2025 17:31:13 -0400 Subject: [PATCH 094/152] Test offline mirroring semantics --- firebase-firestore/firebase-firestore.gradle | 1 + .../pipeline/MirroringSemanticsTests.kt | 193 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MirroringSemanticsTests.kt diff --git a/firebase-firestore/firebase-firestore.gradle b/firebase-firestore/firebase-firestore.gradle index 806babf6236..7a3871fb5a8 100644 --- a/firebase-firestore/firebase-firestore.gradle +++ b/firebase-firestore/firebase-firestore.gradle @@ -142,6 +142,7 @@ dependencies { implementation libs.grpc.stub implementation libs.kotlin.stdlib implementation libs.kotlinx.coroutines.core + implementation 'com.google.re2j:re2j:1.6' compileOnly libs.autovalue.annotations compileOnly libs.javax.annotation.jsr250 diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MirroringSemanticsTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MirroringSemanticsTests.kt new file mode 100644 index 00000000000..37bf0ed1246 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MirroringSemanticsTests.kt @@ -0,0 +1,193 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firebase.firestore.pipeline.Expr.Companion.add +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContains +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContainsAll +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContainsAny +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayLength +import com.google.firebase.firestore.pipeline.Expr.Companion.byteLength +import com.google.firebase.firestore.pipeline.Expr.Companion.charLength +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.divide +import com.google.firebase.firestore.pipeline.Expr.Companion.endsWith +import com.google.firebase.firestore.pipeline.Expr.Companion.eq +import com.google.firebase.firestore.pipeline.Expr.Companion.eqAny +import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.pipeline.Expr.Companion.gt +import com.google.firebase.firestore.pipeline.Expr.Companion.gte +import com.google.firebase.firestore.pipeline.Expr.Companion.isNan +import com.google.firebase.firestore.pipeline.Expr.Companion.isNotNan +import com.google.firebase.firestore.pipeline.Expr.Companion.like +import com.google.firebase.firestore.pipeline.Expr.Companion.lt +import com.google.firebase.firestore.pipeline.Expr.Companion.lte +import com.google.firebase.firestore.pipeline.Expr.Companion.mod +import com.google.firebase.firestore.pipeline.Expr.Companion.multiply +import com.google.firebase.firestore.pipeline.Expr.Companion.neq +import com.google.firebase.firestore.pipeline.Expr.Companion.notEqAny +import com.google.firebase.firestore.pipeline.Expr.Companion.nullValue +import com.google.firebase.firestore.pipeline.Expr.Companion.regexContains +import com.google.firebase.firestore.pipeline.Expr.Companion.regexMatch +import com.google.firebase.firestore.pipeline.Expr.Companion.reverse +import com.google.firebase.firestore.pipeline.Expr.Companion.startsWith +import com.google.firebase.firestore.pipeline.Expr.Companion.strConcat +import com.google.firebase.firestore.pipeline.Expr.Companion.strContains +import com.google.firebase.firestore.pipeline.Expr.Companion.subtract +import com.google.firebase.firestore.pipeline.Expr.Companion.timestampToUnixMicros +import com.google.firebase.firestore.pipeline.Expr.Companion.timestampToUnixMillis +import com.google.firebase.firestore.pipeline.Expr.Companion.timestampToUnixSeconds +import com.google.firebase.firestore.pipeline.Expr.Companion.toLower +import com.google.firebase.firestore.pipeline.Expr.Companion.toUpper +import com.google.firebase.firestore.pipeline.Expr.Companion.trim +import com.google.firebase.firestore.pipeline.Expr.Companion.unixMicrosToTimestamp +import com.google.firebase.firestore.pipeline.Expr.Companion.unixMillisToTimestamp +import com.google.firebase.firestore.pipeline.Expr.Companion.unixSecondsToTimestamp +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class MirroringSemanticsTests { + + private val NULL_INPUT = nullValue() + // Error: Integer division by zero + private val ERROR_INPUT = divide(constant(1L), constant(0L)) + // Unset: Field that doesn't exist in the default test document + private val UNSET_INPUT = field("non-existent-field") + // Valid: A simple valid input for binary tests + private val VALID_INPUT = constant(42L) + + private enum class ExpectedOutcome { + NULL, + ERROR + } + + private data class UnaryTestCase( + val inputExpr: Expr, + val expectedOutcome: ExpectedOutcome, + val description: String + ) + + private data class BinaryTestCase( + val left: Expr, + val right: Expr, + val expectedOutcome: ExpectedOutcome, + val description: String + ) + + @Test + fun `unary function input mirroring`() { + val unaryFunctionBuilders = + listOf Expr>>( + "isNan" to { v -> isNan(v) }, + "isNotNan" to { v -> isNotNan(v) }, + "arrayLength" to { v -> arrayLength(v) }, + "reverse" to { v -> reverse(v) }, + "charLength" to { v -> charLength(v) }, + "byteLength" to { v -> byteLength(v) }, + "toLower" to { v -> toLower(v) }, + "toUpper" to { v -> toUpper(v) }, + "trim" to { v -> trim(v) }, + "unixMicrosToTimestamp" to { v -> unixMicrosToTimestamp(v) }, + "timestampToUnixMicros" to { v -> timestampToUnixMicros(v) }, + "unixMillisToTimestamp" to { v -> unixMillisToTimestamp(v) }, + "timestampToUnixMillis" to { v -> timestampToUnixMillis(v) }, + "unixSecondsToTimestamp" to { v -> unixSecondsToTimestamp(v) }, + "timestampToUnixSeconds" to { v -> timestampToUnixSeconds(v) } + ) + + val testCases = + listOf( + UnaryTestCase(NULL_INPUT, ExpectedOutcome.NULL, "NULL"), + UnaryTestCase(ERROR_INPUT, ExpectedOutcome.ERROR, "ERROR"), + // Unary ops expect resolved args, so UNSET should lead to an error during evaluation. + UnaryTestCase(UNSET_INPUT, ExpectedOutcome.ERROR, "UNSET") + ) + + for ((funcName, builder) in unaryFunctionBuilders) { + for (testCase in testCases) { + val exprToEvaluate = builder(testCase.inputExpr) + val result = evaluate(exprToEvaluate) // Assumes default document context + + when (testCase.expectedOutcome) { + ExpectedOutcome.NULL -> + assertEvaluatesToNull(result, "Function: %s, Input: %s", funcName, testCase.description) + ExpectedOutcome.ERROR -> + assertEvaluatesToError( + result, + "Function: %s, Input: %s", + funcName, + testCase.description + ) + } + } + } + } + + @Test + fun `binary function input mirroring`() { + val binaryFunctionBuilders = + listOf Expr>>( + // Arithmetic (Variadic, base is binary) + "add" to { v1, v2 -> add(v1, v2) }, + "subtract" to { v1, v2 -> subtract(v1, v2) }, + "multiply" to { v1, v2 -> multiply(v1, v2) }, + "divide" to { v1, v2 -> divide(v1, v2) }, + "mod" to { v1, v2 -> mod(v1, v2) }, + // Comparison + "eq" to { v1, v2 -> eq(v1, v2) }, + "neq" to { v1, v2 -> neq(v1, v2) }, + "lt" to { v1, v2 -> lt(v1, v2) }, + "lte" to { v1, v2 -> lte(v1, v2) }, + "gt" to { v1, v2 -> gt(v1, v2) }, + "gte" to { v1, v2 -> gte(v1, v2) }, + // Array + "arrayContains" to { v1, v2 -> arrayContains(v1, v2) }, + "arrayContainsAll" to { v1, v2 -> arrayContainsAll(v1, v2) }, + "arrayContainsAny" to { v1, v2 -> arrayContainsAny(v1, v2) }, + "eqAny" to { v1, v2 -> eqAny(v1, v2) }, // Maps to EqAnyExpr + "notEqAny" to { v1, v2 -> notEqAny(v1, v2) }, // Maps to NotEqAnyExpr + // String + "like" to { v1, v2 -> like(v1, v2) }, + "regexContains" to { v1, v2 -> regexContains(v1, v2) }, + "regexMatch" to { v1, v2 -> regexMatch(v1, v2) }, + "strContains" to { v1, v2 -> strContains(v1, v2) }, // Maps to StrContainsExpr + "startsWith" to { v1, v2 -> startsWith(v1, v2) }, + "endsWith" to { v1, v2 -> endsWith(v1, v2) }, + "strConcat" to { v1, v2 -> strConcat(v1, v2) } // Maps to StrConcatExpr + // TODO(b/351084804): mapGet is not implemented yet + ) + + val testCases = + listOf( + // Rule 1: NULL, NULL -> NULL (for most ops, some like eq(NULL,NULL) might be NULL) + BinaryTestCase(NULL_INPUT, NULL_INPUT, ExpectedOutcome.NULL, "NULL, NULL -> NULL"), + // Rule 2: Error/Unset propagation + BinaryTestCase(NULL_INPUT, ERROR_INPUT, ExpectedOutcome.ERROR, "NULL, ERROR -> ERROR"), + BinaryTestCase(ERROR_INPUT, NULL_INPUT, ExpectedOutcome.ERROR, "ERROR, NULL -> ERROR"), + BinaryTestCase(NULL_INPUT, UNSET_INPUT, ExpectedOutcome.ERROR, "NULL, UNSET -> ERROR"), + BinaryTestCase(UNSET_INPUT, NULL_INPUT, ExpectedOutcome.ERROR, "UNSET, NULL -> ERROR"), + BinaryTestCase(ERROR_INPUT, ERROR_INPUT, ExpectedOutcome.ERROR, "ERROR, ERROR -> ERROR"), + BinaryTestCase(ERROR_INPUT, UNSET_INPUT, ExpectedOutcome.ERROR, "ERROR, UNSET -> ERROR"), + BinaryTestCase(UNSET_INPUT, ERROR_INPUT, ExpectedOutcome.ERROR, "UNSET, ERROR -> ERROR"), + BinaryTestCase(UNSET_INPUT, UNSET_INPUT, ExpectedOutcome.ERROR, "UNSET, UNSET -> ERROR"), + BinaryTestCase(VALID_INPUT, ERROR_INPUT, ExpectedOutcome.ERROR, "VALID, ERROR -> ERROR"), + BinaryTestCase(ERROR_INPUT, VALID_INPUT, ExpectedOutcome.ERROR, "ERROR, VALID -> ERROR"), + BinaryTestCase(VALID_INPUT, UNSET_INPUT, ExpectedOutcome.ERROR, "VALID, UNSET -> ERROR"), + BinaryTestCase(UNSET_INPUT, VALID_INPUT, ExpectedOutcome.ERROR, "UNSET, VALID -> ERROR") + ) + + for ((funcName, builder) in binaryFunctionBuilders) { + for (testCase in testCases) { + val exprToEvaluate = builder(testCase.left, testCase.right) + val result = evaluate(exprToEvaluate) // Assumes default document context + + when (testCase.expectedOutcome) { + ExpectedOutcome.NULL -> + assertEvaluatesToNull(result, "Function: %s, Case: %s", funcName, testCase.description) + ExpectedOutcome.ERROR -> + assertEvaluatesToError(result, "Function: %s, Case: %s", funcName, testCase.description) + } + } + } + } +} From 5b822c7da5fbcb14ff9694bf20aa7c3265fc8d6a Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 2 Jun 2025 18:03:00 -0400 Subject: [PATCH 095/152] Add realtime tests for mapGet. Fixup implementation. --- .../firebase/firestore/pipeline/evaluation.kt | 18 +++++++ .../firestore/pipeline/expressions.kt | 26 +++++++++- .../firebase/firestore/pipeline/MapTests.kt | 50 +++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MapTests.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index 1aa91595236..d4c801a6467 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -359,6 +359,12 @@ private fun notEqAny(value: Value, list: List): EvaluateResult { return if (foundNull) EvaluateResult.NULL else EvaluateResult.TRUE } +// === Map Functions === + +internal val evaluateMapGet = binaryFunction { map: Map, key: String -> + EvaluateResultValue(map[key] ?: return@binaryFunction EvaluateResultUnset) +} + // === String Functions === internal val evaluateStrConcat = variadicFunction { strings: List -> @@ -741,6 +747,18 @@ private inline fun binaryFunction( } } +@JvmName("binaryMapStringFunction") +private inline fun binaryFunction( + crossinline function: (Map, String) -> EvaluateResult +): EvaluateFunction = + binaryFunctionType( + ValueTypeCase.MAP_VALUE, + { v: Value -> v.mapValue.fieldsMap }, + ValueTypeCase.STRING_VALUE, + Value::getStringValue, + function + ) + @JvmName("binaryValueArrayFunction") private inline fun binaryFunction( crossinline function: (Value, List) -> EvaluateResult 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 567ffac126a..46d90812cb3 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 @@ -1821,7 +1821,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapGet(mapExpression: Expr, key: String): Expr = - FunctionExpr("map_get", notImplemented, mapExpression, key) + FunctionExpr("map_get", evaluateMapGet, mapExpression, key) /** * Accesses a value from a map (object) field using the provided [key]. @@ -1832,7 +1832,29 @@ abstract class Expr internal constructor() { */ @JvmStatic fun mapGet(fieldName: String, key: String): Expr = - FunctionExpr("map_get", notImplemented, fieldName, key) + FunctionExpr("map_get", evaluateMapGet, fieldName, key) + + /** + * Accesses a value from a map (object) field using the provided [keyExpression]. + * + * @param mapExpression The expression representing the map. + * @param keyExpression The key to access in the map. + * @return A new [Expr] representing the value associated with the given key in the map. + */ + @JvmStatic + fun mapGet(mapExpression: Expr, keyExpression: Expr): Expr = + FunctionExpr("map_get", evaluateMapGet, mapExpression, keyExpression) + + /** + * Accesses a value from a map (object) field using the provided [keyExpression]. + * + * @param fieldName The field name of the map field. + * @param keyExpression The key to access in the map. + * @return A new [Expr] representing the value associated with the given key in the map. + */ + @JvmStatic + fun mapGet(fieldName: String, keyExpression: Expr): Expr = + FunctionExpr("map_get", evaluateMapGet, fieldName, keyExpression) /** * Creates an expression that merges multiple maps into a single map. If multiple maps have the diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MapTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MapTests.kt new file mode 100644 index 00000000000..c741b511f07 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MapTests.kt @@ -0,0 +1,50 @@ +package com.google.firebase.firestore.pipeline + +import com.google.firebase.firestore.model.Values.encodeValue +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.map +import com.google.firebase.firestore.pipeline.Expr.Companion.mapGet +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MapTests { + + @Test + fun `mapGet - get existing key returns value`() { + val mapExpr = map(mapOf("a" to 1L, "b" to 2L, "c" to 3L)) + val expr = mapGet(mapExpr, constant("b")) + assertEvaluatesTo(evaluate(expr), encodeValue(2L), "mapGet existing key should return value") + } + + @Test + fun `mapGet - get missing key returns unset`() { + val mapExpr = map(mapOf("a" to 1L, "b" to 2L, "c" to 3L)) + val expr = mapGet(mapExpr, "d") + assertEvaluatesToUnset(evaluate(expr), "mapGet missing key should return unset") + } + + @Test + fun `mapGet - get from empty map returns unset`() { + val mapExpr = map(emptyMap()) + val expr = mapGet(mapExpr, "d") + assertEvaluatesToUnset(evaluate(expr), "mapGet from empty map should return unset") + } + + @Test + fun `mapGet - wrong map type returns error`() { + val mapExpr = constant("not a map") // Pass a string instead of a map + val expr = mapGet(mapExpr, "d") + // This should evaluate to an error because the first argument is not a map. + assertEvaluatesToError(evaluate(expr), "mapGet with wrong map type should return error") + } + + @Test + fun `mapGet - wrong key type returns error`() { + val mapExpr = map(emptyMap()) + val expr = mapGet(mapExpr, constant(false)) + // This should evaluate to an error because the key argument is not a string. + assertEvaluatesToError(evaluate(expr), "mapGet with wrong key type should return error") + } +} From 5c8f2ce4daf3425b65a57f249e4c77d27c731886 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 3 Jun 2025 12:05:43 -0400 Subject: [PATCH 096/152] Comments --- .../firebase/firestore/pipeline/evaluation.kt | 370 +++++++++++++++--- 1 file changed, 315 insertions(+), 55 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index d4c801a6467..b3499e2b919 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -617,6 +617,11 @@ private inline fun catch(f: () -> EvaluateResult): EvaluateResult = EvaluateResultError } +/** + * Basic Unary Function + * - Validates there is exactly 1 parameter. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun unaryFunction( crossinline function: (EvaluateResult) -> EvaluateResult ): EvaluateFunction = { params -> @@ -626,6 +631,13 @@ private inline fun unaryFunction( { input: MutableDocument -> catch { function(p(input)) } } } +/** + * Unary Value Function + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("unaryValueFunction") private inline fun unaryFunction( crossinline function: (Value) -> EvaluateResult @@ -635,44 +647,97 @@ private inline fun unaryFunction( else if (v.hasNullValue()) EvaluateResult.NULL else function(v) } +/** + * Unary Boolean Function + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - Extracts Boolean for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("unaryBooleanFunction") -private inline fun unaryFunction(crossinline stringOp: (Boolean) -> EvaluateResult) = +private inline fun unaryFunction(crossinline function: (Boolean) -> EvaluateResult) = unaryFunctionType( ValueTypeCase.BOOLEAN_VALUE, Value::getBooleanValue, - stringOp, + function, ) +/** + * Unary String Function that wraps the String result + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - Extracts Boolean for [function] evaluation. + * - Wraps the primitive String result as [EvaluateResult]. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("unaryStringFunctionPrimitive") -private inline fun unaryFunctionPrimitive(crossinline stringOp: (String) -> String) = - unaryFunction { s: String -> - EvaluateResult.string(stringOp(s)) - } - +private inline fun unaryFunctionPrimitive(crossinline function: (String) -> String) = + unaryFunction { s: String -> EvaluateResult.string(function(s)) } + +/** + * Unary String Function + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - Extracts String for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("unaryStringFunction") -private inline fun unaryFunction(crossinline stringOp: (String) -> EvaluateResult) = +private inline fun unaryFunction(crossinline function: (String) -> EvaluateResult) = unaryFunctionType( ValueTypeCase.STRING_VALUE, Value::getStringValue, - stringOp, + function, ) +/** + * Unary String Function + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - Extracts String for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("unaryLongFunction") -private inline fun unaryFunction(crossinline longOp: (Long) -> EvaluateResult) = +private inline fun unaryFunction(crossinline function: (Long) -> EvaluateResult) = unaryFunctionType( ValueTypeCase.INTEGER_VALUE, Value::getIntegerValue, - longOp, + function, ) +/** + * Unary Timestamp Function + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - Extracts Timestamp for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("unaryTimestampFunction") -private inline fun unaryFunction(crossinline timestampOp: (Timestamp) -> EvaluateResult) = +private inline fun unaryFunction(crossinline function: (Timestamp) -> EvaluateResult) = unaryFunctionType( ValueTypeCase.TIMESTAMP_VALUE, Value::getTimestampValue, - timestampOp, + function, ) +/** + * Unary Timestamp Function + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value], however NULL [Value]s can appear inside of array. + * - Extracts Timestamp from [Value] for evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("unaryArrayFunction") private inline fun unaryFunction(crossinline longOp: (List) -> EvaluateResult) = unaryFunctionType( @@ -681,6 +746,16 @@ private inline fun unaryFunction(crossinline longOp: (List) -> EvaluateRe longOp, ) +/** + * Unary Bytes/String Function + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - Depending on [Value] type, either the Timestamp or String is extracted and evaluated by + * either [byteOp] or [stringOp]. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun unaryFunction( crossinline byteOp: (ByteString) -> EvaluateResult, crossinline stringOp: (String) -> EvaluateResult @@ -694,6 +769,15 @@ private inline fun unaryFunction( stringOp, ) +/** + * For building type specific Unary Functions + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - If [Value] type is [valueTypeCase] then use [valueExtractor] for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun unaryFunctionType( valueTypeCase: ValueTypeCase, crossinline valueExtractor: (Value) -> T, @@ -709,6 +793,16 @@ private inline fun unaryFunctionType( } } +/** + * For building type specific Unary Functions that can have 2 possible types. + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - If [Value] type is [valueTypeCase1] then use [valueExtractor1] for [function1] evaluation. + * - If [Value] type is [valueTypeCase2] then use [valueExtractor2] for [function2] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun unaryFunctionType( valueTypeCase1: ValueTypeCase, crossinline valueExtractor1: (Value) -> T1, @@ -731,6 +825,13 @@ private inline fun unaryFunctionType( } } +/** + * Binary (Value, Value) Function + * - Validates there is exactly 2 parameters. + * - First, short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value]. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("binaryValueValueFunction") private inline fun binaryFunction( crossinline function: (Value, Value) -> EvaluateResult @@ -747,6 +848,15 @@ private inline fun binaryFunction( } } +/** + * Binary (Map, String) Function + * - Validates there is exactly 2 parameters. + * - First, short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can appear inside of Map. + * - Extracts Map and String for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("binaryMapStringFunction") private inline fun binaryFunction( crossinline function: (Map, String) -> EvaluateResult @@ -759,6 +869,15 @@ private inline fun binaryFunction( function ) +/** + * Binary (Value, Array) Function + * - Validates there is exactly 2 parameters. + * - First, short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can appear inside of Array. + * - Extracts Value and Array for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("binaryValueArrayFunction") private inline fun binaryFunction( crossinline function: (Value, List) -> EvaluateResult @@ -766,6 +885,15 @@ private inline fun binaryFunction( if (v2.hasArrayValue()) function(v1, v2.arrayValue.valuesList) else EvaluateResultError } +/** + * Binary (Array, Value) Function + * - Validates there is exactly 2 parameters. + * - First, short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can appear inside of Array. + * - Extracts Array and Value for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("binaryArrayValueFunction") private inline fun binaryFunction( crossinline function: (List, Value) -> EvaluateResult @@ -773,6 +901,15 @@ private inline fun binaryFunction( if (v1.hasArrayValue()) function(v1.arrayValue.valuesList, v2) else EvaluateResultError } +/** + * Binary (String, String) Function + * - Validates there is exactly 2 parameters. + * - First, short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value]. + * - Extracts String and String for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("binaryStringStringFunction") private inline fun binaryFunction(crossinline function: (String, String) -> EvaluateResult) = binaryFunctionType( @@ -783,6 +920,16 @@ private inline fun binaryFunction(crossinline function: (String, String) -> Eval function ) +/** + * For building binary functions that perform Regex evaluation. + * - Separates the Regex compilation via [patternConstructor] from the [function] evaluation. + * - Caches previously seen Regex to avoid compilation overhead. + * - First, short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value]. + * - Extracts String and Regex via [patternConstructor] for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("binaryStringPatternConstructorFunction") private inline fun binaryPatternConstructorFunction( crossinline patternConstructor: (String) -> Pattern?, @@ -801,6 +948,16 @@ private inline fun binaryPatternConstructorFunction( }) } +/** + * Binary (String, Regex from String) Function + * - Validates there is exactly 2 parameters. + * - First, short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value]. + * - Extracts String and Regex for [function] evaluation. + * - Caches previously seen Regex to avoid compilation overhead. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("binaryStringPatternFunction") private inline fun binaryPatternFunction(crossinline function: (Pattern, String) -> Boolean) = binaryPatternConstructorFunction( @@ -814,6 +971,9 @@ private inline fun binaryPatternFunction(crossinline function: (Pattern, String) function ) +/** + * Simple one entry cache. + */ private inline fun cache(crossinline ifAbsent: (String) -> T): (String) -> T? { var cache: Pair = Pair(null, null) return block@{ s: String -> @@ -826,6 +986,15 @@ private inline fun cache(crossinline ifAbsent: (String) -> T): (String) -> T } } +/** + * Binary (Array, Array) Function + * - Validates there is exactly 2 parameters. + * - First, short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can appear inside of Array. + * - Extracts Array and Array for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("binaryArrayArrayFunction") private inline fun binaryFunction( crossinline function: (List, List) -> EvaluateResult @@ -838,48 +1007,17 @@ private inline fun binaryFunction( function ) -private inline fun ternaryLazyFunction( - crossinline function: - (() -> EvaluateResult, () -> EvaluateResult, () -> EvaluateResult) -> EvaluateResult -): EvaluateFunction = { params -> - if (params.size != 3) - throw Assert.fail("Function should have exactly 3 params, but %d were given.", params.size) - val p1 = params[0] - val p2 = params[1] - val p3 = params[2] - { input: MutableDocument -> catch { function({ p1(input) }, { p2(input) }, { p3(input) }) } } -} - -private inline fun ternaryTimestampFunction( - crossinline function: (Timestamp, String, Long) -> EvaluateResult -): EvaluateFunction = ternaryNullableValueFunction { timestamp: Value, unit: Value, number: Value -> - val t: Timestamp = - when (timestamp.valueTypeCase) { - ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL - ValueTypeCase.TIMESTAMP_VALUE -> timestamp.timestampValue - else -> return@ternaryNullableValueFunction EvaluateResultError - } - val u: String = - if (unit.hasStringValue()) unit.stringValue - else return@ternaryNullableValueFunction EvaluateResultError - val n: Long = - when (number.valueTypeCase) { - ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL - ValueTypeCase.INTEGER_VALUE -> number.integerValue - else -> return@ternaryNullableValueFunction EvaluateResultError - } - function(t, u, n) -} - -private inline fun ternaryNullableValueFunction( - crossinline function: (Value, Value, Value) -> EvaluateResult -): EvaluateFunction = ternaryLazyFunction { p1, p2, p3 -> - val v1 = p1().value ?: return@ternaryLazyFunction EvaluateResultError - val v2 = p2().value ?: return@ternaryLazyFunction EvaluateResultError - val v3 = p3().value ?: return@ternaryLazyFunction EvaluateResultError - function(v1, v2, v3) -} - +/** + * For building type specific Binary Functions + * - Validates there is exactly 2 parameter. + * - First short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value]. + * - First parameter must be [Value] of [valueTypeCase1]. + * - Second parameter must be [Value] of [valueTypeCase2]. + * - Extract parameter values via [valueExtractor1] and [valueExtractor2] for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun binaryFunctionType( valueTypeCase1: ValueTypeCase, crossinline valueExtractor1: (Value) -> T1, @@ -910,6 +1048,18 @@ private inline fun binaryFunctionType( }) } +/** + * For building type specific Binary Functions + * - Has [functionConstructor] for creating stateful evaluation function. + * - Validates there is exactly 2 parameter. + * - First short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value]. + * - First parameter must be [Value] of [valueTypeCase1]. + * - Second parameter must be [Value] of [valueTypeCase2]. + * - Extract parameter values via [valueExtractor1] and [valueExtractor2] for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun binaryFunctionConstructorType( valueTypeCase1: ValueTypeCase, crossinline valueExtractor1: (Value) -> T1, @@ -943,6 +1093,76 @@ private inline fun binaryFunctionConstructorType( }) } +/** + * Ternary (Timestamp, String, Long) Function + * - Validates there is exactly 3 parameters. + * - Passes lazy parameters that delay evaluation of parameters. + * - Catches evaluation exceptions and returns them as an ERROR. + */ +private inline fun ternaryLazyFunction( + crossinline function: + (() -> EvaluateResult, () -> EvaluateResult, () -> EvaluateResult) -> EvaluateResult +): EvaluateFunction = { params -> + if (params.size != 3) + throw Assert.fail("Function should have exactly 3 params, but %d were given.", params.size) + val p1 = params[0] + val p2 = params[1] + val p3 = params[2] + { input: MutableDocument -> catch { function({ p1(input) }, { p2(input) }, { p3(input) }) } } +} + +/** + * Ternary (Timestamp, String, Long) Function + * - Validates there is exactly 3 parameters. + * - First, short circuits UNSET and ERROR parameters to return ERROR. + * - If 2nd parameter is NULL, short circuit and return ERROR. + * - If 1st or 3rd parameter is NULL, short circuit and return NULL. + * - Extracts Timestamp, String and Long for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ +private inline fun ternaryTimestampFunction( + crossinline function: (Timestamp, String, Long) -> EvaluateResult +): EvaluateFunction = ternaryNullableValueFunction { timestamp: Value, unit: Value, number: Value -> + val t: Timestamp = + when (timestamp.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL + ValueTypeCase.TIMESTAMP_VALUE -> timestamp.timestampValue + else -> return@ternaryNullableValueFunction EvaluateResultError + } + val u: String = + if (unit.hasStringValue()) unit.stringValue + else return@ternaryNullableValueFunction EvaluateResultError + val n: Long = + when (number.valueTypeCase) { + ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL + ValueTypeCase.INTEGER_VALUE -> number.integerValue + else -> return@ternaryNullableValueFunction EvaluateResultError + } + function(t, u, n) +} + +/** + * Ternary Value Function + * - Validates there is exactly 3 parameters. + * - Short circuits UNSET and ERROR parameters to return ERROR. + * - Allows passing of NULL [Value]s to [function] for evaluation. + * - Catches evaluation exceptions and returns them as an ERROR. + */ +private inline fun ternaryNullableValueFunction( + crossinline function: (Value, Value, Value) -> EvaluateResult +): EvaluateFunction = ternaryLazyFunction { p1, p2, p3 -> + val v1 = p1().value ?: return@ternaryLazyFunction EvaluateResultError + val v2 = p2().value ?: return@ternaryLazyFunction EvaluateResultError + val v3 = p3().value ?: return@ternaryLazyFunction EvaluateResultError + function(v1, v2, v3) +} + +/** + * Basic Variadic Function + * - No short circuiting of parameter evaluation. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun variadicResultFunction( crossinline function: (List) -> EvaluateResult ): EvaluateFunction = { params -> @@ -952,6 +1172,12 @@ private inline fun variadicResultFunction( } } +/** + * Variadic Value Function with NULLS + * - Short circuits UNSET and ERROR parameters to return ERROR. + * - Allows passing of NULL [Value]s to [function] for evaluation. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("variadicNullableValueFunction") private inline fun variadicNullableValueFunction( crossinline function: (List) -> EvaluateResult @@ -959,12 +1185,29 @@ private inline fun variadicNullableValueFunction( function(l.map { it.value ?: return@variadicResultFunction EvaluateResultError }) } +/** + * Variadic String Function + * - First short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value]. + * - Extract String parameters into List for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("variadicStringFunction") private inline fun variadicFunction( crossinline function: (List) -> EvaluateResult ): EvaluateFunction = variadicFunctionType(ValueTypeCase.STRING_VALUE, Value::getStringValue, function) +/** + * For building type specific Variadic Functions + * - First short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value]. + * - Parameter must be [Value] of [valueTypeCase]. + * - Extract parameter values via [valueExtractor] into List for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun variadicFunctionType( valueTypeCase: ValueTypeCase, crossinline valueExtractor: (Value) -> T, @@ -985,6 +1228,14 @@ private inline fun variadicFunctionType( } } +/** + * Variadic String Function + * - First short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value]. + * - Extract String parameters into BooleanArray for [function] evaluation. + * - All other [Value] types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("variadicBooleanFunction") private inline fun variadicFunction( crossinline function: (BooleanArray) -> EvaluateResult @@ -1004,6 +1255,15 @@ private inline fun variadicFunction( } } +/** + * Binary (Value, Value) Function for Comparisons + * - Validates there is exactly 2 parameters. + * - First, short circuits UNSET and ERROR parameters to return ERROR. + * - Second short circuits NULL [Value] parameters to return NULL [Value]. + * - Third short circuits Double.NaN [Value] parameters to return FALSE. + * - Wraps result as EvaluateResult. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun comparison(crossinline f: (Value, Value) -> Boolean?): EvaluateFunction = binaryFunction { p1: Value, p2: Value -> if (isNanValue(p1) or isNanValue(p2)) EvaluateResult.FALSE From 254a6d6061f1504b71c7e9cb5c39159a0ae817c3 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 3 Jun 2025 13:12:12 -0400 Subject: [PATCH 097/152] Comments --- .../firebase/firestore/pipeline/evaluation.kt | 164 ++++++++++++++---- 1 file changed, 127 insertions(+), 37 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index b3499e2b919..e7894b19b35 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -653,7 +653,7 @@ private inline fun unaryFunction( * - Short circuits UNSET and ERROR parameter to return ERROR. * - Short circuits NULL [Value] parameter to return NULL [Value]. * - Extracts Boolean for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("unaryBooleanFunction") @@ -671,12 +671,14 @@ private inline fun unaryFunction(crossinline function: (Boolean) -> EvaluateResu * - Short circuits NULL [Value] parameter to return NULL [Value]. * - Extracts Boolean for [function] evaluation. * - Wraps the primitive String result as [EvaluateResult]. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("unaryStringFunctionPrimitive") private inline fun unaryFunctionPrimitive(crossinline function: (String) -> String) = - unaryFunction { s: String -> EvaluateResult.string(function(s)) } + unaryFunction { s: String -> + EvaluateResult.string(function(s)) + } /** * Unary String Function @@ -684,7 +686,7 @@ private inline fun unaryFunctionPrimitive(crossinline function: (String) -> Stri * - Short circuits UNSET and ERROR parameter to return ERROR. * - Short circuits NULL [Value] parameter to return NULL [Value]. * - Extracts String for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("unaryStringFunction") @@ -701,7 +703,7 @@ private inline fun unaryFunction(crossinline function: (String) -> EvaluateResul * - Short circuits UNSET and ERROR parameter to return ERROR. * - Short circuits NULL [Value] parameter to return NULL [Value]. * - Extracts String for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("unaryLongFunction") @@ -718,7 +720,7 @@ private inline fun unaryFunction(crossinline function: (Long) -> EvaluateResult) * - Short circuits UNSET and ERROR parameter to return ERROR. * - Short circuits NULL [Value] parameter to return NULL [Value]. * - Extracts Timestamp for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("unaryTimestampFunction") @@ -733,9 +735,10 @@ private inline fun unaryFunction(crossinline function: (Timestamp) -> EvaluateRe * Unary Timestamp Function * - Validates there is exactly 1 parameter. * - Short circuits UNSET and ERROR parameter to return ERROR. - * - Short circuits NULL [Value] parameter to return NULL [Value], however NULL [Value]s can appear inside of array. + * - Short circuits NULL [Value] parameter to return NULL [Value], however NULL [Value]s can appear + * inside of array. * - Extracts Timestamp from [Value] for evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("unaryArrayFunction") @@ -751,9 +754,9 @@ private inline fun unaryFunction(crossinline longOp: (List) -> EvaluateRe * - Validates there is exactly 1 parameter. * - Short circuits UNSET and ERROR parameter to return ERROR. * - Short circuits NULL [Value] parameter to return NULL [Value]. - * - Depending on [Value] type, either the Timestamp or String is extracted and evaluated by - * either [byteOp] or [stringOp]. - * - All other [Value] types return ERROR. + * - Depending on [Value] type, either the Timestamp or String is extracted and evaluated by either + * [byteOp] or [stringOp]. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ private inline fun unaryFunction( @@ -775,7 +778,7 @@ private inline fun unaryFunction( * - Short circuits UNSET and ERROR parameter to return ERROR. * - Short circuits NULL [Value] parameter to return NULL [Value]. * - If [Value] type is [valueTypeCase] then use [valueExtractor] for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ private inline fun unaryFunctionType( @@ -800,7 +803,7 @@ private inline fun unaryFunctionType( * - Short circuits NULL [Value] parameter to return NULL [Value]. * - If [Value] type is [valueTypeCase1] then use [valueExtractor1] for [function1] evaluation. * - If [Value] type is [valueTypeCase2] then use [valueExtractor2] for [function2] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ private inline fun unaryFunctionType( @@ -852,9 +855,10 @@ private inline fun binaryFunction( * Binary (Map, String) Function * - Validates there is exactly 2 parameters. * - First, short circuits UNSET and ERROR parameters to return ERROR. - * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can appear inside of Map. + * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can + * appear inside of Map. * - Extracts Map and String for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("binaryMapStringFunction") @@ -873,9 +877,10 @@ private inline fun binaryFunction( * Binary (Value, Array) Function * - Validates there is exactly 2 parameters. * - First, short circuits UNSET and ERROR parameters to return ERROR. - * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can appear inside of Array. + * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can + * appear inside of Array. * - Extracts Value and Array for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("binaryValueArrayFunction") @@ -889,9 +894,10 @@ private inline fun binaryFunction( * Binary (Array, Value) Function * - Validates there is exactly 2 parameters. * - First, short circuits UNSET and ERROR parameters to return ERROR. - * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can appear inside of Array. + * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can + * appear inside of Array. * - Extracts Array and Value for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("binaryArrayValueFunction") @@ -907,7 +913,7 @@ private inline fun binaryFunction( * - First, short circuits UNSET and ERROR parameters to return ERROR. * - Second short circuits NULL [Value] parameters to return NULL [Value]. * - Extracts String and String for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("binaryStringStringFunction") @@ -927,7 +933,7 @@ private inline fun binaryFunction(crossinline function: (String, String) -> Eval * - First, short circuits UNSET and ERROR parameters to return ERROR. * - Second short circuits NULL [Value] parameters to return NULL [Value]. * - Extracts String and Regex via [patternConstructor] for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("binaryStringPatternConstructorFunction") @@ -955,7 +961,7 @@ private inline fun binaryPatternConstructorFunction( * - Second short circuits NULL [Value] parameters to return NULL [Value]. * - Extracts String and Regex for [function] evaluation. * - Caches previously seen Regex to avoid compilation overhead. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("binaryStringPatternFunction") @@ -971,9 +977,7 @@ private inline fun binaryPatternFunction(crossinline function: (Pattern, String) function ) -/** - * Simple one entry cache. - */ +/** Simple one entry cache. */ private inline fun cache(crossinline ifAbsent: (String) -> T): (String) -> T? { var cache: Pair = Pair(null, null) return block@{ s: String -> @@ -990,9 +994,10 @@ private inline fun cache(crossinline ifAbsent: (String) -> T): (String) -> T * Binary (Array, Array) Function * - Validates there is exactly 2 parameters. * - First, short circuits UNSET and ERROR parameters to return ERROR. - * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can appear inside of Array. + * - Second short circuits NULL [Value] parameters to return NULL [Value], however NULL [Value]s can + * appear inside of Array. * - Extracts Array and Array for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("binaryArrayArrayFunction") @@ -1015,7 +1020,7 @@ private inline fun binaryFunction( * - First parameter must be [Value] of [valueTypeCase1]. * - Second parameter must be [Value] of [valueTypeCase2]. * - Extract parameter values via [valueExtractor1] and [valueExtractor2] for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ private inline fun binaryFunctionType( @@ -1057,7 +1062,7 @@ private inline fun binaryFunctionType( * - First parameter must be [Value] of [valueTypeCase1]. * - Second parameter must be [Value] of [valueTypeCase2]. * - Extract parameter values via [valueExtractor1] and [valueExtractor2] for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ private inline fun binaryFunctionConstructorType( @@ -1118,7 +1123,7 @@ private inline fun ternaryLazyFunction( * - If 2nd parameter is NULL, short circuit and return ERROR. * - If 1st or 3rd parameter is NULL, short circuit and return NULL. * - Extracts Timestamp, String and Long for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ private inline fun ternaryTimestampFunction( @@ -1190,7 +1195,7 @@ private inline fun variadicNullableValueFunction( * - First short circuits UNSET and ERROR parameters to return ERROR. * - Second short circuits NULL [Value] parameters to return NULL [Value]. * - Extract String parameters into List for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("variadicStringFunction") @@ -1205,7 +1210,7 @@ private inline fun variadicFunction( * - Second short circuits NULL [Value] parameters to return NULL [Value]. * - Parameter must be [Value] of [valueTypeCase]. * - Extract parameter values via [valueExtractor] into List for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ private inline fun variadicFunctionType( @@ -1233,7 +1238,7 @@ private inline fun variadicFunctionType( * - First short circuits UNSET and ERROR parameters to return ERROR. * - Second short circuits NULL [Value] parameters to return NULL [Value]. * - Extract String parameters into BooleanArray for [function] evaluation. - * - All other [Value] types return ERROR. + * - All other parameter types return ERROR. * - Catches evaluation exceptions and returns them as an ERROR. */ @JvmName("variadicBooleanFunction") @@ -1270,6 +1275,17 @@ private inline fun comparison(crossinline f: (Value, Value) -> Boolean?): Evalua else EvaluateResult.boolean(f(p1, p2)) } +/** + * Unary (Number) Arithmetic Function + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - If parameter type is Integer then [intOp] will be used for evaluation. + * - If parameter type is Double then [doubleOp] will be used for evaluation. + * - All other parameter types return ERROR. + * - Primitive result is wrapped as EvaluateResult. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun arithmeticPrimitive( crossinline intOp: (Long) -> Long, crossinline doubleOp: (Double) -> Double @@ -1279,6 +1295,18 @@ private inline fun arithmeticPrimitive( { x: Double -> EvaluateResult.double(doubleOp(x)) } ) +/** + * Binary Arithmetic Function + * - Validates there is exactly 2 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - If both parameter types are Integer then [intOp] will be used for evaluation. + * - Otherwise if both parameters are either Integer or Double, then the values are converted to + * Double, and then [doubleOp] will be used for evaluation. + * - All other parameter types return ERROR. + * - Primitive result is wrapped as EvaluateResult. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun arithmeticPrimitive( crossinline intOp: (Long, Long) -> Long, crossinline doubleOp: (Double, Double) -> Double @@ -1288,13 +1316,43 @@ private inline fun arithmeticPrimitive( { x: Double, y: Double -> EvaluateResult.double(doubleOp(x, y)) } ) +/** + * Binary Arithmetic Function + * - Validates there is exactly 2 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - If any of parameters are Integer, they will be converted to Double. + * - After conversion, if both parameters are Double, the [doubleOp] will be used for evaluation. + * - All other parameter types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun arithmeticPrimitive( crossinline doubleOp: (Double, Double) -> Double ): EvaluateFunction = arithmetic { x: Double, y: Double -> EvaluateResult.double(doubleOp(x, y)) } -private inline fun arithmetic(crossinline doubleOp: (Double) -> EvaluateResult): EvaluateFunction = - arithmetic({ n: Long -> doubleOp(n.toDouble()) }, doubleOp) +/** + * Unary Arithmetic Function + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - If parameter is Integer, it will be converted to Double. + * - After conversion, if parameter is Double, the [function] will be used for evaluation. + * - All other parameter types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ +private inline fun arithmetic(crossinline function: (Double) -> EvaluateResult): EvaluateFunction = + arithmetic({ n: Long -> function(n.toDouble()) }, function) +/** + * Unary Arithmetic Function + * - Validates there is exactly 1 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - If [Value] type is Integer then [intOp] will be used for evaluation. + * - If [Value] type is Double then [doubleOp] will be used for evaluation. + * - All other parameter types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun arithmetic( crossinline intOp: (Long) -> EvaluateResult, crossinline doubleOp: (Double) -> EvaluateResult @@ -1308,6 +1366,17 @@ private inline fun arithmetic( doubleOp, ) +/** + * Binary Arithmetic Function + * - Validates there is exactly 2 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - Second parameter is expected to be Long. + * - If first parameter type is Integer then [intOp] will be used for evaluation. + * - If first parameter type is Double then [doubleOp] will be used for evaluation. + * - All other parameter types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ @JvmName("arithmeticNumberLong") private inline fun arithmetic( crossinline intOp: (Long, Long) -> EvaluateResult, @@ -1322,6 +1391,17 @@ private inline fun arithmetic( else EvaluateResultError } +/** + * Binary Arithmetic Function + * - Validates there is exactly 2 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - If both parameter types are Integer then [intOp] will be used for evaluation. + * - Otherwise if both parameters are either Integer or Double, then the values are converted to + * Double, and then [doubleOp] will be used for evaluation. + * - All other parameter types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun arithmetic( crossinline intOp: (Long, Long) -> EvaluateResult, crossinline doubleOp: (Double, Double) -> EvaluateResult @@ -1343,8 +1423,18 @@ private inline fun arithmetic( } } +/** + * Binary Arithmetic Function + * - Validates there is exactly 2 parameter. + * - Short circuits UNSET and ERROR parameter to return ERROR. + * - Short circuits NULL [Value] parameter to return NULL [Value]. + * - If any of parameters are Integer, they will be converted to Double. + * - After conversion, if both parameters are Double, the [function] will be used for evaluation. + * - All other parameter types return ERROR. + * - Catches evaluation exceptions and returns them as an ERROR. + */ private inline fun arithmetic( - crossinline op: (Double, Double) -> EvaluateResult + crossinline function: (Double, Double) -> EvaluateResult ): EvaluateFunction = binaryFunction { p1: Value, p2: Value -> val v1: Double = when (p1.valueTypeCase) { @@ -1358,5 +1448,5 @@ private inline fun arithmetic( ValueTypeCase.DOUBLE_VALUE -> p2.doubleValue else -> return@binaryFunction EvaluateResultError } - op(v1, v2) + function(v1, v2) } From 4e98dfc015157fafc92fc2bd2ee4f5864ef6122b Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 3 Jun 2025 13:27:51 -0400 Subject: [PATCH 098/152] Add copyright --- .../firestore/pipeline/EvaluateResult.kt | 14 ++++++++++ .../firebase/firestore/pipeline/evaluation.kt | 14 ++++++++++ .../firestore/pipeline/expressions.kt | 1 - .../firestore/pipeline/ArithmeticTests.kt | 14 ++++++++++ .../firebase/firestore/pipeline/ArrayTests.kt | 14 ++++++++++ .../firestore/pipeline/ComparisonTests.kt | 14 ++++++++++ .../firebase/firestore/pipeline/DebugTests.kt | 14 ++++++++++ .../firebase/firestore/pipeline/FieldTests.kt | 28 +++++++++---------- .../firestore/pipeline/LogicalTests.kt | 28 +++++++++---------- .../firebase/firestore/pipeline/MapTests.kt | 14 ++++++++++ .../pipeline/MirroringSemanticsTests.kt | 14 ++++++++++ .../firestore/pipeline/StringTests.kt | 14 ++++++++++ .../firestore/pipeline/TimestampTests.kt | 14 ++++++++++ .../firebase/firestore/pipeline/testUtil.kt | 14 ++++++++++ 14 files changed, 180 insertions(+), 31 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt index dc785d465ed..c39946c07e2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.firebase.firestore.model.Values diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index e7894b19b35..3f473fd93a9 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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. + @file:JvmName("Evaluation") package com.google.firebase.firestore.pipeline 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 46d90812cb3..16fc8a77c48 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 @@ -27,7 +27,6 @@ import com.google.firebase.firestore.model.FieldPath as ModelFieldPath import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue -import com.google.firebase.firestore.pipeline.Expr.Companion import com.google.firebase.firestore.pipeline.Expr.Companion.field import com.google.firebase.firestore.util.CustomClassMapper import com.google.firestore.v1.MapValue diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt index 0aad32d6c86..31ca2d811a3 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArithmeticTests.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.common.truth.Truth.assertThat diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArrayTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArrayTests.kt index 4c4b94e2468..d08ac925dd4 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArrayTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ArrayTests.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.common.truth.Truth.assertWithMessage diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt index 9185275c3a6..78fe34d83b6 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/ComparisonTests.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.firebase.Timestamp // For creating Timestamp instances diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/DebugTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/DebugTests.kt index 2ddd9a34d63..faa1c6ce867 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/DebugTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/DebugTests.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.firebase.firestore.pipeline.Expr.Companion.array diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/FieldTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/FieldTests.kt index d213d6584a7..7eb0a5d965c 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/FieldTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/FieldTests.kt @@ -1,18 +1,16 @@ -/* - * Copyright 2025 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. - */ +// Copyright 2025 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.firebase.firestore.pipeline diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LogicalTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LogicalTests.kt index 8e398939302..5fb4a1d6e03 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LogicalTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LogicalTests.kt @@ -1,18 +1,16 @@ -/* - * Copyright 2025 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. - */ +// Copyright 2025 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.firebase.firestore.pipeline diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MapTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MapTests.kt index c741b511f07..71a740a1b90 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MapTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MapTests.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.firebase.firestore.model.Values.encodeValue diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MirroringSemanticsTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MirroringSemanticsTests.kt index 37bf0ed1246..716b1d9c903 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MirroringSemanticsTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/MirroringSemanticsTests.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.firebase.firestore.pipeline.Expr.Companion.add diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StringTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StringTests.kt index 4e769057af7..2885637427a 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StringTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/StringTests.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.firebase.firestore.model.Values.encodeValue diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/TimestampTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/TimestampTests.kt index b2ae0a9ba80..ec39dffce25 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/TimestampTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/TimestampTests.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.firebase.Timestamp diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt index 2d4e4e0e9be..aca2f903848 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt @@ -1,3 +1,17 @@ +// Copyright 2025 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.firebase.firestore.pipeline import com.google.common.truth.Truth.assertWithMessage From b1f40065ee39a2c9e9d0ce6259d81d9ae22dc8bf Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 4 Jun 2025 12:49:40 -0400 Subject: [PATCH 099/152] Where tests --- .../com/google/firebase/firestore/Pipeline.kt | 8 +- .../firebase/firestore/core/pipeline.kt | 16 - .../firestore/pipeline/expressions.kt | 40 +- .../firebase/firestore/pipeline/stage.kt | 64 +- .../{core => pipeline}/PipelineTests.kt | 19 +- .../firebase/firestore/pipeline/WhereTests.kt | 601 ++++++++++++++++++ .../firebase/firestore/pipeline/testUtil.kt | 12 + 7 files changed, 701 insertions(+), 59 deletions(-) delete mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/core/pipeline.kt rename firebase-firestore/src/test/java/com/google/firebase/firestore/{core => pipeline}/PipelineTests.kt (58%) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 309721199c4..c15bf2300f1 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -25,6 +25,7 @@ import com.google.firebase.firestore.pipeline.AddFieldsStage import com.google.firebase.firestore.pipeline.AggregateFunction import com.google.firebase.firestore.pipeline.AggregateStage import com.google.firebase.firestore.pipeline.AggregateWithAlias +import com.google.firebase.firestore.pipeline.BaseStage import com.google.firebase.firestore.pipeline.BooleanExpr import com.google.firebase.firestore.pipeline.CollectionGroupSource import com.google.firebase.firestore.pipeline.CollectionSource @@ -38,11 +39,11 @@ import com.google.firebase.firestore.pipeline.Field import com.google.firebase.firestore.pipeline.FindNearestStage import com.google.firebase.firestore.pipeline.FunctionExpr import com.google.firebase.firestore.pipeline.InternalOptions -import com.google.firebase.firestore.pipeline.Stage import com.google.firebase.firestore.pipeline.LimitStage import com.google.firebase.firestore.pipeline.OffsetStage import com.google.firebase.firestore.pipeline.Ordering import com.google.firebase.firestore.pipeline.PipelineOptions +import com.google.firebase.firestore.pipeline.RawStage import com.google.firebase.firestore.pipeline.RealtimePipelineOptions import com.google.firebase.firestore.pipeline.RemoveFieldsStage import com.google.firebase.firestore.pipeline.ReplaceStage @@ -50,7 +51,6 @@ import com.google.firebase.firestore.pipeline.SampleStage import com.google.firebase.firestore.pipeline.SelectStage import com.google.firebase.firestore.pipeline.Selectable import com.google.firebase.firestore.pipeline.SortStage -import com.google.firebase.firestore.pipeline.BaseStage import com.google.firebase.firestore.pipeline.UnionStage import com.google.firebase.firestore.pipeline.UnnestStage import com.google.firebase.firestore.pipeline.WhereStage @@ -159,10 +159,10 @@ private constructor( * This method provides a way to call stages that are supported by the Firestore backend but that * are not implemented in the SDK version being used. * - * @param stage An [Stage] object that specifies stage name and parameters. + * @param rawStage An [RawStage] object that specifies stage name and parameters. * @return A new [Pipeline] object with this stage appended to the stage list. */ - fun addStage(stage: Stage): Pipeline = append(stage) + fun addStage(rawStage: RawStage): Pipeline = append(rawStage) /** * Adds new fields to outputs from previous stages. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/pipeline.kt deleted file mode 100644 index 480cf87af3b..00000000000 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/pipeline.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.google.firebase.firestore.core - -import com.google.firebase.firestore.AbstractPipeline -import com.google.firebase.firestore.model.MutableDocument -import com.google.firebase.firestore.pipeline.EvaluationContext -import kotlinx.coroutines.flow.Flow - -internal fun runPipeline( - pipeline: AbstractPipeline, - input: Flow -): Flow { - val context = EvaluationContext(pipeline.userDataReader) - return pipeline.stages.fold(input) { documentFlow, stage -> - stage.evaluate(context, documentFlow) - } -} 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 16fc8a77c48..0764af06a9c 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 @@ -92,7 +92,7 @@ abstract class Expr internal constructor() { } .toTypedArray() ) - is List<*> -> ListOfExprs(value.map(toExpr).toTypedArray()) + is List<*> -> array(value) else -> null } } @@ -1018,7 +1018,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun eqAny(expression: Expr, values: List): BooleanExpr = - eqAny(expression, ListOfExprs(toArrayOfExprOrConstant(values))) + eqAny(expression, array(values)) /** * Creates an expression that checks if an [expression], when evaluated, is equal to any of the @@ -1043,7 +1043,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun eqAny(fieldName: String, values: List): BooleanExpr = - eqAny(fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + eqAny(fieldName, array(values)) /** * Creates an expression that checks if a field's value is equal to any of the elements of @@ -1068,7 +1068,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun notEqAny(expression: Expr, values: List): BooleanExpr = - notEqAny(expression, ListOfExprs(toArrayOfExprOrConstant(values))) + notEqAny(expression, array(values)) /** * Creates an expression that checks if an [expression], when evaluated, is not equal to all the @@ -1093,7 +1093,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun notEqAny(fieldName: String, values: List): BooleanExpr = - notEqAny(fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + notEqAny(fieldName, array(values)) /** * Creates an expression that checks if a field's value is not equal to all of the elements of @@ -2776,7 +2776,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAll(array: Expr, values: List) = - arrayContainsAll(array, ListOfExprs(toArrayOfExprOrConstant(values))) + arrayContainsAll(array, array(values)) /** * Creates an expression that checks if [array] contains all elements of [arrayExpression]. @@ -2802,7 +2802,7 @@ abstract class Expr internal constructor() { "array_contains_all", evaluateArrayContainsAll, arrayFieldName, - ListOfExprs(toArrayOfExprOrConstant(values)) + array(values) ) /** @@ -2829,7 +2829,7 @@ abstract class Expr internal constructor() { "array_contains_any", evaluateArrayContainsAny, array, - ListOfExprs(toArrayOfExprOrConstant(values)) + array(values) ) /** @@ -2856,7 +2856,7 @@ abstract class Expr internal constructor() { "array_contains_any", evaluateArrayContainsAny, arrayFieldName, - ListOfExprs(toArrayOfExprOrConstant(values)) + array(values) ) /** @@ -4197,17 +4197,6 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select } } -internal class ListOfExprs(private val expressions: Array) : Expr() { - override fun toProto(userDataReader: UserDataReader): Value = - encodeValue(expressions.map { it.toProto(userDataReader) }) - - override fun evaluateContext( - context: EvaluationContext - ): (input: MutableDocument) -> EvaluateResult { - TODO("Not yet implemented") - } -} - /** * This class defines the base class for Firestore [Pipeline] functions, which can be evaluated * within pipeline execution. @@ -4378,7 +4367,7 @@ internal constructor(name: String, function: EvaluateFunction, params: Array> -internal constructor(protected val name: String, internal val options: InternalOptions) { +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList + +sealed class BaseStage>( + protected val name: String, + internal val options: InternalOptions +) { internal fun toProtoStage(userDataReader: UserDataReader): Pipeline.Stage { val builder = Pipeline.Stage.newBuilder() builder.setName(name) @@ -106,32 +111,32 @@ internal constructor(protected val name: String, internal val options: InternalO * This class provides a way to call stages that are supported by the Firestore backend but that are * not implemented in the SDK version being used. */ -class Stage +class RawStage private constructor( name: String, private val arguments: List, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage(name, options) { +) : BaseStage(name, options) { companion object { /** * Specify name of stage * * @param name The unique name of the stage to add. - * @return [Stage] with specified parameters. + * @return [RawStage] with specified parameters. */ - @JvmStatic fun ofName(name: String) = Stage(name, emptyList(), InternalOptions.EMPTY) + @JvmStatic fun ofName(name: String) = RawStage(name, emptyList(), InternalOptions.EMPTY) } - override fun self(options: InternalOptions) = Stage(name, arguments, options) + override fun self(options: InternalOptions) = RawStage(name, arguments, options) /** * Specify arguments to stage. * * @param arguments A list of ordered parameters to configure the stage's behavior. - * @return [Stage] with specified parameters. + * @return [RawStage] with specified parameters. */ - fun withArguments(vararg arguments: Any): Stage = - Stage(name, arguments.map(GenericArg::from), options) + fun withArguments(vararg arguments: Any): RawStage = + RawStage(name, arguments.map(GenericArg::from), options) override fun args(userDataReader: UserDataReader): Sequence = arguments.asSequence().map { it.toProto(userDataReader) } @@ -546,6 +551,43 @@ internal constructor( override fun self(options: InternalOptions) = SortStage(orders, options) override fun args(userDataReader: UserDataReader): Sequence = orders.asSequence().map { it.toProto(userDataReader) } + + override fun evaluate( + context: EvaluationContext, + inputs: Flow + ): Flow { + val evaluates: Array = + orders.map { it.expr.evaluateContext(context) }.toTypedArray() + val directions: Array = orders.map { it.dir }.toTypedArray() + return flow { + inputs + // For each document, lazily evaluate order expression values. + .map { doc -> + val orderValues = + evaluates + .map { lazy(LazyThreadSafetyMode.PUBLICATION) { it(doc).value ?: Values.MIN_VALUE } } + .toTypedArray>() + Pair(doc, orderValues) + } + .toList() + .sortedWith( + Comparator { px, py -> + val x = px.second + val y = py.second + directions.forEachIndexed { i, dir -> + val r = + when (dir) { + Ordering.Direction.ASCENDING -> Values.compare(x[i].value, y[i].value) + Ordering.Direction.DESCENDING -> Values.compare(y[i].value, x[i].value) + } + if (r != 0) return@Comparator r + } + 0 + } + ) + .forEach { p -> emit(p.first) } + } + } } internal class DistinctStage diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/PipelineTests.kt similarity index 58% rename from firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt rename to firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/PipelineTests.kt index 459e9c173d9..5b750560f90 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/PipelineTests.kt @@ -1,4 +1,18 @@ -package com.google.firebase.firestore.core +// Copyright 2025 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.firebase.firestore.pipeline import com.google.common.truth.Truth.assertThat import com.google.firebase.firestore.RealtimePipelineSource @@ -10,7 +24,10 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) internal class PipelineTests { @Test diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt new file mode 100644 index 00000000000..0276c937dd7 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt @@ -0,0 +1,601 @@ +// Copyright 2025 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.firebase.firestore.pipeline + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.firestore.RealtimePipelineSource +import com.google.firebase.firestore.TestUtil +import com.google.firebase.firestore.model.MutableDocument +import com.google.firebase.firestore.pipeline.Expr.Companion.and +import com.google.firebase.firestore.pipeline.Expr.Companion.array +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.eqAny +import com.google.firebase.firestore.pipeline.Expr.Companion.exists +import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.pipeline.Expr.Companion.not +import com.google.firebase.firestore.pipeline.Expr.Companion.or +import com.google.firebase.firestore.pipeline.Expr.Companion.xor +import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class WhereTests { + + @Test + fun `empty database returns no results`(): Unit = runBlocking { + val documents = emptyList() + val pipeline = + RealtimePipelineSource(TestUtil.firestore()).collection("users").where(field("age").gte(10L)) + + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `duplicate conditions`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) // Match + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(and(field("age").gte(10.0), field("age").gte(20.0))) + // age >= 10.0 AND age >= 20.0 => age >= 20.0 + // Matches: doc1 (75.5), doc2 (25.0), doc3 (100.0) + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2, doc3) + } + + @Test + fun `logical equivalent condition equal`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline1 = + RealtimePipelineSource(TestUtil.firestore()).collection("users").where(field("age").eq(25.0)) + + val pipeline2 = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(constant(25.0).eq(field("age"))) + + val result1 = runPipeline(pipeline1, flowOf(*documents.toTypedArray())).toList() + val result2 = runPipeline(pipeline2, flowOf(*documents.toTypedArray())).toList() + + assertThat(result1).containsExactly(doc2) + assertThat(result1).isEqualTo(result2) + } + + @Test + fun `logical equivalent condition and`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline1 = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(and(field("age").gt(10.0), field("age").lt(70.0))) + + val pipeline2 = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(and(field("age").lt(70.0), field("age").gt(10.0))) + + val result1 = runPipeline(pipeline1, flowOf(*documents.toTypedArray())).toList() + val result2 = runPipeline(pipeline2, flowOf(*documents.toTypedArray())).toList() + + assertThat(result1).containsExactly(doc2) + assertThat(result1).isEqualTo(result2) + } + + @Test + fun `logical equivalent condition or`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) // Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline1 = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(or(field("age").lt(10.0), field("age").gt(80.0))) + + val pipeline2 = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(or(field("age").gt(80.0), field("age").lt(10.0))) + val result1 = runPipeline(pipeline1, flowOf(*documents.toTypedArray())).toList() + val result2 = runPipeline(pipeline2, flowOf(*documents.toTypedArray())).toList() + + assertThat(result1).containsExactly(doc3) + assertThat(result1).isEqualTo(result2) + } + + @Test + fun `logical equivalent condition in`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) // Match + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val documents = listOf(doc1, doc2, doc3) + + val values = listOf("alice", "matthew", "joe") + + val pipeline1 = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(field("name").eqAny(values)) + + val pipeline2 = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(eqAny(field("name"), array(values))) + + val result1 = runPipeline(pipeline1, flowOf(*documents.toTypedArray())).toList() + val result2 = runPipeline(pipeline2, flowOf(*documents.toTypedArray())).toList() + + assertThat(result1).containsExactly(doc1) + assertThat(result1).isEqualTo(result2) + } + + @Test + fun `repeated stages`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) // Match + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) // Match + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(field("age").gte(10.0)) + .where(field("age").gte(20.0)) + + // age >= 10.0 THEN age >= 20.0 => age >= 20.0 + // Matches: doc1 (75.5), doc2 (25.0), doc3 (100.0) + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2, doc3) + } + + @Test + fun `composite equalities`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("height" to 60L, "age" to 75L)) + val doc2 = doc("users/b", 1000, mapOf("height" to 55L, "age" to 50L)) + val doc3 = doc("users/c", 1000, mapOf("height" to 55.0, "age" to 75L)) // Match + val doc4 = doc("users/d", 1000, mapOf("height" to 50L, "age" to 41L)) + val doc5 = doc("users/e", 1000, mapOf("height" to 80L, "age" to 75L)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(field("age").eq(75L)) + .where(field("height").eq(55L)) // 55L will also match 55.0 + + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3) + } + + @Test + fun `composite inequalities`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("height" to 60L, "age" to 75L)) // Match + val doc2 = doc("users/b", 1000, mapOf("height" to 55L, "age" to 50L)) + val doc3 = doc("users/c", 1000, mapOf("height" to 55.0, "age" to 75L)) // Match + val doc4 = doc("users/d", 1000, mapOf("height" to 50L, "age" to 41L)) + val doc5 = doc("users/e", 1000, mapOf("height" to 80L, "age" to 75L)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(field("age").gt(50L)) + .where(field("height").lt(75L)) + + // age > 50 AND height < 75 + // doc1: 75 > 50 (T) AND 60 < 75 (T) -> True + // doc2: 50 > 50 (F) + // doc3: 75 > 50 (T) AND 55.0 < 75 (T) -> True + // doc4: 41 > 50 (F) + // doc5: 75 > 50 (T) AND 80 < 75 (F) -> False + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc3) + } + + @Test + fun `composite non seekable`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("first" to "alice", "last" to "smith")) + val doc2 = doc("users/b", 1000, mapOf("first" to "bob", "last" to "smith")) + val doc3 = doc("users/c", 1000, mapOf("first" to "charlie", "last" to "baker")) // Match + val doc4 = doc("users/d", 1000, mapOf("first" to "diane", "last" to "miller")) // Match + val doc5 = doc("users/e", 1000, mapOf("first" to "eric", "last" to "davis")) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + // Using regexMatch for LIKE '%a%' -> ".*a.*" + .where(field("first").regexMatch(".*a.*")) + // Using regexMatch for LIKE '%er' -> ".*er$" + .where(field("last").regexMatch(".*er$")) + + // first contains 'a' AND last ends with 'er' + // doc1: alice (yes), smith (no) + // doc2: bob (no), smith (no) + // doc3: charlie (yes), baker (yes) -> Match + // doc4: diane (yes), miller (yes) -> Match + // doc5: eric (no), davis (no) + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc4) + } + + @Test + fun `composite mixed`(): Unit = runBlocking { + val doc1 = + doc( + "users/a", + 1000, + mapOf("first" to "alice", "last" to "smith", "age" to 75L, "height" to 40L) + ) + val doc2 = + doc( + "users/b", + 1000, + mapOf("first" to "bob", "last" to "smith", "age" to 75L, "height" to 50L) + ) + val doc3 = + doc( + "users/c", + 1000, + mapOf("first" to "charlie", "last" to "baker", "age" to 75L, "height" to 50L) + ) // Match + val doc4 = + doc( + "users/d", + 1000, + mapOf("first" to "diane", "last" to "miller", "age" to 75L, "height" to 50L) + ) // Match + val doc5 = + doc( + "users/e", + 1000, + mapOf("first" to "eric", "last" to "davis", "age" to 80L, "height" to 50L) + ) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(field("age").eq(75L)) + .where(field("height").gt(45L)) + .where(field("last").regexMatch(".*er$")) // ends with 'er' + + // age == 75 AND height > 45 AND last ends with 'er' + // doc1: 75==75 (T), 40>45 (F) -> False + // doc2: 75==75 (T), 50>45 (T), smith ends er (F) -> False + // doc3: 75==75 (T), 50>45 (T), baker ends er (T) -> True + // doc4: 75==75 (T), 50>45 (T), miller ends er (T) -> True + // doc5: 80==75 (F) -> False + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc4) + } + + @Test + fun `exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) // Match + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) // Match + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) + val doc5 = doc("users/e", 1000, mapOf("other" to true)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()).collection("users").where(exists(field("name"))) + + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2, doc3) + } + + @Test + fun `not exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) // Match + val doc5 = doc("users/e", 1000, mapOf("other" to true)) // Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(not(exists(field("name")))) + + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4, doc5) + } + + @Test + fun `not not exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) // Match + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) // Match + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) + val doc5 = doc("users/e", 1000, mapOf("other" to true)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(not(not(exists(field("name"))))) + + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2, doc3) + } + + @Test + fun `exists and exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) // Match + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) + val doc5 = doc("users/e", 1000, mapOf("other" to true)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(and(exists(field("name")), exists(field("age")))) + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2) + } + + @Test + fun `exists or exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) // Match + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) // Match + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) // Match + val doc5 = doc("users/e", 1000, mapOf("other" to true)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(or(exists(field("name")), exists(field("age")))) + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2, doc3, doc4) + } + + @Test + fun `not exists and exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) // Match + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) // Match + val doc5 = doc("users/e", 1000, mapOf("other" to true)) // Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(not(and(exists(field("name")), exists(field("age"))))) + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc4, doc5) + } + + @Test + fun `not exists or exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) + val doc5 = doc("users/e", 1000, mapOf("other" to true)) // Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(not(or(exists(field("name")), exists(field("age"))))) + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc5) + } + + @Test + fun `not exists xor exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) // Match + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) + val doc5 = doc("users/e", 1000, mapOf("other" to true)) // Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(not(xor(exists(field("name")), exists(field("age"))))) + // NOT ( (name exists AND NOT age exists) OR (NOT name exists AND age exists) ) + // = (name exists AND age exists) OR (NOT name exists AND NOT age exists) + // Matches: doc1, doc2, doc5 + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2, doc5) + } + + @Test + fun `and not exists not exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) + val doc5 = doc("users/e", 1000, mapOf("other" to true)) // Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(and(not(exists(field("name"))), not(exists(field("age"))))) + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc5) + } + + @Test + fun `or not exists not exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) // Match + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) // Match + val doc5 = doc("users/e", 1000, mapOf("other" to true)) // Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(or(not(exists(field("name"))), not(exists(field("age"))))) + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc4, doc5) + } + + @Test + fun `xor not exists not exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) // Match + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) // Match + val doc5 = doc("users/e", 1000, mapOf("other" to true)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(xor(not(exists(field("name"))), not(exists(field("age"))))) + // (NOT name exists AND NOT (NOT age exists)) OR (NOT (NOT name exists) AND NOT age exists) + // (NOT name exists AND age exists) OR (name exists AND NOT age exists) + // Matches: doc3, doc4 + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc4) + } + + @Test + fun `and not exists exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) // Match + val doc5 = doc("users/e", 1000, mapOf("other" to true)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(and(not(exists(field("name"))), exists(field("age")))) + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4) + } + + @Test + fun `or not exists exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) // Match + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) // Match + val doc5 = doc("users/e", 1000, mapOf("other" to true)) // Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(or(not(exists(field("name"))), exists(field("age")))) + // (NOT name exists) OR (age exists) + // Matches: doc1, doc2, doc4, doc5 + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2, doc4, doc5) + } + + @Test + fun `xor not exists exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) // Match + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) + val doc4 = doc("users/d", 1000, mapOf("age" to 30.0)) + val doc5 = doc("users/e", 1000, mapOf("other" to true)) // Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(xor(not(exists(field("name"))), exists(field("age")))) + // (NOT name exists AND NOT age exists) OR (name exists AND age exists) + // Matches: doc1, doc2, doc5 + val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2, doc5) + } + + @Test + fun `and expression logically equivalent to separated stages`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("a" to 1L, "b" to 1L)) + val doc2 = doc("users/b", 1000, mapOf("a" to 1L, "b" to 2L)) // Match + val doc3 = doc("users/c", 1000, mapOf("a" to 2L, "b" to 2L)) + val documents = listOf(doc1, doc2, doc3) + + val equalityArgument1 = field("a").eq(1L) + val equalityArgument2 = field("b").eq(2L) + + // Combined AND + val pipelineAnd1 = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(and(equalityArgument1, equalityArgument2)) + val resultAnd1 = runPipeline(pipelineAnd1, flowOf(*documents.toTypedArray())).toList() + assertThat(resultAnd1).containsExactly(doc2) + + // Combined AND (reversed order) + val pipelineAnd2 = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(and(equalityArgument2, equalityArgument1)) + val resultAnd2 = runPipeline(pipelineAnd2, flowOf(*documents.toTypedArray())).toList() + assertThat(resultAnd2).containsExactly(doc2) + + // Separate Stages + val pipelineSep1 = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(equalityArgument1) + .where(equalityArgument2) + val resultSep1 = runPipeline(pipelineSep1, flowOf(*documents.toTypedArray())).toList() + assertThat(resultSep1).containsExactly(doc2) + + // Separate Stages (reversed order) + val pipelineSep2 = + RealtimePipelineSource(TestUtil.firestore()) + .collection("users") + .where(equalityArgument2) + .where(equalityArgument1) + val resultSep2 = runPipeline(pipelineSep2, flowOf(*documents.toTypedArray())).toList() + assertThat(resultSep2).containsExactly(doc2) + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt index aca2f903848..f17fa1ca199 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt @@ -15,6 +15,7 @@ package com.google.firebase.firestore.pipeline import com.google.common.truth.Truth.assertWithMessage +import com.google.firebase.firestore.AbstractPipeline import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.model.DatabaseId import com.google.firebase.firestore.model.MutableDocument @@ -22,6 +23,7 @@ import com.google.firebase.firestore.model.Values.NULL_VALUE import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.testutil.TestUtilKtx.doc import com.google.firestore.v1.Value +import kotlinx.coroutines.flow.Flow val DATABASE_ID = UserDataReader(DatabaseId.forDatabase("project", "(default)")) val EMPTY_DOC: MutableDocument = doc("foo/1", 0, mapOf()) @@ -70,3 +72,13 @@ internal fun assertEvaluatesToUnset(result: EvaluateResult, format: String, vara internal fun assertEvaluatesToError(result: EvaluateResult, format: String, vararg args: Any?) { assertWithMessage(format, *args).that(result).isSameInstanceAs(EvaluateResultError) } + +internal fun runPipeline( + pipeline: AbstractPipeline, + input: Flow +): Flow { + val context = EvaluationContext(pipeline.userDataReader) + return pipeline.stages.fold(input) { documentFlow, stage -> + stage.evaluate(context, documentFlow) + } +} From bbe85f4a613c4ed2b972b6e1a346c513f1179e09 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 4 Jun 2025 16:55:07 -0400 Subject: [PATCH 100/152] Sort tests --- .../firebase/firestore/DocumentReference.java | 2 +- .../com/google/firebase/firestore/Pipeline.kt | 3 + .../firebase/firestore/pipeline/evaluation.kt | 6 +- .../firestore/pipeline/expressions.kt | 23 +- .../firebase/firestore/pipeline/stage.kt | 14 + .../firestore/pipeline/PipelineTests.kt | 3 +- .../firebase/firestore/pipeline/SortTests.kt | 803 ++++++++++++++++++ .../firebase/firestore/pipeline/WhereTests.kt | 69 +- .../firebase/firestore/pipeline/testUtil.kt | 18 +- .../com/google/firebase/firestore/testUtil.kt | 16 + 10 files changed, 898 insertions(+), 59 deletions(-) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/SortTests.kt create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/testUtil.kt diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java index f31e3103060..0509e9a94c5 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java @@ -71,7 +71,7 @@ public final class DocumentReference { } /** @hide */ - static DocumentReference forPath(ResourcePath path, FirebaseFirestore firestore) { + public static DocumentReference forPath(ResourcePath path, FirebaseFirestore firestore) { if (path.length() % 2 != 0) { throw new IllegalArgumentException( "Invalid document reference. Document references must have an even number " diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index c15bf2300f1..7b08760b7d7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -786,6 +786,9 @@ internal constructor( fun select(fieldName: String, vararg additionalSelections: Any): RealtimePipeline = append(SelectStage.of(fieldName, *additionalSelections)) + fun sort(order: Ordering, vararg additionalOrders: Ordering): RealtimePipeline = + append(SortStage(arrayOf(order, *additionalOrders))) + fun where(condition: BooleanExpr): RealtimePipeline = append(WhereStage(condition)) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index 3f473fd93a9..5bf6aee43ec 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -20,6 +20,7 @@ import com.google.common.math.LongMath import com.google.common.math.LongMath.checkedAdd import com.google.common.math.LongMath.checkedMultiply import com.google.common.math.LongMath.checkedSubtract +import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values @@ -42,7 +43,10 @@ import kotlin.math.log10 import kotlin.math.pow import kotlin.math.sqrt -internal class EvaluationContext(val userDataReader: UserDataReader) +internal class EvaluationContext( + val db: FirebaseFirestore, + val userDataReader: UserDataReader +) internal typealias EvaluateDocument = (input: MutableDocument) -> EvaluateResult 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 0764af06a9c..a16b2b36cdf 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 @@ -23,6 +23,9 @@ import com.google.firebase.firestore.Pipeline import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.VectorValue import com.google.firebase.firestore.model.DocumentKey +import com.google.firebase.firestore.model.FieldPath.CREATE_TIME_PATH +import com.google.firebase.firestore.model.FieldPath.KEY_PATH +import com.google.firebase.firestore.model.FieldPath.UPDATE_TIME_PATH import com.google.firebase.firestore.model.FieldPath as ModelFieldPath import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values @@ -295,10 +298,12 @@ abstract class Expr internal constructor() { */ @JvmStatic fun field(name: String): Field { - if (name == DocumentKey.KEY_FIELD_NAME) { - return Field(ModelFieldPath.KEY_PATH) + return when (name) { + DocumentKey.KEY_FIELD_NAME -> Field(KEY_PATH) + ModelFieldPath.CREATE_TIME_NAME -> Field(CREATE_TIME_PATH) + ModelFieldPath.UPDATE_TIME_NAME -> Field(UPDATE_TIME_PATH) + else -> Field(FieldPath.fromDotSeparatedPath(name).internalPath) } - return Field(FieldPath.fromDotSeparatedPath(name).internalPath) } /** @@ -4189,11 +4194,13 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select internal fun toProto(): Value = Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() - override fun evaluateContext(context: EvaluationContext) = ::evaluateInternal - - private fun evaluateInternal(input: MutableDocument): EvaluateResult { - val value: Value? = input.getField(fieldPath) - return if (value === null) EvaluateResultUnset else EvaluateResultValue(value) + override fun evaluateContext(context: EvaluationContext) = block@{ input: MutableDocument -> + EvaluateResultValue(when (fieldPath) { + KEY_PATH -> encodeValue(DocumentReference.forPath(input.key.path, context.db)) + CREATE_TIME_PATH -> encodeValue(input.createTime.timestamp) + UPDATE_TIME_PATH -> encodeValue(input.version.timestamp) + else -> input.getField(fieldPath) ?: return@block EvaluateResultUnset + }) } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt index e6ae26451f3..ed52a43e3dd 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/stage.kt @@ -30,7 +30,9 @@ import com.google.firestore.v1.Value import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList sealed class BaseStage>( @@ -244,6 +246,13 @@ private constructor(private val collectionId: String, options: InternalOptions) override fun self(options: InternalOptions) = CollectionGroupSource(collectionId, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setReferenceValue("").build(), encodeValue(collectionId)) + override fun evaluate( + context: EvaluationContext, + inputs: Flow + ): Flow { + // TODO: Does this need to do more? + return inputs + } companion object { @@ -511,6 +520,11 @@ internal class LimitStage internal constructor(private val limit: Int, options: InternalOptions = InternalOptions.EMPTY) : BaseStage("limit", options) { override fun self(options: InternalOptions) = LimitStage(limit, options) + override fun evaluate( + context: EvaluationContext, + inputs: Flow + ): Flow = if (limit > 0) inputs.take(limit) else flowOf() + override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(limit)) } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/PipelineTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/PipelineTests.kt index 5b750560f90..ace8254c152 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/PipelineTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/PipelineTests.kt @@ -19,6 +19,7 @@ import com.google.firebase.firestore.RealtimePipelineSource import com.google.firebase.firestore.TestUtil import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.runPipeline import com.google.firebase.firestore.testutil.TestUtilKtx.doc import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList @@ -39,7 +40,7 @@ internal class PipelineTests { val doc2: MutableDocument = doc("foo/2", 0, mapOf("bar" to "43")) val doc3: MutableDocument = doc("xxx/1", 0, mapOf("bar" to 42)) - val list = runPipeline(pipeline, flowOf(doc1, doc2, doc3)).toList() + val list = runPipeline(firestore, pipeline, flowOf(doc1, doc2, doc3)).toList() assertThat(list).hasSize(1) } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/SortTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/SortTests.kt new file mode 100644 index 00000000000..f4cb82549ca --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/SortTests.kt @@ -0,0 +1,803 @@ +// Copyright 2025 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.firebase.firestore.pipeline + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.firestore.RealtimePipelineSource +import com.google.firebase.firestore.TestUtil +import com.google.firebase.firestore.model.MutableDocument +import com.google.firebase.firestore.pipeline.Expr.Companion.add +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.exists +import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.pipeline.Expr.Companion.not +import com.google.firebase.firestore.runPipeline +import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import com.google.firebase.firestore.FieldPath as PublicFieldPath + +@RunWith(RobolectricTestRunner::class) +internal class SortTests { + + private val db = TestUtil.firestore() + + @Test + fun `empty ascending`(): Unit = runBlocking { + val documents = emptyList() + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .sort(field("age").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `empty descending`(): Unit = runBlocking { + val documents = emptyList() + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .sort(field("age").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `single result ascending`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) + val documents = listOf(doc1) + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .sort(field("age").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `single result ascending explicit exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) + val documents = listOf(doc1) + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field("age"))) + .sort(field("age").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `single result ascending explicit not exists empty`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) + val documents = listOf(doc1) + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(not(exists(field("age")))) + .sort(field("age").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `single result ascending implicit exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) + val documents = listOf(doc1) + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(field("age").eq(10L)) + .sort(field("age").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `single result descending`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) + val documents = listOf(doc1) + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .sort(field("age").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `single result descending explicit exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) + val documents = listOf(doc1) + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field("age"))) + .sort(field("age").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `single result descending implicit exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) + val documents = listOf(doc1) + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(field("age").eq(10L)) + .sort(field("age").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `multiple results ambiguous order`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .sort(field("age").descending()) + + // Order: doc3 (100.0), doc1 (75.5), doc2 (25.0), then doc4 and doc5 (10.0) are ambiguous + // Firestore backend sorts by document key as a tie-breaker. + // So expected order: doc3, doc1, doc2, doc4, doc5 (if 'd' < 'e') or doc3, doc1, doc2, doc5, doc4 (if 'e' < 'd') + // Since the C++ test uses UnorderedElementsAre, we'll use containsExactlyElementsIn. + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc3, doc1, doc2, doc4, doc5)).inOrder() + // Actually, the local pipeline implementation might not guarantee tie-breaking by key unless explicitly added. + // The C++ test uses UnorderedElementsAre, which means the exact order of doc4 and doc5 is not tested. + // Let's stick to what the C++ test implies: the overall set is correct, but the order of tied elements is not strictly defined by this single sort. + // However, the local pipeline *does* sort by key as a final tie-breaker. + // Expected: doc3 (100.0), doc1 (75.5), doc2 (25.0), doc4 (10.0, key d), doc5 (10.0, key e) + // So the order should be doc3, doc1, doc2, doc4, doc5 + assertThat(result).containsExactly(doc3, doc1, doc2, doc4, doc5).inOrder() + } + + @Test + fun `multiple results ambiguous order explicit exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field("age"))) + .sort(field("age").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc1, doc2, doc4, doc5).inOrder() + } + + @Test + fun `multiple results ambiguous order implicit exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(field("age").gt(0.0)) + .sort(field("age").descending()) + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc1, doc2, doc4, doc5).inOrder() + } + + @Test + fun `multiple results full order`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .sort(field("age").descending(), field("name").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + // age desc: 100(c), 75.5(a), 25(b), 10(d), 10(e) + // name asc for 10: diane(d), eric(e) + // Expected: c, a, b, d, e + assertThat(result).containsExactly(doc3, doc1, doc2, doc4, doc5).inOrder() + } + + @Test + fun `multiple results full order explicit exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field("age"))) + .where(exists(field("name"))) + .sort(field("age").descending(), field("name").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc1, doc2, doc4, doc5).inOrder() + } + + @Test + fun `multiple results full order explicit not exists empty`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob")) + val doc3 = doc("users/c", 1000, mapOf("age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("other_name" to "diane")) // Matches + val doc5 = doc("users/e", 1000, mapOf("other_age" to 10.0)) // Matches + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(not(exists(field("age")))) + .where(not(exists(field("name")))) + .sort(field("age").descending(), field("name").ascending()) + // Filtered: doc4, doc5 + // Sort by missing age (no op), then missing name (no op), then by key ascending. + // d < e + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4, doc5).inOrder() + } + + @Test + fun `multiple results full order implicit exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(field("age").eq(field("age"))) // Implicit exists age + .where(field("name").regexMatch(".*")) // Implicit exists name + .sort(field("age").descending(), field("name").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc1, doc2, doc4, doc5).inOrder() + } + + @Test + fun `multiple results full order partial explicit exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field("name"))) + .sort(field("age").descending(), field("name").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc1, doc2, doc4, doc5).inOrder() + } + + @Test + fun `multiple results full order partial explicit not exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("age" to 25.0)) // name missing -> Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // age missing, name exists + val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing, name exists + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(not(exists(field("name")))) // Only doc2 matches + .sort(field("age").descending(), field("name").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc2) + } + + @Test + fun `multiple results full order partial explicit not exists sort on non exist field first`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("age" to 25.0)) // name missing -> Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // age missing, name exists + val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing, name exists + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(not(exists(field("name")))) // Only doc2 matches + .sort(field("name").descending(), field("age").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc2) + } + + @Test + fun `multiple results full order partial implicit exists`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(field("name").regexMatch(".*")) + .sort(field("age").descending(), field("name").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc1, doc2, doc4, doc5).inOrder() + } + + @Test + fun `missing field all fields`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .sort(field("not_age").descending()) + + // Sorting by a missing field results in undefined order relative to each other, + // but documents are secondarily sorted by key. + // Since it's descending for not_age (all are null essentially), key order will be ascending. + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2, doc3, doc4, doc5).inOrder() + } + + @Test + fun `missing field with exist empty`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val documents = listOf(doc1) + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field("not_age"))) + .sort(field("not_age").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `missing field partial fields`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob")) // age missing + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // age missing + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .sort(field("age").ascending()) + + // Missing fields sort first in ascending order, then by key. b < d + // Then existing fields sorted by value: e (10.0) < a (75.5) < c (100.0) + // Expected: doc2, doc4, doc5, doc1, doc3 + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc2, doc4, doc5, doc1, doc3).inOrder() + } + + @Test + fun `missing field partial fields with exist`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob")) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field("age"))) // Filters to doc1, doc3, doc5 + .sort(field("age").ascending()) + + // Sort remaining: doc5 (10.0), doc1 (75.5), doc3 (100.0) + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc5, doc1, doc3).inOrder() + } + + @Test + fun `missing field partial fields with not exist`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob")) // Match + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // Match + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(not(exists(field("age")))) // Filters to doc2, doc4 + .sort(field("age").ascending()) // Sort by non-existent field, then key + + // Sort remaining by key: doc2, doc4 + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc2, doc4).inOrder() + } + + @Test + fun `limit after sort`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .sort(field("age").ascending()) // Sort: d, e, b, a, c (key tie-break for d,e) + .limit(2) + + // Expected: doc4, doc5 + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4, doc5).inOrder() + } + + @Test + fun `limit after sort with exist`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("age" to 25.0)) // name missing + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // age missing + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field("age"))) // Filter: a, b, c, e + .sort(field("age").ascending()) // Sort: e (10), b (25), a (75.5), c (100) + .limit(2) // Limit 2: e, b + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc5, doc2).inOrder() + } + + @Test + fun `limit after sort with not exist`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("age" to 25.0)) // name missing + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // age missing -> Match + val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing -> Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(not(exists(field("age")))) // Filter: d, e + .sort(field("age").ascending()) // Sort by missing field -> key order: d, e + .limit(2) // Limit 2: d, e + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4, doc5).inOrder() + } + + @Test + fun `limit zero after sort`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val documents = listOf(doc1) + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .sort(field("age").ascending()) + .limit(0) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `limit before sort`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 10.0)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + // Note: Limit before sort has different semantics online vs offline. + // Offline evaluation applies limit first based on implicit key order. + val pipeline = + RealtimePipelineSource(db) + .collectionGroup("users") // C++ test uses CollectionGroupSource here + .limit(1) // Limits to doc1 (key "users/a" is first by default key order) + .sort(field("age").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `limit before sort with exist`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collectionGroup("users") + .where(exists(field("age"))) // Filter: a,b,c,e. Implicit key order: a,b,c,e + .limit(1) // Limits to doc1 (key "users/a") + .sort(field("age").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `limit before sort with not exist`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // Match + val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collectionGroup("users") + .where(not(exists(field("age")))) // Filter: d, e. Implicit key order: d, e + .limit(1) // Limits to doc4 (key "users/d") + .sort(field("age").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4) + } + + @Test + fun `limit before not exist filter`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("age" to 25.0)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collectionGroup("users") + .limit(2) // Limit to a, b (by key) + .where(not(exists(field("age")))) // Filter out a, b (both have age) + .sort(field("age").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `limit zero before sort`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val documents = listOf(doc1) + val pipeline = + RealtimePipelineSource(db) + .collectionGroup("users") + .limit(0) + .sort(field("age").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `sort expression`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 30L)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 50L)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane", "age" to 40L)) + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 20L)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collectionGroup("users") + .sort(add(field("age"), constant(10L)).descending()) // age + 10 + + // Sort by (age+10) desc: + // doc3: 50+10 = 60 + // doc4: 40+10 = 50 + // doc2: 30+10 = 40 + // doc5: 20+10 = 30 + // doc1: 10+10 = 20 + // Expected: doc3, doc4, doc2, doc5, doc1 + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc4, doc2, doc5, doc1).inOrder() + } + + @Test + fun `sort expression with exist`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) + val doc2 = doc("users/b", 1000, mapOf("age" to 30L)) // name missing + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 50L)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // age missing + val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 20L)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collectionGroup("users") + .where(exists(field("age"))) // Filter: a, b, c, e + .sort(add(field("age"), constant(10L)).descending()) + + // Filtered: doc1 (10), doc2 (30), doc3 (50), doc5 (20) + // Sort by (age+10) desc: + // doc3: 50+10 = 60 + // doc2: 30+10 = 40 + // doc5: 20+10 = 30 + // doc1: 10+10 = 20 + // Expected: doc3, doc2, doc5, doc1 + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc2, doc5, doc1).inOrder() + } + + @Test + fun `sort expression with not exist`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) + val doc2 = doc("users/b", 1000, mapOf("age" to 30L)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 50L)) + val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // age missing -> Match + val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing -> Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collectionGroup("users") + .where(not(exists(field("age")))) // Filter: d, e + .sort(add(field("age"), constant(10L)).descending()) // Sort by missing field -> key order + + // Filtered: doc4, doc5 + // Sort by (age+10) desc where age is missing. This means they are treated as null for the expression. + // Then tie-broken by key: d, e + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4, doc5).inOrder() + } + + @Test + fun `sort on path and other field on different stages`(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("name" to "alice", "age" to 40L)) + val doc2 = doc("users/2", 1000, mapOf("name" to "bob", "age" to 30L)) + val doc3 = doc("users/3", 1000, mapOf("name" to "charlie", "age" to 50L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field(PublicFieldPath.documentId()))) // Ensure __name__ is considered + .sort(field(PublicFieldPath.documentId()).ascending()) // Sort by key: 1, 2, 3 + .sort(field("age").ascending()) // Sort by age: 2(30), 1(40), 3(50) - Last sort takes precedence + + // The C++ test implies that the *last* sort stage defines the primary sort order. + // This is different from how multiple orderBy clauses usually work in Firestore (they form a composite sort). + // However, if these are separate stages, the last one would indeed re-sort the entire output of the previous. + // Let's assume the Kotlin pipeline behaves this way for separate .orderBy() calls. + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc2, doc1, doc3).inOrder() + } + + @Test + fun `sort on other field and path on different stages`(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("name" to "alice", "age" to 40L)) + val doc2 = doc("users/2", 1000, mapOf("name" to "bob", "age" to 30L)) + val doc3 = doc("users/3", 1000, mapOf("name" to "charlie", "age" to 50L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field(PublicFieldPath.documentId()))) + .sort(field("age").ascending()) // Sort by age: 2(30), 1(40), 3(50) + .sort(field(PublicFieldPath.documentId()).ascending()) // Sort by key: 1, 2, 3 + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2, doc3).inOrder() + } + + // The C++ tests `SortOnKeyAndOtherFieldOnMultipleStages` and `SortOnOtherFieldAndKeyOnMultipleStages` + // are identical to the `Path` versions because `kDocumentKeyPath` is used. + // These are effectively duplicates of the above two tests in Kotlin if we use `PublicFieldPath.documentId()`. + // I'll include them for completeness, mirroring the C++ structure. + + @Test + fun `sort on key and other field on multiple stages`(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("name" to "alice", "age" to 40L)) + val doc2 = doc("users/2", 1000, mapOf("name" to "bob", "age" to 30L)) + val doc3 = doc("users/3", 1000, mapOf("name" to "charlie", "age" to 50L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field(PublicFieldPath.documentId()))) + .sort(field(PublicFieldPath.documentId()).ascending()) + .sort(field("age").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc2, doc1, doc3).inOrder() + } + + @Test + fun `sort on other field and key on multiple stages`(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("name" to "alice", "age" to 40L)) + val doc2 = doc("users/2", 1000, mapOf("name" to "bob", "age" to 30L)) + val doc3 = doc("users/3", 1000, mapOf("name" to "charlie", "age" to 50L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(exists(field(PublicFieldPath.documentId()))) + .sort(field("age").ascending()) + .sort(field(PublicFieldPath.documentId()).ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc2, doc3).inOrder() + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt index 0276c937dd7..45cc4f1b6b8 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt @@ -27,6 +27,7 @@ import com.google.firebase.firestore.pipeline.Expr.Companion.field import com.google.firebase.firestore.pipeline.Expr.Companion.not import com.google.firebase.firestore.pipeline.Expr.Companion.or import com.google.firebase.firestore.pipeline.Expr.Companion.xor +import com.google.firebase.firestore.runPipeline import com.google.firebase.firestore.testutil.TestUtilKtx.doc import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList @@ -38,13 +39,15 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) internal class WhereTests { + private val db = TestUtil.firestore() + @Test fun `empty database returns no results`(): Unit = runBlocking { val documents = emptyList() val pipeline = RealtimePipelineSource(TestUtil.firestore()).collection("users").where(field("age").gte(10L)) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).isEmpty() } @@ -63,7 +66,7 @@ internal class WhereTests { .where(and(field("age").gte(10.0), field("age").gte(20.0))) // age >= 10.0 AND age >= 20.0 => age >= 20.0 // Matches: doc1 (75.5), doc2 (25.0), doc3 (100.0) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1, doc2, doc3) } @@ -82,8 +85,8 @@ internal class WhereTests { .collection("users") .where(constant(25.0).eq(field("age"))) - val result1 = runPipeline(pipeline1, flowOf(*documents.toTypedArray())).toList() - val result2 = runPipeline(pipeline2, flowOf(*documents.toTypedArray())).toList() + val result1 = runPipeline(db, pipeline1, flowOf(*documents.toTypedArray())).toList() + val result2 = runPipeline(db, pipeline2, flowOf(*documents.toTypedArray())).toList() assertThat(result1).containsExactly(doc2) assertThat(result1).isEqualTo(result2) @@ -106,8 +109,8 @@ internal class WhereTests { .collection("users") .where(and(field("age").lt(70.0), field("age").gt(10.0))) - val result1 = runPipeline(pipeline1, flowOf(*documents.toTypedArray())).toList() - val result2 = runPipeline(pipeline2, flowOf(*documents.toTypedArray())).toList() + val result1 = runPipeline(db, pipeline1, flowOf(*documents.toTypedArray())).toList() + val result2 = runPipeline(db, pipeline2, flowOf(*documents.toTypedArray())).toList() assertThat(result1).containsExactly(doc2) assertThat(result1).isEqualTo(result2) @@ -129,8 +132,8 @@ internal class WhereTests { RealtimePipelineSource(TestUtil.firestore()) .collection("users") .where(or(field("age").gt(80.0), field("age").lt(10.0))) - val result1 = runPipeline(pipeline1, flowOf(*documents.toTypedArray())).toList() - val result2 = runPipeline(pipeline2, flowOf(*documents.toTypedArray())).toList() + val result1 = runPipeline(db, pipeline1, flowOf(*documents.toTypedArray())).toList() + val result2 = runPipeline(db, pipeline2, flowOf(*documents.toTypedArray())).toList() assertThat(result1).containsExactly(doc3) assertThat(result1).isEqualTo(result2) @@ -155,8 +158,8 @@ internal class WhereTests { .collection("users") .where(eqAny(field("name"), array(values))) - val result1 = runPipeline(pipeline1, flowOf(*documents.toTypedArray())).toList() - val result2 = runPipeline(pipeline2, flowOf(*documents.toTypedArray())).toList() + val result1 = runPipeline(db, pipeline1, flowOf(*documents.toTypedArray())).toList() + val result2 = runPipeline(db, pipeline2, flowOf(*documents.toTypedArray())).toList() assertThat(result1).containsExactly(doc1) assertThat(result1).isEqualTo(result2) @@ -179,7 +182,7 @@ internal class WhereTests { // age >= 10.0 THEN age >= 20.0 => age >= 20.0 // Matches: doc1 (75.5), doc2 (25.0), doc3 (100.0) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1, doc2, doc3) } @@ -198,7 +201,7 @@ internal class WhereTests { .where(field("age").eq(75L)) .where(field("height").eq(55L)) // 55L will also match 55.0 - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc3) } @@ -223,7 +226,7 @@ internal class WhereTests { // doc3: 75 > 50 (T) AND 55.0 < 75 (T) -> True // doc4: 41 > 50 (F) // doc5: 75 > 50 (T) AND 80 < 75 (F) -> False - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1, doc3) } @@ -250,7 +253,7 @@ internal class WhereTests { // doc3: charlie (yes), baker (yes) -> Match // doc4: diane (yes), miller (yes) -> Match // doc5: eric (no), davis (no) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc3, doc4) } @@ -301,7 +304,7 @@ internal class WhereTests { // doc3: 75==75 (T), 50>45 (T), baker ends er (T) -> True // doc4: 75==75 (T), 50>45 (T), miller ends er (T) -> True // doc5: 80==75 (F) -> False - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc3, doc4) } @@ -317,7 +320,7 @@ internal class WhereTests { val pipeline = RealtimePipelineSource(TestUtil.firestore()).collection("users").where(exists(field("name"))) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1, doc2, doc3) } @@ -335,7 +338,7 @@ internal class WhereTests { .collection("users") .where(not(exists(field("name")))) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc4, doc5) } @@ -353,7 +356,7 @@ internal class WhereTests { .collection("users") .where(not(not(exists(field("name"))))) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1, doc2, doc3) } @@ -370,7 +373,7 @@ internal class WhereTests { RealtimePipelineSource(TestUtil.firestore()) .collection("users") .where(and(exists(field("name")), exists(field("age")))) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1, doc2) } @@ -387,7 +390,7 @@ internal class WhereTests { RealtimePipelineSource(TestUtil.firestore()) .collection("users") .where(or(exists(field("name")), exists(field("age")))) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1, doc2, doc3, doc4) } @@ -404,7 +407,7 @@ internal class WhereTests { RealtimePipelineSource(TestUtil.firestore()) .collection("users") .where(not(and(exists(field("name")), exists(field("age"))))) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc3, doc4, doc5) } @@ -421,7 +424,7 @@ internal class WhereTests { RealtimePipelineSource(TestUtil.firestore()) .collection("users") .where(not(or(exists(field("name")), exists(field("age"))))) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc5) } @@ -441,7 +444,7 @@ internal class WhereTests { // NOT ( (name exists AND NOT age exists) OR (NOT name exists AND age exists) ) // = (name exists AND age exists) OR (NOT name exists AND NOT age exists) // Matches: doc1, doc2, doc5 - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1, doc2, doc5) } @@ -458,7 +461,7 @@ internal class WhereTests { RealtimePipelineSource(TestUtil.firestore()) .collection("users") .where(and(not(exists(field("name"))), not(exists(field("age"))))) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc5) } @@ -475,7 +478,7 @@ internal class WhereTests { RealtimePipelineSource(TestUtil.firestore()) .collection("users") .where(or(not(exists(field("name"))), not(exists(field("age"))))) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc3, doc4, doc5) } @@ -495,7 +498,7 @@ internal class WhereTests { // (NOT name exists AND NOT (NOT age exists)) OR (NOT (NOT name exists) AND NOT age exists) // (NOT name exists AND age exists) OR (name exists AND NOT age exists) // Matches: doc3, doc4 - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc3, doc4) } @@ -512,7 +515,7 @@ internal class WhereTests { RealtimePipelineSource(TestUtil.firestore()) .collection("users") .where(and(not(exists(field("name"))), exists(field("age")))) - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc4) } @@ -531,7 +534,7 @@ internal class WhereTests { .where(or(not(exists(field("name"))), exists(field("age")))) // (NOT name exists) OR (age exists) // Matches: doc1, doc2, doc4, doc5 - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1, doc2, doc4, doc5) } @@ -550,7 +553,7 @@ internal class WhereTests { .where(xor(not(exists(field("name"))), exists(field("age")))) // (NOT name exists AND NOT age exists) OR (name exists AND age exists) // Matches: doc1, doc2, doc5 - val result = runPipeline(pipeline, flowOf(*documents.toTypedArray())).toList() + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1, doc2, doc5) } @@ -569,7 +572,7 @@ internal class WhereTests { RealtimePipelineSource(TestUtil.firestore()) .collection("users") .where(and(equalityArgument1, equalityArgument2)) - val resultAnd1 = runPipeline(pipelineAnd1, flowOf(*documents.toTypedArray())).toList() + val resultAnd1 = runPipeline(db, pipelineAnd1, flowOf(*documents.toTypedArray())).toList() assertThat(resultAnd1).containsExactly(doc2) // Combined AND (reversed order) @@ -577,7 +580,7 @@ internal class WhereTests { RealtimePipelineSource(TestUtil.firestore()) .collection("users") .where(and(equalityArgument2, equalityArgument1)) - val resultAnd2 = runPipeline(pipelineAnd2, flowOf(*documents.toTypedArray())).toList() + val resultAnd2 = runPipeline(db, pipelineAnd2, flowOf(*documents.toTypedArray())).toList() assertThat(resultAnd2).containsExactly(doc2) // Separate Stages @@ -586,7 +589,7 @@ internal class WhereTests { .collection("users") .where(equalityArgument1) .where(equalityArgument2) - val resultSep1 = runPipeline(pipelineSep1, flowOf(*documents.toTypedArray())).toList() + val resultSep1 = runPipeline(db, pipelineSep1, flowOf(*documents.toTypedArray())).toList() assertThat(resultSep1).containsExactly(doc2) // Separate Stages (reversed order) @@ -595,7 +598,7 @@ internal class WhereTests { .collection("users") .where(equalityArgument2) .where(equalityArgument1) - val resultSep2 = runPipeline(pipelineSep2, flowOf(*documents.toTypedArray())).toList() + val resultSep2 = runPipeline(db, pipelineSep2, flowOf(*documents.toTypedArray())).toList() assertThat(resultSep2).containsExactly(doc2) } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt index f17fa1ca199..58dd9778d51 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt @@ -15,19 +15,16 @@ package com.google.firebase.firestore.pipeline import com.google.common.truth.Truth.assertWithMessage -import com.google.firebase.firestore.AbstractPipeline -import com.google.firebase.firestore.UserDataReader -import com.google.firebase.firestore.model.DatabaseId +import com.google.firebase.firestore.TestUtil.FIRESTORE +import com.google.firebase.firestore.TestUtil.USER_DATA_READER import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values.NULL_VALUE import com.google.firebase.firestore.model.Values.encodeValue import com.google.firebase.firestore.testutil.TestUtilKtx.doc import com.google.firestore.v1.Value -import kotlinx.coroutines.flow.Flow -val DATABASE_ID = UserDataReader(DatabaseId.forDatabase("project", "(default)")) val EMPTY_DOC: MutableDocument = doc("foo/1", 0, mapOf()) -internal val EVALUATION_CONTEXT = EvaluationContext(DATABASE_ID) +internal val EVALUATION_CONTEXT: EvaluationContext = EvaluationContext(FIRESTORE, USER_DATA_READER) internal fun evaluate(expr: Expr): EvaluateResult = evaluate(expr, EMPTY_DOC) @@ -73,12 +70,3 @@ internal fun assertEvaluatesToError(result: EvaluateResult, format: String, vara assertWithMessage(format, *args).that(result).isSameInstanceAs(EvaluateResultError) } -internal fun runPipeline( - pipeline: AbstractPipeline, - input: Flow -): Flow { - val context = EvaluationContext(pipeline.userDataReader) - return pipeline.stages.fold(input) { documentFlow, stage -> - stage.evaluate(context, documentFlow) - } -} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/testUtil.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/testUtil.kt new file mode 100644 index 00000000000..fb757de4fca --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/testUtil.kt @@ -0,0 +1,16 @@ +package com.google.firebase.firestore + +import com.google.firebase.firestore.model.MutableDocument +import com.google.firebase.firestore.pipeline.EvaluationContext +import kotlinx.coroutines.flow.Flow + +internal fun runPipeline( + db: FirebaseFirestore, + pipeline: AbstractPipeline, + input: Flow +): Flow { + val context = EvaluationContext(db, db.userDataReader) + return pipeline.stages.fold(input) { documentFlow, stage -> + stage.evaluate(context, documentFlow) + } +} \ No newline at end of file From 77dfc074fee3cadb84ede438cb914290f9c5ba78 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 4 Jun 2025 17:05:19 -0400 Subject: [PATCH 101/152] Fixes --- .../firebase/firestore/model/Document.java | 2 + .../firebase/firestore/model/FieldPath.java | 5 ++ .../firestore/model/MutableDocument.java | 6 +++ .../firebase/firestore/pipeline/evaluation.kt | 5 +- .../firestore/pipeline/expressions.kt | 51 +++++++------------ 5 files changed, 32 insertions(+), 37 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Document.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Document.java index 75b7dc3dbb4..0696ef97f6c 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Document.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Document.java @@ -35,6 +35,8 @@ public interface Document { */ SnapshotVersion getVersion(); + SnapshotVersion getCreateTime(); + /** * Returns the timestamp at which this document was read from the remote server. Returns * `SnapshotVersion.NONE` for documents created by the user. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java index 051dfce922b..e4176a42e17 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java @@ -22,7 +22,12 @@ /** A dot separated path for navigating sub-objects with in a document */ public final class FieldPath extends BasePath { + public static final String UPDATE_TIME_NAME = "__update_time__"; + public static final String CREATE_TIME_NAME = "__create_time__"; + public static final FieldPath KEY_PATH = fromSingleSegment(DocumentKey.KEY_FIELD_NAME); + public static final FieldPath UPDATE_TIME_PATH = fromSingleSegment(UPDATE_TIME_NAME); + public static final FieldPath CREATE_TIME_PATH = fromSingleSegment(CREATE_TIME_NAME); public static final FieldPath EMPTY_PATH = new FieldPath(Collections.emptyList()); private FieldPath(List segments) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/MutableDocument.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/MutableDocument.java index 96ed610fd79..55f53480316 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/MutableDocument.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/MutableDocument.java @@ -63,6 +63,7 @@ private enum DocumentState { private DocumentType documentType; private SnapshotVersion version; private SnapshotVersion readTime; + private SnapshotVersion createTime; private ObjectValue value; private DocumentState documentState; @@ -173,6 +174,11 @@ public DocumentKey getKey() { return key; } + @Override + public SnapshotVersion getCreateTime() { + return createTime; + } + @Override public SnapshotVersion getVersion() { return version; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt index 5bf6aee43ec..f7aee53825e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt @@ -43,10 +43,7 @@ import kotlin.math.log10 import kotlin.math.pow import kotlin.math.sqrt -internal class EvaluationContext( - val db: FirebaseFirestore, - val userDataReader: UserDataReader -) +internal class EvaluationContext(val db: FirebaseFirestore, val userDataReader: UserDataReader) internal typealias EvaluateDocument = (input: MutableDocument) -> EvaluateResult 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 a16b2b36cdf..6f7d69f5655 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 @@ -23,10 +23,10 @@ import com.google.firebase.firestore.Pipeline import com.google.firebase.firestore.UserDataReader import com.google.firebase.firestore.VectorValue import com.google.firebase.firestore.model.DocumentKey +import com.google.firebase.firestore.model.FieldPath as ModelFieldPath import com.google.firebase.firestore.model.FieldPath.CREATE_TIME_PATH import com.google.firebase.firestore.model.FieldPath.KEY_PATH import com.google.firebase.firestore.model.FieldPath.UPDATE_TIME_PATH -import com.google.firebase.firestore.model.FieldPath as ModelFieldPath import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.model.Values.encodeValue @@ -1022,8 +1022,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the 'IN' comparison. */ @JvmStatic - fun eqAny(expression: Expr, values: List): BooleanExpr = - eqAny(expression, array(values)) + fun eqAny(expression: Expr, values: List): BooleanExpr = eqAny(expression, array(values)) /** * Creates an expression that checks if an [expression], when evaluated, is equal to any of the @@ -1047,8 +1046,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the 'IN' comparison. */ @JvmStatic - fun eqAny(fieldName: String, values: List): BooleanExpr = - eqAny(fieldName, array(values)) + fun eqAny(fieldName: String, values: List): BooleanExpr = eqAny(fieldName, array(values)) /** * Creates an expression that checks if a field's value is equal to any of the elements of @@ -2780,8 +2778,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the arrayContainsAll operation. */ @JvmStatic - fun arrayContainsAll(array: Expr, values: List) = - arrayContainsAll(array, array(values)) + fun arrayContainsAll(array: Expr, values: List) = arrayContainsAll(array, array(values)) /** * Creates an expression that checks if [array] contains all elements of [arrayExpression]. @@ -2803,12 +2800,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAll(arrayFieldName: String, values: List) = - BooleanExpr( - "array_contains_all", - evaluateArrayContainsAll, - arrayFieldName, - array(values) - ) + BooleanExpr("array_contains_all", evaluateArrayContainsAll, arrayFieldName, array(values)) /** * Creates an expression that checks if array field contains all elements of [arrayExpression]. @@ -2830,12 +2822,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAny(array: Expr, values: List) = - BooleanExpr( - "array_contains_any", - evaluateArrayContainsAny, - array, - array(values) - ) + BooleanExpr("array_contains_any", evaluateArrayContainsAny, array, array(values)) /** * Creates an expression that checks if [array] contains any elements of [arrayExpression]. @@ -2857,12 +2844,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAny(arrayFieldName: String, values: List) = - BooleanExpr( - "array_contains_any", - evaluateArrayContainsAny, - arrayFieldName, - array(values) - ) + BooleanExpr("array_contains_any", evaluateArrayContainsAny, arrayFieldName, array(values)) /** * Creates an expression that checks if array field contains any elements of [arrayExpression]. @@ -4194,14 +4176,17 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select internal fun toProto(): Value = Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() - override fun evaluateContext(context: EvaluationContext) = block@{ input: MutableDocument -> - EvaluateResultValue(when (fieldPath) { - KEY_PATH -> encodeValue(DocumentReference.forPath(input.key.path, context.db)) - CREATE_TIME_PATH -> encodeValue(input.createTime.timestamp) - UPDATE_TIME_PATH -> encodeValue(input.version.timestamp) - else -> input.getField(fieldPath) ?: return@block EvaluateResultUnset - }) - } + override fun evaluateContext(context: EvaluationContext) = + block@{ input: MutableDocument -> + EvaluateResultValue( + when (fieldPath) { + KEY_PATH -> encodeValue(DocumentReference.forPath(input.key.path, context.db)) + CREATE_TIME_PATH -> encodeValue(input.createTime.timestamp) + UPDATE_TIME_PATH -> encodeValue(input.version.timestamp) + else -> input.getField(fieldPath) ?: return@block EvaluateResultUnset + } + ) + } } /** From 60eb2723e293040dc89db7caeb5126b6d4240001 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 4 Jun 2025 17:08:30 -0400 Subject: [PATCH 102/152] Fixes --- .../google/firebase/firestore/TestUtil.java | 4 +- .../firebase/firestore/pipeline/SortTests.kt | 96 ++++++++----------- .../firebase/firestore/pipeline/WhereTests.kt | 2 +- .../firebase/firestore/pipeline/testUtil.kt | 1 - .../com/google/firebase/firestore/testUtil.kt | 8 +- 5 files changed, 48 insertions(+), 63 deletions(-) diff --git a/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java b/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java index df19245d01f..47672e7aec4 100644 --- a/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java +++ b/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java @@ -40,9 +40,9 @@ public class TestUtil { - private static final FirebaseFirestore FIRESTORE = mock(FirebaseFirestore.class); + public static final FirebaseFirestore FIRESTORE = mock(FirebaseFirestore.class); private static final DatabaseId DATABASE_ID = DatabaseId.forProject("project"); - private static final UserDataReader USER_DATA_READER = new UserDataReader(DATABASE_ID); + public static final UserDataReader USER_DATA_READER = new UserDataReader(DATABASE_ID); static { when(FIRESTORE.getDatabaseId()).thenReturn(DATABASE_ID); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/SortTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/SortTests.kt index f4cb82549ca..fa64aaa271d 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/SortTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/SortTests.kt @@ -15,6 +15,7 @@ package com.google.firebase.firestore.pipeline import com.google.common.truth.Truth.assertThat +import com.google.firebase.firestore.FieldPath as PublicFieldPath import com.google.firebase.firestore.RealtimePipelineSource import com.google.firebase.firestore.TestUtil import com.google.firebase.firestore.model.MutableDocument @@ -31,7 +32,6 @@ import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -import com.google.firebase.firestore.FieldPath as PublicFieldPath @RunWith(RobolectricTestRunner::class) internal class SortTests { @@ -41,10 +41,7 @@ internal class SortTests { @Test fun `empty ascending`(): Unit = runBlocking { val documents = emptyList() - val pipeline = - RealtimePipelineSource(db) - .collection("users") - .sort(field("age").ascending()) + val pipeline = RealtimePipelineSource(db).collection("users").sort(field("age").ascending()) val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).isEmpty() @@ -53,10 +50,7 @@ internal class SortTests { @Test fun `empty descending`(): Unit = runBlocking { val documents = emptyList() - val pipeline = - RealtimePipelineSource(db) - .collection("users") - .sort(field("age").descending()) + val pipeline = RealtimePipelineSource(db).collection("users").sort(field("age").descending()) val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).isEmpty() @@ -66,10 +60,7 @@ internal class SortTests { fun `single result ascending`(): Unit = runBlocking { val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) val documents = listOf(doc1) - val pipeline = - RealtimePipelineSource(db) - .collection("users") - .sort(field("age").ascending()) + val pipeline = RealtimePipelineSource(db).collection("users").sort(field("age").ascending()) val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1) @@ -121,10 +112,7 @@ internal class SortTests { fun `single result descending`(): Unit = runBlocking { val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 10L)) val documents = listOf(doc1) - val pipeline = - RealtimePipelineSource(db) - .collection("users") - .sort(field("age").descending()) + val pipeline = RealtimePipelineSource(db).collection("users").sort(field("age").descending()) val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc1) @@ -167,24 +155,25 @@ internal class SortTests { val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) val documents = listOf(doc1, doc2, doc3, doc4, doc5) - val pipeline = - RealtimePipelineSource(db) - .collection("users") - .sort(field("age").descending()) + val pipeline = RealtimePipelineSource(db).collection("users").sort(field("age").descending()) // Order: doc3 (100.0), doc1 (75.5), doc2 (25.0), then doc4 and doc5 (10.0) are ambiguous // Firestore backend sorts by document key as a tie-breaker. - // So expected order: doc3, doc1, doc2, doc4, doc5 (if 'd' < 'e') or doc3, doc1, doc2, doc5, doc4 (if 'e' < 'd') + // So expected order: doc3, doc1, doc2, doc4, doc5 (if 'd' < 'e') or doc3, doc1, doc2, doc5, + // doc4 (if 'e' < 'd') // Since the C++ test uses UnorderedElementsAre, we'll use containsExactlyElementsIn. val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactlyElementsIn(listOf(doc3, doc1, doc2, doc4, doc5)).inOrder() - // Actually, the local pipeline implementation might not guarantee tie-breaking by key unless explicitly added. - // The C++ test uses UnorderedElementsAre, which means the exact order of doc4 and doc5 is not tested. - // Let's stick to what the C++ test implies: the overall set is correct, but the order of tied elements is not strictly defined by this single sort. + // Actually, the local pipeline implementation might not guarantee tie-breaking by key unless + // explicitly added. + // The C++ test uses UnorderedElementsAre, which means the exact order of doc4 and doc5 is not + // tested. + // Let's stick to what the C++ test implies: the overall set is correct, but the order of tied + // elements is not strictly defined by this single sort. // However, the local pipeline *does* sort by key as a final tie-breaker. // Expected: doc3 (100.0), doc1 (75.5), doc2 (25.0), doc4 (10.0, key d), doc5 (10.0, key e) // So the order should be doc3, doc1, doc2, doc4, doc5 - assertThat(result).containsExactly(doc3, doc1, doc2, doc4, doc5).inOrder() + assertThat(result).containsExactly(doc3, doc1, doc2, doc4, doc5).inOrder() } @Test @@ -271,7 +260,7 @@ internal class SortTests { val doc2 = doc("users/b", 1000, mapOf("name" to "bob")) val doc3 = doc("users/c", 1000, mapOf("age" to 100.0)) val doc4 = doc("users/d", 1000, mapOf("other_name" to "diane")) // Matches - val doc5 = doc("users/e", 1000, mapOf("other_age" to 10.0)) // Matches + val doc5 = doc("users/e", 1000, mapOf("other_age" to 10.0)) // Matches val documents = listOf(doc1, doc2, doc3, doc4, doc5) val pipeline = @@ -332,7 +321,7 @@ internal class SortTests { val doc2 = doc("users/b", 1000, mapOf("age" to 25.0)) // name missing -> Match val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // age missing, name exists - val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing, name exists + val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing, name exists val documents = listOf(doc1, doc2, doc3, doc4, doc5) val pipeline = @@ -345,13 +334,14 @@ internal class SortTests { assertThat(result).containsExactly(doc2) } - @Test - fun `multiple results full order partial explicit not exists sort on non exist field first`(): Unit = runBlocking { + @Test + fun `multiple results full order partial explicit not exists sort on non exist field first`(): + Unit = runBlocking { val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) val doc2 = doc("users/b", 1000, mapOf("age" to 25.0)) // name missing -> Match val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // age missing, name exists - val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing, name exists + val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing, name exists val documents = listOf(doc1, doc2, doc3, doc4, doc5) val pipeline = @@ -393,9 +383,7 @@ internal class SortTests { val documents = listOf(doc1, doc2, doc3, doc4, doc5) val pipeline = - RealtimePipelineSource(db) - .collection("users") - .sort(field("not_age").descending()) + RealtimePipelineSource(db).collection("users").sort(field("not_age").descending()) // Sorting by a missing field results in undefined order relative to each other, // but documents are secondarily sorted by key. @@ -427,10 +415,7 @@ internal class SortTests { val doc5 = doc("users/e", 1000, mapOf("name" to "eric", "age" to 10.0)) val documents = listOf(doc1, doc2, doc3, doc4, doc5) - val pipeline = - RealtimePipelineSource(db) - .collection("users") - .sort(field("age").ascending()) + val pipeline = RealtimePipelineSource(db).collection("users").sort(field("age").ascending()) // Missing fields sort first in ascending order, then by key. b < d // Then existing fields sorted by value: e (10.0) < a (75.5) < c (100.0) @@ -525,7 +510,7 @@ internal class SortTests { val doc2 = doc("users/b", 1000, mapOf("age" to 25.0)) // name missing val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // age missing -> Match - val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing -> Match + val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing -> Match val documents = listOf(doc1, doc2, doc3, doc4, doc5) val pipeline = @@ -544,10 +529,7 @@ internal class SortTests { val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) val documents = listOf(doc1) val pipeline = - RealtimePipelineSource(db) - .collection("users") - .sort(field("age").ascending()) - .limit(0) + RealtimePipelineSource(db).collection("users").sort(field("age").ascending()).limit(0) val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).isEmpty() @@ -599,7 +581,7 @@ internal class SortTests { val doc2 = doc("users/b", 1000, mapOf("age" to 25.0)) val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100.0)) val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // Match - val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // Match + val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // Match val documents = listOf(doc1, doc2, doc3, doc4, doc5) val pipeline = @@ -638,10 +620,7 @@ internal class SortTests { val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) val documents = listOf(doc1) val pipeline = - RealtimePipelineSource(db) - .collectionGroup("users") - .limit(0) - .sort(field("age").ascending()) + RealtimePipelineSource(db).collectionGroup("users").limit(0).sort(field("age").ascending()) val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).isEmpty() @@ -704,7 +683,7 @@ internal class SortTests { val doc2 = doc("users/b", 1000, mapOf("age" to 30L)) val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 50L)) val doc4 = doc("users/d", 1000, mapOf("name" to "diane")) // age missing -> Match - val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing -> Match + val doc5 = doc("users/e", 1000, mapOf("name" to "eric")) // age missing -> Match val documents = listOf(doc1, doc2, doc3, doc4, doc5) val pipeline = @@ -714,7 +693,8 @@ internal class SortTests { .sort(add(field("age"), constant(10L)).descending()) // Sort by missing field -> key order // Filtered: doc4, doc5 - // Sort by (age+10) desc where age is missing. This means they are treated as null for the expression. + // Sort by (age+10) desc where age is missing. This means they are treated as null for the + // expression. // Then tie-broken by key: d, e val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc4, doc5).inOrder() @@ -732,11 +712,15 @@ internal class SortTests { .collection("users") .where(exists(field(PublicFieldPath.documentId()))) // Ensure __name__ is considered .sort(field(PublicFieldPath.documentId()).ascending()) // Sort by key: 1, 2, 3 - .sort(field("age").ascending()) // Sort by age: 2(30), 1(40), 3(50) - Last sort takes precedence + .sort( + field("age").ascending() + ) // Sort by age: 2(30), 1(40), 3(50) - Last sort takes precedence // The C++ test implies that the *last* sort stage defines the primary sort order. - // This is different from how multiple orderBy clauses usually work in Firestore (they form a composite sort). - // However, if these are separate stages, the last one would indeed re-sort the entire output of the previous. + // This is different from how multiple orderBy clauses usually work in Firestore (they form a + // composite sort). + // However, if these are separate stages, the last one would indeed re-sort the entire output of + // the previous. // Let's assume the Kotlin pipeline behaves this way for separate .orderBy() calls. val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).containsExactly(doc2, doc1, doc3).inOrder() @@ -760,9 +744,11 @@ internal class SortTests { assertThat(result).containsExactly(doc1, doc2, doc3).inOrder() } - // The C++ tests `SortOnKeyAndOtherFieldOnMultipleStages` and `SortOnOtherFieldAndKeyOnMultipleStages` + // The C++ tests `SortOnKeyAndOtherFieldOnMultipleStages` and + // `SortOnOtherFieldAndKeyOnMultipleStages` // are identical to the `Path` versions because `kDocumentKeyPath` is used. - // These are effectively duplicates of the above two tests in Kotlin if we use `PublicFieldPath.documentId()`. + // These are effectively duplicates of the above two tests in Kotlin if we use + // `PublicFieldPath.documentId()`. // I'll include them for completeness, mirroring the C++ structure. @Test diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt index 45cc4f1b6b8..2e7042640ee 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt @@ -40,7 +40,7 @@ import org.robolectric.RobolectricTestRunner internal class WhereTests { private val db = TestUtil.firestore() - + @Test fun `empty database returns no results`(): Unit = runBlocking { val documents = emptyList() diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt index 58dd9778d51..75701a8f8b5 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt @@ -69,4 +69,3 @@ internal fun assertEvaluatesToUnset(result: EvaluateResult, format: String, vara internal fun assertEvaluatesToError(result: EvaluateResult, format: String, vararg args: Any?) { assertWithMessage(format, *args).that(result).isSameInstanceAs(EvaluateResultError) } - diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/testUtil.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/testUtil.kt index fb757de4fca..f1acd6953c1 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/testUtil.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/testUtil.kt @@ -5,12 +5,12 @@ import com.google.firebase.firestore.pipeline.EvaluationContext import kotlinx.coroutines.flow.Flow internal fun runPipeline( - db: FirebaseFirestore, - pipeline: AbstractPipeline, - input: Flow + db: FirebaseFirestore, + pipeline: AbstractPipeline, + input: Flow ): Flow { val context = EvaluationContext(db, db.userDataReader) return pipeline.stages.fold(input) { documentFlow, stage -> stage.evaluate(context, documentFlow) } -} \ No newline at end of file +} From fbb3cd1c22e91213fe66b7d973d6e4505468d5fd Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 4 Jun 2025 17:09:12 -0400 Subject: [PATCH 103/152] Limit tests --- .../firebase/firestore/pipeline/LimitTests.kt | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LimitTests.kt diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LimitTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LimitTests.kt new file mode 100644 index 00000000000..69c142de866 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LimitTests.kt @@ -0,0 +1,169 @@ +// Copyright 2025 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.firebase.firestore.pipeline + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.firestore.RealtimePipelineSource +import com.google.firebase.firestore.TestUtil +import com.google.firebase.firestore.model.MutableDocument +import com.google.firebase.firestore.runPipeline +import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class LimitTests { + + private val db = TestUtil.firestore() + + private fun createDocs(): List { + val doc1 = doc("k/a", 1000, mapOf("a" to 1L, "b" to 2L)) + val doc2 = doc("k/b", 1000, mapOf("a" to 3L, "b" to 4L)) + val doc3 = doc("k/c", 1000, mapOf("a" to 5L, "b" to 6L)) + val doc4 = doc("k/d", 1000, mapOf("a" to 7L, "b" to 8L)) + return listOf(doc1, doc2, doc3, doc4) + } + + @Test + fun `limit zero`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = RealtimePipelineSource(db).collection("k").limit(0) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `limit zero duplicated`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = RealtimePipelineSource(db).collection("k").limit(0).limit(0).limit(0) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `limit one`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = RealtimePipelineSource(db).collection("k").limit(1) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(1) + } + + @Test + fun `limit one duplicated`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = RealtimePipelineSource(db).collection("k").limit(1).limit(1).limit(1) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(1) + } + + @Test + fun `limit two`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = RealtimePipelineSource(db).collection("k").limit(2) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(2) + } + + @Test + fun `limit two duplicated`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = RealtimePipelineSource(db).collection("k").limit(2).limit(2).limit(2) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(2) + } + + @Test + fun `limit three`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = RealtimePipelineSource(db).collection("k").limit(3) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(3) + } + + @Test + fun `limit three duplicated`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = RealtimePipelineSource(db).collection("k").limit(3).limit(3).limit(3) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(3) + } + + @Test + fun `limit four`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = RealtimePipelineSource(db).collection("k").limit(4) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(4) + } + + @Test + fun `limit four duplicated`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = RealtimePipelineSource(db).collection("k").limit(4).limit(4).limit(4) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(4) + } + + @Test + fun `limit five`(): Unit = runBlocking { + val documents = createDocs() // Only 4 docs created + val pipeline = RealtimePipelineSource(db).collection("k").limit(5) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(4) // Limited by actual doc count + } + + @Test + fun `limit five duplicated`(): Unit = runBlocking { + val documents = createDocs() // Only 4 docs created + val pipeline = RealtimePipelineSource(db).collection("k").limit(5).limit(5).limit(5) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(4) // Limited by actual doc count + } + + @Test + fun `limit max`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = RealtimePipelineSource(db).collection("k").limit(Int.MAX_VALUE) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(4) + } + + @Test + fun `limit max duplicated`(): Unit = runBlocking { + val documents = createDocs() + val pipeline = + RealtimePipelineSource(db).collection("k").limit(Int.MAX_VALUE).limit(Int.MAX_VALUE).limit(Int.MAX_VALUE) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).hasSize(4) + } +} From 9461e3b11b39e78e9491167ee84d3cdd76e51e24 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 4 Jun 2025 17:21:52 -0400 Subject: [PATCH 104/152] Style --- .../java/com/google/firebase/firestore/pipeline/WhereTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt index 2e7042640ee..3b9d98004e6 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/WhereTests.kt @@ -309,7 +309,7 @@ internal class WhereTests { } @Test - fun `exists`(): Unit = runBlocking { + fun exists(): Unit = runBlocking { val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) // Match val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25.0)) // Match val doc3 = doc("users/c", 1000, mapOf("name" to "charlie")) // Match From 28f8845f4a4ea7731716c52e3cf75fa321fcda1f Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 4 Jun 2025 17:54:37 -0400 Subject: [PATCH 105/152] Add Null Semantics Tests --- .../firestore/pipeline/NullSemanticsTests.kt | 1191 +++++++++++++++++ 1 file changed, 1191 insertions(+) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/NullSemanticsTests.kt diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/NullSemanticsTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/NullSemanticsTests.kt new file mode 100644 index 00000000000..cfeccd025df --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/NullSemanticsTests.kt @@ -0,0 +1,1191 @@ +// Copyright 2025 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.firebase.firestore.pipeline + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.firestore.RealtimePipelineSource +import com.google.firebase.firestore.TestUtil +import com.google.firebase.firestore.pipeline.Expr.Companion.and +import com.google.firebase.firestore.pipeline.Expr.Companion.array +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContains +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContainsAll +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContainsAny +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.eq +import com.google.firebase.firestore.pipeline.Expr.Companion.eqAny +import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.pipeline.Expr.Companion.gt +import com.google.firebase.firestore.pipeline.Expr.Companion.gte +import com.google.firebase.firestore.pipeline.Expr.Companion.isError +import com.google.firebase.firestore.pipeline.Expr.Companion.isNotNull +import com.google.firebase.firestore.pipeline.Expr.Companion.isNull +import com.google.firebase.firestore.pipeline.Expr.Companion.lt +import com.google.firebase.firestore.pipeline.Expr.Companion.lte +import com.google.firebase.firestore.pipeline.Expr.Companion.map +import com.google.firebase.firestore.pipeline.Expr.Companion.neq +import com.google.firebase.firestore.pipeline.Expr.Companion.not +import com.google.firebase.firestore.pipeline.Expr.Companion.notEqAny +import com.google.firebase.firestore.pipeline.Expr.Companion.nullValue +import com.google.firebase.firestore.pipeline.Expr.Companion.or +import com.google.firebase.firestore.pipeline.Expr.Companion.xor +import com.google.firebase.firestore.runPipeline +import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class NullSemanticsTests { + + private val db = TestUtil.firestore() + + // =================================================================== + // Where Tests + // =================================================================== + @Test + fun whereIsNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) // score: null -> Match + val doc2 = doc("users/2", 1000, mapOf("score" to emptyList())) // score: [] + val doc3 = doc("users/3", 1000, mapOf("score" to listOf(null))) // score: [null] + val doc4 = doc("users/4", 1000, mapOf("score" to emptyMap())) // score: {} + val doc5 = doc("users/5", 1000, mapOf("score" to 42L)) // score: 42 + val doc6 = doc("users/6", 1000, mapOf("score" to Double.NaN)) // score: NaN + val doc7 = doc("users/7", 1000, mapOf("not-score" to 42L)) // score: missing + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7) + + val pipeline = RealtimePipelineSource(db).collection("users").where(isNull(field("score"))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun whereIsNotNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) // score: null + val doc2 = doc("users/2", 1000, mapOf("score" to emptyList())) // score: [] -> Match + val doc3 = doc("users/3", 1000, mapOf("score" to listOf(null))) // score: [null] -> Match + val doc4 = + doc("users/4", 1000, mapOf("score" to emptyMap())) // score: {} -> Match + val doc5 = doc("users/5", 1000, mapOf("score" to 42L)) // score: 42 -> Match + val doc6 = doc("users/6", 1000, mapOf("score" to Double.NaN)) // score: NaN -> Match + val doc7 = doc("users/7", 1000, mapOf("not-score" to 42L)) // score: missing + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7) + + val pipeline = RealtimePipelineSource(db).collection("users").where(isNotNull(field("score"))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc2, doc3, doc4, doc5, doc6)) + } + + @Test + fun whereIsNullAndIsNotNullEmpty(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("score" to null)) + val doc2 = doc("users/b", 1000, mapOf("score" to listOf(null))) + val doc3 = doc("users/c", 1000, mapOf("score" to 42L)) + val doc4 = doc("users/d", 1000, mapOf("bar" to 42L)) + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(isNull(field("score")), isNotNull(field("score")))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqConstantAsNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to 42L)) + val doc3 = doc("users/3", 1000, mapOf("score" to Double.NaN)) + val doc4 = doc("users/4", 1000, mapOf("not-score" to 42L)) + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(eq(field("score"), nullValue())) // Equality filters never match null or missing + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqFieldAsNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null, "rank" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to 42L, "rank" to null)) + val doc3 = doc("users/3", 1000, mapOf("score" to null, "rank" to 42L)) + val doc4 = doc("users/4", 1000, mapOf("score" to null)) + val doc5 = doc("users/5", 1000, mapOf("rank" to null)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(eq(field("score"), field("rank"))) // Equality filters never match null + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqSegmentField(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to mapOf("bonus" to null))) + val doc2 = doc("users/2", 1000, mapOf("score" to mapOf("bonus" to 42L))) + val doc3 = doc("users/3", 1000, mapOf("score" to mapOf("bonus" to Double.NaN))) + val doc4 = doc("users/4", 1000, mapOf("score" to mapOf("not-bonus" to 42L))) + val doc5 = doc("users/5", 1000, mapOf("score" to "foo-bar")) + val doc6 = doc("users/6", 1000, mapOf("not-score" to mapOf("bonus" to 42L))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6) + + val pipeline = + RealtimePipelineSource(db).collection("users").where(eq(field("score.bonus"), nullValue())) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqSingleFieldAndSegmentField(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to mapOf("bonus" to null), "rank" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to mapOf("bonus" to 42L), "rank" to null)) + val doc3 = doc("users/3", 1000, mapOf("score" to mapOf("bonus" to Double.NaN), "rank" to null)) + val doc4 = doc("users/4", 1000, mapOf("score" to mapOf("not-bonus" to 42L), "rank" to null)) + val doc5 = doc("users/5", 1000, mapOf("score" to "foo-bar")) + val doc6 = doc("users/6", 1000, mapOf("not-score" to mapOf("bonus" to 42L), "rank" to null)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(eq(field("score.bonus"), nullValue()), eq(field("rank"), nullValue()))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqNullInArray(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to listOf(null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to listOf(1.0, null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to listOf(null, Double.NaN))) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db).collection("k").where(eq(field("foo"), array(nullValue()))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqNullOtherInArray(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to listOf(null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to listOf(1.0, null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to listOf(1L, null))) // Note: 1L becomes 1.0 + val doc4 = doc("k/4", 1000, mapOf("foo" to listOf(null, Double.NaN))) + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(eq(field("foo"), array(constant(1.0), nullValue()))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqNullNanInArray(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to listOf(null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to listOf(1.0, null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to listOf(null, Double.NaN))) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(eq(field("foo"), array(nullValue(), constant(Double.NaN)))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqNullInMap(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to mapOf("a" to null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to mapOf("a" to 1.0, "b" to null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to mapOf("a" to null, "b" to Double.NaN))) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(eq(field("foo"), map(mapOf("a" to nullValue())))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqNullOtherInMap(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to mapOf("a" to null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to mapOf("a" to 1.0, "b" to null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to mapOf("a" to 1L, "b" to null))) + val doc4 = doc("k/4", 1000, mapOf("foo" to mapOf("a" to null, "b" to Double.NaN))) + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(eq(field("foo"), map(mapOf("a" to constant(1.0), "b" to nullValue())))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqNullNanInMap(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to mapOf("a" to null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to mapOf("a" to 1.0, "b" to null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to mapOf("a" to null, "b" to Double.NaN))) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(eq(field("foo"), map(mapOf("a" to nullValue(), "b" to constant(Double.NaN))))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqMapWithNullArray(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to mapOf("a" to listOf(null)))) + val doc2 = doc("k/2", 1000, mapOf("foo" to mapOf("a" to listOf(1.0, null)))) + val doc3 = doc("k/3", 1000, mapOf("foo" to mapOf("a" to listOf(null, Double.NaN)))) + val doc4 = doc("k/4", 1000, mapOf("foo" to mapOf("a" to emptyList()))) + val doc5 = doc("k/5", 1000, mapOf("foo" to mapOf("a" to listOf(1.0)))) + val doc6 = doc("k/6", 1000, mapOf("foo" to mapOf("a" to listOf(null, 1.0)))) + val doc7 = doc("k/7", 1000, mapOf("foo" to mapOf("not-a" to listOf(null)))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(eq(field("foo"), map(mapOf("a" to array(nullValue()))))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqMapWithNullOtherArray(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to mapOf("a" to listOf(null)))) + val doc2 = doc("k/2", 1000, mapOf("foo" to mapOf("a" to listOf(1.0, null)))) + val doc3 = doc("k/3", 1000, mapOf("foo" to mapOf("a" to listOf(1L, null)))) + val doc4 = doc("k/4", 1000, mapOf("foo" to mapOf("a" to listOf(null, Double.NaN)))) + val doc5 = doc("k/5", 1000, mapOf("foo" to mapOf("a" to emptyList()))) + val doc6 = doc("k/6", 1000, mapOf("foo" to mapOf("a" to listOf(1.0)))) + val doc7 = doc("k/7", 1000, mapOf("foo" to mapOf("a" to listOf(null, 1.0)))) + val doc8 = doc("k/8", 1000, mapOf("foo" to mapOf("not-a" to listOf(null)))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(eq(field("foo"), map(mapOf("a" to array(constant(1.0), nullValue()))))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqMapWithNullNanArray(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to mapOf("a" to listOf(null)))) + val doc2 = doc("k/2", 1000, mapOf("foo" to mapOf("a" to listOf(1.0, null)))) + val doc3 = doc("k/3", 1000, mapOf("foo" to mapOf("a" to listOf(null, Double.NaN)))) + val doc4 = doc("k/4", 1000, mapOf("foo" to mapOf("a" to emptyList()))) + val doc5 = doc("k/5", 1000, mapOf("foo" to mapOf("a" to listOf(1.0)))) + val doc6 = doc("k/6", 1000, mapOf("foo" to mapOf("a" to listOf(null, 1.0)))) + val doc7 = doc("k/7", 1000, mapOf("foo" to mapOf("not-a" to listOf(null)))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(eq(field("foo"), map(mapOf("a" to array(nullValue(), constant(Double.NaN)))))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereCompositeConditionWithNull(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("score" to 42L, "rank" to null)) + val doc2 = doc("users/b", 1000, mapOf("score" to 42L, "rank" to 42L)) + val documents = listOf(doc1, doc2) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(eq(field("score"), constant(42L)), eq(field("rank"), nullValue()))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqAnyNullOnly(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("score" to null)) + val doc2 = doc("users/b", 1000, mapOf("score" to 42L)) + val doc3 = doc("users/c", 1000, mapOf("rank" to 42L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(eqAny(field("score"), array(nullValue()))) // IN filters never match null + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereEqAnyPartialNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to emptyList())) + val doc3 = doc("users/3", 1000, mapOf("score" to 25L)) + val doc4 = doc("users/4", 1000, mapOf("score" to 100L)) // Match + val doc5 = doc("users/5", 1000, mapOf("not-score" to 100L)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(eqAny(field("score"), array(nullValue(), constant(100L)))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4) + } + + @Test + fun whereArrayContainsNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to emptyList())) + val doc3 = doc("users/3", 1000, mapOf("score" to listOf(null))) + val doc4 = doc("users/4", 1000, mapOf("score" to listOf(null, 42L))) + val doc5 = doc("users/5", 1000, mapOf("score" to listOf(101L, null))) + val doc6 = doc("users/6", 1000, mapOf("score" to listOf("foo", "bar"))) + val doc7 = doc("users/7", 1000, mapOf("not-score" to listOf("foo", "bar"))) + val doc8 = doc("users/8", 1000, mapOf("not-score" to listOf("foo", null))) + val doc9 = doc("users/9", 1000, mapOf("not-score" to listOf(null, "foo"))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8, doc9) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(arrayContains(field("score"), nullValue())) // arrayContains does not match null + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereArrayContainsAnyOnlyNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to emptyList())) + val doc3 = doc("users/3", 1000, mapOf("score" to listOf(null))) + val doc4 = doc("users/4", 1000, mapOf("score" to listOf(null, 42L))) + val doc5 = doc("users/5", 1000, mapOf("score" to listOf(101L, null))) + val doc6 = doc("users/6", 1000, mapOf("score" to listOf("foo", "bar"))) + val doc7 = doc("users/7", 1000, mapOf("not-score" to listOf("foo", "bar"))) + val doc8 = doc("users/8", 1000, mapOf("not-score" to listOf("foo", null))) + val doc9 = doc("users/9", 1000, mapOf("not-score" to listOf(null, "foo"))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8, doc9) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where( + arrayContainsAny(field("score"), array(nullValue())) + ) // arrayContainsAny does not match null + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereArrayContainsAnyPartialNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to emptyList())) + val doc3 = doc("users/3", 1000, mapOf("score" to listOf(null))) + val doc4 = doc("users/4", 1000, mapOf("score" to listOf(null, 42L))) + val doc5 = doc("users/5", 1000, mapOf("score" to listOf(101L, null))) + val doc6 = doc("users/6", 1000, mapOf("score" to listOf("foo", "bar"))) // Match 'foo' + val doc7 = doc("users/7", 1000, mapOf("not-score" to listOf("foo", "bar"))) + val doc8 = doc("users/8", 1000, mapOf("not-score" to listOf("foo", null))) + val doc9 = doc("users/9", 1000, mapOf("not-score" to listOf(null, "foo"))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8, doc9) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(arrayContainsAny(field("score"), array(nullValue(), constant("foo")))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc6) + } + + @Test + fun whereArrayContainsAllOnlyNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to emptyList())) + val doc3 = doc("users/3", 1000, mapOf("score" to listOf(null))) + val doc4 = doc("users/4", 1000, mapOf("score" to listOf(null, 42L))) + val doc5 = doc("users/5", 1000, mapOf("score" to listOf(101L, null))) + val doc6 = doc("users/6", 1000, mapOf("score" to listOf("foo", "bar"))) + val doc7 = doc("users/7", 1000, mapOf("not-score" to listOf("foo", "bar"))) + val doc8 = doc("users/8", 1000, mapOf("not-score" to listOf("foo", null))) + val doc9 = doc("users/9", 1000, mapOf("not-score" to listOf(null, "foo"))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8, doc9) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where( + arrayContainsAll(field("score"), array(nullValue())) + ) // arrayContainsAll does not match null + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereArrayContainsAllPartialNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to emptyList())) + val doc3 = doc("users/3", 1000, mapOf("score" to listOf(null))) + val doc4 = doc("users/4", 1000, mapOf("score" to listOf(null, 42L))) + val doc5 = doc("users/5", 1000, mapOf("score" to listOf(101L, null))) + val doc6 = doc("users/6", 1000, mapOf("score" to listOf("foo", "bar"))) + val doc7 = doc("users/7", 1000, mapOf("not-score" to listOf("foo", "bar"))) + val doc8 = doc("users/8", 1000, mapOf("not-score" to listOf("foo", null))) + val doc9 = doc("users/9", 1000, mapOf("not-score" to listOf(null, "foo"))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8, doc9) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where( + arrayContainsAll(field("score"), array(nullValue(), constant(42L))) + ) // arrayContainsAll does not match null + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereNeqConstantAsNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to 42L)) + val doc3 = doc("users/3", 1000, mapOf("score" to Double.NaN)) + val doc4 = doc("users/4", 1000, mapOf("not-score" to 42L)) + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(neq(field("score"), nullValue())) // != null is not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereNeqFieldAsNull(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null, "rank" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to 42L, "rank" to null)) + val doc3 = doc("users/3", 1000, mapOf("score" to null, "rank" to 42L)) + val doc4 = doc("users/4", 1000, mapOf("score" to null)) + val doc5 = doc("users/5", 1000, mapOf("rank" to null)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(neq(field("score"), field("rank"))) // != null is not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereNeqNullInArray(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to listOf(null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to listOf(1.0, null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to listOf(null, Double.NaN))) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(neq(field("foo"), array(nullValue()))) // != [null] is not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc2, doc3)) + } + + @Test + fun whereNeqNullOtherInArray(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to listOf(null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to listOf(1.0, null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to listOf(1L, null))) + val doc4 = doc("k/4", 1000, mapOf("foo" to listOf(null, Double.NaN))) + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where( + neq(field("foo"), array(constant(1.0), nullValue())) + ) // != [1.0, null] is not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun whereNeqNullNanInArray(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to listOf(null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to listOf(1.0, null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to listOf(null, Double.NaN))) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where( + neq(field("foo"), array(nullValue(), constant(Double.NaN))) + ) // != [null, NaN] is not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3)) + } + + @Test + fun whereNeqNullInMap(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to mapOf("a" to null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to mapOf("a" to 1.0, "b" to null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to mapOf("a" to null, "b" to Double.NaN))) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(neq(field("foo"), map(mapOf("a" to nullValue())))) // != {a:null} is not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc2, doc3)) + } + + @Test + fun whereNeqNullOtherInMap(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to mapOf("a" to null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to mapOf("a" to 1.0, "b" to null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to mapOf("a" to 1L, "b" to null))) + val doc4 = doc("k/4", 1000, mapOf("foo" to mapOf("a" to null, "b" to Double.NaN))) + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where( + neq(field("foo"), map(mapOf("a" to constant(1.0), "b" to nullValue()))) + ) // != {a:1.0,b:null} not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun whereNeqNullNanInMap(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("foo" to mapOf("a" to null))) + val doc2 = doc("k/2", 1000, mapOf("foo" to mapOf("a" to 1.0, "b" to null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to mapOf("a" to null, "b" to Double.NaN))) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where( + neq(field("foo"), map(mapOf("a" to nullValue(), "b" to constant(Double.NaN)))) + ) // != {a:null,b:NaN} not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3)) + } + + @Test + fun whereNotEqAnyWithNull(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("score" to null)) + val doc2 = doc("users/b", 1000, mapOf("score" to 42L)) + val documents = listOf(doc1, doc2) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(notEqAny(field("score"), array(nullValue()))) // NOT IN [null] is not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereGt(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to 42L)) + val doc3 = doc("users/3", 1000, mapOf("score" to "hello world")) + val doc4 = doc("users/4", 1000, mapOf("score" to Double.NaN)) + val doc5 = doc("users/5", 1000, mapOf("not-score" to 42L)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(gt(field("score"), nullValue())) // > null is not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereGte(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to 42L)) + val doc3 = doc("users/3", 1000, mapOf("score" to "hello world")) + val doc4 = doc("users/4", 1000, mapOf("score" to Double.NaN)) + val doc5 = doc("users/5", 1000, mapOf("not-score" to 42L)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(gte(field("score"), nullValue())) // >= null is not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereLt(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to 42L)) + val doc3 = doc("users/3", 1000, mapOf("score" to "hello world")) + val doc4 = doc("users/4", 1000, mapOf("score" to Double.NaN)) + val doc5 = doc("users/5", 1000, mapOf("not-score" to 42L)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(lt(field("score"), nullValue())) // < null is not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereLte(): Unit = runBlocking { + val doc1 = doc("users/1", 1000, mapOf("score" to null)) + val doc2 = doc("users/2", 1000, mapOf("score" to 42L)) + val doc3 = doc("users/3", 1000, mapOf("score" to "hello world")) + val doc4 = doc("users/4", 1000, mapOf("score" to Double.NaN)) + val doc5 = doc("users/5", 1000, mapOf("not-score" to 42L)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(lte(field("score"), nullValue())) // <= null is not supported + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun whereAnd(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("a" to true, "b" to null)) + val doc2 = doc("k/2", 1000, mapOf("a" to false, "b" to null)) + val doc3 = doc("k/3", 1000, mapOf("a" to null, "b" to null)) + val doc4 = doc("k/4", 1000, mapOf("a" to true, "b" to true)) // Match + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(and(eq(field("a"), constant(true)), eq(field("b"), constant(true)))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4) + } + + @Test + fun whereIsNullAnd(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("a" to null, "b" to null)) + val doc2 = doc("k/2", 1000, mapOf("a" to null)) + val doc3 = doc("k/3", 1000, mapOf("a" to null, "b" to true)) + val doc4 = doc("k/4", 1000, mapOf("a" to null, "b" to false)) + val doc5 = doc("k/5", 1000, mapOf("b" to null)) + val doc6 = doc("k/6", 1000, mapOf("a" to true, "b" to null)) + val doc7 = doc("k/7", 1000, mapOf("a" to false, "b" to null)) + val doc8 = doc("k/8", 1000, mapOf("not-a" to true, "not-b" to true)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(isNull(and(eq(field("a"), constant(true)), eq(field("b"), constant(true))))) + // (a==true AND b==true) is NULL if: + // (true AND null) -> null (doc6) + // (null AND true) -> null (doc3) + // (null AND null) -> null (doc1) + // (false AND null) -> false + // (null AND false) -> false + // (missing AND true) -> error + // (true AND missing) -> error + // (missing AND null) -> error + // (null AND missing) -> error + // (missing AND missing) -> error + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3, doc6)) + } + + @Test + fun whereIsErrorAnd(): Unit = runBlocking { + val doc1 = + doc( + "k/1", + 1000, + mapOf("a" to null, "b" to null) + ) // a=null, b=null -> AND is null -> isError(null) is false + val doc2 = + doc( + "k/2", + 1000, + mapOf("a" to null) + ) // a=null, b=missing -> AND is error -> isError(error) is true -> Match + val doc3 = + doc( + "k/3", + 1000, + mapOf("a" to null, "b" to true) + ) // a=null, b=true -> AND is null -> isError(null) is false + val doc4 = + doc( + "k/4", + 1000, + mapOf("a" to null, "b" to false) + ) // a=null, b=false -> AND is false -> isError(false) is false + val doc5 = + doc( + "k/5", + 1000, + mapOf("b" to null) + ) // a=missing, b=null -> AND is error -> isError(error) is true -> Match + val doc6 = + doc( + "k/6", + 1000, + mapOf("a" to true, "b" to null) + ) // a=true, b=null -> AND is null -> isError(null) is false + val doc7 = + doc( + "k/7", + 1000, + mapOf("a" to false, "b" to null) + ) // a=false, b=null -> AND is false -> isError(false) is false + val doc8 = + doc( + "k/8", + 1000, + mapOf("not-a" to true, "not-b" to true) + ) // a=missing, b=missing -> AND is error -> isError(error) is true -> Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(isError(and(eq(field("a"), constant(true)), eq(field("b"), constant(true))))) + // This happens if either a or b is missing. + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc2, doc5, doc8)) + } + + @Test + fun whereOr(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("a" to true, "b" to null)) // Match + val doc2 = doc("k/2", 1000, mapOf("a" to false, "b" to null)) + val doc3 = doc("k/3", 1000, mapOf("a" to null, "b" to null)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(or(eq(field("a"), constant(true)), eq(field("b"), constant(true)))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun whereIsNullOr(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("a" to null, "b" to null)) + val doc2 = doc("k/2", 1000, mapOf("a" to null)) + val doc3 = doc("k/3", 1000, mapOf("a" to null, "b" to true)) + val doc4 = doc("k/4", 1000, mapOf("a" to null, "b" to false)) + val doc5 = doc("k/5", 1000, mapOf("b" to null)) + val doc6 = doc("k/6", 1000, mapOf("a" to true, "b" to null)) + val doc7 = doc("k/7", 1000, mapOf("a" to false, "b" to null)) + val doc8 = doc("k/8", 1000, mapOf("not-a" to true, "not-b" to true)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(isNull(or(eq(field("a"), constant(true)), eq(field("b"), constant(true))))) + // (a==true OR b==true) is NULL if: + // (false OR null) -> null (doc7) + // (null OR false) -> null (doc4) + // (null OR null) -> null (doc1) + // (true OR null) -> true + // (null OR true) -> true + // (missing OR false) -> error + // (false OR missing) -> error + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc4, doc7)) + } + + @Test + fun whereIsErrorOr(): Unit = runBlocking { + val doc1 = + doc( + "k/1", + 1000, + mapOf("a" to null, "b" to null) + ) // a=null, b=null -> OR is null -> isError(null) is false + val doc2 = + doc( + "k/2", + 1000, + mapOf("a" to null) + ) // a=null, b=missing -> OR is error -> isError(error) is true -> Match + val doc3 = + doc( + "k/3", + 1000, + mapOf("a" to null, "b" to true) + ) // a=null, b=true -> OR is true -> isError(true) is false + val doc4 = + doc( + "k/4", + 1000, + mapOf("a" to null, "b" to false) + ) // a=null, b=false -> OR is null -> isError(null) is false + val doc5 = + doc( + "k/5", + 1000, + mapOf("b" to null) + ) // a=missing, b=null -> OR is error -> isError(error) is true -> Match + val doc6 = + doc( + "k/6", + 1000, + mapOf("a" to true, "b" to null) + ) // a=true, b=null -> OR is true -> isError(true) is false + val doc7 = + doc( + "k/7", + 1000, + mapOf("a" to false, "b" to null) + ) // a=false, b=null -> OR is null -> isError(null) is false + val doc8 = + doc( + "k/8", + 1000, + mapOf("not-a" to true, "not-b" to true) + ) // a=missing, b=missing -> OR is error -> isError(error) is true -> Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(isError(or(eq(field("a"), constant(true)), eq(field("b"), constant(true))))) + // This happens if either a or b is missing. + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc2, doc5, doc8)) + } + + @Test + fun whereXor(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("a" to true, "b" to null)) // a=T, b=null -> XOR is null + val doc2 = doc("k/2", 1000, mapOf("a" to false, "b" to null)) // a=F, b=null -> XOR is null + val doc3 = doc("k/3", 1000, mapOf("a" to null, "b" to null)) // a=null, b=null -> XOR is null + val doc4 = + doc("k/4", 1000, mapOf("a" to true, "b" to false)) // a=T, b=F -> XOR is true -> Match + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(xor(eq(field("a"), constant(true)), eq(field("b"), constant(true)))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4) + } + + @Test + fun whereIsNullXor(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("a" to null, "b" to null)) + val doc2 = doc("k/2", 1000, mapOf("a" to null)) + val doc3 = doc("k/3", 1000, mapOf("a" to null, "b" to true)) + val doc4 = doc("k/4", 1000, mapOf("a" to null, "b" to false)) + val doc5 = doc("k/5", 1000, mapOf("b" to null)) + val doc6 = doc("k/6", 1000, mapOf("a" to true, "b" to null)) + val doc7 = doc("k/7", 1000, mapOf("a" to false, "b" to null)) + val doc8 = doc("k/8", 1000, mapOf("not-a" to true, "not-b" to true)) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(isNull(xor(eq(field("a"), constant(true)), eq(field("b"), constant(true))))) + // (a==true XOR b==true) is NULL if: + // (true XOR null) -> null (doc6) + // (false XOR null) -> null (doc7) + // (null XOR true) -> null (doc3) + // (null XOR false) -> null (doc4) + // (null XOR null) -> null (doc1) + // (missing XOR true) -> error + // (true XOR missing) -> error + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3, doc4, doc6, doc7)) + } + + @Test + fun whereIsErrorXor(): Unit = runBlocking { + val doc1 = + doc( + "k/1", + 1000, + mapOf("a" to null, "b" to null) + ) // a=null, b=null -> XOR is null -> isError(null) is false + val doc2 = + doc( + "k/2", + 1000, + mapOf("a" to null) + ) // a=null, b=missing -> XOR is error -> isError(error) is true -> Match + val doc3 = + doc( + "k/3", + 1000, + mapOf("a" to null, "b" to true) + ) // a=null, b=true -> XOR is null -> isError(null) is false + val doc4 = + doc( + "k/4", + 1000, + mapOf("a" to null, "b" to false) + ) // a=null, b=false -> XOR is null -> isError(null) is false + val doc5 = + doc( + "k/5", + 1000, + mapOf("b" to null) + ) // a=missing, b=null -> XOR is error -> isError(error) is true -> Match + val doc6 = + doc( + "k/6", + 1000, + mapOf("a" to true, "b" to null) + ) // a=true, b=null -> XOR is null -> isError(null) is false + val doc7 = + doc( + "k/7", + 1000, + mapOf("a" to false, "b" to null) + ) // a=false, b=null -> XOR is null -> isError(null) is false + val doc8 = + doc( + "k/8", + 1000, + mapOf("not-a" to true, "not-b" to true) + ) // a=missing, b=missing -> XOR is error -> isError(error) is true -> Match + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(isError(xor(eq(field("a"), constant(true)), eq(field("b"), constant(true))))) + // This happens if either a or b is missing. + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc2, doc5, doc8)) + } + + @Test + fun whereNot(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("a" to true)) + val doc2 = doc("k/2", 1000, mapOf("a" to false)) // Match + val doc3 = doc("k/3", 1000, mapOf("a" to null)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db).collection("k").where(not(eq(field("a"), constant(true)))) + + // Based on C++ test's interpretation of TS behavior for NOT + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc2) + } + + @Test + fun whereIsNullNot(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("a" to true)) + val doc2 = doc("k/2", 1000, mapOf("a" to false)) + val doc3 = doc("k/3", 1000, mapOf("a" to null)) // Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db).collection("k").where(isNull(not(eq(field("a"), constant(true))))) + // NOT(null_operand) -> null. So ISNULL(null) -> true. + // NOT(true) -> false. ISNULL(false) -> false. + // NOT(false) -> true. ISNULL(true) -> false. + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3) + } + + @Test + fun whereIsErrorNot(): Unit = runBlocking { + val doc1 = doc("k/1", 1000, mapOf("a" to true)) // a=T -> NOT(a==T) is F -> isError(F) is false + val doc2 = doc("k/2", 1000, mapOf("a" to false)) // a=F -> NOT(a==T) is T -> isError(T) is false + val doc3 = + doc("k/3", 1000, mapOf("a" to null)) // a=null -> NOT(a==T) is null -> isError(T) is false + val doc4 = + doc( + "k/4", + 1000, + mapOf("not-a" to true) + ) // a=missing -> NOT(a==T) is error -> isError(error) is true -> Match + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db).collection("k").where(isError(not(eq(field("a"), constant(true))))) + // This happens if a is missing. + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4) + } + + // =================================================================== + // Sort Tests + // =================================================================== + @Test + fun sortNullInArrayAscending(): Unit = runBlocking { + val doc0 = doc("k/0", 1000, mapOf("not-foo" to emptyList())) // foo missing + val doc1 = doc("k/1", 1000, mapOf("foo" to emptyList())) // [] + val doc2 = doc("k/2", 1000, mapOf("foo" to listOf(null))) // [null] + val doc3 = doc("k/3", 1000, mapOf("foo" to listOf(null, null))) // [null, null] + val doc4 = doc("k/4", 1000, mapOf("foo" to listOf(null, 1L))) // [null, 1] + val doc5 = doc("k/5", 1000, mapOf("foo" to listOf(null, 2L))) // [null, 2] + val doc6 = doc("k/6", 1000, mapOf("foo" to listOf(1L, null))) // [1, null] + val doc7 = doc("k/7", 1000, mapOf("foo" to listOf(2L, null))) // [2, null] + val doc8 = doc("k/8", 1000, mapOf("foo" to listOf(2L, 1L))) // [2, 1] + val documents = listOf(doc0, doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = RealtimePipelineSource(db).collection("k").sort(field("foo").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result) + .containsExactly(doc0, doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + .inOrder() + } + + @Test + fun sortNullInArrayDescending(): Unit = runBlocking { + val doc0 = doc("k/0", 1000, mapOf("not-foo" to emptyList())) + val doc1 = doc("k/1", 1000, mapOf("foo" to emptyList())) + val doc2 = doc("k/2", 1000, mapOf("foo" to listOf(null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to listOf(null, null))) + val doc4 = doc("k/4", 1000, mapOf("foo" to listOf(null, 1L))) + val doc5 = doc("k/5", 1000, mapOf("foo" to listOf(null, 2L))) + val doc6 = doc("k/6", 1000, mapOf("foo" to listOf(1L, null))) + val doc7 = doc("k/7", 1000, mapOf("foo" to listOf(2L, null))) + val doc8 = doc("k/8", 1000, mapOf("foo" to listOf(2L, 1L))) + val documents = listOf(doc0, doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = RealtimePipelineSource(db).collection("k").sort(field("foo").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result) + .containsExactly(doc8, doc7, doc6, doc5, doc4, doc3, doc2, doc1, doc0) + .inOrder() + } + + @Test + fun sortNullInMapAscending(): Unit = runBlocking { + val doc0 = doc("k/0", 1000, mapOf("not-foo" to emptyMap())) // foo missing + val doc1 = doc("k/1", 1000, mapOf("foo" to emptyMap())) // {} + val doc2 = doc("k/2", 1000, mapOf("foo" to mapOf("a" to null))) // {a:null} + val doc3 = doc("k/3", 1000, mapOf("foo" to mapOf("a" to null, "b" to null))) // {a:null, b:null} + val doc4 = doc("k/4", 1000, mapOf("foo" to mapOf("a" to null, "b" to 1L))) // {a:null, b:1} + val doc5 = doc("k/5", 1000, mapOf("foo" to mapOf("a" to null, "b" to 2L))) // {a:null, b:2} + val doc6 = doc("k/6", 1000, mapOf("foo" to mapOf("a" to 1L, "b" to null))) // {a:1, b:null} + val doc7 = doc("k/7", 1000, mapOf("foo" to mapOf("a" to 2L, "b" to null))) // {a:2, b:null} + val doc8 = doc("k/8", 1000, mapOf("foo" to mapOf("a" to 2L, "b" to 1L))) // {a:2, b:1} + val documents = listOf(doc0, doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = RealtimePipelineSource(db).collection("k").sort(field("foo").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result) + .containsExactly(doc0, doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + .inOrder() + } + + @Test + fun sortNullInMapDescending(): Unit = runBlocking { + val doc0 = doc("k/0", 1000, mapOf("not-foo" to emptyMap())) + val doc1 = doc("k/1", 1000, mapOf("foo" to emptyMap())) + val doc2 = doc("k/2", 1000, mapOf("foo" to mapOf("a" to null))) + val doc3 = doc("k/3", 1000, mapOf("foo" to mapOf("a" to null, "b" to null))) + val doc4 = doc("k/4", 1000, mapOf("foo" to mapOf("a" to null, "b" to 1L))) + val doc5 = doc("k/5", 1000, mapOf("foo" to mapOf("a" to null, "b" to 2L))) + val doc6 = doc("k/6", 1000, mapOf("foo" to mapOf("a" to 1L, "b" to null))) + val doc7 = doc("k/7", 1000, mapOf("foo" to mapOf("a" to 2L, "b" to null))) + val doc8 = doc("k/8", 1000, mapOf("foo" to mapOf("a" to 2L, "b" to 1L))) + val documents = listOf(doc0, doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = RealtimePipelineSource(db).collection("k").sort(field("foo").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result) + .containsExactly(doc8, doc7, doc6, doc5, doc4, doc3, doc2, doc1, doc0) + .inOrder() + } +} From 5324096be6b46c7b4314cc0229c5a4f67b6ee666 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 5 Jun 2025 09:33:46 -0400 Subject: [PATCH 106/152] Pretty --- .../com/google/firebase/firestore/pipeline/LimitTests.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LimitTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LimitTests.kt index 69c142de866..9ae21fe5133 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LimitTests.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/LimitTests.kt @@ -161,7 +161,11 @@ internal class LimitTests { fun `limit max duplicated`(): Unit = runBlocking { val documents = createDocs() val pipeline = - RealtimePipelineSource(db).collection("k").limit(Int.MAX_VALUE).limit(Int.MAX_VALUE).limit(Int.MAX_VALUE) + RealtimePipelineSource(db) + .collection("k") + .limit(Int.MAX_VALUE) + .limit(Int.MAX_VALUE) + .limit(Int.MAX_VALUE) val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() assertThat(result).hasSize(4) From 45280be8a9c7c0f4b1269c8e8f46c5330843dca4 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 5 Jun 2025 10:14:38 -0400 Subject: [PATCH 107/152] Fixes, addition, whitespace. --- firebase-firestore/api.txt | 66 ++++--- .../firebase/firestore/PipelineTest.java | 13 +- .../firestore/QueryToPipelineTest.java | 23 ++- .../testutil/IntegrationTestUtil.java | 2 +- .../com/google/firebase/firestore/Pipeline.kt | 20 +- .../firebase/firestore/core/FieldFilter.java | 18 +- .../google/firebase/firestore/core/Query.java | 14 +- .../google/firebase/firestore/model/Values.kt | 9 +- .../firestore/pipeline/expressions.kt | 185 ++++++++++++------ .../firebase/firestore/pipeline/stage.kt | 54 ++--- 10 files changed, 236 insertions(+), 168 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index cb86b4b0932..dd6a20ddcc8 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -421,7 +421,6 @@ package com.google.firebase.firestore { public final class Pipeline { method public com.google.firebase.firestore.Pipeline addFields(com.google.firebase.firestore.pipeline.Selectable field, com.google.firebase.firestore.pipeline.Selectable... additionalFields); - method public com.google.firebase.firestore.Pipeline addStage(com.google.firebase.firestore.pipeline.Stage stage); method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateStage aggregateStage); method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateWithAlias accumulator, com.google.firebase.firestore.pipeline.AggregateWithAlias... additionalAccumulators); method public com.google.firebase.firestore.Pipeline distinct(com.google.firebase.firestore.pipeline.Selectable group, java.lang.Object... additionalGroups); @@ -435,6 +434,7 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.Pipeline findNearest(String vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline limit(int limit); method public com.google.firebase.firestore.Pipeline offset(int offset); + method public com.google.firebase.firestore.Pipeline rawStage(com.google.firebase.firestore.pipeline.RawStage rawStage); method public com.google.firebase.firestore.Pipeline removeFields(com.google.firebase.firestore.pipeline.Field field, com.google.firebase.firestore.pipeline.Field... additionalFields); method public com.google.firebase.firestore.Pipeline removeFields(String field, java.lang.String... additionalFields); method public com.google.firebase.firestore.Pipeline replace(com.google.firebase.firestore.pipeline.Expr mapValue); @@ -722,7 +722,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.AggregateFunction sum(String fieldName); } - public final class AggregateStage extends com.google.firebase.firestore.pipeline.BaseStage { + public final class AggregateStage extends com.google.firebase.firestore.pipeline.Stage { method public static com.google.firebase.firestore.pipeline.AggregateStage withAccumulators(com.google.firebase.firestore.pipeline.AggregateWithAlias accumulator, com.google.firebase.firestore.pipeline.AggregateWithAlias... additionalAccumulators); method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(com.google.firebase.firestore.pipeline.Selectable group, java.lang.Object... additionalGroups); method public com.google.firebase.firestore.pipeline.AggregateStage withGroups(String groupField, java.lang.Object... additionalGroups); @@ -736,22 +736,12 @@ package com.google.firebase.firestore.pipeline { public final class AggregateWithAlias { } - public abstract class BaseStage> { - method protected final String getName(); - method public final T with(String key, boolean value); - method public final T with(String key, com.google.firebase.firestore.pipeline.Field value); - method public final T with(String key, double value); - method protected final T with(String key, error.NonExistentClass value); - method public final T with(String key, String value); - method public final T with(String key, long value); - property protected final String name; - } - public class BooleanExpr extends com.google.firebase.firestore.pipeline.FunctionExpr { method public final com.google.firebase.firestore.pipeline.Expr cond(com.google.firebase.firestore.pipeline.Expr thenExpr, com.google.firebase.firestore.pipeline.Expr elseExpr); method public final com.google.firebase.firestore.pipeline.Expr cond(Object thenValue, Object elseValue); method public final com.google.firebase.firestore.pipeline.AggregateFunction countIf(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); + method public final com.google.firebase.firestore.pipeline.BooleanExpr ifError(com.google.firebase.firestore.pipeline.BooleanExpr catchExpr); method public final com.google.firebase.firestore.pipeline.BooleanExpr not(); field public static final com.google.firebase.firestore.pipeline.BooleanExpr.Companion Companion; } @@ -760,7 +750,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr generic(String name, com.google.firebase.firestore.pipeline.Expr... expr); } - public final class CollectionGroupSource extends com.google.firebase.firestore.pipeline.BaseStage { + public final class CollectionGroupSource extends com.google.firebase.firestore.pipeline.Stage { method public static com.google.firebase.firestore.pipeline.CollectionGroupSource of(String collectionId); method public error.NonExistentClass withForceIndex(String value); field public static final com.google.firebase.firestore.pipeline.CollectionGroupSource.Companion Companion; @@ -770,7 +760,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.CollectionGroupSource of(String collectionId); } - public final class CollectionSource extends com.google.firebase.firestore.pipeline.BaseStage { + public final class CollectionSource extends com.google.firebase.firestore.pipeline.Stage { method public static com.google.firebase.firestore.pipeline.CollectionSource of(com.google.firebase.firestore.CollectionReference ref); method public static com.google.firebase.firestore.pipeline.CollectionSource of(String path); method public error.NonExistentClass withForceIndex(String value); @@ -844,6 +834,8 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, Number second); method public com.google.firebase.firestore.pipeline.ExprWithAlias alias(String alias); method public static final com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); + method public static final com.google.firebase.firestore.pipeline.Expr array(java.lang.Object?... elements); + method public static final com.google.firebase.firestore.pipeline.Expr array(java.util.List elements); method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); method public static final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, Object secondArray, java.lang.Object... otherArrays); method public final com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); @@ -915,6 +907,7 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr bitsOther); method public static final com.google.firebase.firestore.pipeline.Expr bitXor(String bitsFieldName, byte[] bitsOther); method public static final com.google.firebase.firestore.pipeline.Expr bitXor(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr bitsOther); + method public static final com.google.firebase.firestore.pipeline.Expr blob(byte[] bytes); method public final com.google.firebase.firestore.pipeline.Expr byteLength(); method public static final com.google.firebase.firestore.pipeline.Expr byteLength(com.google.firebase.firestore.pipeline.Expr value); method public static final com.google.firebase.firestore.pipeline.Expr byteLength(String fieldName); @@ -1022,6 +1015,8 @@ package com.google.firebase.firestore.pipeline { method public final com.google.firebase.firestore.pipeline.BooleanExpr isAbsent(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isAbsent(com.google.firebase.firestore.pipeline.Expr value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isAbsent(String fieldName); + method public final com.google.firebase.firestore.pipeline.BooleanExpr isError(); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr isError(com.google.firebase.firestore.pipeline.Expr expr); method public final com.google.firebase.firestore.pipeline.BooleanExpr isNan(); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(com.google.firebase.firestore.pipeline.Expr expr); method public static final com.google.firebase.firestore.pipeline.BooleanExpr isNan(String fieldName); @@ -1061,8 +1056,10 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object value); method public static final com.google.firebase.firestore.pipeline.Expr map(java.util.Map elements); + method public static final com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr mapExpression, com.google.firebase.firestore.pipeline.Expr keyExpression); method public static final com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr mapExpression, String key); method public final com.google.firebase.firestore.pipeline.Expr mapGet(String key); + method public static final com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr keyExpression); method public static final com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, String key); method public static final com.google.firebase.firestore.pipeline.Expr mapMerge(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); method public final com.google.firebase.firestore.pipeline.Expr mapMerge(com.google.firebase.firestore.pipeline.Expr mapExpr, com.google.firebase.firestore.pipeline.Expr... otherMaps); @@ -1228,6 +1225,8 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, com.google.firebase.firestore.pipeline.Expr second); method public com.google.firebase.firestore.pipeline.Expr add(String numericFieldName, Number second); method public com.google.firebase.firestore.pipeline.BooleanExpr and(com.google.firebase.firestore.pipeline.BooleanExpr condition, com.google.firebase.firestore.pipeline.BooleanExpr... conditions); + method public com.google.firebase.firestore.pipeline.Expr array(java.lang.Object?... elements); + method public com.google.firebase.firestore.pipeline.Expr array(java.util.List elements); method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); method public com.google.firebase.firestore.pipeline.Expr arrayConcat(com.google.firebase.firestore.pipeline.Expr firstArray, Object secondArray, java.lang.Object... otherArrays); method public com.google.firebase.firestore.pipeline.Expr arrayConcat(String firstArrayField, com.google.firebase.firestore.pipeline.Expr secondArray, java.lang.Object... otherArrays); @@ -1274,6 +1273,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr bitXor(com.google.firebase.firestore.pipeline.Expr bits, com.google.firebase.firestore.pipeline.Expr bitsOther); method public com.google.firebase.firestore.pipeline.Expr bitXor(String bitsFieldName, byte[] bitsOther); method public com.google.firebase.firestore.pipeline.Expr bitXor(String bitsFieldName, com.google.firebase.firestore.pipeline.Expr bitsOther); + method public com.google.firebase.firestore.pipeline.Expr blob(byte[] bytes); method public com.google.firebase.firestore.pipeline.Expr byteLength(com.google.firebase.firestore.pipeline.Expr value); method public com.google.firebase.firestore.pipeline.Expr byteLength(String fieldName); method public com.google.firebase.firestore.pipeline.Expr ceil(com.google.firebase.firestore.pipeline.Expr numericExpr); @@ -1349,6 +1349,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.Expr ifError(com.google.firebase.firestore.pipeline.Expr tryExpr, Object catchValue); method public com.google.firebase.firestore.pipeline.BooleanExpr isAbsent(com.google.firebase.firestore.pipeline.Expr value); method public com.google.firebase.firestore.pipeline.BooleanExpr isAbsent(String fieldName); + method public com.google.firebase.firestore.pipeline.BooleanExpr isError(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.BooleanExpr isNan(com.google.firebase.firestore.pipeline.Expr expr); method public com.google.firebase.firestore.pipeline.BooleanExpr isNan(String fieldName); method public com.google.firebase.firestore.pipeline.BooleanExpr isNotNan(com.google.firebase.firestore.pipeline.Expr expr); @@ -1374,7 +1375,9 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, Object value); method public com.google.firebase.firestore.pipeline.Expr map(java.util.Map elements); + method public com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr mapExpression, com.google.firebase.firestore.pipeline.Expr keyExpression); method public com.google.firebase.firestore.pipeline.Expr mapGet(com.google.firebase.firestore.pipeline.Expr mapExpression, String key); + method public com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, com.google.firebase.firestore.pipeline.Expr keyExpression); method public com.google.firebase.firestore.pipeline.Expr mapGet(String fieldName, String key); method public com.google.firebase.firestore.pipeline.Expr mapMerge(com.google.firebase.firestore.pipeline.Expr firstMap, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); method public com.google.firebase.firestore.pipeline.Expr mapMerge(String firstMapFieldName, com.google.firebase.firestore.pipeline.Expr secondMap, com.google.firebase.firestore.pipeline.Expr... otherMaps); @@ -1492,7 +1495,7 @@ package com.google.firebase.firestore.pipeline { public static final class Field.Companion { } - public final class FindNearestStage extends com.google.firebase.firestore.pipeline.BaseStage { + public final class FindNearestStage extends com.google.firebase.firestore.pipeline.Stage { method public static com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public static com.google.firebase.firestore.pipeline.FindNearestStage of(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public static com.google.firebase.firestore.pipeline.FindNearestStage of(String vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); @@ -1576,7 +1579,17 @@ package com.google.firebase.firestore.pipeline { public static final class PipelineOptions.IndexMode.Companion { } - public final class SampleStage extends com.google.firebase.firestore.pipeline.BaseStage { + public final class RawStage extends com.google.firebase.firestore.pipeline.Stage { + method public static com.google.firebase.firestore.pipeline.RawStage ofName(String name); + method public com.google.firebase.firestore.pipeline.RawStage withArguments(java.lang.Object... arguments); + field public static final com.google.firebase.firestore.pipeline.RawStage.Companion Companion; + } + + public static final class RawStage.Companion { + method public com.google.firebase.firestore.pipeline.RawStage ofName(String name); + } + + public final class SampleStage extends com.google.firebase.firestore.pipeline.Stage { method public static com.google.firebase.firestore.pipeline.SampleStage withDocLimit(int documents); method public static com.google.firebase.firestore.pipeline.SampleStage withPercentage(double percentage); field public static final com.google.firebase.firestore.pipeline.SampleStage.Companion Companion; @@ -1602,17 +1615,18 @@ package com.google.firebase.firestore.pipeline { ctor public Selectable(); } - public final class Stage extends com.google.firebase.firestore.pipeline.BaseStage { - method public static com.google.firebase.firestore.pipeline.Stage ofName(String name); - method public com.google.firebase.firestore.pipeline.Stage withArguments(java.lang.Object... arguments); - field public static final com.google.firebase.firestore.pipeline.Stage.Companion Companion; - } - - public static final class Stage.Companion { - method public com.google.firebase.firestore.pipeline.Stage ofName(String name); + public abstract class Stage> { + method protected final String getName(); + method public final T with(String key, boolean value); + method public final T with(String key, com.google.firebase.firestore.pipeline.Field value); + method public final T with(String key, double value); + method protected final T with(String key, error.NonExistentClass value); + method public final T with(String key, String value); + method public final T with(String key, long value); + property protected final String name; } - public final class UnnestStage extends com.google.firebase.firestore.pipeline.BaseStage { + public final class UnnestStage extends com.google.firebase.firestore.pipeline.Stage { method public static com.google.firebase.firestore.pipeline.UnnestStage withField(com.google.firebase.firestore.pipeline.Selectable arrayWithAlias); method public static com.google.firebase.firestore.pipeline.UnnestStage withField(String arrayField, String alias); method public com.google.firebase.firestore.pipeline.UnnestStage withIndexField(String indexField); 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 5c9e26f5acb..da140a27c49 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 @@ -47,7 +47,7 @@ import com.google.firebase.firestore.pipeline.AggregateFunction; import com.google.firebase.firestore.pipeline.AggregateStage; import com.google.firebase.firestore.pipeline.Expr; -import com.google.firebase.firestore.pipeline.Stage; +import com.google.firebase.firestore.pipeline.RawStage; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import java.util.Collections; import java.util.LinkedHashMap; @@ -279,15 +279,14 @@ public void groupAndAccumulateResultsGeneric() { firestore .pipeline() .collection(randomCol) - .addStage(Stage.ofName("where").withArguments(lt(field("published"), 1984))) - .addStage( - Stage.ofName("aggregate") + .rawStage(RawStage.ofName("where").withArguments(lt(field("published"), 1984))) + .rawStage( + RawStage.ofName("aggregate") .withArguments( ImmutableMap.of("avgRating", AggregateFunction.avg("rating")), ImmutableMap.of("genre", field("genre")))) - .addStage(Stage.ofName("where").withArguments(gt("avgRating", 4.3))) - .addStage( - Stage.ofName("sort").withArguments(field("avgRating").descending())) + .rawStage(RawStage.ofName("where").withArguments(gt("avgRating", 4.3))) + .rawStage(RawStage.ofName("sort").withArguments(field("avgRating").descending())) .execute(); assertThat(waitFor(execute).getResults()) .comparingElementsUsing(DATA_CORRESPONDENCE) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java index 942d6291668..edec58cff66 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java @@ -613,40 +613,39 @@ public void testQueriesCanUseArrayContainsAnyFilters() { FirebaseFirestore db = collection.firestore; // Search for "array" to contain [42, 43]. - Pipeline pipeline = db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", asList(42L, 43L))); + Pipeline pipeline = + db.pipeline().convertFrom(collection.whereArrayContainsAny("array", asList(42L, 43L))); PipelineSnapshot snapshot = waitFor(pipeline.execute()); assertEquals(asList(docA, docB, docD, docE), pipelineSnapshotToValues(snapshot)); // With objects. - pipeline = db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", asList(map("a", 42L)))); + pipeline = + db.pipeline().convertFrom(collection.whereArrayContainsAny("array", asList(map("a", 42L)))); snapshot = waitFor(pipeline.execute()); assertEquals(asList(docF), pipelineSnapshotToValues(snapshot)); // With null. - pipeline = db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", nullList())); + pipeline = db.pipeline().convertFrom(collection.whereArrayContainsAny("array", nullList())); snapshot = waitFor(pipeline.execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With null and a value. List inputList = nullList(); inputList.add(43L); - pipeline = db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", inputList)); + pipeline = db.pipeline().convertFrom(collection.whereArrayContainsAny("array", inputList)); snapshot = waitFor(pipeline.execute()); assertEquals(asList(docE), pipelineSnapshotToValues(snapshot)); // With NaN. - pipeline = db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", asList(Double.NaN))); + pipeline = + db.pipeline().convertFrom(collection.whereArrayContainsAny("array", asList(Double.NaN))); snapshot = waitFor(pipeline.execute()); assertEquals(new ArrayList<>(), pipelineSnapshotToValues(snapshot)); // With NaN and a value. - pipeline = db.pipeline() - .convertFrom(collection.whereArrayContainsAny("array", asList(Double.NaN, 43L))); + pipeline = + db.pipeline() + .convertFrom(collection.whereArrayContainsAny("array", asList(Double.NaN, 43L))); snapshot = waitFor(pipeline.execute()); assertEquals(asList(docE), pipelineSnapshotToValues(snapshot)); } diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index 717e3f02ebc..fbc517bb774 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -575,7 +575,7 @@ public static void checkQueryAndPipelineResultsMatch(Query query, String... expe } PipelineSnapshot docsFromPipeline; try { - docsFromPipeline = waitFor(query.getFirestore().pipeline().convertFrom(query).execute()); + docsFromPipeline = waitFor(query.getFirestore().pipeline().convertFrom(query).execute()); } catch (Exception e) { throw new RuntimeException("Pipeline FAILED", e); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index fb7d8313737..c1b09bcf94e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -36,18 +36,18 @@ import com.google.firebase.firestore.pipeline.ExprWithAlias import com.google.firebase.firestore.pipeline.Field import com.google.firebase.firestore.pipeline.FindNearestStage import com.google.firebase.firestore.pipeline.FunctionExpr -import com.google.firebase.firestore.pipeline.Stage import com.google.firebase.firestore.pipeline.LimitStage import com.google.firebase.firestore.pipeline.OffsetStage import com.google.firebase.firestore.pipeline.Ordering import com.google.firebase.firestore.pipeline.PipelineOptions +import com.google.firebase.firestore.pipeline.RawStage import com.google.firebase.firestore.pipeline.RemoveFieldsStage import com.google.firebase.firestore.pipeline.ReplaceStage import com.google.firebase.firestore.pipeline.SampleStage import com.google.firebase.firestore.pipeline.SelectStage import com.google.firebase.firestore.pipeline.Selectable import com.google.firebase.firestore.pipeline.SortStage -import com.google.firebase.firestore.pipeline.BaseStage +import com.google.firebase.firestore.pipeline.Stage import com.google.firebase.firestore.pipeline.UnionStage import com.google.firebase.firestore.pipeline.UnnestStage import com.google.firebase.firestore.pipeline.WhereStage @@ -59,15 +59,15 @@ class Pipeline internal constructor( internal val firestore: FirebaseFirestore, internal val userDataReader: UserDataReader, - private val stages: FluentIterable> + private val stages: FluentIterable> ) { internal constructor( firestore: FirebaseFirestore, userDataReader: UserDataReader, - stage: BaseStage<*> + stage: Stage<*> ) : this(firestore, userDataReader, FluentIterable.of(stage)) - private fun append(stage: BaseStage<*>): Pipeline { + private fun append(stage: Stage<*>): Pipeline { return Pipeline(firestore, userDataReader, stages.append(stage)) } @@ -103,17 +103,17 @@ internal constructor( .build() /** - * Adds a stage to the pipeline by specifying the stage name as an argument. This does not offer - * any type safety on the stage params and requires the caller to know the order (and optionally - * names) of parameters accepted by the stage. + * Adds a raw stage to the pipeline by specifying the stage name as an argument. This does not + * offer any type safety on the stage params and requires the caller to know the order (and + * optionally names) of parameters accepted by the stage. * * This method provides a way to call stages that are supported by the Firestore backend but that * are not implemented in the SDK version being used. * - * @param stage An [Stage] object that specifies stage name and parameters. + * @param rawStage An [RawStage] object that specifies stage name and parameters. * @return A new [Pipeline] object with this stage appended to the stage list. */ - fun addStage(stage: Stage): Pipeline = append(stage) + fun rawStage(rawStage: RawStage): Pipeline = append(rawStage) /** * Adds new fields to outputs from previous stages. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java index b702746f066..3b7c2ef4c06 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java @@ -20,7 +20,6 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; import androidx.annotation.NonNull; - import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.FieldPath; import com.google.firebase.firestore.model.Values; @@ -29,7 +28,6 @@ import com.google.firebase.firestore.pipeline.Field; import com.google.firebase.firestore.util.Assert; import com.google.firestore.v1.Value; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -219,14 +217,16 @@ BooleanExpr toPipelineExpr() { return and(exists, x.arrayContainsAny(value.getArrayValue().getValuesList())); case IN: return and(exists, x.eqAny(value.getArrayValue().getValuesList())); - case NOT_IN: { - List list = value.getArrayValue().getValuesList(); - if (hasNaN(list)) { - return and(exists, x.notEqAny(filterNaN(list)), ifError(x.isNotNan(), Expr.constant(true))); - } else { - return and(exists, x.notEqAny(list)); + case NOT_IN: + { + List list = value.getArrayValue().getValuesList(); + if (hasNaN(list)) { + return and( + exists, x.notEqAny(filterNaN(list)), ifError(x.isNotNan(), Expr.constant(true))); + } else { + return and(exists, x.notEqAny(list)); + } } - } default: // Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed throw new IllegalArgumentException("Unsupported operator: " + operator); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index ac4d499854f..f4ecf8fcbc7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -37,7 +37,7 @@ import com.google.firebase.firestore.pipeline.FunctionExpr; import com.google.firebase.firestore.pipeline.InternalOptions; import com.google.firebase.firestore.pipeline.Ordering; -import com.google.firebase.firestore.pipeline.BaseStage; +import com.google.firebase.firestore.pipeline.Stage; import com.google.firebase.firestore.util.BiFunction; import com.google.firebase.firestore.util.Function; import com.google.firebase.firestore.util.IntFunction; @@ -549,7 +549,8 @@ public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataR if (fields.size() == 1) { p = p.where(fields.get(0).exists()); } else { - BooleanExpr[] conditions = skipFirstToArray(fields, BooleanExpr[]::new, Expr.Companion::exists); + BooleanExpr[] conditions = + skipFirstToArray(fields, BooleanExpr[]::new, Expr.Companion::exists); p = p.where(and(fields.get(0).exists(), conditions)); } @@ -587,17 +588,18 @@ private static T[] skipFirstToArray(List list, IntFunction generator int size = list.size(); T[] result = generator.apply(size - 1); for (int i = 1; i < size; i++) { - result[i-1] = list.get(i); + result[i - 1] = list.get(i); } return result; } // Many Pipelines require first parameter to be separated out from rest. - private static R[] skipFirstToArray(List list, IntFunction generator, Function map) { + private static R[] skipFirstToArray( + List list, IntFunction generator, Function map) { int size = list.size(); R[] result = generator.apply(size - 1); for (int i = 1; i < size; i++) { - result[i-1] = map.apply(list.get(i)); + result[i - 1] = map.apply(list.get(i)); } return result; } @@ -621,7 +623,7 @@ private static BooleanExpr whereConditionsFromCursor( } @NonNull - private BaseStage pipelineSource(FirebaseFirestore firestore) { + private Stage pipelineSource(FirebaseFirestore firestore) { if (isDocumentQuery()) { return new DocumentsSource(path.canonicalString()); } else if (isCollectionGroupQuery()) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 568a64899ec..cd93238b8d6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -599,14 +599,11 @@ internal object Values { .build() } - @JvmField - val TRUE: Value = Value.newBuilder().setBooleanValue(true).build() + @JvmField val TRUE: Value = Value.newBuilder().setBooleanValue(true).build() - @JvmField - val FALSE: Value = Value.newBuilder().setBooleanValue(false).build() + @JvmField val FALSE: Value = Value.newBuilder().setBooleanValue(false).build() - @JvmStatic - fun encodeValue(value: Boolean): Value = if (value) TRUE else FALSE + @JvmStatic fun encodeValue(value: Boolean): Value = if (value) TRUE else FALSE @JvmStatic fun encodeValue(geoPoint: GeoPoint): Value = 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 5eb5e6c3698..3388b4b8b4d 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 @@ -31,7 +31,6 @@ import com.google.firebase.firestore.util.CustomClassMapper import com.google.firestore.v1.MapValue import com.google.firestore.v1.Value import java.util.Date -import kotlin.reflect.KFunction1 /** * Represents an expression that can be evaluated to a value within the execution of a [Pipeline]. @@ -48,8 +47,11 @@ import kotlin.reflect.KFunction1 */ abstract class Expr internal constructor() { - private class ValueConstant(val value: Value) : Expr() { + private class Constant(val value: Value) : Expr() { override fun toProto(userDataReader: UserDataReader): Value = value + override fun toString(): String { + return "Constant(value=$value)" + } } companion object { @@ -61,7 +63,7 @@ abstract class Expr internal constructor() { toExpr(value, ::pojoToExprOrConstant) ?: throw IllegalArgumentException("Unknown type: $value") - private fun toExpr(value: Any?, toExpr: KFunction1): Expr? { + private inline fun toExpr(value: Any?, toExpr: (Any?) -> Expr): Expr? { if (value == null) return NULL return when (value) { is Expr -> value @@ -75,7 +77,7 @@ abstract class Expr internal constructor() { is DocumentReference -> constant(value) is ByteArray -> constant(value) is VectorValue -> constant(value) - is Value -> ValueConstant(value) + is Value -> Constant(value) is Map<*, *> -> map( value @@ -86,18 +88,18 @@ abstract class Expr internal constructor() { } .toTypedArray() ) - is List<*> -> ListOfExprs(value.map(toExpr).toTypedArray()) + is List<*> -> array(value) else -> null } } - internal fun toArrayOfExprOrConstant(others: Iterable): Array = + private fun toArrayOfExprOrConstant(others: Iterable): Array = others.map(::toExprOrConstant).toTypedArray() internal fun toArrayOfExprOrConstant(others: Array): Array = others.map(::toExprOrConstant).toTypedArray() - private val NULL: Expr = ValueConstant(Values.NULL_VALUE) + private val NULL: Expr = Constant(Values.NULL_VALUE) /** * Create a constant for a [String] value. @@ -107,7 +109,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: String): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -118,7 +120,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: Number): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -129,7 +131,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: Date): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -140,7 +142,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: Timestamp): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -174,8 +176,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] constant instance. */ @JvmStatic - fun constant(value: GeoPoint): Expr { - return ValueConstant(encodeValue(value)) + fun constant(value: GeoPoint): Expr { // Ensure this overload exists or is correctly placed + return Constant(encodeValue(value)) } /** @@ -186,7 +188,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: ByteArray): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -197,7 +199,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: Blob): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) } /** @@ -224,7 +226,18 @@ abstract class Expr internal constructor() { */ @JvmStatic fun constant(value: VectorValue): Expr { - return ValueConstant(encodeValue(value)) + return Constant(encodeValue(value)) + } + + /** + * Create a [Blob] constant from a [ByteArray]. + * + * @param bytes The [ByteArray] to convert to a Blob. + * @return A new [Expr] constant instance representing the Blob. + */ + @JvmStatic + fun blob(bytes: ByteArray): Expr { + return constant(Blob.fromBytes(bytes)) } /** @@ -245,7 +258,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun vector(vector: DoubleArray): Expr { - return ValueConstant(Values.encodeVectorValue(vector)) + return Constant(Values.encodeVectorValue(vector)) } /** @@ -256,7 +269,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun vector(vector: VectorValue): Expr { - return ValueConstant(encodeValue(vector)) + return Constant(encodeValue(vector)) } /** @@ -296,7 +309,7 @@ abstract class Expr internal constructor() { * Creates an expression that performs a logical 'AND' operation. * * @param condition The first [BooleanExpr]. - * @param conditions Addition [BooleanExpr]s. + * @param conditions Additional [BooleanExpr]s. * @return A new [BooleanExpr] representing the logical 'AND' operation. */ @JvmStatic @@ -307,7 +320,7 @@ abstract class Expr internal constructor() { * Creates an expression that performs a logical 'OR' operation. * * @param condition The first [BooleanExpr]. - * @param conditions Addition [BooleanExpr]s. + * @param conditions Additional [BooleanExpr]s. * @return A new [BooleanExpr] representing the logical 'OR' operation. */ @JvmStatic @@ -318,7 +331,7 @@ abstract class Expr internal constructor() { * Creates an expression that performs a logical 'XOR' operation. * * @param condition The first [BooleanExpr]. - * @param conditions Addition [BooleanExpr]s. + * @param conditions Additional [BooleanExpr]s. * @return A new [BooleanExpr] representing the logical 'XOR' operation. */ @JvmStatic @@ -753,9 +766,7 @@ abstract class Expr internal constructor() { * @param second Numeric expression to add. * @return A new [Expr] representing the addition operation. */ - @JvmStatic - fun add(first: Expr, second: Expr): Expr = - FunctionExpr("add", first, second) + @JvmStatic fun add(first: Expr, second: Expr): Expr = FunctionExpr("add", first, second) /** * Creates an expression that adds numeric expressions with a constant. @@ -764,9 +775,7 @@ abstract class Expr internal constructor() { * @param second Constant to add. * @return A new [Expr] representing the addition operation. */ - @JvmStatic - fun add(first: Expr, second: Number): Expr = - FunctionExpr("add", first, second) + @JvmStatic fun add(first: Expr, second: Number): Expr = FunctionExpr("add", first, second) /** * Creates an expression that adds a numeric field with a numeric expression. @@ -842,8 +851,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the multiplication operation. */ @JvmStatic - fun multiply(first: Expr, second: Expr): Expr = - FunctionExpr("multiply", first, second) + fun multiply(first: Expr, second: Expr): Expr = FunctionExpr("multiply", first, second) /** * Creates an expression that multiplies numeric expressions with a constant. @@ -853,8 +861,7 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the multiplication operation. */ @JvmStatic - fun multiply(first: Expr, second: Number): Expr = - FunctionExpr("multiply", first, second) + fun multiply(first: Expr, second: Number): Expr = FunctionExpr("multiply", first, second) /** * Creates an expression that multiplies a numeric field with a numeric expression. @@ -974,8 +981,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the 'IN' comparison. */ @JvmStatic - fun eqAny(expression: Expr, values: List): BooleanExpr = - eqAny(expression, ListOfExprs(toArrayOfExprOrConstant(values))) + fun eqAny(expression: Expr, values: List): BooleanExpr = eqAny(expression, array(values)) /** * Creates an expression that checks if an [expression], when evaluated, is equal to any of the @@ -999,8 +1005,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the 'IN' comparison. */ @JvmStatic - fun eqAny(fieldName: String, values: List): BooleanExpr = - eqAny(fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + fun eqAny(fieldName: String, values: List): BooleanExpr = eqAny(fieldName, array(values)) /** * Creates an expression that checks if a field's value is equal to any of the elements of @@ -1025,7 +1030,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun notEqAny(expression: Expr, values: List): BooleanExpr = - notEqAny(expression, ListOfExprs(toArrayOfExprOrConstant(values))) + notEqAny(expression, array(values)) /** * Creates an expression that checks if an [expression], when evaluated, is not equal to all the @@ -1050,7 +1055,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun notEqAny(fieldName: String, values: List): BooleanExpr = - notEqAny(fieldName, ListOfExprs(toArrayOfExprOrConstant(values))) + notEqAny(fieldName, array(values)) /** * Creates an expression that checks if a field's value is not equal to all of the elements of @@ -1761,6 +1766,28 @@ abstract class Expr internal constructor() { @JvmStatic fun mapGet(fieldName: String, key: String): Expr = FunctionExpr("map_get", fieldName, key) + /** + * Accesses a value from a map (object) field using the provided [keyExpression]. + * + * @param mapExpression The expression representing the map. + * @param keyExpression The key to access in the map. + * @return A new [Expr] representing the value associated with the given key in the map. + */ + @JvmStatic + fun mapGet(mapExpression: Expr, keyExpression: Expr): Expr = + FunctionExpr("map_get", mapExpression, keyExpression) + + /** + * Accesses a value from a map (object) field using the provided [keyExpression]. + * + * @param fieldName The field name of the map field. + * @param keyExpression The key to access in the map. + * @return A new [Expr] representing the value associated with the given key in the map. + */ + @JvmStatic + fun mapGet(fieldName: String, keyExpression: Expr): Expr = + FunctionExpr("map_get", fieldName, keyExpression) + /** * Creates an expression that merges multiple maps into a single map. If multiple maps have the * same key, the later value is used. @@ -2511,6 +2538,25 @@ abstract class Expr internal constructor() { @JvmStatic fun lte(fieldName: String, value: Any): BooleanExpr = BooleanExpr("lte", fieldName, value) + /** + * Creates an expression that creates a Firestore array value from an input array. + * + * @param elements The input array to evaluate in the expression. + * @return A new [Expr] representing the array function. + */ + @JvmStatic + fun array(vararg elements: Any?): Expr = + FunctionExpr("array", elements.map(::toExprOrConstant).toTypedArray()) + + /** + * Creates an expression that creates a Firestore array value from an input array. + * + * @param elements The input array to evaluate in the expression. + * @return A new [Expr] representing the array function. + */ + @JvmStatic + fun array(elements: List): Expr = + FunctionExpr("array", elements.map(::toExprOrConstant).toTypedArray()) /** * Creates an expression that concatenates an array with other arrays. * @@ -2628,8 +2674,7 @@ abstract class Expr internal constructor() { * @return A new [BooleanExpr] representing the arrayContainsAll operation. */ @JvmStatic - fun arrayContainsAll(array: Expr, values: List) = - arrayContainsAll(array, ListOfExprs(toArrayOfExprOrConstant(values))) + fun arrayContainsAll(array: Expr, values: List) = arrayContainsAll(array, array(values)) /** * Creates an expression that checks if [array] contains all elements of [arrayExpression]. @@ -2651,11 +2696,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAll(arrayFieldName: String, values: List) = - BooleanExpr( - "array_contains_all", - arrayFieldName, - ListOfExprs(toArrayOfExprOrConstant(values)) - ) + BooleanExpr("array_contains_all", arrayFieldName, array(values)) /** * Creates an expression that checks if array field contains all elements of [arrayExpression]. @@ -2677,7 +2718,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAny(array: Expr, values: List) = - BooleanExpr("array_contains_any", array, ListOfExprs(toArrayOfExprOrConstant(values))) + BooleanExpr("array_contains_any", array, array(values)) /** * Creates an expression that checks if [array] contains any elements of [arrayExpression]. @@ -2699,11 +2740,7 @@ abstract class Expr internal constructor() { */ @JvmStatic fun arrayContainsAny(arrayFieldName: String, values: List) = - BooleanExpr( - "array_contains_any", - arrayFieldName, - ListOfExprs(toArrayOfExprOrConstant(values)) - ) + BooleanExpr("array_contains_any", arrayFieldName, array(values)) /** * Creates an expression that checks if array field contains any elements of [arrayExpression]. @@ -2845,12 +2882,21 @@ abstract class Expr internal constructor() { * This overload will return [BooleanExpr] when both parameters are also [BooleanExpr]. * * @param tryExpr The try boolean expression. - * @param catchExpr The catch boolean expression that will be evaluated and returned if the [tryExpr] - * produces an error. + * @param catchExpr The catch boolean expression that will be evaluated and returned if the + * [tryExpr] produces an error. * @return A new [BooleanExpr] representing the ifError operation. */ @JvmStatic - fun ifError(tryExpr: BooleanExpr, catchExpr: BooleanExpr): BooleanExpr = BooleanExpr("if_error", tryExpr, catchExpr) + fun ifError(tryExpr: BooleanExpr, catchExpr: BooleanExpr): BooleanExpr = + BooleanExpr("if_error", tryExpr, catchExpr) + + /** + * Creates an expression that checks if a given expression produces an error. + * + * @param expr The expression to check. + * @return A new [BooleanExpr] representing the `isError` check. + */ + @JvmStatic fun isError(expr: Expr): BooleanExpr = BooleanExpr("is_error", expr) /** * Creates an expression that returns the [catchValue] argument if there is an error, else @@ -3953,6 +3999,13 @@ abstract class Expr internal constructor() { */ fun ifError(catchValue: Any): Expr = Companion.ifError(this, catchValue) + /** + * Creates an expression that checks if this expression produces an error. + * + * @return A new [BooleanExpr] representing the `isError` check. + */ + fun isError(): BooleanExpr = Companion.isError(this) + internal abstract fun toProto(userDataReader: UserDataReader): Value } @@ -4010,11 +4063,6 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select Value.newBuilder().setFieldReferenceValue(fieldPath.canonicalString()).build() } -internal class ListOfExprs(private val expressions: Array) : Expr() { - override fun toProto(userDataReader: UserDataReader): Value = - encodeValue(expressions.map { it.toProto(userDataReader) }) -} - /** * This class defines the base class for Firestore [Pipeline] functions, which can be evaluated * within pipeline execution. @@ -4132,6 +4180,18 @@ open class BooleanExpr internal constructor(name: String, params: Array> +abstract class Stage> internal constructor(protected val name: String, internal val options: InternalOptions) { internal fun toProtoStage(userDataReader: UserDataReader): Pipeline.Stage { val builder = Pipeline.Stage.newBuilder() @@ -96,32 +96,32 @@ internal constructor(protected val name: String, internal val options: InternalO * This class provides a way to call stages that are supported by the Firestore backend but that are * not implemented in the SDK version being used. */ -class Stage +class RawStage private constructor( name: String, private val arguments: List, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage(name, options) { +) : Stage(name, options) { companion object { /** * Specify name of stage * * @param name The unique name of the stage to add. - * @return [Stage] with specified parameters. + * @return [RawStage] with specified parameters. */ - @JvmStatic fun ofName(name: String) = Stage(name, emptyList(), InternalOptions.EMPTY) + @JvmStatic fun ofName(name: String) = RawStage(name, emptyList(), InternalOptions.EMPTY) } - override fun self(options: InternalOptions) = Stage(name, arguments, options) + override fun self(options: InternalOptions) = RawStage(name, arguments, options) /** * Specify arguments to stage. * * @param arguments A list of ordered parameters to configure the stage's behavior. - * @return [Stage] with specified parameters. + * @return [RawStage] with specified parameters. */ - fun withArguments(vararg arguments: Any): Stage = - Stage(name, arguments.map(GenericArg::from), options) + fun withArguments(vararg arguments: Any): RawStage = + RawStage(name, arguments.map(GenericArg::from), options) override fun args(userDataReader: UserDataReader): Sequence = arguments.asSequence().map { it.toProto(userDataReader) } @@ -167,7 +167,7 @@ internal sealed class GenericArg { internal class DatabaseSource @JvmOverloads internal constructor(options: InternalOptions = InternalOptions.EMPTY) : - BaseStage("database", options) { + Stage("database", options) { override fun self(options: InternalOptions) = DatabaseSource(options) override fun args(userDataReader: UserDataReader): Sequence = emptySequence() } @@ -178,7 +178,7 @@ internal constructor( // We validate [firestore.databaseId] when adding to pipeline. internal val firestore: FirebaseFirestore?, options: InternalOptions -) : BaseStage("collection", options) { +) : Stage("collection", options) { override fun self(options: InternalOptions): CollectionSource = CollectionSource(path, firestore, options) override fun args(userDataReader: UserDataReader): Sequence = @@ -216,7 +216,7 @@ internal constructor( class CollectionGroupSource private constructor(private val collectionId: String, options: InternalOptions) : - BaseStage("collection_group", options) { + Stage("collection_group", options) { override fun self(options: InternalOptions) = CollectionGroupSource(collectionId, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setReferenceValue("").build(), encodeValue(collectionId)) @@ -246,7 +246,7 @@ internal class DocumentsSource internal constructor( private val documents: Array, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("documents", options) { +) : Stage("documents", options) { internal constructor(document: String) : this(arrayOf(document)) override fun self(options: InternalOptions) = DocumentsSource(documents, options) override fun args(userDataReader: UserDataReader): Sequence = @@ -257,7 +257,7 @@ internal class AddFieldsStage internal constructor( private val fields: Array, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("add_fields", options) { +) : Stage("add_fields", options) { override fun self(options: InternalOptions) = AddFieldsStage(fields, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto(userDataReader) })) @@ -284,7 +284,7 @@ internal constructor( private val accumulators: Map, private val groups: Map, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("aggregate", options) { +) : Stage("aggregate", options) { private constructor(accumulators: Map) : this(accumulators, emptyMap()) companion object { @@ -349,7 +349,7 @@ internal class WhereStage internal constructor( private val condition: BooleanExpr, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("where", options) { +) : Stage("where", options) { override fun self(options: InternalOptions) = WhereStage(condition, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(condition.toProto(userDataReader)) @@ -365,7 +365,7 @@ internal constructor( private val vector: Expr, private val distanceMeasure: DistanceMeasure, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("find_nearest", options) { +) : Stage("find_nearest", options) { companion object { @@ -477,7 +477,7 @@ internal constructor( internal class LimitStage internal constructor(private val limit: Int, options: InternalOptions = InternalOptions.EMPTY) : - BaseStage("limit", options) { + Stage("limit", options) { override fun self(options: InternalOptions) = LimitStage(limit, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(limit)) @@ -485,7 +485,7 @@ internal constructor(private val limit: Int, options: InternalOptions = Internal internal class OffsetStage internal constructor(private val offset: Int, options: InternalOptions = InternalOptions.EMPTY) : - BaseStage("offset", options) { + Stage("offset", options) { override fun self(options: InternalOptions) = OffsetStage(offset, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(offset)) @@ -495,7 +495,7 @@ internal class SelectStage internal constructor( private val fields: Array, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("select", options) { +) : Stage("select", options) { override fun self(options: InternalOptions) = SelectStage(fields, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(fields.associate { it.getAlias() to it.toProto(userDataReader) })) @@ -505,7 +505,7 @@ internal class SortStage internal constructor( private val orders: Array, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("sort", options) { +) : Stage("sort", options) { override fun self(options: InternalOptions) = SortStage(orders, options) override fun args(userDataReader: UserDataReader): Sequence = orders.asSequence().map { it.toProto(userDataReader) } @@ -515,7 +515,7 @@ internal class DistinctStage internal constructor( private val groups: Array, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("distinct", options) { +) : Stage("distinct", options) { override fun self(options: InternalOptions) = DistinctStage(groups, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(encodeValue(groups.associate { it.getAlias() to it.toProto(userDataReader) })) @@ -525,7 +525,7 @@ internal class RemoveFieldsStage internal constructor( private val fields: Array, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("remove_fields", options) { +) : Stage("remove_fields", options) { override fun self(options: InternalOptions) = RemoveFieldsStage(fields, options) override fun args(userDataReader: UserDataReader): Sequence = fields.asSequence().map(Field::toProto) @@ -536,7 +536,7 @@ internal constructor( private val mapValue: Expr, private val mode: Mode, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("replace", options) { +) : Stage("replace", options) { class Mode private constructor(internal val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) companion object { @@ -563,7 +563,7 @@ private constructor( private val size: Number, private val mode: Mode, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("sample", options) { +) : Stage("sample", options) { override fun self(options: InternalOptions) = SampleStage(size, mode, options) class Mode private constructor(internal val proto: Value) { private constructor(protoString: String) : this(encodeValue(protoString)) @@ -606,7 +606,7 @@ internal class UnionStage internal constructor( private val other: com.google.firebase.firestore.Pipeline, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("union", options) { +) : Stage("union", options) { override fun self(options: InternalOptions) = UnionStage(other, options) override fun args(userDataReader: UserDataReader): Sequence = sequenceOf(Value.newBuilder().setPipelineValue(other.toPipelineProto()).build()) @@ -620,7 +620,7 @@ class UnnestStage internal constructor( private val selectable: Selectable, options: InternalOptions = InternalOptions.EMPTY -) : BaseStage("unnest", options) { +) : Stage("unnest", options) { companion object { /** From 38b6f39c9eba36a7d12bccb393a7dac7832f955f Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 5 Jun 2025 10:15:33 -0400 Subject: [PATCH 108/152] Number Semantics Test --- .../pipeline/NumberSemanticsTests.kt | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/NumberSemanticsTests.kt diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/NumberSemanticsTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/NumberSemanticsTests.kt new file mode 100644 index 00000000000..3b6d97b314e --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/NumberSemanticsTests.kt @@ -0,0 +1,297 @@ +// Copyright 2025 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.firebase.firestore.pipeline + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.firestore.RealtimePipelineSource +import com.google.firebase.firestore.TestUtil +import com.google.firebase.firestore.pipeline.Expr.Companion.array +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContains +import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContainsAny +import com.google.firebase.firestore.pipeline.Expr.Companion.constant +import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.pipeline.Expr.Companion.notEqAny +import com.google.firebase.firestore.runPipeline +import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class NumberSemanticsTests { + + private val db = TestUtil.firestore() + + @Test + fun `zero negative double zero`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("score" to 0L)) // Integer 0 + val doc3 = doc("users/c", 1000, mapOf("score" to 0.0)) // Double 0.0 + val doc4 = doc("users/d", 1000, mapOf("score" to -0.0)) // Double -0.0 + val doc5 = doc("users/e", 1000, mapOf("score" to 1L)) // Integer 1 + val documents = listOf(doc1, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db).collection("users").where(field("score").eq(constant(-0.0))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3, doc4)) + } + + @Test + fun `zero negative integer zero`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("score" to 0L)) + val doc3 = doc("users/c", 1000, mapOf("score" to 0.0)) + val doc4 = doc("users/d", 1000, mapOf("score" to -0.0)) + val doc5 = doc("users/e", 1000, mapOf("score" to 1L)) + val documents = listOf(doc1, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(field("score").eq(constant(0L))) // Firestore -0LL is 0L + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3, doc4)) + } + + @Test + fun `zero positive double zero`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("score" to 0L)) + val doc3 = doc("users/c", 1000, mapOf("score" to 0.0)) + val doc4 = doc("users/d", 1000, mapOf("score" to -0.0)) + val doc5 = doc("users/e", 1000, mapOf("score" to 1L)) + val documents = listOf(doc1, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db).collection("users").where(field("score").eq(constant(0.0))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3, doc4)) + } + + @Test + fun `zero positive integer zero`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("score" to 0L)) + val doc3 = doc("users/c", 1000, mapOf("score" to 0.0)) + val doc4 = doc("users/d", 1000, mapOf("score" to -0.0)) + val doc5 = doc("users/e", 1000, mapOf("score" to 1L)) + val documents = listOf(doc1, doc3, doc4, doc5) + + val pipeline = + RealtimePipelineSource(db).collection("users").where(field("score").eq(constant(0L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3, doc4)) + } + + @Test + fun `equal Nan`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to Double.NaN)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25L)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db).collection("users").where(field("age").eq(constant(Double.NaN))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `less than Nan`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to Double.NaN)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to null)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db).collection("users").where(field("age").lt(constant(Double.NaN))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `less than equal Nan`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to Double.NaN)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to null)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db).collection("users").where(field("age").lte(constant(Double.NaN))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `greater than equal Nan`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to Double.NaN)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 100L)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db).collection("users").where(field("age").gte(constant(Double.NaN))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `greater than Nan`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to Double.NaN)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 100L)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db).collection("users").where(field("age").gt(constant(Double.NaN))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `not equal Nan`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to Double.NaN)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25L)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db).collection("users").where(field("age").neq(constant(Double.NaN))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc2, doc3)) + } + + @Test + fun `eqAny contains Nan`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to 75.5)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25L)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(field("name").eqAny(array(Double.NaN, "alice"))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `eqAny contains Nan only is empty`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to Double.NaN)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25L)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db).collection("users").where(field("age").eqAny(array(Double.NaN))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `arrayContains Nan only is empty`(): Unit = runBlocking { + // Documents where 'age' is scalar, not an array. + // arrayContains should not match if the field is not an array or if element is NaN. + val doc1 = doc("users/a", 1000, mapOf("name" to "alice", "age" to Double.NaN)) + val doc2 = doc("users/b", 1000, mapOf("name" to "bob", "age" to 25L)) + val doc3 = doc("users/c", 1000, mapOf("name" to "charlie", "age" to 100L)) + // Example doc if 'age' were an array: + // val docWithArray = doc("users/d", 1000, mapOf("name" to "diana", "age" to + // listOf(Double.NaN))) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(arrayContains(field("age"), constant(Double.NaN))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `arrayContainsAny with Nan`(): Unit = runBlocking { + val doc1 = doc("k/a", 1000, mapOf("field" to listOf(Double.NaN))) + val doc2 = doc("k/b", 1000, mapOf("field" to listOf(Double.NaN, 42L))) + val doc3 = doc("k/c", 1000, mapOf("field" to listOf("foo", 42L))) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("k") + .where(arrayContainsAny(field("field"), array(Double.NaN, "foo"))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3) + } + + @Test + fun `notEqAny contains Nan`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("age" to 42L)) + val doc2 = doc("users/b", 1000, mapOf("age" to Double.NaN)) + val doc3 = doc("users/c", 1000, mapOf("age" to 25L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(notEqAny(field("age"), array(Double.NaN, 42L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc2, doc3)) + } + + @Test + fun `notEqAny contains Nan only matches all`(): Unit = runBlocking { + val doc1 = doc("users/a", 1000, mapOf("age" to 42L)) + val doc2 = doc("users/b", 1000, mapOf("age" to Double.NaN)) + val doc3 = doc("users/c", 1000, mapOf("age" to 25L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(notEqAny(field("age"), array(Double.NaN))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc2, doc3)) + } + + @Test + fun `array with Nan`(): Unit = runBlocking { + val doc1 = doc("k/a", 1000, mapOf("foo" to listOf(Double.NaN))) + val doc2 = doc("k/b", 1000, mapOf("foo" to listOf(42L))) + val documents = listOf(doc1, doc2) + + val pipeline = + RealtimePipelineSource(db).collection("k").where(field("foo").eq(array(Double.NaN))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } +} From 64f26ea99f8b7cb89048c4c4a66dd8831ff2312c Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 5 Jun 2025 10:25:34 -0400 Subject: [PATCH 109/152] Fix after merge --- .../com/google/firebase/firestore/Pipeline.kt | 9 ++-- .../firestore/pipeline/expressions.kt | 41 ------------------- 2 files changed, 4 insertions(+), 46 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index cea1f572607..cbe72ab292c 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -25,7 +25,6 @@ import com.google.firebase.firestore.pipeline.AddFieldsStage import com.google.firebase.firestore.pipeline.AggregateFunction import com.google.firebase.firestore.pipeline.AggregateStage import com.google.firebase.firestore.pipeline.AggregateWithAlias -import com.google.firebase.firestore.pipeline.BaseStage import com.google.firebase.firestore.pipeline.BooleanExpr import com.google.firebase.firestore.pipeline.CollectionGroupSource import com.google.firebase.firestore.pipeline.CollectionSource @@ -132,7 +131,7 @@ class Pipeline private constructor( firestore: FirebaseFirestore, userDataReader: UserDataReader, - stages: FluentIterable> + stages: FluentIterable> ) : AbstractPipeline(firestore, userDataReader, stages) { internal constructor( firestore: FirebaseFirestore, @@ -761,15 +760,15 @@ class RealtimePipeline internal constructor( firestore: FirebaseFirestore, userDataReader: UserDataReader, - stages: FluentIterable> + stages: FluentIterable> ) : AbstractPipeline(firestore, userDataReader, stages) { internal constructor( firestore: FirebaseFirestore, userDataReader: UserDataReader, - stage: BaseStage<*> + stage: Stage<*> ) : this(firestore, userDataReader, FluentIterable.of(stage)) - private fun append(stage: BaseStage<*>): RealtimePipeline { + private fun append(stage: Stage<*>): RealtimePipeline { return RealtimePipeline(firestore, userDataReader, stages.append(stage)) } 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 38670ec0d4b..09240ebcdfe 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 @@ -1854,28 +1854,6 @@ abstract class Expr internal constructor() { fun mapGet(fieldName: String, keyExpression: Expr): Expr = FunctionExpr("map_get", evaluateMapGet, fieldName, keyExpression) - /** - * Accesses a value from a map (object) field using the provided [keyExpression]. - * - * @param mapExpression The expression representing the map. - * @param keyExpression The key to access in the map. - * @return A new [Expr] representing the value associated with the given key in the map. - */ - @JvmStatic - fun mapGet(mapExpression: Expr, keyExpression: Expr): Expr = - FunctionExpr("map_get", mapExpression, keyExpression) - - /** - * Accesses a value from a map (object) field using the provided [keyExpression]. - * - * @param fieldName The field name of the map field. - * @param keyExpression The key to access in the map. - * @return A new [Expr] representing the value associated with the given key in the map. - */ - @JvmStatic - fun mapGet(fieldName: String, keyExpression: Expr): Expr = - FunctionExpr("map_get", fieldName, keyExpression) - /** * Creates an expression that merges multiple maps into a single map. If multiple maps have the * same key, the later value is used. @@ -2677,25 +2655,6 @@ abstract class Expr internal constructor() { fun array(elements: List): Expr = FunctionExpr("array", evaluateArray, elements.map(::toExprOrConstant).toTypedArray()) - /** - * Creates an expression that creates a Firestore array value from an input array. - * - * @param elements The input array to evaluate in the expression. - * @return A new [Expr] representing the array function. - */ - @JvmStatic - fun array(vararg elements: Any?): Expr = - FunctionExpr("array", elements.map(::toExprOrConstant).toTypedArray()) - - /** - * Creates an expression that creates a Firestore array value from an input array. - * - * @param elements The input array to evaluate in the expression. - * @return A new [Expr] representing the array function. - */ - @JvmStatic - fun array(elements: List): Expr = - FunctionExpr("array", elements.map(::toExprOrConstant).toTypedArray()) /** * Creates an expression that concatenates an array with other arrays. * From 5eb6a0128ee475c3f1d576ecba7d59b55ab6d210 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 5 Jun 2025 15:03:52 -0400 Subject: [PATCH 110/152] Fixes / Refactor of Values --- .../core/number/NumberComparisonHelper.java | 7 +- .../google/firebase/firestore/GeoPoint.java | 7 +- .../google/firebase/firestore/core/View.java | 4 +- .../firestore/local/DocumentReference.java | 6 +- .../firestore/local/MemoryMutationQueue.java | 5 +- .../firestore/local/SQLiteMutationQueue.java | 3 +- .../firebase/firestore/model/BasePath.java | 2 +- .../google/firebase/firestore/model/Values.kt | 56 +++++++++------- .../google/firebase/firestore/util/Util.java | 65 +------------------ 9 files changed, 48 insertions(+), 107 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/cloud/datastore/core/number/NumberComparisonHelper.java b/firebase-firestore/src/main/java/com/google/cloud/datastore/core/number/NumberComparisonHelper.java index 6af2ea76995..270ff8462f3 100644 --- a/firebase-firestore/src/main/java/com/google/cloud/datastore/core/number/NumberComparisonHelper.java +++ b/firebase-firestore/src/main/java/com/google/cloud/datastore/core/number/NumberComparisonHelper.java @@ -50,7 +50,7 @@ public static int firestoreCompareDoubleWithLong(double doubleValue, long longVa } long doubleAsLong = (long) doubleValue; - int cmp = compareLongs(doubleAsLong, longValue); + int cmp = Long.compare(doubleAsLong, longValue); if (cmp != 0) { return cmp; } @@ -60,11 +60,6 @@ public static int firestoreCompareDoubleWithLong(double doubleValue, long longVa return firestoreCompareDoubles(doubleValue, longAsDouble); } - /** Compares longs. */ - public static int compareLongs(long leftLong, long rightLong) { - return Long.compare(leftLong, rightLong); - } - /** * Compares doubles with Firestore query semantics: NaN precedes all other numbers and equals * itself, all zeroes are equal. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/GeoPoint.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/GeoPoint.java index a989189a1bd..0c257bbabf8 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/GeoPoint.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/GeoPoint.java @@ -14,9 +14,10 @@ package com.google.firebase.firestore; +import static com.google.cloud.datastore.core.number.NumberComparisonHelper.firestoreCompareDoubles; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.firebase.firestore.util.Util; /** Immutable class representing a {@code GeoPoint} in Cloud Firestore */ public class GeoPoint implements Comparable { @@ -52,9 +53,9 @@ public double getLongitude() { @Override public int compareTo(@NonNull GeoPoint other) { - int comparison = Util.compareDoubles(latitude, other.latitude); + int comparison = firestoreCompareDoubles(latitude, other.latitude); if (comparison == 0) { - return Util.compareDoubles(longitude, other.longitude); + return firestoreCompareDoubles(longitude, other.longitude); } else { return comparison; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java index a3a508df207..849f0b10f9e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java @@ -17,7 +17,6 @@ import static com.google.firebase.firestore.core.Query.LimitType.LIMIT_TO_FIRST; import static com.google.firebase.firestore.core.Query.LimitType.LIMIT_TO_LAST; import static com.google.firebase.firestore.util.Assert.hardAssert; -import static com.google.firebase.firestore.util.Util.compareIntegers; import androidx.annotation.Nullable; import com.google.firebase.database.collection.ImmutableSortedMap; @@ -301,7 +300,8 @@ public ViewChange applyChanges( Collections.sort( viewChanges, (DocumentViewChange o1, DocumentViewChange o2) -> { - int typeComp = compareIntegers(View.changeTypeOrder(o1), View.changeTypeOrder(o2)); + int i1 = View.changeTypeOrder(o1); + int typeComp = Integer.compare(i1, View.changeTypeOrder(o2)); if (typeComp != 0) { return typeComp; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentReference.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentReference.java index 512b419906d..74dcfe63da0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentReference.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentReference.java @@ -14,8 +14,6 @@ package com.google.firebase.firestore.local; -import static com.google.firebase.firestore.util.Util.compareIntegers; - import com.google.firebase.firestore.model.DocumentKey; import java.util.Comparator; @@ -60,12 +58,12 @@ int getId() { return keyComp; } - return compareIntegers(o1.targetOrBatchId, o2.targetOrBatchId); + return Integer.compare(o1.targetOrBatchId, o2.targetOrBatchId); }; static final Comparator BY_TARGET = (o1, o2) -> { - int targetComp = compareIntegers(o1.targetOrBatchId, o2.targetOrBatchId); + int targetComp = Integer.compare(o1.targetOrBatchId, o2.targetOrBatchId); if (targetComp != 0) { return targetComp; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java index 7a238dd20c1..23b5c1e730d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java @@ -28,7 +28,6 @@ import com.google.firebase.firestore.model.mutation.Mutation; import com.google.firebase.firestore.model.mutation.MutationBatch; import com.google.firebase.firestore.remote.WriteStream; -import com.google.firebase.firestore.util.Util; import com.google.protobuf.ByteString; import java.util.ArrayList; import java.util.Collections; @@ -216,7 +215,7 @@ public List getAllMutationBatchesAffectingDocumentKey(DocumentKey public List getAllMutationBatchesAffectingDocumentKeys( Iterable documentKeys) { ImmutableSortedSet uniqueBatchIDs = - new ImmutableSortedSet(emptyList(), Util.comparator()); + new ImmutableSortedSet(emptyList(), Comparable::compareTo); for (DocumentKey key : documentKeys) { DocumentReference start = new DocumentReference(key, 0); @@ -255,7 +254,7 @@ public List getAllMutationBatchesAffectingQuery(Query query) { // Find unique batchIDs referenced by all documents potentially matching the query. ImmutableSortedSet uniqueBatchIDs = - new ImmutableSortedSet(emptyList(), Util.comparator()); + new ImmutableSortedSet(emptyList(), Comparable::compareTo); Iterator iterator = batchesByDocumentKey.iteratorFrom(start); while (iterator.hasNext()) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java index dd70a58d02b..1c5c96f794a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java @@ -30,7 +30,6 @@ import com.google.firebase.firestore.model.mutation.MutationBatch; import com.google.firebase.firestore.remote.WriteStream; import com.google.firebase.firestore.util.Consumer; -import com.google.firebase.firestore.util.Util; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageLite; @@ -324,7 +323,7 @@ public List getAllMutationBatchesAffectingDocumentKeys( Collections.sort( result, (MutationBatch lhs, MutationBatch rhs) -> - Util.compareIntegers(lhs.getBatchId(), rhs.getBatchId())); + Integer.compare(lhs.getBatchId(), rhs.getBatchId())); } return result; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/BasePath.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/BasePath.java index 6d800de05d4..d002c8aead4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/BasePath.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/BasePath.java @@ -107,7 +107,7 @@ public int compareTo(@NonNull B o) { } i++; } - return Util.compareIntegers(myLength, theirLength); + return Integer.compare(myLength, theirLength); } private static int compareSegments(String lhs, String rhs) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index cd93238b8d6..9d2b3424623 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -13,6 +13,8 @@ // limitations under the License. package com.google.firebase.firestore.model +import com.google.cloud.datastore.core.number.NumberComparisonHelper.firestoreCompareDoubleWithLong +import com.google.cloud.datastore.core.number.NumberComparisonHelper.firestoreCompareDoubles import com.google.firebase.firestore.Blob import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.GeoPoint @@ -28,7 +30,6 @@ import com.google.protobuf.ByteString import com.google.protobuf.NullValue import com.google.protobuf.Timestamp import com.google.type.LatLng -import java.lang.Double.doubleToLongBits import java.util.Date import java.util.TreeMap import kotlin.math.min @@ -135,17 +136,25 @@ internal object Values { } } - private fun numberEquals(left: Value, right: Value): Boolean { - if (left.valueTypeCase != right.valueTypeCase) { - return false - } - return when (left.valueTypeCase) { - ValueTypeCase.INTEGER_VALUE -> left.integerValue == right.integerValue + private fun numberEquals(left: Value, right: Value): Boolean = + when (left.valueTypeCase) { + ValueTypeCase.INTEGER_VALUE -> + when (right.valueTypeCase) { + ValueTypeCase.INTEGER_VALUE -> left.integerValue == right.integerValue + ValueTypeCase.DOUBLE_VALUE -> + firestoreCompareDoubleWithLong(right.doubleValue, left.integerValue) == 0 + else -> false + } ValueTypeCase.DOUBLE_VALUE -> - doubleToLongBits(left.doubleValue) == doubleToLongBits(right.doubleValue) + when (right.valueTypeCase) { + ValueTypeCase.INTEGER_VALUE -> + firestoreCompareDoubleWithLong(left.doubleValue, right.integerValue) == 0 + ValueTypeCase.DOUBLE_VALUE -> + firestoreCompareDoubles(left.doubleValue, right.doubleValue) == 0 + else -> false + } else -> false } - } private fun arrayEquals(left: Value, right: Value): Boolean { val leftArray = left.arrayValue @@ -199,13 +208,13 @@ internal object Values { val rightType = typeOrder(right) if (leftType != rightType) { - return Util.compareIntegers(leftType, rightType) + return leftType.compareTo(rightType) } return when (leftType) { TYPE_ORDER_NULL, TYPE_ORDER_MAX_VALUE -> 0 - TYPE_ORDER_BOOLEAN -> Util.compareBooleans(left.booleanValue, right.booleanValue) + TYPE_ORDER_BOOLEAN -> left.booleanValue.compareTo(right.booleanValue) TYPE_ORDER_NUMBER -> compareNumbers(left, right) TYPE_ORDER_TIMESTAMP -> compareTimestamps(left.timestampValue, right.timestampValue) TYPE_ORDER_SERVER_TIMESTAMP -> @@ -269,15 +278,15 @@ internal object Values { private fun compareNumbers(left: Value, right: Value): Int { if (left.hasDoubleValue()) { if (right.hasDoubleValue()) { - return Util.compareDoubles(left.doubleValue, right.doubleValue) + return firestoreCompareDoubles(left.doubleValue, right.doubleValue) } else if (right.hasIntegerValue()) { - return Util.compareMixed(left.doubleValue, right.integerValue) + return firestoreCompareDoubleWithLong(left.doubleValue, right.integerValue) } } else if (left.hasIntegerValue()) { if (right.hasIntegerValue()) { - return Util.compareLongs(left.integerValue, right.integerValue) + return java.lang.Long.compare(left.integerValue, right.integerValue) } else if (right.hasDoubleValue()) { - return -1 * Util.compareMixed(right.doubleValue, left.integerValue) + return -1 * firestoreCompareDoubleWithLong(right.doubleValue, left.integerValue) } } @@ -285,11 +294,11 @@ internal object Values { } private fun compareTimestamps(left: Timestamp, right: Timestamp): Int { - val cmp = Util.compareLongs(left.seconds, right.seconds) + val cmp = left.seconds.compareTo(right.seconds) if (cmp != 0) { return cmp } - return Util.compareIntegers(left.nanos, right.nanos) + return left.nanos.compareTo(right.nanos) } private fun compareReferences(leftPath: String, rightPath: String): Int { @@ -303,13 +312,13 @@ internal object Values { return cmp } } - return Util.compareIntegers(leftSegments.size, rightSegments.size) + return leftSegments.size.compareTo(rightSegments.size) } private fun compareGeoPoints(left: LatLng, right: LatLng): Int { - val comparison = Util.compareDoubles(left.latitude, right.latitude) + val comparison = firestoreCompareDoubles(left.latitude, right.latitude) if (comparison == 0) { - return Util.compareDoubles(left.longitude, right.longitude) + return firestoreCompareDoubles(left.longitude, right.longitude) } return comparison } @@ -322,7 +331,7 @@ internal object Values { return cmp } } - return Util.compareIntegers(left.valuesCount, right.valuesCount) + return left.valuesCount.compareTo(right.valuesCount) } private fun compareMaps(left: MapValue, right: MapValue): Int { @@ -342,7 +351,7 @@ internal object Values { } // Only equal if both iterators are exhausted. - return Util.compareBooleans(iterator1.hasNext(), iterator2.hasNext()) + return iterator1.hasNext().compareTo(iterator2.hasNext()) } private fun compareVectors(left: MapValue, right: MapValue): Int { @@ -353,8 +362,7 @@ internal object Values { val leftArrayValue = leftMap[VECTOR_MAP_VECTORS_KEY]!!.arrayValue val rightArrayValue = rightMap[VECTOR_MAP_VECTORS_KEY]!!.arrayValue - val lengthCompare = - Util.compareIntegers(leftArrayValue.valuesCount, rightArrayValue.valuesCount) + val lengthCompare = leftArrayValue.valuesCount.compareTo(rightArrayValue.valuesCount) if (lengthCompare != 0) { return lengthCompare } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java index 543da11e7d3..c4aa523836e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java @@ -19,7 +19,6 @@ import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.gms.tasks.Continuation; -import com.google.cloud.datastore.core.number.NumberComparisonHelper; import com.google.firebase.firestore.FieldPath; import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.FirebaseFirestoreException.Code; @@ -57,34 +56,6 @@ public static String autoId() { return builder.toString(); } - /** - * Utility function to compare booleans. Note that we can't use Boolean.compare because it's only - * available after Android 19. - */ - public static int compareBooleans(boolean b1, boolean b2) { - if (b1 == b2) { - return 0; - } else if (b1) { - return 1; - } else { - return -1; - } - } - - /** - * Utility function to compare integers. Note that we can't use Integer.compare because it's only - * available after Android 19. - */ - public static int compareIntegers(int i1, int i2) { - if (i1 < i2) { - return -1; - } else if (i1 > i2) { - return 1; - } else { - return 0; - } - } - /** Compare strings in UTF-8 encoded byte order */ public static int compareUtf8Strings(String left, String right) { ByteString leftBytes = ByteString.copyFromUtf8(left); @@ -92,28 +63,6 @@ public static int compareUtf8Strings(String left, String right) { return compareByteStrings(leftBytes, rightBytes); } - /** - * Utility function to compare longs. Note that we can't use Long.compare because it's only - * available after Android 19. - */ - public static int compareLongs(long i1, long i2) { - return NumberComparisonHelper.compareLongs(i1, i2); - } - - /** Utility function to compare doubles (using Firestore semantics for NaN). */ - public static int compareDoubles(double i1, double i2) { - return NumberComparisonHelper.firestoreCompareDoubles(i1, i2); - } - - /** Compares a double and a long (using Firestore semantics for NaN). */ - public static int compareMixed(double doubleValue, long longValue) { - return NumberComparisonHelper.firestoreCompareDoubleWithLong(doubleValue, longValue); - } - - public static > Comparator comparator() { - return Comparable::compareTo; - } - public static FirebaseFirestoreException exceptionFromStatus(Status error) { StatusException statusException = error.asException(); return new FirebaseFirestoreException( @@ -136,15 +85,6 @@ private static Exception convertStatusException(Exception e) { } } - /** Turns a Throwable into an exception, converting it from a StatusException if necessary. */ - public static Exception convertThrowableToException(Throwable t) { - if (t instanceof Exception) { - return Util.convertStatusException((Exception) t); - } else { - return new Exception(t); - } - } - private static final Continuation VOID_ERROR_TRANSFORMER = task -> { if (task.isSuccessful()) { @@ -237,7 +177,7 @@ public static int compareByteArrays(byte[] left, byte[] right) { } // Byte values are equal, continue with comparison } - return Util.compareIntegers(left.length, right.length); + return Integer.compare(left.length, right.length); } public static int compareByteStrings(ByteString left, ByteString right) { @@ -253,7 +193,8 @@ public static int compareByteStrings(ByteString left, ByteString right) { } // Byte values are equal, continue with comparison } - return Util.compareIntegers(left.size(), right.size()); + int i1 = left.size(); + return Integer.compare(i1, right.size()); } public static StringBuilder repeatSequence( From 285a529601c675e2f23ab0b2d84177bf2a82cb4c Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 5 Jun 2025 15:11:56 -0400 Subject: [PATCH 111/152] Spotless --- .../google/firebase/firestore/model/Values.kt | 35 ++++++++++--------- .../firestore/pipeline/expressions.kt | 12 ++++--- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 2015db8a209..1089a847628 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -191,25 +191,25 @@ internal object Values { else -> false } - private fun strictArrayEquals(left: Value, right: Value): Boolean? { - val leftArray = left.arrayValue - val rightArray = right.arrayValue + private fun strictArrayEquals(left: Value, right: Value): Boolean? { + val leftArray = left.arrayValue + val rightArray = right.arrayValue - if (leftArray.valuesCount != rightArray.valuesCount) { - return false - } + if (leftArray.valuesCount != rightArray.valuesCount) { + return false + } - var foundNull = false - for (i in 0 until leftArray.valuesCount) { - val equals = strictEquals(leftArray.getValues(i), rightArray.getValues(i)) - if (equals === null) { - foundNull = true - } else if (!equals) { - return false - } - } - return if (foundNull) null else true + var foundNull = false + for (i in 0 until leftArray.valuesCount) { + val equals = strictEquals(leftArray.getValues(i), rightArray.getValues(i)) + if (equals === null) { + foundNull = true + } else if (!equals) { + return false + } } + return if (foundNull) null else true + } private fun arrayEquals(left: Value, right: Value): Boolean { val leftArray = left.arrayValue @@ -677,7 +677,8 @@ internal object Values { fun encodeValue(timestamp: com.google.firebase.Timestamp): Value = encodeValue(timestamp(timestamp.seconds, timestamp.nanoseconds)) - @JvmStatic fun encodeValue(value: Timestamp): Value = Value.newBuilder().setTimestampValue(value).build() + @JvmStatic + fun encodeValue(value: Timestamp): Value = Value.newBuilder().setTimestampValue(value).build() @JvmField val TRUE_VALUE: Value = Value.newBuilder().setBooleanValue(true).build() 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 09240ebcdfe..6f7d69f5655 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 @@ -798,7 +798,8 @@ abstract class Expr internal constructor() { * @param second Numeric expression to add. * @return A new [Expr] representing the addition operation. */ - @JvmStatic fun add(first: Expr, second: Expr): Expr = FunctionExpr("add", evaluateAdd, first, second) + @JvmStatic + fun add(first: Expr, second: Expr): Expr = FunctionExpr("add", evaluateAdd, first, second) /** * Creates an expression that adds numeric expressions with a constant. @@ -807,7 +808,8 @@ abstract class Expr internal constructor() { * @param second Constant to add. * @return A new [Expr] representing the addition operation. */ - @JvmStatic fun add(first: Expr, second: Number): Expr = FunctionExpr("add", evaluateAdd, first, second) + @JvmStatic + fun add(first: Expr, second: Number): Expr = FunctionExpr("add", evaluateAdd, first, second) /** * Creates an expression that adds a numeric field with a numeric expression. @@ -883,7 +885,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the multiplication operation. */ @JvmStatic - fun multiply(first: Expr, second: Expr): Expr = FunctionExpr("multiply", evaluateMultiply, first, second) + fun multiply(first: Expr, second: Expr): Expr = + FunctionExpr("multiply", evaluateMultiply, first, second) /** * Creates an expression that multiplies numeric expressions with a constant. @@ -893,7 +896,8 @@ abstract class Expr internal constructor() { * @return A new [Expr] representing the multiplication operation. */ @JvmStatic - fun multiply(first: Expr, second: Number): Expr = FunctionExpr("multiply", evaluateMultiply, first, second) + fun multiply(first: Expr, second: Number): Expr = + FunctionExpr("multiply", evaluateMultiply, first, second) /** * Creates an expression that multiplies a numeric field with a numeric expression. From 7ddca59abb5e325a54e7d64002262c0819355808 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 5 Jun 2025 15:12:09 -0400 Subject: [PATCH 112/152] Generate API --- firebase-firestore/api.txt | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index dd6a20ddcc8..e3ac8fecd5a 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -1,6 +1,10 @@ // Signature format: 3.0 package com.google.firebase.firestore { + public class AbstractPipeline { + method protected final com.google.android.gms.tasks.Task execute(com.google.firebase.firestore.pipeline.InternalOptions? options); + } + public abstract class AggregateField { method public static com.google.firebase.firestore.AggregateField.AverageAggregateField average(com.google.firebase.firestore.FieldPath); method public static com.google.firebase.firestore.AggregateField.AverageAggregateField average(String); @@ -419,14 +423,14 @@ package com.google.firebase.firestore { method public com.google.firebase.firestore.PersistentCacheSettings.Builder setSizeBytes(long); } - public final class Pipeline { + public final class Pipeline extends com.google.firebase.firestore.AbstractPipeline { method public com.google.firebase.firestore.Pipeline addFields(com.google.firebase.firestore.pipeline.Selectable field, com.google.firebase.firestore.pipeline.Selectable... additionalFields); method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateStage aggregateStage); method public com.google.firebase.firestore.Pipeline aggregate(com.google.firebase.firestore.pipeline.AggregateWithAlias accumulator, com.google.firebase.firestore.pipeline.AggregateWithAlias... additionalAccumulators); method public com.google.firebase.firestore.Pipeline distinct(com.google.firebase.firestore.pipeline.Selectable group, java.lang.Object... additionalGroups); method public com.google.firebase.firestore.Pipeline distinct(String groupField, java.lang.Object... additionalGroups); method public com.google.android.gms.tasks.Task execute(); - method public com.google.android.gms.tasks.Task execute(com.google.firebase.firestore.pipeline.PipelineOptions options); + method public com.google.android.gms.tasks.Task execute(com.google.firebase.firestore.pipeline.RealtimePipelineOptions options); method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Field vectorField, com.google.firebase.firestore.VectorValue vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.Field vectorField, double[] vectorValue, com.google.firebase.firestore.pipeline.FindNearestStage.DistanceMeasure distanceMeasure); method public com.google.firebase.firestore.Pipeline findNearest(com.google.firebase.firestore.pipeline.FindNearestStage stage); @@ -560,6 +564,25 @@ package com.google.firebase.firestore { method public java.util.List toObjects(Class, com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior); } + public final class RealtimePipeline extends com.google.firebase.firestore.AbstractPipeline { + method public com.google.android.gms.tasks.Task execute(); + method public com.google.android.gms.tasks.Task execute(com.google.firebase.firestore.pipeline.PipelineOptions options); + method public com.google.firebase.firestore.RealtimePipeline limit(int limit); + method public com.google.firebase.firestore.RealtimePipeline offset(int offset); + method public com.google.firebase.firestore.RealtimePipeline select(com.google.firebase.firestore.pipeline.Selectable selection, java.lang.Object... additionalSelections); + method public com.google.firebase.firestore.RealtimePipeline select(String fieldName, java.lang.Object... additionalSelections); + method public com.google.firebase.firestore.RealtimePipeline sort(com.google.firebase.firestore.pipeline.Ordering order, com.google.firebase.firestore.pipeline.Ordering... additionalOrders); + method public com.google.firebase.firestore.RealtimePipeline where(com.google.firebase.firestore.pipeline.BooleanExpr condition); + } + + public final class RealtimePipelineSource { + method public com.google.firebase.firestore.RealtimePipeline collection(com.google.firebase.firestore.CollectionReference ref); + method public com.google.firebase.firestore.RealtimePipeline collection(com.google.firebase.firestore.pipeline.CollectionSource stage); + method public com.google.firebase.firestore.RealtimePipeline collection(String path); + method public com.google.firebase.firestore.RealtimePipeline collectionGroup(String collectionId); + method public com.google.firebase.firestore.RealtimePipeline pipeline(com.google.firebase.firestore.pipeline.CollectionGroupSource stage); + } + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD}) public @interface ServerTimestamp { } @@ -1048,7 +1071,7 @@ package com.google.firebase.firestore.pipeline { method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); method public final com.google.firebase.firestore.pipeline.BooleanExpr lt(Object value); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); - method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object right); + method public static final com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object value); method public final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr other); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public static final com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); @@ -1369,7 +1392,7 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); - method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object right); + method public com.google.firebase.firestore.pipeline.BooleanExpr lt(String fieldName, Object value); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, com.google.firebase.firestore.pipeline.Expr right); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(com.google.firebase.firestore.pipeline.Expr left, Object right); method public com.google.firebase.firestore.pipeline.BooleanExpr lte(String fieldName, com.google.firebase.firestore.pipeline.Expr expression); @@ -1589,6 +1612,9 @@ package com.google.firebase.firestore.pipeline { method public com.google.firebase.firestore.pipeline.RawStage ofName(String name); } + public final class RealtimePipelineOptions extends com.google.firebase.firestore.pipeline.AbstractOptions { + } + public final class SampleStage extends com.google.firebase.firestore.pipeline.Stage { method public static com.google.firebase.firestore.pipeline.SampleStage withDocLimit(int documents); method public static com.google.firebase.firestore.pipeline.SampleStage withPercentage(double percentage); @@ -1615,7 +1641,7 @@ package com.google.firebase.firestore.pipeline { ctor public Selectable(); } - public abstract class Stage> { + public abstract sealed class Stage> { method protected final String getName(); method public final T with(String key, boolean value); method public final T with(String key, com.google.firebase.firestore.pipeline.Field value); From 1263e8e596434f5fc5e0528e40f104ca46edf242 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 5 Jun 2025 17:22:19 -0400 Subject: [PATCH 113/152] Refactor Values --- .../google/firebase/firestore/model/ObjectValue.java | 2 +- .../model/mutation/ArrayTransformOperation.java | 2 +- .../firebase/firestore/UserDataWriterTest.java | 3 ++- .../google/firebase/firestore/core/TargetTest.java | 12 ++++++------ .../google/firebase/firestore/model/ValuesTest.java | 2 +- .../firestore/model/mutation/MutationTest.java | 2 +- .../firestore/remote/RemoteSerializerTest.java | 1 - 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java index 581bbf8481b..80202537283 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/ObjectValue.java @@ -247,7 +247,7 @@ public boolean equals(Object o) { if (this == o) { return true; } else if (o instanceof ObjectValue) { - return Values.equals(buildProto(), ((ObjectValue) o).buildProto()); + return buildProto().equals(((ObjectValue) o).buildProto()); } return false; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/ArrayTransformOperation.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/ArrayTransformOperation.java index d27471ad9d1..ca814abf6d5 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/ArrayTransformOperation.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/ArrayTransformOperation.java @@ -123,7 +123,7 @@ protected Value apply(@Nullable Value previousValue) { ArrayValue.Builder result = coercedFieldValuesArray(previousValue); for (Value removeElement : getElements()) { for (int i = 0; i < result.getValuesCount(); ) { - if (Values.equals(result.getValues(i), removeElement)) { + if (result.getValues(i).equals(removeElement)) { result.removeValues(i); } else { ++i; diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/UserDataWriterTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/UserDataWriterTest.java index a856f316ff1..6082b0c8a11 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/UserDataWriterTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/UserDataWriterTest.java @@ -14,6 +14,7 @@ package com.google.firebase.firestore; +import static com.google.common.truth.Truth.assertThat; import static com.google.firebase.firestore.testutil.TestUtil.blob; import static com.google.firebase.firestore.testutil.TestUtil.field; import static com.google.firebase.firestore.testutil.TestUtil.map; @@ -264,7 +265,7 @@ public void testConvertsLists() { ArrayValue.Builder expectedArray = ArrayValue.newBuilder().addValues(wrap("value")).addValues(wrap(true)); Value actual = wrap(asList("value", true)); - assertTrue(Values.equals(Value.newBuilder().setArrayValue(expectedArray).build(), actual)); + assertThat(actual).isEqualTo(Value.newBuilder().setArrayValue(expectedArray).build()); } @Test diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/TargetTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/TargetTest.java index bad5ee427fa..ca3326dc5b5 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/TargetTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/TargetTest.java @@ -151,12 +151,12 @@ public void orderByQueryBound() { Bound lowerBound = target.getLowerBound(index); assertEquals(1, lowerBound.getPosition().size()); - assertTrue(Values.equals(lowerBound.getPosition().get(0), Values.MIN_VALUE)); + assertEquals(Values.MIN_VALUE, lowerBound.getPosition().get(0)); assertTrue(lowerBound.isInclusive()); Bound upperBound = target.getUpperBound(index); assertEquals(1, upperBound.getPosition().size()); - assertTrue(Values.equals(upperBound.getPosition().get(0), Values.MAX_VALUE)); + assertEquals(Values.MAX_VALUE, upperBound.getPosition().get(0)); assertTrue(upperBound.isInclusive()); } @@ -183,7 +183,7 @@ public void startAtQueryBound() { Bound upperBound = target.getUpperBound(index); assertEquals(1, upperBound.getPosition().size()); - assertTrue(Values.equals(upperBound.getPosition().get(0), Values.MAX_VALUE)); + assertEquals(Values.MAX_VALUE, upperBound.getPosition().get(0)); assertTrue(upperBound.isInclusive()); } @@ -259,7 +259,7 @@ public void endAtQueryBound() { Bound lowerBound = target.getLowerBound(index); assertEquals(1, lowerBound.getPosition().size()); - assertTrue(Values.equals(lowerBound.getPosition().get(0), Values.MIN_VALUE)); + assertEquals(Values.MIN_VALUE, lowerBound.getPosition().get(0)); assertTrue(lowerBound.isInclusive()); Bound upperBound = target.getUpperBound(index); @@ -349,11 +349,11 @@ private void verifyBound(Bound bound, boolean inclusive, Object... values) { assertEquals("size", values.length, position.size()); for (int i = 0; i < values.length; ++i) { Value expectedValue = wrap(values[i]); - assertTrue( + assertEquals( String.format( "Values should be equal: Expected: %s, Actual: %s", Values.canonicalId(expectedValue), Values.canonicalId(position.get(i))), - Values.equals(position.get(i), expectedValue)); + expectedValue, position.get(i)); } } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/model/ValuesTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/model/ValuesTest.java index 6a7dbe9c259..ffa36796d24 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/model/ValuesTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/model/ValuesTest.java @@ -368,7 +368,7 @@ static class EqualsWrapper implements Comparable { @Override public boolean equals(Object o) { - return o instanceof EqualsWrapper && Values.equals(proto, ((EqualsWrapper) o).proto); + return o instanceof EqualsWrapper && proto.equals(((EqualsWrapper) o).proto); } @Override diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java index 70a988c208f..025fc8c4032 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java @@ -678,7 +678,7 @@ public void testNumericIncrementBaseValue() { 0, "nested", map("double", 42.0, "long", 42, "string", 0, "map", 0, "missing", 0))); - assertTrue(Values.equals(expected, baseValue.get(FieldPath.EMPTY_PATH))); + assertEquals(expected, baseValue.get(FieldPath.EMPTY_PATH)); } @Test diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java index 52eec0ac4cd..b8de98598df 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java @@ -123,7 +123,6 @@ public void setUp() { private void assertRoundTrip(Value actual, Value proto, Value.ValueTypeCase typeCase) { assertEquals(typeCase, actual.getValueTypeCase()); assertEquals(proto, actual); - assertTrue(Values.equals(actual, proto)); } @Test From b754f235a1698ea261d710308eec7dde23b4ccb7 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 5 Jun 2025 18:02:13 -0400 Subject: [PATCH 114/152] Inequality tests --- .../firestore/pipeline/InequalityTests.kt | 728 ++++++++++++++++++ 1 file changed, 728 insertions(+) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/InequalityTests.kt diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/InequalityTests.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/InequalityTests.kt new file mode 100644 index 00000000000..4787571d825 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/InequalityTests.kt @@ -0,0 +1,728 @@ +// Copyright 2025 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.firebase.firestore.pipeline + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.Timestamp +import com.google.firebase.firestore.GeoPoint +import com.google.firebase.firestore.RealtimePipelineSource +import com.google.firebase.firestore.TestUtil +import com.google.firebase.firestore.pipeline.Expr.Companion.and +import com.google.firebase.firestore.pipeline.Expr.Companion.array +import com.google.firebase.firestore.pipeline.Expr.Companion.field +import com.google.firebase.firestore.pipeline.Expr.Companion.not +import com.google.firebase.firestore.pipeline.Expr.Companion.or +import com.google.firebase.firestore.runPipeline +import com.google.firebase.firestore.testutil.TestUtilKtx.doc +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class InequalityTests { + + private val db = TestUtil.firestore() + + @Test + fun `greater than`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L)) + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L)) + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = RealtimePipelineSource(db).collection("users").where(field("score").gt(90L)) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3) + } + + @Test + fun `greater than or equal`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L)) + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L)) + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = RealtimePipelineSource(db).collection("users").where(field("score").gte(90L)) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3)) + } + + @Test + fun `less than`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L)) + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L)) + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = RealtimePipelineSource(db).collection("users").where(field("score").lt(90L)) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc2) + } + + @Test + fun `less than or equal`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L)) + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L)) + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = RealtimePipelineSource(db).collection("users").where(field("score").lte(90L)) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc2)) + } + + @Test + fun `not equal`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L)) + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L)) + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = RealtimePipelineSource(db).collection("users").where(field("score").neq(90L)) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc2, doc3)) + } + + @Test + fun `not equal returns mixed types`(): Unit = runBlocking { + val doc1 = doc("users/alice", 1000, mapOf("score" to 90L)) // Should be filtered out + val doc2 = doc("users/boc", 1000, mapOf("score" to true)) + val doc3 = doc("users/charlie", 1000, mapOf("score" to 42.0)) + val doc4 = doc("users/drew", 1000, mapOf("score" to "abc")) + val doc5 = doc("users/eric", 1000, mapOf("score" to Timestamp(0, 2000000))) + val doc6 = doc("users/francis", 1000, mapOf("score" to GeoPoint(0.0, 0.0))) + val doc7 = doc("users/george", 1000, mapOf("score" to listOf(42L))) + val doc8 = doc("users/hope", 1000, mapOf("score" to mapOf("foo" to 42L))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = RealtimePipelineSource(db).collection("users").where(field("score").neq(90L)) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc2, doc3, doc4, doc5, doc6, doc7, doc8)) + } + + @Test + fun `comparison has implicit bound`(): Unit = runBlocking { + val doc1 = doc("users/alice", 1000, mapOf("score" to 42L)) + val doc2 = doc("users/boc", 1000, mapOf("score" to 100.0)) // Matches > 42 + val doc3 = doc("users/charlie", 1000, mapOf("score" to true)) + val doc4 = doc("users/drew", 1000, mapOf("score" to "abc")) + val doc5 = doc("users/eric", 1000, mapOf("score" to Timestamp(0, 2000000))) + val doc6 = doc("users/francis", 1000, mapOf("score" to GeoPoint(0.0, 0.0))) + val doc7 = doc("users/george", 1000, mapOf("score" to listOf(42L))) + val doc8 = doc("users/hope", 1000, mapOf("score" to mapOf("foo" to 42L))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = RealtimePipelineSource(db).collection("users").where(field("score").gt(42L)) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc2) + } + + @Test + fun `not comparison returns mixed type`(): Unit = runBlocking { + val doc1 = doc("users/alice", 1000, mapOf("score" to 42L)) // !(42 > 90) -> !F -> T + val doc2 = doc("users/boc", 1000, mapOf("score" to 100.0)) // !(100 > 90) -> !T -> F + val doc3 = doc("users/charlie", 1000, mapOf("score" to true)) // !(true > 90) -> !F -> T + val doc4 = doc("users/drew", 1000, mapOf("score" to "abc")) // !("abc" > 90) -> !F -> T + val doc5 = + doc("users/eric", 1000, mapOf("score" to Timestamp(0, 2000000))) // !(T > 90) -> !F -> T + val doc6 = + doc("users/francis", 1000, mapOf("score" to GeoPoint(0.0, 0.0))) // !(G > 90) -> !F -> T + val doc7 = doc("users/george", 1000, mapOf("score" to listOf(42L))) // !(A > 90) -> !F -> T + val doc8 = + doc("users/hope", 1000, mapOf("score" to mapOf("foo" to 42L))) // !(M > 90) -> !F -> T + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8) + + val pipeline = RealtimePipelineSource(db).collection("users").where(not(field("score").gt(90L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3, doc4, doc5, doc6, doc7, doc8)) + } + + @Test + fun `inequality with equality on different field`(): Unit = runBlocking { + val doc1 = + doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // rank=2, score=90 > 80 -> Match + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // rank!=2 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) // rank!=2 + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("rank").eq(2L), field("score").gt(80L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `inequality with equality on same field`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L)) // score=90, score > 80 -> Match + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L)) // score!=90 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L)) // score!=90 + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("score").eq(90L), field("score").gt(80L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `with sort on same field`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L)) + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L)) // score < 90 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(field("score").gte(90L)) + .sort(field("score").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc3).inOrder() + } + + @Test + fun `with sort on different fields`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // score < 90 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(field("score").gte(90L)) + .sort(field("rank").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc1).inOrder() + } + + @Test + fun `with or on single field`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L)) // score not > 90 and not < 60 + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L)) // score < 60 -> Match + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L)) // score > 90 -> Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(or(field("score").gt(90L), field("score").lt(60L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc2, doc3)) + } + + @Test + fun `with or on different fields`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // score > 80 -> Match + val doc2 = + doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // score !> 80, rank !< 2 + val doc3 = + doc( + "users/charlie", + 1000, + mapOf("score" to 97L, "rank" to 1L) + ) // score > 80, rank < 2 -> Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(or(field("score").gt(80L), field("rank").lt(2L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3)) + } + + @Test + fun `with eqAny on single field`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L)) // score > 80, but not in [50, 80, 97] + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L)) // score !> 80 + val doc3 = + doc( + "users/charlie", + 1000, + mapOf("score" to 97L) + ) // score > 80, score in [50, 80, 97] -> Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("score").gt(80L), field("score").eqAny(listOf(50L, 80L, 97L)))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3) + } + + @Test + fun `with eqAny on different fields`(): Unit = runBlocking { + val doc1 = + doc( + "users/bob", + 1000, + mapOf("score" to 90L, "rank" to 2L) + ) // rank < 3, score not in [50, 80, 97] + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // rank !< 3 + val doc3 = + doc( + "users/charlie", + 1000, + mapOf("score" to 97L, "rank" to 1L) + ) // rank < 3, score in [50, 80, 97] -> Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("rank").lt(3L), field("score").eqAny(listOf(50L, 80L, 97L)))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3) + } + + @Test + fun `with notEqAny on single field`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("notScore" to 90L)) // score missing + val doc2 = + doc("users/alice", 1000, mapOf("score" to 90L)) // score > 80, but score is in [90, 95] + val doc3 = doc("users/charlie", 1000, mapOf("score" to 50L)) // score !> 80 + val doc4 = + doc("users/diane", 1000, mapOf("score" to 97L)) // score > 80, score not in [90, 95] -> Match + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("score").gt(80L), field("score").notEqAny(listOf(90L, 95L)))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4) + } + + @Test + fun `with notEqAny returns mixed types`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("notScore" to 90L)) + val doc2 = doc("users/alice", 1000, mapOf("score" to 90L)) + val doc3 = doc("users/charlie", 1000, mapOf("score" to true)) + val doc4 = doc("users/diane", 1000, mapOf("score" to 42.0)) + val doc5 = doc("users/eric", 1000, mapOf("score" to Double.NaN)) + val doc6 = doc("users/francis", 1000, mapOf("score" to "abc")) + val doc7 = doc("users/george", 1000, mapOf("score" to Timestamp(0, 2000000))) + val doc8 = doc("users/hope", 1000, mapOf("score" to GeoPoint(0.0, 0.0))) + val doc9 = doc("users/isla", 1000, mapOf("score" to listOf(42L))) + val doc10 = doc("users/jack", 1000, mapOf("score" to mapOf("foo" to 42L))) + val documents = listOf(doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8, doc9, doc10) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(field("score").notEqAny(listOf("foo", 90L, false))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result) + .containsExactlyElementsIn(listOf(doc3, doc4, doc5, doc6, doc7, doc8, doc9, doc10)) + } + + @Test + fun `with notEqAny on different fields`(): Unit = runBlocking { + val doc1 = + doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // rank < 3, score is in [90, 95] + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // rank !< 3 + val doc3 = + doc( + "users/charlie", + 1000, + mapOf("score" to 97L, "rank" to 1L) + ) // rank < 3, score not in [90, 95] -> Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("rank").lt(3L), field("score").notEqAny(listOf(90L, 95L)))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3) + } + + @Test + fun `sort by equality`(): Unit = runBlocking { + val doc1 = + doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // rank=2, score > 80 -> Match + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 4L)) // rank!=2 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) // rank!=2 + val doc4 = + doc("users/david", 1000, mapOf("score" to 91L, "rank" to 2L)) // rank=2, score > 80 -> Match + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("rank").eq(2L), field("score").gt(80L))) + .sort(field("rank").ascending(), field("score").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc4).inOrder() + } + + @Test + fun `with eqAny sort by equality`(): Unit = runBlocking { + val doc1 = + doc( + "users/bob", + 1000, + mapOf("score" to 90L, "rank" to 3L) + ) // rank in [2,3,4], score > 80 -> Match + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 4L)) // score !> 80 + val doc3 = + doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) // rank not in [2,3,4] + val doc4 = + doc( + "users/david", + 1000, + mapOf("score" to 91L, "rank" to 2L) + ) // rank in [2,3,4], score > 80 -> Match + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("rank").eqAny(listOf(2L, 3L, 4L)), field("score").gt(80L))) + .sort(field("rank").ascending(), field("score").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc4, doc1).inOrder() + } + + @Test + fun `with array`(): Unit = runBlocking { + val doc1 = + doc( + "users/bob", + 1000, + mapOf("scores" to listOf(80L, 85L, 90L), "rounds" to listOf(1L, 2L, 3L)) + ) // scores <= [90,90,90], rounds > [1,2] -> Match + val doc2 = + doc( + "users/alice", + 1000, + mapOf("scores" to listOf(50L, 65L), "rounds" to listOf(1L, 2L)) + ) // rounds !> [1,2] + val doc3 = + doc( + "users/charlie", + 1000, + mapOf("scores" to listOf(90L, 95L, 97L), "rounds" to listOf(1L, 2L, 4L)) + ) // scores !<= [90,90,90] + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("scores").lte(array(90L, 90L, 90L)), field("rounds").gt(array(1L, 2L)))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `with arrayContainsAny`(): Unit = runBlocking { // Renamed from C++: withArrayContains + val doc1 = + doc( + "users/bob", + 1000, + mapOf("scores" to listOf(80L, 85L, 90L), "rounds" to listOf(1L, 2L, 3L)) + ) // scores <= [90,90,90], rounds contains 3 -> Match + val doc2 = + doc( + "users/alice", + 1000, + mapOf("scores" to listOf(50L, 65L), "rounds" to listOf(1L, 2L)) + ) // rounds does not contain 3 + val doc3 = + doc( + "users/charlie", + 1000, + mapOf("scores" to listOf(90L, 95L, 97L), "rounds" to listOf(1L, 2L, 4L)) + ) // scores !<= [90,90,90] + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where( + and( + field("scores").lte(array(90L, 90L, 90L)), + field("rounds").arrayContains(3L) // C++ used ArrayContainsExpr + ) + ) + // In Kotlin, arrayContains is the equivalent of C++ ArrayContainsExpr for a single element. + // For multiple elements, it would be arrayContainsAny. + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `with sort and limit`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 3L)) + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 4L)) // score !> 80 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) + val doc4 = doc("users/david", 1000, mapOf("score" to 91L, "rank" to 2L)) + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(field("score").gt(80L)) + .sort(field("rank").ascending()) + .limit(2) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc4).inOrder() + } + + @Test + fun `multiple inequalities on single field`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L)) // score !> 90 + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L)) // score !> 90 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L)) // score > 90 and < 100 -> Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("score").gt(90L), field("score").lt(100L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3) + } + + @Test + fun `multiple inequalities on different fields single match`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // rank !< 2 + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // score !> 90 + val doc3 = + doc( + "users/charlie", + 1000, + mapOf("score" to 97L, "rank" to 1L) + ) // score > 90, rank < 2 -> Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("score").gt(90L), field("rank").lt(2L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3) + } + + @Test + fun `multiple inequalities on different fields multiple match`(): Unit = runBlocking { + val doc1 = + doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // score > 80, rank < 3 -> Match + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // score !> 80 + val doc3 = + doc( + "users/charlie", + 1000, + mapOf("score" to 97L, "rank" to 1L) + ) // score > 80, rank < 3 -> Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("score").gt(80L), field("rank").lt(3L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc3)) + } + + @Test + fun `multiple inequalities on different fields all match`(): Unit = runBlocking { + val doc1 = + doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // score > 40, rank < 4 -> Match + val doc2 = + doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // score > 40, rank < 4 -> Match + val doc3 = + doc( + "users/charlie", + 1000, + mapOf("score" to 97L, "rank" to 1L) + ) // score > 40, rank < 4 -> Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("score").gt(40L), field("rank").lt(4L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactlyElementsIn(listOf(doc1, doc2, doc3)) + } + + @Test + fun `multiple inequalities on different fields no match`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // rank !> 3 + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // score !< 90 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) // rank !> 3 + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("score").lt(90L), field("rank").gt(3L))) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).isEmpty() + } + + @Test + fun `multiple inequalities with bounded ranges`(): Unit = runBlocking { + val doc1 = + doc( + "users/bob", + 1000, + mapOf("score" to 90L, "rank" to 2L) + ) // rank > 0 & < 4, score > 80 & < 95 -> Match + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 4L)) // rank !< 4 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) // score !< 95 + val doc4 = doc("users/david", 1000, mapOf("score" to 80L, "rank" to 3L)) // score !> 80 + val documents = listOf(doc1, doc2, doc3, doc4) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where( + and( + field("rank").gt(0L), + field("rank").lt(4L), + field("score").gt(80L), + field("score").lt(95L) + ) + ) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1) + } + + @Test + fun `multiple inequalities with single sort asc`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // Match + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // score !> 80 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) // Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("rank").lt(3L), field("score").gt(80L))) + .sort(field("rank").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc1).inOrder() + } + + @Test + fun `multiple inequalities with single sort desc`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // Match + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // score !> 80 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) // Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("rank").lt(3L), field("score").gt(80L))) + .sort(field("rank").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc3).inOrder() + } + + @Test + fun `multiple inequalities with multiple sort asc`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // Match + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // score !> 80 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) // Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("rank").lt(3L), field("score").gt(80L))) + .sort(field("rank").ascending(), field("score").ascending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc1).inOrder() + } + + @Test + fun `multiple inequalities with multiple sort desc`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // Match + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // score !> 80 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) // Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("rank").lt(3L), field("score").gt(80L))) + .sort(field("rank").descending(), field("score").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc1, doc3).inOrder() + } + + @Test + fun `multiple inequalities with multiple sort desc on reverse index`(): Unit = runBlocking { + val doc1 = doc("users/bob", 1000, mapOf("score" to 90L, "rank" to 2L)) // Match + val doc2 = doc("users/alice", 1000, mapOf("score" to 50L, "rank" to 3L)) // score !> 80 + val doc3 = doc("users/charlie", 1000, mapOf("score" to 97L, "rank" to 1L)) // Match + val documents = listOf(doc1, doc2, doc3) + + val pipeline = + RealtimePipelineSource(db) + .collection("users") + .where(and(field("rank").lt(3L), field("score").gt(80L))) + .sort(field("score").descending(), field("rank").descending()) + + val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList() + assertThat(result).containsExactly(doc3, doc1).inOrder() + } +} From a2d0d46f945e2296afa175d8ef852c3d62648945 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 6 Jun 2025 10:43:35 -0400 Subject: [PATCH 115/152] Pretty --- .../java/com/google/firebase/firestore/core/TargetTest.java | 3 ++- .../google/firebase/firestore/model/mutation/MutationTest.java | 2 -- .../google/firebase/firestore/remote/RemoteSerializerTest.java | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/TargetTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/TargetTest.java index ca3326dc5b5..b2af9ba2e90 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/TargetTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/TargetTest.java @@ -353,7 +353,8 @@ private void verifyBound(Bound bound, boolean inclusive, Object... values) { String.format( "Values should be equal: Expected: %s, Actual: %s", Values.canonicalId(expectedValue), Values.canonicalId(position.get(i))), - expectedValue, position.get(i)); + expectedValue, + position.get(i)); } } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java index 025fc8c4032..cc3671ff242 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/model/mutation/MutationTest.java @@ -32,7 +32,6 @@ import static com.google.firebase.firestore.testutil.TestUtil.wrapObject; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import androidx.annotation.Nullable; import com.google.common.collect.Collections2; @@ -45,7 +44,6 @@ import com.google.firebase.firestore.model.MutableDocument; import com.google.firebase.firestore.model.ObjectValue; import com.google.firebase.firestore.model.ServerTimestamps; -import com.google.firebase.firestore.model.Values; import com.google.firestore.v1.Value; import java.util.Arrays; import java.util.Collection; diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java index b8de98598df..b208da20c52 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java @@ -56,7 +56,6 @@ import com.google.firebase.firestore.model.ObjectValue; import com.google.firebase.firestore.model.ResourcePath; import com.google.firebase.firestore.model.SnapshotVersion; -import com.google.firebase.firestore.model.Values; import com.google.firebase.firestore.model.mutation.Mutation; import com.google.firebase.firestore.remote.WatchChange.WatchTargetChange; import com.google.firebase.firestore.remote.WatchChange.WatchTargetChangeType; From 7999ab4d0a5042da2c2d5f8956db2a6e1b300dc9 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 6 Jun 2025 11:04:07 -0400 Subject: [PATCH 116/152] Merge main (#7016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: David Motsonashvili Co-authored-by: David Motsonashvili Co-authored-by: Rodrigo Lazo Co-authored-by: Konstantin Svist Co-authored-by: Matthew Robertson Co-authored-by: Daymon <17409137+daymxn@users.noreply.github.com> Co-authored-by: Lee Kellogg Co-authored-by: Tushar Khandelwal <64364243+tusharkhandelwal8@users.noreply.github.com> Co-authored-by: emilypgoogle <110422458+emilypgoogle@users.noreply.github.com> Co-authored-by: Denver Coneybeare Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mila <107142260+milaGGL@users.noreply.github.com> Co-authored-by: welishr <65972773+welishr@users.noreply.github.com> Co-authored-by: mustafa jadid Co-authored-by: Google Open Source Bot Co-authored-by: VinayGuthal Co-authored-by: VinayGuthal Co-authored-by: Andrew Heard Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Rodrigo Lazo Paz Co-authored-by: Kai Bolay Co-authored-by: Rosário P. Fernandes Co-authored-by: emilypgoogle Co-authored-by: Andrew Heard Co-authored-by: Tejas Deshpande <4131300+tejasd@users.noreply.github.com> Co-authored-by: rlazo <368578+rlazo@users.noreply.github.com> Co-authored-by: rachelsaunders <52258509+rachelsaunders@users.noreply.github.com> Co-authored-by: Ehsan Co-authored-by: Angel Leon Co-authored-by: themiswang Co-authored-by: Michael Hoisie Co-authored-by: Greg Sakakihara Co-authored-by: Tejas Deshpande --- .../dataconnect-send-notifications/action.yml | 71 + .github/workflows/api-information.yml | 6 +- .github/workflows/build-release-artifacts.yml | 10 +- .github/workflows/changelog.yml | 4 +- ...ses.yml => check-firebaseai-responses.yml} | 26 +- .github/workflows/check-head-dependencies.yml | 4 +- .github/workflows/check_format.yml | 8 +- .github/workflows/ci_tests.yml | 37 +- .github/workflows/config-e2e.yml | 8 +- .github/workflows/copyright-check.yml | 4 +- .github/workflows/create_releases.yml | 12 +- .github/workflows/dataconnect.yml | 225 ++- .github/workflows/dataconnect_demo_app.yml | 92 +- .github/workflows/diff-javadoc.yml | 6 +- .github/workflows/fireci.yml | 6 +- .github/workflows/fireperf-e2e.yml | 16 +- .github/workflows/firestore_ci_tests.yml | 48 +- .github/workflows/health-metrics.yml | 30 +- .github/workflows/jekyll-gh-pages.yml | 10 +- .github/workflows/make-bom.yml | 12 +- .github/workflows/merge-to-main.yml | 36 +- .github/workflows/metalava-semver-check.yml | 32 + .github/workflows/plugins-check.yml | 9 +- .github/workflows/post_release_cleanup.yml | 8 +- .github/workflows/private-mirror-sync.yml | 4 +- .github/workflows/release-note-changes.yml | 26 +- .github/workflows/scorecards.yml | 4 +- .github/workflows/semver-check.yml | 4 +- .github/workflows/sessions-e2e.yml | 8 +- .github/workflows/smoke-tests.yml | 10 +- .../workflows/update-cpp-sdk-on-release.yml | 6 +- .github/workflows/validate-dependencies.yml | 4 +- .github/workflows/version-check.yml | 4 +- build.gradle.kts | 1 + ci/fireci/fireciplugins/api_information.py | 4 +- ci/fireci/pyproject.toml | 43 +- ci/fireci/setup.cfg | 45 - ci/run.sh | 7 +- .../firebase/encoders/proto/codegen/Types.kt | 2 +- firebase-ai/CHANGELOG.md | 28 + firebase-ai/README.md | 32 + firebase-ai/api.txt | 939 ++++++++++++ firebase-ai/consumer-rules.pro | 24 + firebase-ai/firebase-ai.gradle.kts | 129 ++ firebase-ai/gradle.properties | 16 + firebase-ai/lint-baseline.xml | 20 + firebase-ai/proguard-rules.pro | 21 + firebase-ai/src/main/AndroidManifest.xml | 35 + .../kotlin/com/google/firebase/ai/Chat.kt | 224 +++ .../com/google/firebase/ai/FirebaseAI.kt | 247 ++++ .../ai/FirebaseAIMultiResourceComponent.kt | 54 + .../google/firebase/ai/FirebaseAIRegistrar.kt | 68 + .../com/google/firebase/ai/GenerativeModel.kt | 255 ++++ .../com/google/firebase/ai/ImagenModel.kt | 122 ++ .../google/firebase/ai/LiveGenerativeModel.kt | 126 ++ .../firebase/ai/common/APIController.kt | 364 +++++ .../ai/common/AppCheckHeaderProvider.kt | 64 + .../google/firebase/ai/common/Exceptions.kt | 150 ++ .../com/google/firebase/ai/common/Request.kt | 97 ++ .../google/firebase/ai/common/util/android.kt | 50 + .../google/firebase/ai/common/util/kotlin.kt | 103 ++ .../google/firebase/ai/common/util/ktor.kt | 101 ++ .../firebase/ai/common/util/serialization.kt | 90 ++ .../google/firebase/ai/common/util/util.kt | 27 + .../google/firebase/ai/java/ChatFutures.kt | 83 ++ .../ai/java/GenerativeModelFutures.kt | 108 ++ .../firebase/ai/java/ImagenModelFutures.kt | 59 + .../firebase/ai/java/LiveModelFutures.kt | 52 + .../firebase/ai/java/LiveSessionFutures.kt | 183 +++ .../google/firebase/ai/type/AudioHelper.kt | 213 +++ .../com/google/firebase/ai/type/Candidate.kt | 319 ++++ .../com/google/firebase/ai/type/Content.kt | 123 ++ .../firebase/ai/type/ContentModality.kt | 77 + .../firebase/ai/type/CountTokensResponse.kt | 62 + .../com/google/firebase/ai/type/Exceptions.kt | 224 +++ .../firebase/ai/type/FunctionCallingConfig.kt | 84 ++ .../firebase/ai/type/FunctionDeclaration.kt | 72 + .../ai/type/GenerateContentResponse.kt | 73 + .../firebase/ai/type/GenerationConfig.kt | 247 ++++ .../firebase/ai/type/GenerativeBackend.kt | 48 + .../firebase/ai/type/HarmBlockMethod.kt | 51 + .../firebase/ai/type/HarmBlockThreshold.kt | 67 + .../google/firebase/ai/type/HarmCategory.kt | 77 + .../firebase/ai/type/HarmProbability.kt | 62 + .../google/firebase/ai/type/HarmSeverity.kt | 62 + .../firebase/ai/type/ImagenAspectRatio.kt | 34 + .../google/firebase/ai/type/ImagenGCSImage.kt | 27 + .../ai/type/ImagenGenerationConfig.kt | 119 ++ .../ai/type/ImagenGenerationResponse.kt | 61 + .../firebase/ai/type/ImagenImageFormat.kt | 55 + .../firebase/ai/type/ImagenInlineImage.kt | 39 + .../ai/type/ImagenPersonFilterLevel.kt | 49 + .../ai/type/ImagenSafetyFilterLevel.kt | 40 + .../firebase/ai/type/ImagenSafetySettings.kt | 29 + .../ai/type/LiveClientSetupMessage.kt | 50 + .../firebase/ai/type/LiveGenerationConfig.kt | 217 +++ .../firebase/ai/type/LiveServerMessage.kt | 190 +++ .../google/firebase/ai/type/LiveSession.kt | 446 ++++++ .../com/google/firebase/ai/type/MediaData.kt | 40 + .../firebase/ai/type/ModalityTokenCount.kt | 41 + .../com/google/firebase/ai/type/Part.kt | 252 ++++ .../google/firebase/ai/type/PromptFeedback.kt | 90 ++ .../firebase/ai/type/PublicPreviewAPI.kt | 26 + .../google/firebase/ai/type/RequestOptions.kt | 44 + .../firebase/ai/type/ResponseModality.kt | 59 + .../google/firebase/ai/type/SafetySetting.kt | 44 + .../com/google/firebase/ai/type/Schema.kt | 266 ++++ .../google/firebase/ai/type/SpeechConfig.kt | 40 + .../com/google/firebase/ai/type/Tool.kt | 49 + .../com/google/firebase/ai/type/ToolConfig.kt | 49 + .../com/google/firebase/ai/type/Type.kt | 47 + .../google/firebase/ai/type/UsageMetadata.kt | 58 + .../com/google/firebase/ai/type/Voice.kt | 34 + .../com/google/firebase/ai/type/Voices.kt | 79 + .../ai/DevAPIStreamingSnapshotTests.kt | 109 ++ .../firebase/ai/DevAPIUnarySnapshotTests.kt | 98 ++ .../firebase/ai/GenerativeModelTesting.kt | 157 ++ .../com/google/firebase/ai/SchemaTests.kt | 221 +++ .../google/firebase/ai/SerializationTests.kt | 215 +++ .../ai/VertexAIStreamingSnapshotTests.kt | 246 ++++ .../firebase/ai/VertexAIUnarySnapshotTests.kt | 592 ++++++++ .../firebase/ai/common/APIControllerTests.kt | 435 ++++++ .../firebase/ai/common/EnumUpdateTests.kt | 71 + .../ai/common/util/descriptorToJson.kt | 166 +++ .../google/firebase/ai/common/util/kotlin.kt | 35 + .../google/firebase/ai/common/util/tests.kt | 116 ++ .../ai/type/FunctionDeclarationTest.kt | 100 ++ .../com/google/firebase/ai/util/kotlin.kt | 35 + .../java/com/google/firebase/ai/util/tests.kt | 276 ++++ firebase-ai/src/test/resources/README.md | 2 + .../google/firebase/ai/JavaCompileTests.java | 379 +++++ firebase-ai/update_responses.sh | 37 + firebase-appdistribution-api/CHANGELOG.md | 10 + .../gradle.properties | 4 +- firebase-appdistribution/CHANGELOG.md | 3 + firebase-appdistribution/gradle.properties | 4 +- .../src/main/AndroidManifest.xml | 6 + .../impl/TesterSignInManager.java | 11 +- firebase-config/CHANGELOG.md | 20 + .../bandwagoner/src/main/AndroidManifest.xml | 37 +- firebase-config/gradle.properties | 4 +- .../remoteconfig/FirebaseRemoteConfig.java | 9 +- .../internal/ConfigAutoFetch.java | 31 +- .../internal/ConfigRealtimeHandler.java | 2 +- .../internal/ConfigRealtimeHttpClient.java | 117 +- .../FirebaseRemoteConfigTest.java | 68 +- firebase-crashlytics-ndk/CHANGELOG.md | 10 + .../firebase-crashlytics-ndk.gradle | 2 +- firebase-crashlytics-ndk/gradle.properties | 4 +- .../src/main/jni/Application.mk | 2 +- .../jni/crashpad/crashpad_client/Android.mk | 2 +- .../jni/crashpad/crashpad_compat/Android.mk | 2 +- .../crashpad/crashpad_handler_lib/Android.mk | 2 +- .../jni/crashpad/crashpad_minidump/Android.mk | 2 +- .../jni/crashpad/crashpad_snapshot/Android.mk | 2 +- .../crashpad/crashpad_tool_support/Android.mk | 2 +- .../jni/crashpad/crashpad_util/Android.mk | 3 +- .../crashpad/mini_chromium_base/Android.mk | 3 +- .../main/jni/libcrashlytics-common/Android.mk | 2 +- .../jni/libcrashlytics-handler/Android.mk | 2 +- .../jni/libcrashlytics-trampoline/Android.mk | 2 +- .../src/main/jni/libcrashlytics/Android.mk | 2 +- .../src/third_party/crashpad | 2 +- firebase-crashlytics-ndk/src/third_party/lss | 2 +- .../src/third_party/mini_chromium | 2 +- firebase-crashlytics/CHANGELOG.md | 35 +- firebase-crashlytics/gradle.properties | 4 +- .../firebase/crashlytics/KeyValueBuilder.kt | 2 +- .../internal/ProcessDetailsProvider.kt | 6 +- .../internal/common/CommonUtils.java | 16 +- .../common/CrashlyticsController.java | 45 +- .../common/SessionReportingCoordinator.java | 15 +- firebase-dataconnect/CHANGELOG.md | 21 + firebase-dataconnect/api.txt | 41 + firebase-dataconnect/ci/README.md | 22 + .../calculate_github_issue_for_commenting.py | 148 ++ ...culate_github_issue_for_commenting_test.py | 162 +++ .../ci/logcat_error_report.py | 175 +++ .../ci/logcat_error_report_test.py | 149 ++ .../ci/post_comment_for_job_results.py | 229 +++ firebase-dataconnect/ci/pyproject.toml | 48 + firebase-dataconnect/ci/requirements.txt | 11 + firebase-dataconnect/ci/util.py | 60 + firebase-dataconnect/demo/build.gradle.kts | 22 +- .../firebase/dataconnect/dataconnect.yaml | 2 +- .../dataconnect/minimaldemo/MyApplication.kt | 6 +- .../connector/person/person_ops.gql | 17 + .../emulator/dataconnect/dataconnect.yaml | 2 +- firebase-dataconnect/emulator/emulator.sh | 5 +- .../firebase-dataconnect.gradle.kts | 1 - firebase-dataconnect/gradle.properties | 4 +- .../plugin/DataConnectExecutableVersions.json | 362 ++++- .../scripts/generateApiTxtFile.sh | 31 + .../dataconnect/AuthIntegrationTest.kt | 5 +- .../GrpcMetadataIntegrationTest.kt | 23 +- ...OperationExecutionErrorsIntegrationTest.kt | 287 ++++ .../QuerySubscriptionIntegrationTest.kt | 6 +- .../FirebaseDataConnectInternalExts.kt | 26 + .../testutil/schemas/PersonSchema.kt | 8 +- .../firebase/dataconnect/DataConnectError.kt | 89 -- .../DataConnectOperationException.kt | 29 + .../DataConnectOperationFailureResponse.kt | 116 ++ .../dataconnect/DataConnectPathSegment.kt | 56 + .../dataconnect/DataConnectUntypedData.kt | 2 +- .../DataConnectCredentialsTokenManager.kt | 274 ++-- .../dataconnect/core/DataConnectGrpcClient.kt | 103 +- ...DataConnectOperationFailureResponseImpl.kt | 65 + .../core/FirebaseDataConnectImpl.kt | 394 ++--- .../dataconnect/core/MutationRefImpl.kt | 3 +- .../firebase/dataconnect/core/QueryRefImpl.kt | 2 +- .../dataconnect/core/QuerySubscriptionImpl.kt | 28 +- .../querymgr/RegisteredDataDeserialzer.kt | 12 +- .../firebase/dataconnect/util/ProtoUtil.kt | 10 +- .../dataconnect/proto/connector_service.proto | 2 +- .../dataconnect/proto/graphql_error.proto | 2 +- .../dataconnect/DataConnectErrorUnitTest.kt | 305 ---- .../DataConnectPathSegmentUnitTest.kt | 226 +++ .../DataConnectSettingsUnitTest.kt | 3 +- .../dataconnect/PathSegmentFieldUnitTest.kt | 107 -- .../PathSegmentListIndexUnitTest.kt | 107 -- .../core/DataConnectAuthUnitTest.kt | 92 +- .../core/DataConnectGrpcClientUnitTest.kt | 303 ++-- ...ectOperationFailureResponseImplUnitTest.kt | 350 +++++ .../core/MutationRefImplUnitTest.kt | 27 +- .../dataconnect/core/QueryRefImplUnitTest.kt | 9 +- .../LocalDateSerializerUnitTest.kt | 6 +- .../testutil/property/arbitrary/arbs.kt | 58 +- .../DataConnectOperationExceptionTestUtils.kt | 78 + .../testutil/SuspendingCountDownLatch.kt | 13 +- .../testutil/property/arbitrary/arbs.kt | 23 + firebase-firestore/CHANGELOG.md | 20 + firebase-firestore/gradle.properties | 4 +- .../firestore/CompositeIndexQueryTest.java | 23 +- .../firebase/firestore/FirestoreTest.java | 221 ++- .../google/firebase/firestore/QueryTest.java | 94 +- .../google/firebase/firestore/VectorTest.java | 9 +- .../testutil/CompositeIndexTestHelper.java | 10 +- .../testutil/IntegrationTestUtil.java | 36 +- .../firebase/firestore/core/FieldFilter.java | 4 +- .../firebase/firestore/core/NotInFilter.java | 4 +- .../firestore/remote/AbstractStream.java | 16 +- .../google/firebase/firestore/util/Util.java | 41 +- .../firebase/firestore/core/QueryTest.java | 4 +- .../firebase/firestore/util/UtilTest.java | 182 +++ firebase-functions/CHANGELOG.md | 28 + firebase-functions/api.txt | 17 + .../firebase-functions.gradle.kts | 3 + firebase-functions/gradle.properties | 4 +- .../androidTest/backend/functions/index.js | 112 ++ .../backend/functions/package.json | 6 +- .../FirebaseContextProviderTest.java | 10 +- .../google/firebase/functions/StreamTests.kt | 239 +++ .../functions/FirebaseContextProvider.kt | 4 +- .../firebase/functions/FirebaseFunctions.kt | 16 + .../functions/HttpsCallableReference.kt | 56 +- .../firebase/functions/PublisherStream.kt | 345 +++++ .../firebase/functions/StreamResponse.kt | 57 + firebase-inappmessaging-display/CHANGELOG.md | 9 + .../gradle.properties | 4 +- firebase-inappmessaging/CHANGELOG.md | 9 + firebase-inappmessaging/gradle.properties | 4 +- .../internal/ForegroundNotifierTest.java | 5 +- firebase-messaging-directboot/CHANGELOG.md | 3 + .../gradle.properties | 4 +- firebase-messaging/CHANGELOG.md | 11 + firebase-messaging/gradle.properties | 4 +- .../google/firebase/messaging/SyncTask.java | 10 +- .../messaging/WithinAppServiceConnection.java | 5 +- firebase-perf/CHANGELOG.md | 10 + firebase-perf/firebase-perf.gradle | 2 +- firebase-perf/gradle.properties | 4 +- .../validator/PerfMetricValidator.java | 9 +- .../perf/network/InstrHttpInputStream.java | 37 +- .../network/InstrHttpInputStreamTest.java | 34 +- firebase-sessions/CHANGELOG.md | 29 + firebase-sessions/benchmark/README.md | 5 + .../benchmark/benchmark.gradle.kts | 63 + .../benchmark/src/main/AndroidManifest.xml | 17 + .../benchmark/sessions/StartupBenchmark.kt | 42 + .../firebase-sessions.gradle.kts | 12 +- firebase-sessions/gradle.properties | 4 +- .../firebase/sessions/EventGDTLogger.kt | 14 +- .../firebase/sessions/FirebaseSessions.kt | 10 +- .../sessions/FirebaseSessionsComponent.kt | 163 +++ .../sessions/FirebaseSessionsRegistrar.kt | 121 +- .../sessions/ProcessDetailsProvider.kt | 2 +- .../firebase/sessions/SessionDatastore.kt | 41 +- .../sessions/SessionFirelogPublisher.kt | 12 +- .../firebase/sessions/SessionGenerator.kt | 17 +- .../sessions/SessionLifecycleService.kt | 2 +- .../sessions/SessionLifecycleServiceBinder.kt | 10 +- .../google/firebase/sessions/TimeProvider.kt | 5 +- .../google/firebase/sessions/UuidGenerator.kt | 29 + .../settings/LocalOverrideSettings.kt | 15 +- .../sessions/settings/RemoteSettings.kt | 19 +- .../settings/RemoteSettingsFetcher.kt | 17 +- .../sessions/settings/SessionsSettings.kt | 76 +- .../sessions/settings/SettingsCache.kt | 15 +- .../firebase/sessions/SessionGeneratorTest.kt | 41 +- .../sessions/SessionLifecycleClientTest.kt | 25 +- .../sessions/SessionLifecycleServiceTest.kt | 40 +- .../SessionsActivityLifecycleCallbacksTest.kt | 25 +- .../sessions/settings/RemoteSettingsTest.kt | 93 +- .../sessions/settings/SessionsSettingsTest.kt | 21 +- .../sessions/testing/FakeUuidGenerator.kt | 37 + .../testing/FirebaseSessionsFakeComponent.kt | 68 + .../testing/FirebaseSessionsFakeRegistrar.kt | 85 +- .../test-app/src/main/AndroidManifest.xml | 13 +- .../firebase/testing/sessions/BaseActivity.kt | 28 +- .../testing/sessions/FirstFragment.kt | 20 + .../testing/sessions/SecondActivity.kt | 15 +- .../src/main/res/layout/activity_second.xml | 14 + .../src/main/res/layout/fragment_first.xml | 14 + .../test-app/src/main/res/values/strings.xml | 1 + .../test-app/test-app.gradle.kts | 22 +- firebase-storage/CHANGELOG.md | 11 + firebase-storage/gradle.properties | 4 +- .../google/firebase/storage/DownloadTest.java | 9 +- .../com/google/firebase/storage/TestUtil.java | 6 +- firebase-vertexai/CHANGELOG.md | 54 + firebase-vertexai/api.txt | 1279 ++++++++++------- .../firebase-vertexai.gradle.kts | 10 +- firebase-vertexai/gradle.properties | 4 +- firebase-vertexai/lint-baseline.xml | 30 + .../src/main/AndroidManifest.xml | 6 +- .../com/google/firebase/vertexai/Chat.kt | 4 + .../firebase/vertexai/FirebaseVertexAI.kt | 82 +- .../FirebaseVertexAIMultiResourceComponent.kt | 18 +- .../vertexai/FirebaseVertexAIRegistrar.kt | 7 + .../firebase/vertexai/GenerativeModel.kt | 9 + .../google/firebase/vertexai/ImagenModel.kt | 7 + .../firebase/vertexai/LiveGenerativeModel.kt | 130 ++ .../firebase/vertexai/common/APIController.kt | 46 +- .../firebase/vertexai/common/Request.kt | 12 +- .../firebase/vertexai/common/util/android.kt | 50 + .../firebase/vertexai/common/util/kotlin.kt | 62 + .../vertexai/common/util/serialization.kt | 8 +- .../firebase/vertexai/java/ChatFutures.kt | 4 + .../vertexai/java/GenerativeModelFutures.kt | 4 + .../vertexai/java/ImagenModelFutures.kt | 4 + .../vertexai/java/LiveModelFutures.kt | 56 + .../vertexai/java/LiveSessionFutures.kt | 187 +++ .../firebase/vertexai/type/AudioHelper.kt | 213 +++ .../firebase/vertexai/type/Candidate.kt | 53 +- .../google/firebase/vertexai/type/Content.kt | 9 + .../firebase/vertexai/type/ContentModality.kt | 13 + .../vertexai/type/CountTokensResponse.kt | 4 + .../firebase/vertexai/type/Exceptions.kt | 124 ++ .../vertexai/type/FunctionCallingConfig.kt | 4 + .../vertexai/type/FunctionDeclaration.kt | 4 + .../vertexai/type/GenerateContentResponse.kt | 16 + .../vertexai/type/GenerationConfig.kt | 20 +- .../firebase/vertexai/type/HarmBlockMethod.kt | 4 + .../vertexai/type/HarmBlockThreshold.kt | 14 + .../firebase/vertexai/type/HarmCategory.kt | 4 + .../firebase/vertexai/type/HarmProbability.kt | 4 + .../firebase/vertexai/type/HarmSeverity.kt | 4 + .../vertexai/type/ImagenAspectRatio.kt | 4 + .../vertexai/type/ImagenGenerationConfig.kt | 42 +- .../vertexai/type/ImagenGenerationResponse.kt | 10 +- .../vertexai/type/ImagenImageFormat.kt | 6 + .../vertexai/type/ImagenInlineImage.kt | 13 +- .../vertexai/type/ImagenPersonFilterLevel.kt | 4 + .../vertexai/type/ImagenSafetyFilterLevel.kt | 4 + .../vertexai/type/ImagenSafetySettings.kt | 4 + .../vertexai/type/LiveClientSetupMessage.kt | 50 + .../vertexai/type/LiveGenerationConfig.kt | 225 +++ .../vertexai/type/LiveServerMessage.kt | 211 +++ .../firebase/vertexai/type/LiveSession.kt | 449 ++++++ .../firebase/vertexai/type/MediaData.kt | 44 + .../vertexai/type/ModalityTokenCount.kt | 4 + .../com/google/firebase/vertexai/type/Part.kt | 100 +- .../firebase/vertexai/type/PromptFeedback.kt | 10 +- .../firebase/vertexai/type/RequestOptions.kt | 4 + .../vertexai/type/ResponseModality.kt | 63 + .../firebase/vertexai/type/SafetySetting.kt | 4 + .../google/firebase/vertexai/type/Schema.kt | 12 + .../firebase/vertexai/type/SpeechConfig.kt | 44 + .../com/google/firebase/vertexai/type/Tool.kt | 4 + .../firebase/vertexai/type/ToolConfig.kt | 4 + .../firebase/vertexai/type/UsageMetadata.kt | 4 + .../google/firebase/vertexai/type/Voices.kt | 82 ++ .../vertexai/GenerativeModelTesting.kt | 20 + .../firebase/vertexai/SerializationTests.kt | 215 +++ ...s.kt => VertexAIStreamingSnapshotTests.kt} | 64 +- ...Tests.kt => VertexAIUnarySnapshotTests.kt} | 134 +- .../vertexai/common/APIControllerTests.kt | 76 + .../vertexai/common/StreamingSnapshotTests.kt | 202 --- .../vertexai/common/UnarySnapshotTests.kt | 364 ----- .../vertexai/common/util/descriptorToJson.kt | 167 +++ .../firebase/vertexai/common/util/tests.kt | 107 +- .../google/firebase/vertexai/util/tests.kt | 52 +- .../streaming/failure-api-key.txt | 21 - .../streaming/failure-empty-content.txt | 1 - .../failure-finish-reason-safety.txt | 2 - .../streaming/failure-http-error.txt | 13 - .../streaming/failure-image-rejected.txt | 7 - .../failure-prompt-blocked-safety.txt | 2 - .../failure-recitation-no-content.txt | 6 - .../streaming/failure-unknown-model.txt | 13 - .../streaming/success-basic-reply-long.txt | 12 - .../streaming/success-basic-reply-short.txt | 2 - .../streaming/success-citations-altname.txt | 12 - .../streaming/success-citations.txt | 12 - .../streaming/success-quotes-escaped.txt | 7 - .../streaming/success-unknown-enum.txt | 11 - .../golden-files/unary/failure-api-key.json | 21 - .../unary/failure-empty-content.json | 28 - .../unary/failure-finish-reason-safety.json | 54 - .../unary/failure-http-error.json | 13 - .../unary/failure-image-rejected.json | 13 - .../unary/failure-invalid-response.json | 14 - .../unary/failure-malformed-content.json | 30 - .../unary/failure-prompt-blocked-safety.json | 23 - .../unary/failure-quota-exceeded.json | 31 - .../unary/failure-service-disabled.json | 27 - .../unary/failure-unknown-model.json | 13 - .../failure-unsupported-user-location.json | 13 - .../unary/success-basic-reply-long.json | 54 - .../unary/success-basic-reply-short.json | 54 - .../unary/success-citations-altname.json | 70 - .../unary/success-citations-nolicense.json | 58 - .../golden-files/unary/success-citations.json | 70 - .../unary/success-code-execution.json | 48 - .../success-constraint-decoding-json.json | 34 - ...success-function-call-empty-arguments.json | 18 - .../success-function-call-json-literal.json | 45 - .../unary/success-function-call-null.json | 45 - .../unary/success-including-severity.json | 50 - .../unary/success-partial-usage-metadata.json | 57 - .../unary/success-quote-reply.json | 54 - .../unary/success-unknown-enum.json | 52 - .../unary/success-usage-metadata.json | 59 - .../firebase/vertexai/JavaCompileTests.java | 340 +++++ firebase-vertexai/update_responses.sh | 4 +- gradle/libs.versions.toml | 26 +- .../template/app/build.gradle.mustache | 4 +- .../template/macrobenchmark/build.gradle | 2 +- plugins/build.gradle.kts | 2 +- .../GenerateTutorialBundleTask.kt | 10 +- .../firebase/gradle/plugins/CopyApiTask.kt | 33 + .../plugins/FirebaseAndroidLibraryPlugin.kt | 12 + .../plugins/FirebaseJavaLibraryPlugin.kt | 20 +- .../plugins/FirebaseLibraryExtension.kt | 3 + .../gradle/plugins/FiresiteTransformTask.kt | 28 +- .../firebase/gradle/plugins/Metalava.kt | 1 - .../gradle/plugins/PostReleasePlugin.kt | 7 +- .../gradle/plugins/PublishingPlugin.kt | 13 +- .../firebase/gradle/plugins/SemVerTask.kt | 103 ++ .../gradle/plugins/VersionBumpTask.kt | 17 +- protolite-well-known-types/CHANGELOG.md | 9 + protolite-well-known-types/README.md | 2 +- protolite-well-known-types/gradle.properties | 4 +- .../protolite-well-known-types.gradle | 7 +- smoke-tests/build.gradle | 6 + .../functions/functions/package-lock.json | 12 +- subprojects.cfg | 2 + 457 files changed, 23270 insertions(+), 4746 deletions(-) create mode 100644 .github/actions/dataconnect-send-notifications/action.yml rename .github/workflows/{check-vertexai-responses.yml => check-firebaseai-responses.yml} (63%) create mode 100644 .github/workflows/metalava-semver-check.yml delete mode 100644 ci/fireci/setup.cfg create mode 100644 firebase-ai/CHANGELOG.md create mode 100644 firebase-ai/README.md create mode 100644 firebase-ai/api.txt create mode 100644 firebase-ai/consumer-rules.pro create mode 100644 firebase-ai/firebase-ai.gradle.kts create mode 100644 firebase-ai/gradle.properties create mode 100644 firebase-ai/lint-baseline.xml create mode 100644 firebase-ai/proguard-rules.pro create mode 100644 firebase-ai/src/main/AndroidManifest.xml create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/Chat.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIMultiResourceComponent.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIRegistrar.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/common/AppCheckHeaderProvider.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Exceptions.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/android.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/kotlin.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/ktor.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/serialization.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/util.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ChatFutures.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ImagenModelFutures.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveModelFutures.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/AudioHelper.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Content.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ContentModality.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/CountTokensResponse.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionCallingConfig.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionDeclaration.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerationConfig.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerativeBackend.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockMethod.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockThreshold.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmCategory.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmProbability.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmSeverity.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenAspectRatio.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGCSImage.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationConfig.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationResponse.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenImageFormat.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenInlineImage.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenPersonFilterLevel.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetyFilterLevel.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetySettings.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveClientSetupMessage.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveGenerationConfig.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveServerMessage.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/MediaData.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ModalityTokenCount.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PromptFeedback.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/RequestOptions.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ResponseModality.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/SafetySetting.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Schema.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/SpeechConfig.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ToolConfig.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Type.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/UsageMetadata.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voice.kt create mode 100644 firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voices.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/DevAPIStreamingSnapshotTests.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/SchemaTests.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/VertexAIStreamingSnapshotTests.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/VertexAIUnarySnapshotTests.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/common/EnumUpdateTests.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/common/util/descriptorToJson.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/common/util/kotlin.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/type/FunctionDeclarationTest.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/util/kotlin.kt create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt create mode 100644 firebase-ai/src/test/resources/README.md create mode 100644 firebase-ai/src/testUtil/java/com/google/firebase/ai/JavaCompileTests.java create mode 100755 firebase-ai/update_responses.sh create mode 100644 firebase-dataconnect/ci/README.md create mode 100644 firebase-dataconnect/ci/calculate_github_issue_for_commenting.py create mode 100644 firebase-dataconnect/ci/calculate_github_issue_for_commenting_test.py create mode 100644 firebase-dataconnect/ci/logcat_error_report.py create mode 100644 firebase-dataconnect/ci/logcat_error_report_test.py create mode 100644 firebase-dataconnect/ci/post_comment_for_job_results.py create mode 100644 firebase-dataconnect/ci/pyproject.toml create mode 100644 firebase-dataconnect/ci/requirements.txt create mode 100644 firebase-dataconnect/ci/util.py create mode 100755 firebase-dataconnect/scripts/generateApiTxtFile.sh create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/OperationExecutionErrorsIntegrationTest.kt create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/FirebaseDataConnectInternalExts.kt delete mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectError.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationException.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationFailureResponse.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectPathSegment.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImpl.kt delete mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorUnitTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectPathSegmentUnitTest.kt delete mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldUnitTest.kt delete mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexUnitTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImplUnitTest.kt create mode 100644 firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectOperationExceptionTestUtils.kt create mode 100644 firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt create mode 100644 firebase-sessions/benchmark/README.md create mode 100644 firebase-sessions/benchmark/benchmark.gradle.kts create mode 100644 firebase-sessions/benchmark/src/main/AndroidManifest.xml create mode 100644 firebase-sessions/benchmark/src/main/kotlin/com/google/firebase/benchmark/sessions/StartupBenchmark.kt create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt create mode 100644 firebase-vertexai/lint-baseline.xml create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/LiveGenerativeModel.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/android.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/LiveModelFutures.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/LiveSessionFutures.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/AudioHelper.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveClientSetupMessage.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveGenerationConfig.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveServerMessage.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveSession.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/MediaData.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ResponseModality.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/SpeechConfig.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Voices.kt create mode 100644 firebase-vertexai/src/test/java/com/google/firebase/vertexai/SerializationTests.kt rename firebase-vertexai/src/test/java/com/google/firebase/vertexai/{StreamingSnapshotTests.kt => VertexAIStreamingSnapshotTests.kt} (75%) rename firebase-vertexai/src/test/java/com/google/firebase/vertexai/{UnarySnapshotTests.kt => VertexAIUnarySnapshotTests.kt} (77%) delete mode 100644 firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt delete mode 100644 firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt create mode 100644 firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/descriptorToJson.kt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-api-key.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-empty-content.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-finish-reason-safety.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-http-error.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-image-rejected.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-prompt-blocked-safety.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-recitation-no-content.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-unknown-model.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-long.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-short.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-citations-altname.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-citations.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-quotes-escaped.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-unknown-enum.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-api-key.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-empty-content.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-finish-reason-safety.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-http-error.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-image-rejected.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-invalid-response.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-malformed-content.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-prompt-blocked-safety.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-quota-exceeded.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-service-disabled.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-unknown-model.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-unsupported-user-location.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-long.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-short.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-citations-altname.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-citations-nolicense.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-citations.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-code-execution.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-constraint-decoding-json.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-empty-arguments.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-json-literal.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-null.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-including-severity.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-partial-usage-metadata.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-quote-reply.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-unknown-enum.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-usage-metadata.json create mode 100644 firebase-vertexai/src/testUtil/java/com/google/firebase/vertexai/JavaCompileTests.java create mode 100644 plugins/src/main/java/com/google/firebase/gradle/plugins/CopyApiTask.kt create mode 100644 plugins/src/main/java/com/google/firebase/gradle/plugins/SemVerTask.kt diff --git a/.github/actions/dataconnect-send-notifications/action.yml b/.github/actions/dataconnect-send-notifications/action.yml new file mode 100644 index 00000000000..27133b5031c --- /dev/null +++ b/.github/actions/dataconnect-send-notifications/action.yml @@ -0,0 +1,71 @@ +name: Data Connect Workflow Notifications +description: Notify a GitHub Issue with the results of a workflow. + +inputs: + python-version: + required: true + default: "3.13" + github-issue-for-scheduled-runs: + required: true + job-results-file: + required: true + +runs: + using: "composite" + steps: + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + with: + python-version: ${{ inputs.python-version }} + + - run: pip install -r requirements.txt + shell: bash + working-directory: firebase-dataconnect/ci + + - id: issue-id + name: Determine GitHub Issue For Commenting + working-directory: firebase-dataconnect/ci + shell: bash + run: | + args=( + python + calculate_github_issue_for_commenting.py + --issue-output-file=github_issue_number.txt + --github-repository='${{ github.repository }}' + --github-ref='${{ github.ref }}' + --github-event-name='${{ github.event_name }}' + --pr-body-github-issue-key=trksmnkncd_notification_issue + --github-issue-for-scheduled-run='${{ inputs.github-issue-for-scheduled-runs }}' + ) + echo "${args[*]}" + "${args[@]}" + + set -xv + issue="$(cat github_issue_number.txt)" + echo "issue=$issue" >> "$GITHUB_OUTPUT" + + - name: Post Comment on GitHub Issue + if: steps.issue-id.outputs.issue != '' + working-directory: firebase-dataconnect/ci + shell: bash + run: | + args=( + python + post_comment_for_job_results.py + --github-issue='${{ steps.issue-id.outputs.issue }}' + --github-workflow='${{ github.workflow }}' + --github-repository='${{ github.repository }}' + --github-ref='${{ github.ref }}' + --github-event-name='${{ github.event_name }}' + --github-sha='${{ github.sha }}' + --github-repository-html-url='${{ github.event.repository.html_url }}' + --github-run-id='${{ github.run_id }}' + --github-run-number='${{ github.run_number }}' + --github-run-attempt='${{ github.run_attempt }}' + ) + + while read -r line; do + args=("${args[@]}" "$line") + done <'${{ inputs.job-results-file }}' + + echo "${args[*]}" + exec "${args[@]}" diff --git a/.github/workflows/api-information.yml b/.github/workflows/api-information.yml index f0f1c57d650..df514aa39d5 100644 --- a/.github/workflows/api-information.yml +++ b/.github/workflows/api-information.yml @@ -7,18 +7,18 @@ jobs: if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.10' - name: Set up fireci diff --git a/.github/workflows/build-release-artifacts.yml b/.github/workflows/build-release-artifacts.yml index 313226dce97..328deabfdb6 100644 --- a/.github/workflows/build-release-artifacts.yml +++ b/.github/workflows/build-release-artifacts.yml @@ -12,10 +12,10 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -26,21 +26,21 @@ jobs: ./gradlew firebasePublish - name: Upload m2 repo - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: m2repository path: build/m2repository/ retention-days: 15 - name: Upload release notes - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: release_notes path: build/release-notes/ retention-days: 15 - name: Upload kotlindocs - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: kotlindocs path: build/firebase-kotlindoc/ diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 7937f67acd5..60660863235 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -13,11 +13,11 @@ jobs: env: BUNDLE_GEMFILE: ./ci/danger/Gemfile steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 100 submodules: true - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@1a615958ad9d422dd932dc1d5823942ee002799f # v1.227.0 with: ruby-version: '2.7' - name: Setup Bundler diff --git a/.github/workflows/check-vertexai-responses.yml b/.github/workflows/check-firebaseai-responses.yml similarity index 63% rename from .github/workflows/check-vertexai-responses.yml rename to .github/workflows/check-firebaseai-responses.yml index 482254c553d..80d43bb81fe 100644 --- a/.github/workflows/check-vertexai-responses.yml +++ b/.github/workflows/check-firebaseai-responses.yml @@ -1,40 +1,42 @@ -name: Check Vertex AI Responses +name: Check Firebase AI Responses on: pull_request jobs: check-version: runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Clone mock responses - run: firebase-vertexai/update_responses.sh + run: firebase-ai/update_responses.sh - name: Find cloned and latest versions run: | CLONED=$(git describe --tags) LATEST=$(git tag --sort=v:refname | tail -n1) echo "cloned_tag=$CLONED" >> $GITHUB_ENV echo "latest_tag=$LATEST" >> $GITHUB_ENV - working-directory: firebase-vertexai/src/test/resources/vertexai-sdk-test-data + working-directory: firebase-ai/src/test/resources/vertexai-sdk-test-data - name: Find comment from previous run if exists - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 id: fc with: issue-number: ${{github.event.number}} - body-includes: Vertex AI Mock Responses Check + body-includes: Firebase AI Mock Responses Check - name: Comment on PR if newer version is available if: ${{env.cloned_tag != env.latest_tag && !steps.fc.outputs.comment-id}} - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 with: issue-number: ${{github.event.number}} body: > - ### Vertex AI Mock Responses Check :warning: - - A newer major version of the mock responses for Vertex AI unit tests is available. - [update_responses.sh](https://github.com/firebase/firebase-android-sdk/blob/main/firebase-vertexai/update_responses.sh) + ### Firebase AI Mock Responses Check :warning: + + A newer major version of the mock responses for Firebase AI unit tests is available. + [update_responses.sh](https://github.com/firebase/firebase-android-sdk/blob/main/firebase-ai/update_responses.sh) should be updated to clone the latest version of the responses: `${{env.latest_tag}}` - name: Delete comment when version gets updated if: ${{env.cloned_tag == env.latest_tag && steps.fc.outputs.comment-id}} - uses: detomarco/delete-comment@850734dd44d8b15fef55b45252613b903ceb06f0 + uses: detomarco/delete-comment@dd37d1026c669ebfb0ffa5d23890010759ff05d5 # v1.1.0 with: comment-id: ${{ steps.fc.outputs.comment-id }} diff --git a/.github/workflows/check-head-dependencies.yml b/.github/workflows/check-head-dependencies.yml index 088724bf1d4..189b0a0c87c 100644 --- a/.github/workflows/check-head-dependencies.yml +++ b/.github/workflows/check-head-dependencies.yml @@ -10,9 +10,9 @@ jobs: check-head-dependencies: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin diff --git a/.github/workflows/check_format.yml b/.github/workflows/check_format.yml index 6bdfb0ea4d1..83fdc3ec605 100644 --- a/.github/workflows/check_format.yml +++ b/.github/workflows/check_format.yml @@ -16,13 +16,13 @@ jobs: outputs: modules: ${{ steps.changed-modules.outputs.modules }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -44,13 +44,13 @@ jobs: module: ${{ fromJSON(needs.determine_changed.outputs.modules) }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index c706aa614bd..487d0229d3b 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -16,13 +16,13 @@ jobs: outputs: modules: ${{ steps.changed-modules.outputs.modules }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -44,30 +44,25 @@ jobs: module: ${{ fromJSON(needs.determine_changed.outputs.modules) }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - - name: Pull genai-common + - name: Clone vertexai mock responses if: matrix.module == ':firebase-vertexai' - run: | - git clone https://github.com/google-gemini/generative-ai-android.git - cd generative-ai-android - ./gradlew :common:updateVersion common:publishToMavenLocal - cd .. + run: firebase-vertexai/update_responses.sh - - name: Clone mock responses - if: matrix.module == ':firebase-vertexai' - run: | - firebase-vertexai/update_responses.sh + - name: Clone ai mock responses + if: matrix.module == ':firebase-ai' + run: firebase-ai/update_responses.sh - name: Add google-services.json env: @@ -85,7 +80,7 @@ jobs: MODULE=${{matrix.module}} echo "ARTIFACT_NAME=${MODULE//:/_}" >> $GITHUB_ENV - name: Upload Test Results - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 if: always() with: name: unit-test-result-${{env.ARTIFACT_NAME}} @@ -122,13 +117,13 @@ jobs: - module: :firebase-functions:ktx steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -139,10 +134,10 @@ jobs: INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} run: | echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: ${{ matrix.module }} Integ Tests env: FIREBASE_CI: 1 @@ -168,11 +163,11 @@ jobs: steps: - name: Download Artifacts - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: path: artifacts - name: Publish Test Results - uses: EnricoMi/publish-unit-test-result-action@82082dac68ad6a19d980f8ce817e108b9f496c2a + uses: EnricoMi/publish-unit-test-result-action@170bf24d20d201b842d7a52403b73ed297e6645b # v2.18.0 with: files: "artifacts/**/*.xml" diff --git a/.github/workflows/config-e2e.yml b/.github/workflows/config-e2e.yml index 604115b324d..15091c2d3f9 100644 --- a/.github/workflows/config-e2e.yml +++ b/.github/workflows/config-e2e.yml @@ -18,10 +18,10 @@ jobs: steps: - name: Checkout firebase-config - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: '17' distribution: 'temurin' @@ -31,10 +31,10 @@ jobs: run: | echo $REMOTE_CONFIG_E2E_GOOGLE_SERVICES | base64 -d > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_service_account }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Run Remote Config end-to-end tests env: FTL_RESULTS_BUCKET: fireescape diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index b9e3aeba227..4f90b26f7f6 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -10,8 +10,8 @@ jobs: copyright-check: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4.1.1 - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.9' - run: | diff --git a/.github/workflows/create_releases.yml b/.github/workflows/create_releases.yml index c47cfac9713..0da1384927e 100644 --- a/.github/workflows/create_releases.yml +++ b/.github/workflows/create_releases.yml @@ -15,6 +15,9 @@ on: jobs: create-branches: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: @@ -25,12 +28,15 @@ jobs: create-pull-request: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -40,7 +46,7 @@ jobs: ./gradlew generateReleaseConfig -PcurrentRelease=${{ inputs.name }} -PpastRelease=${{ inputs.past-name }} -PprintOutput=true - name: Create Pull Request - uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: base: 'releases/${{ inputs.name }}' branch: 'releases/${{ inputs.name }}.release' diff --git a/.github/workflows/dataconnect.yml b/.github/workflows/dataconnect.yml index 3a0b9aa4b93..2808b19e7d6 100644 --- a/.github/workflows/dataconnect.yml +++ b/.github/workflows/dataconnect.yml @@ -9,6 +9,7 @@ on: firebaseToolsVersion: gradleInfoLog: type: boolean + pythonVersion: pull_request: paths: - .github/workflows/dataconnect.yml @@ -24,9 +25,10 @@ env: FDC_JAVA_VERSION: ${{ inputs.javaVersion || '17' }} FDC_ANDROID_EMULATOR_API_LEVEL: ${{ inputs.androidEmulatorApiLevel || '34' }} FDC_NODEJS_VERSION: ${{ inputs.nodeJsVersion || '20' }} - FDC_FIREBASE_TOOLS_VERSION: ${{ inputs.firebaseToolsVersion || '13.29.1' }} + FDC_FIREBASE_TOOLS_VERSION: ${{ inputs.firebaseToolsVersion || '14.5.1' }} FDC_FIREBASE_TOOLS_DIR: /tmp/firebase-tools FDC_FIREBASE_COMMAND: /tmp/firebase-tools/node_modules/.bin/firebase + FDC_PYTHON_VERSION: ${{ inputs.pythonVersion || '3.13' }} concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -51,30 +53,36 @@ jobs: - 5432:5432 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: show-progress: false - - uses: actions/setup-java@v4 + - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: ${{ env.FDC_JAVA_VERSION }} distribution: temurin - - uses: actions/setup-node@v4 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: ${{ env.FDC_NODEJS_VERSION }} - - name: install firebase-tools + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + with: + python-version: ${{ env.FDC_PYTHON_VERSION }} + + - run: pip install -r firebase-dataconnect/ci/requirements.txt + + - name: Install Firebase Tools ("firebase" command-line tool) run: | + set -euo pipefail set -v mkdir -p ${{ env.FDC_FIREBASE_TOOLS_DIR }} cd ${{ env.FDC_FIREBASE_TOOLS_DIR }} echo '{}' > package.json npm install --fund=false --audit=false --save --save-exact firebase-tools@${{ env.FDC_FIREBASE_TOOLS_VERSION }} - - name: Restore Gradle cache - id: restore-gradle-cache - uses: actions/cache/restore@v4 + - name: Restore Gradle Cache + uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf # 4.2.2 if: github.event_name != 'schedule' with: path: | @@ -84,9 +92,11 @@ jobs: restore-keys: | gradle-cache-jqnvfzw6w7- - - name: tool versions + - name: Print Command-Line Tool Versions continue-on-error: true run: | + set -euo pipefail + function run_cmd { echo "===============================================================================" echo "Running Command: $*" @@ -105,6 +115,7 @@ jobs: - name: Gradle assembleDebugAndroidTest run: | + set -euo pipefail set -v # Speed up build times and also avoid configuring firebase-crashlytics-ndk @@ -117,24 +128,25 @@ jobs: ${{ (inputs.gradleInfoLog && '--info') || '' }} \ :firebase-dataconnect:assembleDebugAndroidTest - - name: Save Gradle cache - uses: actions/cache/save@v4 + - name: Save Gradle Cache + uses: actions/cache/save@d4323d4df104b026a6aa633fdb11d772146be0bf # 4.2.2 if: github.event_name == 'schedule' with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: ${{ steps.restore-gradle-cache.outputs.cache-primary-key }} + key: gradle-cache-jqnvfzw6w7-${{ github.run_id }} - - name: Enable KVM group permissions for Android Emulator + - name: Enable KVM Group Permissions for Android Emulator run: | + set -euo pipefail echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Restore AVD cache - uses: actions/cache/restore@v4 + - name: Restore AVD Cache + uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf # 4.2.2 if: github.event_name != 'schedule' id: restore-avd-cache with: @@ -146,48 +158,50 @@ jobs: avd-cache-zhdsn586je-api${{ env.FDC_ANDROID_EMULATOR_API_LEVEL }}- - name: Create AVD - if: github.event_name == 'schedule' || steps.restore-avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2 + if: github.event_name == 'schedule' || steps.restore-avd-cache.outputs.cache-matched-key == '' + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 with: api-level: ${{ env.FDC_ANDROID_EMULATOR_API_LEVEL }} arch: x86_64 force-avd-creation: false emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true - script: echo "Generated AVD snapshot for caching." + script: 'echo "Generated AVD snapshot for caching; event_name=${{ github.event_name }}, cache-matched-key=${{ steps.restore-avd-cache.outputs.cache-matched-key }}"' - - name: Save AVD cache - uses: actions/cache/save@v4 + - name: Save AVD Cache + uses: actions/cache/save@d4323d4df104b026a6aa633fdb11d772146be0bf # 4.2.2 if: github.event_name == 'schedule' with: path: | ~/.android/avd/* ~/.android/adb* - key: ${{ steps.restore-avd-cache.outputs.cache-primary-key }} + key: avd-cache-zhdsn586je-api${{ env.FDC_ANDROID_EMULATOR_API_LEVEL }}-${{ github.run_id }} - - name: Data Connect Emulator + - name: Start Firebase Emulators run: | - set -x + set -xveuo pipefail - echo 'emulator.postgresConnectionUrl=postgresql://postgres:password@127.0.0.1:5432?sslmode=disable' > firebase-dataconnect/dataconnect.local.properties + # Use the same dataconnect binary as was used for code generation in gradle assemble + DATACONNECT_EMULATOR_BINARY_PATH="$(find "$PWD"/firebase-dataconnect/connectors/build/intermediates/dataconnect/debug/executable -type f)" + if [[ -z $DATACONNECT_EMULATOR_BINARY_PATH ]] ; then + echo "INTERNAL ERROR v7kg2dfhbc: unable to find data connect binary" >&2 + exit 1 + fi + export DATACONNECT_EMULATOR_BINARY_PATH - ./gradlew \ - ${{ (inputs.gradleInfoLog && '--info') || '' }} \ - :firebase-dataconnect:connectors:runDebugDataConnectEmulator \ - >firebase.emulator.dataconnect.log 2>&1 & - - - name: Firebase Auth Emulator - run: | - set -x + export FIREBASE_DATACONNECT_POSTGRESQL_STRING='postgresql://postgres:password@127.0.0.1:5432?sslmode=disable' cd firebase-dataconnect/emulator - ${{ env.FDC_FIREBASE_COMMAND }} emulators:start --only=auth >firebase.emulator.auth.log 2>&1 & + ${{ env.FDC_FIREBASE_COMMAND }} emulators:start --only=auth,dataconnect >firebase.emulators.log 2>&1 & - - name: Capture Logcat Logs - run: adb logcat >logcat.log & + - name: Start Logcat Capture + continue-on-error: true + run: | + set -xveuo pipefail + "$ANDROID_HOME/platform-tools/adb" logcat >logcat.log 2>&1 & - name: Gradle connectedCheck id: connectedCheck - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 # Allow this GitHub Actions "job" to continue even if the tests fail so that logs from a # failed test run get uploaded as "artifacts" and are available to investigate failed runs. # A later step in this "job" will fail the job if this step fails @@ -201,26 +215,36 @@ jobs: script: | set -eux && ./gradlew ${{ (inputs.gradleInfoLog && '--info') || '' }} :firebase-dataconnect:connectedCheck :firebase-dataconnect:connectors:connectedCheck - - name: Upload log file artifacts - uses: actions/upload-artifact@v4 + - name: Upload Log Files + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: integration_test_logs path: "**/*.log" if-no-files-found: warn compression-level: 9 - - name: Upload Gradle build report artifacts - uses: actions/upload-artifact@v4 + - name: Upload Gradle Build Reports + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: integration_test_gradle_build_reports path: firebase-dataconnect/**/build/reports/ if-no-files-found: warn compression-level: 9 - - name: Check test result + - name: Verify "Gradle connectedCheck" Step Was Successful if: steps.connectedCheck.outcome != 'success' run: | - echo "Failing the job since the connectedCheck step failed" + set -euo pipefail + + if [[ ! -e logcat.log ]] ; then + echo "WARNING dsdta43sxk: logcat log file not found; skipping scanning for test failures" >&2 + else + echo "Scanning logcat output for failure details" + python firebase-dataconnect/ci/logcat_error_report.py --logcat-file=logcat.log + echo + fi + + echo 'Failing because the outcome of the "Gradle connectedCheck" step ("${{ steps.connectedCheck.outcome }}") was not successful' exit 1 # Check this yml file with "actionlint": https://github.com/rhysd/actionlint @@ -230,9 +254,124 @@ jobs: continue-on-error: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: show-progress: false + sparse-checkout: '.github/' - uses: docker://rhysd/actionlint:1.7.7 with: args: -color /github/workspace/.github/workflows/dataconnect.yml + + python-ci-unit-tests: + continue-on-error: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + show-progress: false + sparse-checkout: 'firebase-dataconnect/ci/' + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + with: + python-version: ${{ env.FDC_PYTHON_VERSION }} + - run: pip install -r firebase-dataconnect/ci/requirements.txt + - name: pytest + working-directory: firebase-dataconnect/ci + run: pytest --verbose --full-trace --color=no --strict-config + + python-ci-lint: + continue-on-error: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + show-progress: false + sparse-checkout: 'firebase-dataconnect/ci/' + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + with: + python-version: ${{ env.FDC_PYTHON_VERSION }} + - run: pip install -r firebase-dataconnect/ci/requirements.txt + - name: ruff check + working-directory: firebase-dataconnect/ci + run: ruff check --diff --verbose --no-cache --output-format=github --exit-non-zero-on-fix + + python-ci-format: + continue-on-error: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + show-progress: false + sparse-checkout: 'firebase-dataconnect/ci/' + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + with: + python-version: ${{ env.FDC_PYTHON_VERSION }} + - run: pip install -r firebase-dataconnect/ci/requirements.txt + - name: ruff format + working-directory: firebase-dataconnect/ci + run: ruff format --diff --verbose --no-cache + + python-ci-type-check: + continue-on-error: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + show-progress: false + sparse-checkout: 'firebase-dataconnect/ci/' + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + with: + python-version: ${{ env.FDC_PYTHON_VERSION }} + - run: pip install -r firebase-dataconnect/ci/requirements.txt + - name: pyright + working-directory: firebase-dataconnect/ci + run: pyright --warnings --stats + + # The "send-notifications" job adds a comment to GitHub Issue + # https://github.com/firebase/firebase-android-sdk/issues/6857 with the results of the scheduled + # nightly runs. Interested parties can then subscribe to that issue to be aprised of the outcome + # of the nightly runs. + # + # When testing the comment-adding logic itself, you can add the line + # trksmnkncd_notification_issue=6863 + # into the PR's description to instead post a comment to issue #6863, an issue specifically + # created for testing, avoiding spamming the main issue to which others are subscribed. + send-notifications: + needs: + - 'integration-test' + - 'actionlint-dataconnect-yml' + - 'python-ci-unit-tests' + - 'python-ci-lint' + - 'python-ci-format' + - 'python-ci-type-check' + if: always() + permissions: + issues: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + show-progress: false + sparse-checkout: | + firebase-dataconnect/ci/ + .github/ + + - name: gh auth login + run: echo '${{ secrets.GITHUB_TOKEN }}' | gh auth login --with-token + + - name: Create Job Results File + run: | + set -xveuo pipefail + cat >'${{ runner.temp }}/job_results.txt' <github_actions_demo_test_cache_key.txt echo "${{ env.FDC_FIREBASE_TOOLS_VERSION }}" >github_actions_demo_assemble_firebase_tools_version.txt - - uses: actions/setup-node@v3 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: ${{ env.FDC_NODE_VERSION }} cache: 'npm' cache-dependency-path: | - firebase-dataconnect/demo/github_actions_demo_test_cache_key.txt - firebase-dataconnect/demo/github_actions_demo_assemble_firebase_tools_version.txt + github_actions_demo_test_cache_key.txt + github_actions_demo_assemble_firebase_tools_version.txt - name: cache package-lock.json id: package_json_lock - uses: actions/cache@v4 + uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # 4.2.2 with: path: ${{ env.FDC_FIREBASE_TOOLS_DIR }}/package*.json key: firebase_tools_package_json-${{ env.FDC_FIREBASE_TOOLS_VERSION }} @@ -73,9 +70,9 @@ jobs: if: steps.package_json_lock.outputs.cache-hit == 'true' run: | cd ${{ env.FDC_FIREBASE_TOOLS_DIR }} - npm ci --fund=false --audit=false + npm ci --fund=false --audit=false - - uses: actions/setup-java@v4 + - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: ${{ env.FDC_JAVA_VERSION }} distribution: temurin @@ -84,7 +81,7 @@ jobs: firebase-dataconnect/demo/build.gradle.kts firebase-dataconnect/demo/gradle.properties firebase-dataconnect/demo/gradle/wrapper/gradle-wrapper.properties - firebase-dataconnect/demo/github_actions_demo_test_cache_key.txt + github_actions_demo_test_cache_key.txt - name: tool versions continue-on-error: true @@ -102,26 +99,27 @@ jobs: run_cmd which node run_cmd node --version run_cmd ${{ env.FDC_FIREBASE_COMMAND }} --version - run_cmd ./gradlew --version + run_cmd firebase-dataconnect/demo/gradlew --version - - name: ./gradlew assemble test + - name: gradle assemble test run: | set -x - ./gradlew \ + firebase-dataconnect/demo/gradlew \ + --project-dir firebase-dataconnect/demo \ --no-daemon \ ${{ (inputs.gradleInfoLog && '--info') || '' }} \ --profile \ -PdataConnect.minimalApp.firebaseCommand=${{ env.FDC_FIREBASE_COMMAND }} \ assemble test - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: apks path: firebase-dataconnect/demo/build/**/*.apk if-no-files-found: warn compression-level: 0 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: gradle_build_reports path: firebase-dataconnect/demo/build/reports/ @@ -132,14 +130,14 @@ jobs: continue-on-error: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: sparse-checkout: firebase-dataconnect/demo - name: Create Cache Key Files run: echo "h99ee4egfd" >github_actions_demo_spotless_cache_key.txt - - uses: actions/setup-java@v4 + - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: ${{ env.FDC_JAVA_VERSION }} distribution: temurin @@ -148,7 +146,7 @@ jobs: firebase-dataconnect/demo/build.gradle.kts firebase-dataconnect/demo/gradle.properties firebase-dataconnect/demo/gradle/wrapper/gradle-wrapper.properties - firebase-dataconnect/demo/github_actions_demo_spotless_cache_key.txt + github_actions_demo_spotless_cache_key.txt - name: tool versions continue-on-error: true @@ -158,12 +156,56 @@ jobs: java -version which javac javac -version - ./gradlew --version + firebase-dataconnect/demo/gradlew --version - - name: ./gradlew spotlessCheck + - name: gradle spotlessCheck run: | set -x - ./gradlew \ + firebase-dataconnect/demo/gradlew \ + --project-dir firebase-dataconnect/demo \ --no-daemon \ ${{ (inputs.gradleInfoLog && '--info') || '' }} \ spotlessCheck + + # The "send-notifications" job adds a comment to GitHub Issue + # https://github.com/firebase/firebase-android-sdk/issues/6891 with the results of the scheduled + # nightly runs. Interested parties can then subscribe to that issue to be aprised of the outcome + # of the nightly runs. + # + # When testing the comment-adding logic itself, you can add the line + # trksmnkncd_notification_issue=6863 + # into the PR's description to instead post a comment to issue #6863, an issue specifically + # created for testing, avoiding spamming the main issue to which others are subscribed. + send-notifications: + needs: + - 'test' + - 'spotlessCheck' + if: always() + permissions: + issues: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + show-progress: false + sparse-checkout: | + firebase-dataconnect/ci/ + .github/ + + - name: gh auth login + run: echo '${{ secrets.GITHUB_TOKEN }}' | gh auth login --with-token + + - name: Create Job Results File + id: create-job-results-file + run: | + set -xveuo pipefail + cat >'${{ runner.temp }}/job_results.txt' < google-services.json - name: Run fireperf end-to-end tests @@ -52,7 +52,7 @@ jobs: --target_environment=${{ matrix.environment }} - name: Notify developers upon failures if: ${{ failure() }} - uses: actions/github-script@v6 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const owner = context.repo.owner; @@ -98,7 +98,7 @@ jobs: } - name: Upload test artifacts if: always() - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: test-artifacts (${{ matrix.environment }}) path: | diff --git a/.github/workflows/firestore_ci_tests.yml b/.github/workflows/firestore_ci_tests.yml index 00ce91b4e92..a7ea11b1624 100644 --- a/.github/workflows/firestore_ci_tests.yml +++ b/.github/workflows/firestore_ci_tests.yml @@ -16,13 +16,13 @@ jobs: outputs: modules: ${{ steps.changed-modules.outputs.modules }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -44,7 +44,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true @@ -53,10 +53,10 @@ jobs: run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm + sudo udevadm trigger --name-match=kvm - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -67,12 +67,12 @@ jobs: INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} run: | echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: firebase-firestore Integ Tests - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 env: FIREBASE_CI: 1 FTL_RESULTS_BUCKET: android-ci @@ -88,7 +88,7 @@ jobs: ./gradlew firebase-firestore:connectedCheck withErrorProne -PtargetBackend="prod" - name: Upload logs if: failure() - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: logcat.txt path: logcat.txt @@ -107,7 +107,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true @@ -116,10 +116,10 @@ jobs: run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm + sudo udevadm trigger --name-match=kvm - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -130,14 +130,14 @@ jobs: INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} run: | echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 # create composite indexes with Terraform - name: Setup Terraform - uses: hashicorp/setup-terraform@v2 + uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 - name: Terraform Init run: | cd firebase-firestore @@ -164,7 +164,7 @@ jobs: - name: Firestore Named DB Integ Tests timeout-minutes: 20 - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 env: FIREBASE_CI: 1 FTL_RESULTS_BUCKET: android-ci @@ -180,7 +180,7 @@ jobs: ./gradlew firebase-firestore:connectedCheck withErrorProne -PtargetBackend="prod" - name: Upload logs if: failure() - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: named-db-logcat.txt path: logcat.txt @@ -198,7 +198,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true @@ -207,10 +207,10 @@ jobs: run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm + sudo udevadm trigger --name-match=kvm - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -221,13 +221,13 @@ jobs: INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.NIGHTLY_INTEG_TESTS_GOOGLE_SERVICES }} run: | echo $INTEG_TESTS_GOOGLE_SERVICES > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Firestore Nightly Integ Tests - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 env: FIREBASE_CI: 1 FTL_RESULTS_BUCKET: android-ci @@ -243,7 +243,7 @@ jobs: ./gradlew firebase-firestore:connectedCheck withErrorProne -PtargetBackend="nightly" - name: Upload logs if: failure() - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: nightly-logcat.txt path: logcat.txt diff --git a/.github/workflows/health-metrics.yml b/.github/workflows/health-metrics.yml index 0b20dcd1078..9e086be9c3a 100644 --- a/.github/workflows/health-metrics.yml +++ b/.github/workflows/health-metrics.yml @@ -24,24 +24,24 @@ jobs: && github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.10' - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: '${{ secrets.GCP_SERVICE_ACCOUNT }}' - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Set up fireci run: pip3 install -e ci/fireci - name: Run coverage tests (presubmit) @@ -59,24 +59,24 @@ jobs: && github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.10' - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: '${{ secrets.GCP_SERVICE_ACCOUNT }}' - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Set up fireci run: pip3 install -e ci/fireci - name: Run size tests (presubmit) @@ -95,24 +95,24 @@ jobs: && github.event.pull_request.base.ref == 'main') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.10' - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: '${{ secrets.GCP_SERVICE_ACCOUNT }}' - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Set up fireci run: pip3 install -e ci/fireci - name: Add google-services.json diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml index c1683b58de8..077b5b465b2 100644 --- a/.github/workflows/jekyll-gh-pages.yml +++ b/.github/workflows/jekyll-gh-pages.yml @@ -31,16 +31,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Pages - uses: actions/configure-pages@v2 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 - name: Build with Jekyll - uses: actions/jekyll-build-pages@v1 + uses: actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697 # v1.0.13 with: source: ./contributor-docs destination: ./_site - name: Upload artifact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa #v3.0.1 deploy: if: ${{ github.event_name == 'push' && github.repository == 'firebase/firebase-android-sdk' }} @@ -52,4 +52,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e #v4.0.5 diff --git a/.github/workflows/make-bom.yml b/.github/workflows/make-bom.yml index 0e7d63f5c96..4643217a528 100644 --- a/.github/workflows/make-bom.yml +++ b/.github/workflows/make-bom.yml @@ -8,14 +8,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.10' - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -26,21 +26,21 @@ jobs: ./gradlew buildBomBundleZip - name: Upload bom - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: bom path: build/bom/ retention-days: 15 - name: Upload release notes - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: bom_release_notes path: build/bomReleaseNotes.md retention-days: 15 - name: Upload recipe version update - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: recipe_version path: build/recipeVersionUpdate.txt diff --git a/.github/workflows/merge-to-main.yml b/.github/workflows/merge-to-main.yml index 4df37c57891..cfc5f72125f 100644 --- a/.github/workflows/merge-to-main.yml +++ b/.github/workflows/merge-to-main.yml @@ -6,8 +6,7 @@ on: - main types: - opened - - labeled - - unlabeled + - synchronize jobs: pr-message: @@ -15,7 +14,37 @@ jobs: permissions: pull-requests: write steps: - - uses: mshick/add-pr-comment@a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + submodules: false + + - name: Filter paths + id: filter + run: | + MERGE_BASE=$(git merge-base origin/${GITHUB_BASE_REF} origin/${GITHUB_HEAD_REF}) + FILES=$(git diff --name-only $MERGE_BASE origin/${GITHUB_HEAD_REF}) + IGNORE=true + for FILE in $FILES; do + if [[ $FILE != plugins/* && $FILE != .github/* ]]; then + IGNORE=false + break + fi + done + + if $IGNORE; then + echo "ignore=true" >> $GITHUB_OUTPUT + echo "Filter result code: ignore = true" + else + echo "ignore=false" >> $GITHUB_OUTPUT + echo "Filter result code: ignore = false" + fi + shell: bash + + - name: Add PR comment + if: steps.filter.outputs.ignore == 'false' + uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 with: message: > ### 📝 PRs merging into main branch @@ -27,4 +56,5 @@ jobs: branch when the code complete and ready to be released. - name: Success + if: steps.filter.outputs.ignore == 'false' run: exit 0 diff --git a/.github/workflows/metalava-semver-check.yml b/.github/workflows/metalava-semver-check.yml new file mode 100644 index 00000000000..0ec7f35e49c --- /dev/null +++ b/.github/workflows/metalava-semver-check.yml @@ -0,0 +1,32 @@ +name: Metalava SemVer Check + +on: + pull_request: + +jobs: + semver-check: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Checkout PR + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up JDK 17 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Copy new api.txt files + run: ./gradlew copyApiTxtFile + + - name: Checkout main + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.base_ref }} + clean: false + + - name: Run Metalava SemVer check + run: ./gradlew metalavaSemver diff --git a/.github/workflows/plugins-check.yml b/.github/workflows/plugins-check.yml index fa482c36d35..3cbb6d2d01b 100644 --- a/.github/workflows/plugins-check.yml +++ b/.github/workflows/plugins-check.yml @@ -11,11 +11,14 @@ concurrency: jobs: plugins-check: + permissions: + checks: write + pull-requests: write runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -26,7 +29,7 @@ jobs: run: | ./gradlew plugins:check - name: Publish Test Results - uses: EnricoMi/publish-unit-test-result-action@82082dac68ad6a19d980f8ce817e108b9f496c2a + uses: EnricoMi/publish-unit-test-result-action@170bf24d20d201b842d7a52403b73ed297e6645b # v2.18.0 with: files: "**/build/test-results/**/*.xml" check_name: "plugins test results" diff --git a/.github/workflows/post_release_cleanup.yml b/.github/workflows/post_release_cleanup.yml index 8206b735a11..d7ee562bb51 100644 --- a/.github/workflows/post_release_cleanup.yml +++ b/.github/workflows/post_release_cleanup.yml @@ -12,11 +12,11 @@ jobs: create-pull-request: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -26,7 +26,7 @@ jobs: ./gradlew postReleaseCleanup - name: Create Pull Request - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: token: ${{ secrets.GOOGLE_OSS_BOT_TOKEN }} committer: google-oss-bot @@ -41,6 +41,6 @@ jobs: title: '${{ inputs.name}} mergeback' body: | Auto-generated PR for cleaning up release ${{ inputs.name}} - + NO_RELEASE_CHANGE commit-message: 'Post release cleanup for ${{ inputs.name }}' diff --git a/.github/workflows/private-mirror-sync.yml b/.github/workflows/private-mirror-sync.yml index 324993eb791..dc17fb289cc 100644 --- a/.github/workflows/private-mirror-sync.yml +++ b/.github/workflows/private-mirror-sync.yml @@ -10,14 +10,14 @@ jobs: if: github.repository == 'FirebasePrivate/firebase-android-sdk' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: firebase/firebase-android-sdk ref: main fetch-depth: 0 submodules: true - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 submodules: true diff --git a/.github/workflows/release-note-changes.yml b/.github/workflows/release-note-changes.yml index 06d42153ea4..95debd4469e 100644 --- a/.github/workflows/release-note-changes.yml +++ b/.github/workflows/release-note-changes.yml @@ -6,10 +6,10 @@ on: - 'main' jobs: - build: + release-notes-changed: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -18,22 +18,26 @@ jobs: - name: Get changed changelog files id: changed-files - uses: tj-actions/changed-files@v41.0.0 - with: - files_ignore: | - plugins/** - files: | - **/CHANGELOG.md + run: | + git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha}} | grep CHANGELOG.md > /tmp/changelog_file_list.txt + if [[ "$?" == "0" ]] + then + echo "any_changed=true" >> $GITHUB_OUTPUT + else + echo "any_changed=false" >> $GITHUB_OUTPUT + fi + echo "all_changed_files=$(cat /tmp/changelog_file_list.txt)" >> $GITHUB_OUTPUT + rm /tmp/changelog_file_list.txt - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 if: ${{ steps.changed-files.outputs.any_changed == 'true' }} with: python-version: '3.10' @@ -50,7 +54,7 @@ jobs: fireci changelog_comment -c "${{ steps.changed-files.outputs.all_changed_files }}" -o ./changelog_comment.md - name: Add PR Comment - uses: mshick/add-pr-comment@v2.8.1 + uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 continue-on-error: true with: status: ${{ steps.generate-comment.outcome }} diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 22bd7f8e3c2..ed18d8c2a2c 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false @@ -73,7 +73,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: Upload artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/semver-check.yml b/.github/workflows/semver-check.yml index 2fc7eb38843..77b528b936b 100644 --- a/.github/workflows/semver-check.yml +++ b/.github/workflows/semver-check.yml @@ -10,9 +10,9 @@ jobs: semver-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin diff --git a/.github/workflows/sessions-e2e.yml b/.github/workflows/sessions-e2e.yml index 048cd92eee9..092a51fc094 100644 --- a/.github/workflows/sessions-e2e.yml +++ b/.github/workflows/sessions-e2e.yml @@ -18,10 +18,10 @@ jobs: steps: - name: Checkout firebase-sessions - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: '11' distribution: 'temurin' @@ -31,10 +31,10 @@ jobs: run: | echo $SESSIONS_E2E_GOOGLE_SERVICES | base64 -d > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Run sessions end-to-end tests env: FTL_RESULTS_BUCKET: fireescape diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 07ab7dbeeb2..d39d6ab6562 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -7,20 +7,20 @@ jobs: if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 # TODO(yifany): make it a fireci plugin and remove the separately distributed jar file - name: Download smoke tests runner @@ -51,7 +51,7 @@ jobs: - name: Upload test artifacts if: always() - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: smoke-tests-artifacts path: | diff --git a/.github/workflows/update-cpp-sdk-on-release.yml b/.github/workflows/update-cpp-sdk-on-release.yml index 60ffbc47285..49e6b0e1392 100644 --- a/.github/workflows/update-cpp-sdk-on-release.yml +++ b/.github/workflows/update-cpp-sdk-on-release.yml @@ -23,7 +23,7 @@ jobs: outputs: released_version_changed: ${{ steps.check_version.outputs.released_version_changed }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Check out the actual head commit, not any merge commit. ref: ${{ github.sha }} @@ -51,12 +51,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: 3.7 - name: Check out firebase-cpp-sdk - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: firebase/firebase-cpp-sdk ref: main diff --git a/.github/workflows/validate-dependencies.yml b/.github/workflows/validate-dependencies.yml index c91ad8aee0c..b6fe70c5133 100644 --- a/.github/workflows/validate-dependencies.yml +++ b/.github/workflows/validate-dependencies.yml @@ -10,9 +10,9 @@ jobs: build-artifacts: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml index f5f285e29a0..7824404d362 100644 --- a/.github/workflows/version-check.yml +++ b/.github/workflows/version-check.yml @@ -10,9 +10,9 @@ jobs: version-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin diff --git a/build.gradle.kts b/build.gradle.kts index a15ce611215..a10ac0119ea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,7 @@ plugins { id("firebase-ci") id("smoke-tests") alias(libs.plugins.google.services) + alias(libs.plugins.kotlinx.serialization) apply false } extra["targetSdkVersion"] = 34 diff --git a/ci/fireci/fireciplugins/api_information.py b/ci/fireci/fireciplugins/api_information.py index d10b6866797..05e4966d47c 100644 --- a/ci/fireci/fireciplugins/api_information.py +++ b/ci/fireci/fireciplugins/api_information.py @@ -37,9 +37,9 @@ def api_information(auth_token, repo_name, issue_number): with open(os.path.join(dir_suffix, filename), 'r') as f: outputlines = f.readlines() for line in outputlines: - if 'error' in line: + if 'error:' in line: formatted_output_lines.append(line[line.find('error:'):]) - elif 'warning' in line: + elif 'warning:' in line: formatted_output_lines.append(line[line.find('warning:'):]) if formatted_output_lines: diff --git a/ci/fireci/pyproject.toml b/ci/fireci/pyproject.toml index 1ec2c8a9d98..8fd3b462353 100644 --- a/ci/fireci/pyproject.toml +++ b/ci/fireci/pyproject.toml @@ -1,3 +1,44 @@ [build-system] -requires = ["setuptools ~= 58.0"] +requires = ["setuptools ~= 70.0"] build-backend = "setuptools.build_meta" + +[project] +name = "fireci" +version = "0.1" +dependencies = [ + "protobuf==3.20.3", + "click==8.1.7", + "google-cloud-storage==2.18.2", + "mypy==1.6.0", + "numpy==1.24.4", + "pandas==1.5.3", + "PyGithub==1.58.2", + "pystache==0.6.0", + "requests==2.32.2", + "seaborn==0.12.2", + "PyYAML==6.0.1", + "termcolor==2.4.0", + "pytest" +] + +[project.scripts] +fireci = "fireci.main:cli" + +[tool.setuptools] +packages = ["fireci", "fireciplugins"] + +[tool.mypy] +strict_optional = false + +[[tool.mypy.overrides]] + module = [ + "google.cloud", + "matplotlib", + "matplotlib.pyplot", + "pandas", + "pystache", + "requests", + "seaborn", + "yaml" + ] + ignore_missing_imports = true diff --git a/ci/fireci/setup.cfg b/ci/fireci/setup.cfg deleted file mode 100644 index 7b49519871c..00000000000 --- a/ci/fireci/setup.cfg +++ /dev/null @@ -1,45 +0,0 @@ -[metadata] -name = fireci -version = 0.1 - -[options] -install_requires = - protobuf==3.20.3 - click==8.1.7 - google-cloud-storage==2.18.2 - mypy==1.6.0 - numpy==1.24.4 - pandas==1.5.3 - PyGithub==1.58.2 - pystache==0.6.0 - requests==2.31.0 - seaborn==0.12.2 - PyYAML==6.0.1 - termcolor==2.4.0 - -[options.extras_require] -test = - pytest - -[options.entry_points] -console_scripts = - fireci = fireci.main:cli - -[mypy] -strict_optional = False -[mypy-google.cloud] -ignore_missing_imports = True -[mypy-matplotlib] -ignore_missing_imports = True -[mypy-matplotlib.pyplot] -ignore_missing_imports = True -[mypy-pandas] -ignore_missing_imports = True -[mypy-pystache] -ignore_missing_imports = True -[mypy-requests] -ignore_missing_imports = True -[mypy-seaborn] -ignore_missing_imports = True -[mypy-yaml] -ignore_missing_imports = True diff --git a/ci/run.sh b/ci/run.sh index be6e0a35a68..3647cf8082d 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -15,7 +15,12 @@ # limitations under the License. set -e +set -x DIRECTORY=$(cd `dirname $0` && pwd) -pip3 install -e $DIRECTORY/fireci >> /dev/null +python3 -m ensurepip --upgrade +python3 -m pip install --upgrade setuptools +python3 -m pip install --upgrade pip +python3 -m pip install --upgrade wheel +python3 -m pip install -e $DIRECTORY/fireci >> /dev/null fireci $@ diff --git a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Types.kt b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Types.kt index 9a902916cf7..f829d95ed52 100644 --- a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Types.kt +++ b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Types.kt @@ -190,7 +190,7 @@ data class ProtoField( val lowerCamelCaseName: String get() { - return SNAKE_CASE_REGEX.replace(name) { it.value.replace("_", "").toUpperCase() } + return SNAKE_CASE_REGEX.replace(name) { it.value.replace("_", "").uppercase() } } val camelCaseName: String diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md new file mode 100644 index 00000000000..dd818c29cec --- /dev/null +++ b/firebase-ai/CHANGELOG.md @@ -0,0 +1,28 @@ +# Unreleased + +* [fixed] Fixed `FirebaseAI.getInstance` StackOverflowException (#6971) +* [fixed] Fixed an issue that was causing the SDK to send empty `FunctionDeclaration` descriptions to the API. +* [changed] Introduced the `Voice` class, which accepts a voice name, and deprecated the `Voices` class. +* [changed] **Breaking Change**: Updated `SpeechConfig` to take in `Voice` class instead of `Voices` class. + * **Action Required:** Update all references of `SpeechConfig` initialization to use `Voice` class. +* [fixed] Fix incorrect model name in count token requests to the developer API backend + + +# 16.0.0 +* [feature] Initial release of the Firebase AI SDK (`firebase-ai`). This SDK *replaces* the previous + Vertex AI in Firebase SDK (`firebase-vertexai`) to accommodate the evolving set of supported + features and services. + * The new Firebase AI SDK provides **Preview** support for the Gemini Developer API, including its + free tier offering. + * Using the Firebase AI SDK with the Vertex AI Gemini API is still generally available (GA). + + If you're using the old `firebase-vertexai`, we recommend + [migrating to `firebase-ai`](/docs/ai-logic/migrate-to-latest-sdk) + because all new development and features will be in this new SDK. +* [feature] **Preview:** Added support for specifying response modalities in `GenerationConfig` + (only available in the new `firebase-ai` package). This includes support for image generation using + [specific Gemini models](/docs/vertex-ai/models). + + Note: This feature is in Public Preview, which means that it is not subject to any SLA or + deprecation policy and could change in backwards-incompatible ways. + diff --git a/firebase-ai/README.md b/firebase-ai/README.md new file mode 100644 index 00000000000..e09f65c6092 --- /dev/null +++ b/firebase-ai/README.md @@ -0,0 +1,32 @@ +# Firebase AI SDK + +For developer documentation, please visit https://firebase.google.com/docs/vertex-ai. +This README is for contributors building and running tests for the SDK. + +## Building + +All Gradle commands should be run from the root of this repository. + +`./gradlew :firebase-ai:publishToMavenLocal` + +## Running Tests + +> [!IMPORTANT] +> These unit tests require mock response files, which can be downloaded by running +`./firebase-ai/update_responses.sh` from the root of this repository. + +Unit tests: + +`./gradlew :firebase-ai:check` + +Integration tests, requiring a running and connected device (emulator or real): + +`./gradlew :firebase-ai:deviceCheck` + +## Code Formatting + +Format Kotlin code in this SDK in Android Studio using +the [spotless plugin]([https://plugins.jetbrains.com/plugin/14912-ktfmt](https://github.com/diffplug/spotless) +by running: + +`./gradlew firebase-ai:spotlessApply` diff --git a/firebase-ai/api.txt b/firebase-ai/api.txt new file mode 100644 index 00000000000..5645b466110 --- /dev/null +++ b/firebase-ai/api.txt @@ -0,0 +1,939 @@ +// Signature format: 3.0 +package com.google.firebase.ai { + + public final class Chat { + ctor public Chat(com.google.firebase.ai.GenerativeModel model, java.util.List history = java.util.ArrayList()); + method public java.util.List getHistory(); + method public suspend Object? sendMessage(android.graphics.Bitmap prompt, kotlin.coroutines.Continuation); + method public suspend Object? sendMessage(com.google.firebase.ai.type.Content prompt, kotlin.coroutines.Continuation); + method public suspend Object? sendMessage(String prompt, kotlin.coroutines.Continuation); + method public kotlinx.coroutines.flow.Flow sendMessageStream(android.graphics.Bitmap prompt); + method public kotlinx.coroutines.flow.Flow sendMessageStream(com.google.firebase.ai.type.Content prompt); + method public kotlinx.coroutines.flow.Flow sendMessageStream(String prompt); + property public final java.util.List history; + } + + public final class FirebaseAI { + method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName); + method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null); + method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null, java.util.List? safetySettings = null); + method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null, java.util.List? safetySettings = null, java.util.List? tools = null); + method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null, java.util.List? safetySettings = null, java.util.List? tools = null, com.google.firebase.ai.type.ToolConfig? toolConfig = null); + method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null, java.util.List? safetySettings = null, java.util.List? tools = null, com.google.firebase.ai.type.ToolConfig? toolConfig = null, com.google.firebase.ai.type.Content? systemInstruction = null); + method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null, java.util.List? safetySettings = null, java.util.List? tools = null, com.google.firebase.ai.type.ToolConfig? toolConfig = null, com.google.firebase.ai.type.Content? systemInstruction = null, com.google.firebase.ai.type.RequestOptions requestOptions = com.google.firebase.ai.type.RequestOptions()); + method public static com.google.firebase.ai.FirebaseAI getInstance(); + method public static com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.ai.type.GenerativeBackend backend); + method public static com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app); + method public static com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend); + method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.ImagenModel imagenModel(String modelName); + method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.ImagenModel imagenModel(String modelName, com.google.firebase.ai.type.ImagenGenerationConfig? generationConfig = null); + method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.ImagenModel imagenModel(String modelName, com.google.firebase.ai.type.ImagenGenerationConfig? generationConfig = null, com.google.firebase.ai.type.ImagenSafetySettings? safetySettings = null); + method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.ImagenModel imagenModel(String modelName, com.google.firebase.ai.type.ImagenGenerationConfig? generationConfig = null, com.google.firebase.ai.type.ImagenSafetySettings? safetySettings = null, com.google.firebase.ai.type.RequestOptions requestOptions = com.google.firebase.ai.type.RequestOptions()); + method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName); + method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null); + method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null, java.util.List? tools = null); + method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null, java.util.List? tools = null, com.google.firebase.ai.type.Content? systemInstruction = null); + method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null, java.util.List? tools = null, com.google.firebase.ai.type.Content? systemInstruction = null, com.google.firebase.ai.type.RequestOptions requestOptions = com.google.firebase.ai.type.RequestOptions()); + property public static final com.google.firebase.ai.FirebaseAI instance; + field public static final com.google.firebase.ai.FirebaseAI.Companion Companion; + } + + public static final class FirebaseAI.Companion { + method public com.google.firebase.ai.FirebaseAI getInstance(); + method public com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.ai.type.GenerativeBackend backend); + method public com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app); + method public com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend); + property public final com.google.firebase.ai.FirebaseAI instance; + } + + public final class FirebaseAIKt { + method public static com.google.firebase.ai.FirebaseAI ai(com.google.firebase.Firebase, com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend = GenerativeBackend.googleAI()); + method public static com.google.firebase.ai.FirebaseAI getAi(com.google.firebase.Firebase); + } + + public final class GenerativeModel { + method public suspend Object? countTokens(android.graphics.Bitmap prompt, kotlin.coroutines.Continuation); + method public suspend Object? countTokens(com.google.firebase.ai.type.Content[] prompt, kotlin.coroutines.Continuation); + method public suspend Object? countTokens(String prompt, kotlin.coroutines.Continuation); + method public suspend Object? generateContent(android.graphics.Bitmap prompt, kotlin.coroutines.Continuation); + method public suspend Object? generateContent(com.google.firebase.ai.type.Content[] prompt, kotlin.coroutines.Continuation); + method public suspend Object? generateContent(String prompt, kotlin.coroutines.Continuation); + method public kotlinx.coroutines.flow.Flow generateContentStream(android.graphics.Bitmap prompt); + method public kotlinx.coroutines.flow.Flow generateContentStream(com.google.firebase.ai.type.Content... prompt); + method public kotlinx.coroutines.flow.Flow generateContentStream(String prompt); + method public com.google.firebase.ai.Chat startChat(java.util.List history = emptyList()); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenModel { + method public suspend Object? generateImages(String prompt, kotlin.coroutines.Continuation>); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveGenerativeModel { + method public suspend Object? connect(kotlin.coroutines.Continuation); + } + +} + +package com.google.firebase.ai.java { + + public abstract class ChatFutures { + method public static final com.google.firebase.ai.java.ChatFutures from(com.google.firebase.ai.Chat chat); + method public abstract com.google.firebase.ai.Chat getChat(); + method public abstract com.google.common.util.concurrent.ListenableFuture sendMessage(com.google.firebase.ai.type.Content prompt); + method public abstract org.reactivestreams.Publisher sendMessageStream(com.google.firebase.ai.type.Content prompt); + field public static final com.google.firebase.ai.java.ChatFutures.Companion Companion; + } + + public static final class ChatFutures.Companion { + method public com.google.firebase.ai.java.ChatFutures from(com.google.firebase.ai.Chat chat); + } + + public abstract class GenerativeModelFutures { + method public abstract com.google.common.util.concurrent.ListenableFuture countTokens(com.google.firebase.ai.type.Content... prompt); + method public static final com.google.firebase.ai.java.GenerativeModelFutures from(com.google.firebase.ai.GenerativeModel model); + method public abstract com.google.common.util.concurrent.ListenableFuture generateContent(com.google.firebase.ai.type.Content... prompt); + method public abstract org.reactivestreams.Publisher generateContentStream(com.google.firebase.ai.type.Content... prompt); + method public abstract com.google.firebase.ai.GenerativeModel getGenerativeModel(); + method public abstract com.google.firebase.ai.java.ChatFutures startChat(); + method public abstract com.google.firebase.ai.java.ChatFutures startChat(java.util.List history); + field public static final com.google.firebase.ai.java.GenerativeModelFutures.Companion Companion; + } + + public static final class GenerativeModelFutures.Companion { + method public com.google.firebase.ai.java.GenerativeModelFutures from(com.google.firebase.ai.GenerativeModel model); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public abstract class ImagenModelFutures { + method public static final com.google.firebase.ai.java.ImagenModelFutures from(com.google.firebase.ai.ImagenModel model); + method public abstract com.google.common.util.concurrent.ListenableFuture> generateImages(String prompt); + method public abstract com.google.firebase.ai.ImagenModel getImageModel(); + field public static final com.google.firebase.ai.java.ImagenModelFutures.Companion Companion; + } + + public static final class ImagenModelFutures.Companion { + method public com.google.firebase.ai.java.ImagenModelFutures from(com.google.firebase.ai.ImagenModel model); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public abstract class LiveModelFutures { + method public abstract com.google.common.util.concurrent.ListenableFuture connect(); + method public static final com.google.firebase.ai.java.LiveModelFutures from(com.google.firebase.ai.LiveGenerativeModel model); + field public static final com.google.firebase.ai.java.LiveModelFutures.Companion Companion; + } + + public static final class LiveModelFutures.Companion { + method public com.google.firebase.ai.java.LiveModelFutures from(com.google.firebase.ai.LiveGenerativeModel model); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public abstract class LiveSessionFutures { + method public abstract com.google.common.util.concurrent.ListenableFuture close(); + method public static final com.google.firebase.ai.java.LiveSessionFutures from(com.google.firebase.ai.type.LiveSession session); + method public abstract org.reactivestreams.Publisher receive(); + method public abstract com.google.common.util.concurrent.ListenableFuture send(com.google.firebase.ai.type.Content content); + method public abstract com.google.common.util.concurrent.ListenableFuture send(String text); + method public abstract com.google.common.util.concurrent.ListenableFuture sendFunctionResponse(java.util.List functionList); + method public abstract com.google.common.util.concurrent.ListenableFuture sendMediaStream(java.util.List mediaChunks); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public abstract com.google.common.util.concurrent.ListenableFuture startAudioConversation(); + method public abstract com.google.common.util.concurrent.ListenableFuture startAudioConversation(kotlin.jvm.functions.Function1? functionCallHandler); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public abstract com.google.common.util.concurrent.ListenableFuture stopAudioConversation(); + method public abstract void stopReceiving(); + field public static final com.google.firebase.ai.java.LiveSessionFutures.Companion Companion; + } + + public static final class LiveSessionFutures.Companion { + method public com.google.firebase.ai.java.LiveSessionFutures from(com.google.firebase.ai.type.LiveSession session); + } + +} + +package com.google.firebase.ai.type { + + public final class AudioRecordInitializationFailedException extends com.google.firebase.ai.type.FirebaseAIException { + ctor public AudioRecordInitializationFailedException(String message); + } + + public final class BlockReason { + method public String getName(); + method public int getOrdinal(); + property public final String name; + property public final int ordinal; + field public static final com.google.firebase.ai.type.BlockReason BLOCKLIST; + field public static final com.google.firebase.ai.type.BlockReason.Companion Companion; + field public static final com.google.firebase.ai.type.BlockReason OTHER; + field public static final com.google.firebase.ai.type.BlockReason PROHIBITED_CONTENT; + field public static final com.google.firebase.ai.type.BlockReason SAFETY; + field public static final com.google.firebase.ai.type.BlockReason UNKNOWN; + } + + public static final class BlockReason.Companion { + } + + public final class Candidate { + method public com.google.firebase.ai.type.CitationMetadata? getCitationMetadata(); + method public com.google.firebase.ai.type.Content getContent(); + method public com.google.firebase.ai.type.FinishReason? getFinishReason(); + method public java.util.List getSafetyRatings(); + property public final com.google.firebase.ai.type.CitationMetadata? citationMetadata; + property public final com.google.firebase.ai.type.Content content; + property public final com.google.firebase.ai.type.FinishReason? finishReason; + property public final java.util.List safetyRatings; + } + + public final class Citation { + method public int getEndIndex(); + method public String? getLicense(); + method public java.util.Calendar? getPublicationDate(); + method public int getStartIndex(); + method public String? getTitle(); + method public String? getUri(); + property public final int endIndex; + property public final String? license; + property public final java.util.Calendar? publicationDate; + property public final int startIndex; + property public final String? title; + property public final String? uri; + } + + public final class CitationMetadata { + method public java.util.List getCitations(); + property public final java.util.List citations; + } + + public final class Content { + ctor public Content(String? role = "user", java.util.List parts); + ctor public Content(java.util.List parts); + method public com.google.firebase.ai.type.Content copy(String? role = role, java.util.List parts = parts); + method public java.util.List getParts(); + method public String? getRole(); + property public final java.util.List parts; + property public final String? role; + } + + public static final class Content.Builder { + ctor public Content.Builder(); + method public com.google.firebase.ai.type.Content.Builder addFileData(String uri, String mimeType); + method public com.google.firebase.ai.type.Content.Builder addImage(android.graphics.Bitmap image); + method public com.google.firebase.ai.type.Content.Builder addInlineData(byte[] bytes, String mimeType); + method public com.google.firebase.ai.type.Content.Builder addPart(T data); + method public com.google.firebase.ai.type.Content.Builder addText(String text); + method public com.google.firebase.ai.type.Content build(); + method public com.google.firebase.ai.type.Content.Builder setParts(java.util.List parts); + method public com.google.firebase.ai.type.Content.Builder setRole(String? role); + field public java.util.List parts; + field public String? role; + } + + public final class ContentBlockedException extends com.google.firebase.ai.type.FirebaseAIException { + } + + public final class ContentKt { + method public static com.google.firebase.ai.type.Content content(String? role = "user", kotlin.jvm.functions.Function1 init); + } + + public final class ContentModality { + method public int getOrdinal(); + property public final int ordinal; + field public static final com.google.firebase.ai.type.ContentModality AUDIO; + field public static final com.google.firebase.ai.type.ContentModality.Companion Companion; + field public static final com.google.firebase.ai.type.ContentModality DOCUMENT; + field public static final com.google.firebase.ai.type.ContentModality IMAGE; + field public static final com.google.firebase.ai.type.ContentModality TEXT; + field public static final com.google.firebase.ai.type.ContentModality UNSPECIFIED; + field public static final com.google.firebase.ai.type.ContentModality VIDEO; + } + + public static final class ContentModality.Companion { + } + + public final class CountTokensResponse { + ctor public CountTokensResponse(int totalTokens, Integer? totalBillableCharacters = null, java.util.List promptTokensDetails = emptyList()); + method public operator int component1(); + method public operator Integer? component2(); + method public operator java.util.List? component3(); + method public java.util.List getPromptTokensDetails(); + method public Integer? getTotalBillableCharacters(); + method public int getTotalTokens(); + property public final java.util.List promptTokensDetails; + property public final Integer? totalBillableCharacters; + property public final int totalTokens; + } + + public final class FileDataPart implements com.google.firebase.ai.type.Part { + ctor public FileDataPart(String uri, String mimeType); + method public String getMimeType(); + method public String getUri(); + property public final String mimeType; + property public final String uri; + } + + public final class FinishReason { + method public String getName(); + method public int getOrdinal(); + property public final String name; + property public final int ordinal; + field public static final com.google.firebase.ai.type.FinishReason BLOCKLIST; + field public static final com.google.firebase.ai.type.FinishReason.Companion Companion; + field public static final com.google.firebase.ai.type.FinishReason MALFORMED_FUNCTION_CALL; + field public static final com.google.firebase.ai.type.FinishReason MAX_TOKENS; + field public static final com.google.firebase.ai.type.FinishReason OTHER; + field public static final com.google.firebase.ai.type.FinishReason PROHIBITED_CONTENT; + field public static final com.google.firebase.ai.type.FinishReason RECITATION; + field public static final com.google.firebase.ai.type.FinishReason SAFETY; + field public static final com.google.firebase.ai.type.FinishReason SPII; + field public static final com.google.firebase.ai.type.FinishReason STOP; + field public static final com.google.firebase.ai.type.FinishReason UNKNOWN; + } + + public static final class FinishReason.Companion { + } + + public abstract class FirebaseAIException extends java.lang.RuntimeException { + } + + public final class FunctionCallPart implements com.google.firebase.ai.type.Part { + ctor public FunctionCallPart(String name, java.util.Map args); + ctor public FunctionCallPart(String name, java.util.Map args, String? id = null); + method public java.util.Map getArgs(); + method public String? getId(); + method public String getName(); + property public final java.util.Map args; + property public final String? id; + property public final String name; + } + + public final class FunctionCallingConfig { + method public static com.google.firebase.ai.type.FunctionCallingConfig any(); + method public static com.google.firebase.ai.type.FunctionCallingConfig any(java.util.List? allowedFunctionNames = null); + method public static com.google.firebase.ai.type.FunctionCallingConfig auto(); + method public static com.google.firebase.ai.type.FunctionCallingConfig none(); + field public static final com.google.firebase.ai.type.FunctionCallingConfig.Companion Companion; + } + + public static final class FunctionCallingConfig.Companion { + method public com.google.firebase.ai.type.FunctionCallingConfig any(); + method public com.google.firebase.ai.type.FunctionCallingConfig any(java.util.List? allowedFunctionNames = null); + method public com.google.firebase.ai.type.FunctionCallingConfig auto(); + method public com.google.firebase.ai.type.FunctionCallingConfig none(); + } + + public final class FunctionDeclaration { + ctor public FunctionDeclaration(String name, String description, java.util.Map parameters, java.util.List optionalParameters = emptyList()); + } + + public final class FunctionResponsePart implements com.google.firebase.ai.type.Part { + ctor public FunctionResponsePart(String name, kotlinx.serialization.json.JsonObject response); + ctor public FunctionResponsePart(String name, kotlinx.serialization.json.JsonObject response, String? id = null); + method public String? getId(); + method public String getName(); + method public kotlinx.serialization.json.JsonObject getResponse(); + property public final String? id; + property public final String name; + property public final kotlinx.serialization.json.JsonObject response; + } + + public final class GenerateContentResponse { + ctor public GenerateContentResponse(java.util.List candidates, com.google.firebase.ai.type.PromptFeedback? promptFeedback, com.google.firebase.ai.type.UsageMetadata? usageMetadata); + method public java.util.List getCandidates(); + method public java.util.List getFunctionCalls(); + method public java.util.List getInlineDataParts(); + method public com.google.firebase.ai.type.PromptFeedback? getPromptFeedback(); + method public String? getText(); + method public com.google.firebase.ai.type.UsageMetadata? getUsageMetadata(); + property public final java.util.List candidates; + property public final java.util.List functionCalls; + property public final java.util.List inlineDataParts; + property public final com.google.firebase.ai.type.PromptFeedback? promptFeedback; + property public final String? text; + property public final com.google.firebase.ai.type.UsageMetadata? usageMetadata; + } + + public final class GenerationConfig { + field public static final com.google.firebase.ai.type.GenerationConfig.Companion Companion; + } + + public static final class GenerationConfig.Builder { + ctor public GenerationConfig.Builder(); + method public com.google.firebase.ai.type.GenerationConfig build(); + method public com.google.firebase.ai.type.GenerationConfig.Builder setCandidateCount(Integer? candidateCount); + method public com.google.firebase.ai.type.GenerationConfig.Builder setFrequencyPenalty(Float? frequencyPenalty); + method public com.google.firebase.ai.type.GenerationConfig.Builder setMaxOutputTokens(Integer? maxOutputTokens); + method public com.google.firebase.ai.type.GenerationConfig.Builder setPresencePenalty(Float? presencePenalty); + method public com.google.firebase.ai.type.GenerationConfig.Builder setResponseMimeType(String? responseMimeType); + method public com.google.firebase.ai.type.GenerationConfig.Builder setResponseModalities(java.util.List? responseModalities); + method public com.google.firebase.ai.type.GenerationConfig.Builder setResponseSchema(com.google.firebase.ai.type.Schema? responseSchema); + method public com.google.firebase.ai.type.GenerationConfig.Builder setStopSequences(java.util.List? stopSequences); + method public com.google.firebase.ai.type.GenerationConfig.Builder setTemperature(Float? temperature); + method public com.google.firebase.ai.type.GenerationConfig.Builder setTopK(Integer? topK); + method public com.google.firebase.ai.type.GenerationConfig.Builder setTopP(Float? topP); + field public Integer? candidateCount; + field public Float? frequencyPenalty; + field public Integer? maxOutputTokens; + field public Float? presencePenalty; + field public String? responseMimeType; + field public java.util.List? responseModalities; + field public com.google.firebase.ai.type.Schema? responseSchema; + field public java.util.List? stopSequences; + field public Float? temperature; + field public Integer? topK; + field public Float? topP; + } + + public static final class GenerationConfig.Companion { + method public com.google.firebase.ai.type.GenerationConfig.Builder builder(); + } + + public final class GenerationConfigKt { + method public static com.google.firebase.ai.type.GenerationConfig generationConfig(kotlin.jvm.functions.Function1 init); + } + + public final class GenerativeBackend { + method public static com.google.firebase.ai.type.GenerativeBackend googleAI(); + method public static com.google.firebase.ai.type.GenerativeBackend vertexAI(); + method public static com.google.firebase.ai.type.GenerativeBackend vertexAI(String location = "us-central1"); + field public static final com.google.firebase.ai.type.GenerativeBackend.Companion Companion; + } + + public static final class GenerativeBackend.Companion { + method public com.google.firebase.ai.type.GenerativeBackend googleAI(); + method public com.google.firebase.ai.type.GenerativeBackend vertexAI(); + method public com.google.firebase.ai.type.GenerativeBackend vertexAI(String location = "us-central1"); + } + + public final class HarmBlockMethod { + method public int getOrdinal(); + property public final int ordinal; + field public static final com.google.firebase.ai.type.HarmBlockMethod.Companion Companion; + field public static final com.google.firebase.ai.type.HarmBlockMethod PROBABILITY; + field public static final com.google.firebase.ai.type.HarmBlockMethod SEVERITY; + } + + public static final class HarmBlockMethod.Companion { + } + + public final class HarmBlockThreshold { + method public int getOrdinal(); + property public final int ordinal; + field public static final com.google.firebase.ai.type.HarmBlockThreshold.Companion Companion; + field public static final com.google.firebase.ai.type.HarmBlockThreshold LOW_AND_ABOVE; + field public static final com.google.firebase.ai.type.HarmBlockThreshold MEDIUM_AND_ABOVE; + field public static final com.google.firebase.ai.type.HarmBlockThreshold NONE; + field public static final com.google.firebase.ai.type.HarmBlockThreshold OFF; + field public static final com.google.firebase.ai.type.HarmBlockThreshold ONLY_HIGH; + } + + public static final class HarmBlockThreshold.Companion { + } + + public final class HarmCategory { + method public int getOrdinal(); + property public final int ordinal; + field public static final com.google.firebase.ai.type.HarmCategory CIVIC_INTEGRITY; + field public static final com.google.firebase.ai.type.HarmCategory.Companion Companion; + field public static final com.google.firebase.ai.type.HarmCategory DANGEROUS_CONTENT; + field public static final com.google.firebase.ai.type.HarmCategory HARASSMENT; + field public static final com.google.firebase.ai.type.HarmCategory HATE_SPEECH; + field public static final com.google.firebase.ai.type.HarmCategory SEXUALLY_EXPLICIT; + field public static final com.google.firebase.ai.type.HarmCategory UNKNOWN; + } + + public static final class HarmCategory.Companion { + } + + public final class HarmProbability { + method public int getOrdinal(); + property public final int ordinal; + field public static final com.google.firebase.ai.type.HarmProbability.Companion Companion; + field public static final com.google.firebase.ai.type.HarmProbability HIGH; + field public static final com.google.firebase.ai.type.HarmProbability LOW; + field public static final com.google.firebase.ai.type.HarmProbability MEDIUM; + field public static final com.google.firebase.ai.type.HarmProbability NEGLIGIBLE; + field public static final com.google.firebase.ai.type.HarmProbability UNKNOWN; + } + + public static final class HarmProbability.Companion { + } + + public final class HarmSeverity { + method public int getOrdinal(); + property public final int ordinal; + field public static final com.google.firebase.ai.type.HarmSeverity.Companion Companion; + field public static final com.google.firebase.ai.type.HarmSeverity HIGH; + field public static final com.google.firebase.ai.type.HarmSeverity LOW; + field public static final com.google.firebase.ai.type.HarmSeverity MEDIUM; + field public static final com.google.firebase.ai.type.HarmSeverity NEGLIGIBLE; + field public static final com.google.firebase.ai.type.HarmSeverity UNKNOWN; + } + + public static final class HarmSeverity.Companion { + } + + public final class ImagePart implements com.google.firebase.ai.type.Part { + ctor public ImagePart(android.graphics.Bitmap image); + method public android.graphics.Bitmap getImage(); + property public final android.graphics.Bitmap image; + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenAspectRatio { + field public static final com.google.firebase.ai.type.ImagenAspectRatio.Companion Companion; + field public static final com.google.firebase.ai.type.ImagenAspectRatio LANDSCAPE_16x9; + field public static final com.google.firebase.ai.type.ImagenAspectRatio LANDSCAPE_4x3; + field public static final com.google.firebase.ai.type.ImagenAspectRatio PORTRAIT_3x4; + field public static final com.google.firebase.ai.type.ImagenAspectRatio PORTRAIT_9x16; + field public static final com.google.firebase.ai.type.ImagenAspectRatio SQUARE_1x1; + } + + public static final class ImagenAspectRatio.Companion { + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenGenerationConfig { + ctor public ImagenGenerationConfig(String? negativePrompt = null, Integer? numberOfImages = 1, com.google.firebase.ai.type.ImagenAspectRatio? aspectRatio = null, com.google.firebase.ai.type.ImagenImageFormat? imageFormat = null, Boolean? addWatermark = null); + method public Boolean? getAddWatermark(); + method public com.google.firebase.ai.type.ImagenAspectRatio? getAspectRatio(); + method public com.google.firebase.ai.type.ImagenImageFormat? getImageFormat(); + method public String? getNegativePrompt(); + method public Integer? getNumberOfImages(); + property public final Boolean? addWatermark; + property public final com.google.firebase.ai.type.ImagenAspectRatio? aspectRatio; + property public final com.google.firebase.ai.type.ImagenImageFormat? imageFormat; + property public final String? negativePrompt; + property public final Integer? numberOfImages; + field public static final com.google.firebase.ai.type.ImagenGenerationConfig.Companion Companion; + } + + public static final class ImagenGenerationConfig.Builder { + ctor public ImagenGenerationConfig.Builder(); + method public com.google.firebase.ai.type.ImagenGenerationConfig build(); + method public com.google.firebase.ai.type.ImagenGenerationConfig.Builder setAddWatermark(boolean addWatermark); + method public com.google.firebase.ai.type.ImagenGenerationConfig.Builder setAspectRatio(com.google.firebase.ai.type.ImagenAspectRatio aspectRatio); + method public com.google.firebase.ai.type.ImagenGenerationConfig.Builder setImageFormat(com.google.firebase.ai.type.ImagenImageFormat imageFormat); + method public com.google.firebase.ai.type.ImagenGenerationConfig.Builder setNegativePrompt(String negativePrompt); + method public com.google.firebase.ai.type.ImagenGenerationConfig.Builder setNumberOfImages(int numberOfImages); + field public Boolean? addWatermark; + field public com.google.firebase.ai.type.ImagenAspectRatio? aspectRatio; + field public com.google.firebase.ai.type.ImagenImageFormat? imageFormat; + field public String? negativePrompt; + field public Integer? numberOfImages; + } + + public static final class ImagenGenerationConfig.Companion { + method public com.google.firebase.ai.type.ImagenGenerationConfig.Builder builder(); + } + + public final class ImagenGenerationConfigKt { + method @com.google.firebase.ai.type.PublicPreviewAPI public static com.google.firebase.ai.type.ImagenGenerationConfig imagenGenerationConfig(kotlin.jvm.functions.Function1 init); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenGenerationResponse { + method public String? getFilteredReason(); + method public java.util.List getImages(); + property public final String? filteredReason; + property public final java.util.List images; + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenImageFormat { + method public Integer? getCompressionQuality(); + method public String getMimeType(); + method public static com.google.firebase.ai.type.ImagenImageFormat jpeg(Integer? compressionQuality = null); + method public static com.google.firebase.ai.type.ImagenImageFormat png(); + property public final Integer? compressionQuality; + property public final String mimeType; + field public static final com.google.firebase.ai.type.ImagenImageFormat.Companion Companion; + } + + public static final class ImagenImageFormat.Companion { + method public com.google.firebase.ai.type.ImagenImageFormat jpeg(Integer? compressionQuality = null); + method public com.google.firebase.ai.type.ImagenImageFormat png(); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenInlineImage { + method public android.graphics.Bitmap asBitmap(); + method public byte[] getData(); + method public String getMimeType(); + property public final byte[] data; + property public final String mimeType; + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenPersonFilterLevel { + field public static final com.google.firebase.ai.type.ImagenPersonFilterLevel ALLOW_ADULT; + field public static final com.google.firebase.ai.type.ImagenPersonFilterLevel ALLOW_ALL; + field public static final com.google.firebase.ai.type.ImagenPersonFilterLevel BLOCK_ALL; + field public static final com.google.firebase.ai.type.ImagenPersonFilterLevel.Companion Companion; + } + + public static final class ImagenPersonFilterLevel.Companion { + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenSafetyFilterLevel { + field public static final com.google.firebase.ai.type.ImagenSafetyFilterLevel BLOCK_LOW_AND_ABOVE; + field public static final com.google.firebase.ai.type.ImagenSafetyFilterLevel BLOCK_MEDIUM_AND_ABOVE; + field public static final com.google.firebase.ai.type.ImagenSafetyFilterLevel BLOCK_NONE; + field public static final com.google.firebase.ai.type.ImagenSafetyFilterLevel BLOCK_ONLY_HIGH; + field public static final com.google.firebase.ai.type.ImagenSafetyFilterLevel.Companion Companion; + } + + public static final class ImagenSafetyFilterLevel.Companion { + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenSafetySettings { + ctor public ImagenSafetySettings(com.google.firebase.ai.type.ImagenSafetyFilterLevel safetyFilterLevel, com.google.firebase.ai.type.ImagenPersonFilterLevel personFilterLevel); + } + + public final class InlineDataPart implements com.google.firebase.ai.type.Part { + ctor public InlineDataPart(byte[] inlineData, String mimeType); + method public byte[] getInlineData(); + method public String getMimeType(); + property public final byte[] inlineData; + property public final String mimeType; + } + + public final class InvalidAPIKeyException extends com.google.firebase.ai.type.FirebaseAIException { + } + + public final class InvalidLocationException extends com.google.firebase.ai.type.FirebaseAIException { + } + + public final class InvalidStateException extends com.google.firebase.ai.type.FirebaseAIException { + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveGenerationConfig { + field public static final com.google.firebase.ai.type.LiveGenerationConfig.Companion Companion; + } + + public static final class LiveGenerationConfig.Builder { + ctor public LiveGenerationConfig.Builder(); + method public com.google.firebase.ai.type.LiveGenerationConfig build(); + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setCandidateCount(Integer? candidateCount); + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setFrequencyPenalty(Float? frequencyPenalty); + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setMaxOutputTokens(Integer? maxOutputTokens); + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setPresencePenalty(Float? presencePenalty); + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setResponseModality(com.google.firebase.ai.type.ResponseModality? responseModality); + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setSpeechConfig(com.google.firebase.ai.type.SpeechConfig? speechConfig); + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setTemperature(Float? temperature); + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setTopK(Integer? topK); + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setTopP(Float? topP); + field public Integer? candidateCount; + field public Float? frequencyPenalty; + field public Integer? maxOutputTokens; + field public Float? presencePenalty; + field public com.google.firebase.ai.type.ResponseModality? responseModality; + field public com.google.firebase.ai.type.SpeechConfig? speechConfig; + field public Float? temperature; + field public Integer? topK; + field public Float? topP; + } + + public static final class LiveGenerationConfig.Companion { + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder builder(); + } + + public final class LiveGenerationConfigKt { + method public static com.google.firebase.ai.type.LiveGenerationConfig liveGenerationConfig(kotlin.jvm.functions.Function1 init); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveServerContent implements com.google.firebase.ai.type.LiveServerMessage { + ctor public LiveServerContent(com.google.firebase.ai.type.Content? content, boolean interrupted, boolean turnComplete, boolean generationComplete); + method public com.google.firebase.ai.type.Content? getContent(); + method public boolean getGenerationComplete(); + method public boolean getInterrupted(); + method public boolean getTurnComplete(); + property public final com.google.firebase.ai.type.Content? content; + property public final boolean generationComplete; + property public final boolean interrupted; + property public final boolean turnComplete; + } + + @com.google.firebase.ai.type.PublicPreviewAPI public interface LiveServerMessage { + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveServerSetupComplete implements com.google.firebase.ai.type.LiveServerMessage { + ctor public LiveServerSetupComplete(); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveServerToolCall implements com.google.firebase.ai.type.LiveServerMessage { + ctor public LiveServerToolCall(java.util.List functionCalls); + method public java.util.List getFunctionCalls(); + property public final java.util.List functionCalls; + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveServerToolCallCancellation implements com.google.firebase.ai.type.LiveServerMessage { + ctor public LiveServerToolCallCancellation(java.util.List functionIds); + method public java.util.List getFunctionIds(); + property public final java.util.List functionIds; + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveSession { + method public suspend Object? close(kotlin.coroutines.Continuation); + method public kotlinx.coroutines.flow.Flow receive(); + method public suspend Object? send(com.google.firebase.ai.type.Content content, kotlin.coroutines.Continuation); + method public suspend Object? send(String text, kotlin.coroutines.Continuation); + method public suspend Object? sendFunctionResponse(java.util.List functionList, kotlin.coroutines.Continuation); + method public suspend Object? sendMediaStream(java.util.List mediaChunks, kotlin.coroutines.Continuation); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public suspend Object? startAudioConversation(kotlin.jvm.functions.Function1? functionCallHandler = null, kotlin.coroutines.Continuation); + method public void stopAudioConversation(); + method public void stopReceiving(); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class MediaData { + ctor public MediaData(byte[] data, String mimeType); + method public byte[] getData(); + method public String getMimeType(); + property public final byte[] data; + property public final String mimeType; + } + + public final class ModalityTokenCount { + method public operator com.google.firebase.ai.type.ContentModality component1(); + method public operator int component2(); + method public com.google.firebase.ai.type.ContentModality getModality(); + method public int getTokenCount(); + property public final com.google.firebase.ai.type.ContentModality modality; + property public final int tokenCount; + } + + public interface Part { + } + + public final class PartKt { + method public static com.google.firebase.ai.type.FileDataPart? asFileDataOrNull(com.google.firebase.ai.type.Part); + method public static android.graphics.Bitmap? asImageOrNull(com.google.firebase.ai.type.Part); + method public static com.google.firebase.ai.type.InlineDataPart? asInlineDataPartOrNull(com.google.firebase.ai.type.Part); + method public static String? asTextOrNull(com.google.firebase.ai.type.Part); + } + + public final class PromptBlockedException extends com.google.firebase.ai.type.FirebaseAIException { + method public com.google.firebase.ai.type.GenerateContentResponse? getResponse(); + property public final com.google.firebase.ai.type.GenerateContentResponse? response; + } + + public final class PromptFeedback { + ctor public PromptFeedback(com.google.firebase.ai.type.BlockReason? blockReason, java.util.List safetyRatings, String? blockReasonMessage); + method public com.google.firebase.ai.type.BlockReason? getBlockReason(); + method public String? getBlockReasonMessage(); + method public java.util.List getSafetyRatings(); + property public final com.google.firebase.ai.type.BlockReason? blockReason; + property public final String? blockReasonMessage; + property public final java.util.List safetyRatings; + } + + @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This API is part of an experimental public preview and may change in " + "backwards-incompatible ways without notice.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface PublicPreviewAPI { + } + + public final class QuotaExceededException extends com.google.firebase.ai.type.FirebaseAIException { + } + + public final class RequestOptions { + ctor public RequestOptions(); + ctor public RequestOptions(long timeoutInMillis = 180.seconds.inWholeMilliseconds); + } + + public final class RequestTimeoutException extends com.google.firebase.ai.type.FirebaseAIException { + } + + public final class ResponseModality { + method public int getOrdinal(); + property public final int ordinal; + field public static final com.google.firebase.ai.type.ResponseModality AUDIO; + field public static final com.google.firebase.ai.type.ResponseModality.Companion Companion; + field public static final com.google.firebase.ai.type.ResponseModality IMAGE; + field public static final com.google.firebase.ai.type.ResponseModality TEXT; + } + + public static final class ResponseModality.Companion { + } + + public final class ResponseStoppedException extends com.google.firebase.ai.type.FirebaseAIException { + method public com.google.firebase.ai.type.GenerateContentResponse getResponse(); + property public final com.google.firebase.ai.type.GenerateContentResponse response; + } + + public final class SafetyRating { + method public Boolean? getBlocked(); + method public com.google.firebase.ai.type.HarmCategory getCategory(); + method public com.google.firebase.ai.type.HarmProbability getProbability(); + method public float getProbabilityScore(); + method public com.google.firebase.ai.type.HarmSeverity? getSeverity(); + method public Float? getSeverityScore(); + property public final Boolean? blocked; + property public final com.google.firebase.ai.type.HarmCategory category; + property public final com.google.firebase.ai.type.HarmProbability probability; + property public final float probabilityScore; + property public final com.google.firebase.ai.type.HarmSeverity? severity; + property public final Float? severityScore; + } + + public final class SafetySetting { + ctor public SafetySetting(com.google.firebase.ai.type.HarmCategory harmCategory, com.google.firebase.ai.type.HarmBlockThreshold threshold, com.google.firebase.ai.type.HarmBlockMethod? method = null); + } + + public final class Schema { + method public static com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items); + method public static com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null); + method public static com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema boolean(); + method public static com.google.firebase.ai.type.Schema boolean(String? description = null); + method public static com.google.firebase.ai.type.Schema boolean(String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema enumeration(java.util.List values); + method public static com.google.firebase.ai.type.Schema enumeration(java.util.List values, String? description = null); + method public static com.google.firebase.ai.type.Schema enumeration(java.util.List values, String? description = null, boolean nullable = false); + method public String? getDescription(); + method public java.util.List? getEnum(); + method public String? getFormat(); + method public com.google.firebase.ai.type.Schema? getItems(); + method public Boolean? getNullable(); + method public java.util.Map? getProperties(); + method public java.util.List? getRequired(); + method public String getType(); + method public static com.google.firebase.ai.type.Schema numDouble(); + method public static com.google.firebase.ai.type.Schema numDouble(String? description = null); + method public static com.google.firebase.ai.type.Schema numDouble(String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema numFloat(); + method public static com.google.firebase.ai.type.Schema numFloat(String? description = null); + method public static com.google.firebase.ai.type.Schema numFloat(String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema numInt(); + method public static com.google.firebase.ai.type.Schema numInt(String? description = null); + method public static com.google.firebase.ai.type.Schema numInt(String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema numLong(); + method public static com.google.firebase.ai.type.Schema numLong(String? description = null); + method public static com.google.firebase.ai.type.Schema numLong(String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema obj(java.util.Map properties); + method public static com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList()); + method public static com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList(), String? description = null); + method public static com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList(), String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema str(); + method public static com.google.firebase.ai.type.Schema str(String? description = null); + method public static com.google.firebase.ai.type.Schema str(String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema str(String? description = null, boolean nullable = false, com.google.firebase.ai.type.StringFormat? format = null); + property public final String? description; + property public final java.util.List? enum; + property public final String? format; + property public final com.google.firebase.ai.type.Schema? items; + property public final Boolean? nullable; + property public final java.util.Map? properties; + property public final java.util.List? required; + property public final String type; + field public static final com.google.firebase.ai.type.Schema.Companion Companion; + } + + public static final class Schema.Companion { + method public com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items); + method public com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null); + method public com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema boolean(); + method public com.google.firebase.ai.type.Schema boolean(String? description = null); + method public com.google.firebase.ai.type.Schema boolean(String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema enumeration(java.util.List values); + method public com.google.firebase.ai.type.Schema enumeration(java.util.List values, String? description = null); + method public com.google.firebase.ai.type.Schema enumeration(java.util.List values, String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema numDouble(); + method public com.google.firebase.ai.type.Schema numDouble(String? description = null); + method public com.google.firebase.ai.type.Schema numDouble(String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema numFloat(); + method public com.google.firebase.ai.type.Schema numFloat(String? description = null); + method public com.google.firebase.ai.type.Schema numFloat(String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema numInt(); + method public com.google.firebase.ai.type.Schema numInt(String? description = null); + method public com.google.firebase.ai.type.Schema numInt(String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema numLong(); + method public com.google.firebase.ai.type.Schema numLong(String? description = null); + method public com.google.firebase.ai.type.Schema numLong(String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema obj(java.util.Map properties); + method public com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList()); + method public com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList(), String? description = null); + method public com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList(), String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema str(); + method public com.google.firebase.ai.type.Schema str(String? description = null); + method public com.google.firebase.ai.type.Schema str(String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema str(String? description = null, boolean nullable = false, com.google.firebase.ai.type.StringFormat? format = null); + } + + public final class SerializationException extends com.google.firebase.ai.type.FirebaseAIException { + } + + public final class ServerException extends com.google.firebase.ai.type.FirebaseAIException { + } + + public final class ServiceConnectionHandshakeFailedException extends com.google.firebase.ai.type.FirebaseAIException { + ctor public ServiceConnectionHandshakeFailedException(String message, Throwable? cause = null); + } + + public final class ServiceDisabledException extends com.google.firebase.ai.type.FirebaseAIException { + } + + public final class SessionAlreadyReceivingException extends com.google.firebase.ai.type.FirebaseAIException { + ctor public SessionAlreadyReceivingException(); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class SpeechConfig { + ctor public SpeechConfig(com.google.firebase.ai.type.Voice voice); + method public com.google.firebase.ai.type.Voice getVoice(); + property public final com.google.firebase.ai.type.Voice voice; + } + + public abstract class StringFormat { + } + + public static final class StringFormat.Custom extends com.google.firebase.ai.type.StringFormat { + ctor public StringFormat.Custom(String value); + } + + public final class TextPart implements com.google.firebase.ai.type.Part { + ctor public TextPart(String text); + method public String getText(); + property public final String text; + } + + public final class Tool { + method public static com.google.firebase.ai.type.Tool functionDeclarations(java.util.List functionDeclarations); + field public static final com.google.firebase.ai.type.Tool.Companion Companion; + } + + public static final class Tool.Companion { + method public com.google.firebase.ai.type.Tool functionDeclarations(java.util.List functionDeclarations); + } + + public final class ToolConfig { + ctor public ToolConfig(com.google.firebase.ai.type.FunctionCallingConfig? functionCallingConfig); + } + + public final class UnknownException extends com.google.firebase.ai.type.FirebaseAIException { + } + + public final class UnsupportedUserLocationException extends com.google.firebase.ai.type.FirebaseAIException { + } + + public final class UsageMetadata { + ctor public UsageMetadata(int promptTokenCount, Integer? candidatesTokenCount, int totalTokenCount, java.util.List promptTokensDetails, java.util.List candidatesTokensDetails); + method public Integer? getCandidatesTokenCount(); + method public java.util.List getCandidatesTokensDetails(); + method public int getPromptTokenCount(); + method public java.util.List getPromptTokensDetails(); + method public int getTotalTokenCount(); + property public final Integer? candidatesTokenCount; + property public final java.util.List candidatesTokensDetails; + property public final int promptTokenCount; + property public final java.util.List promptTokensDetails; + property public final int totalTokenCount; + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class Voice { + ctor public Voice(String voiceName); + method public String getVoiceName(); + property public final String voiceName; + } + + @Deprecated @com.google.firebase.ai.type.PublicPreviewAPI public final class Voices { + method @Deprecated public int getOrdinal(); + property @Deprecated public final int ordinal; + field @Deprecated public static final com.google.firebase.ai.type.Voices AOEDE; + field @Deprecated public static final com.google.firebase.ai.type.Voices CHARON; + field @Deprecated public static final com.google.firebase.ai.type.Voices.Companion Companion; + field @Deprecated public static final com.google.firebase.ai.type.Voices FENRIR; + field @Deprecated public static final com.google.firebase.ai.type.Voices KORE; + field @Deprecated public static final com.google.firebase.ai.type.Voices PUCK; + field @Deprecated public static final com.google.firebase.ai.type.Voices UNSPECIFIED; + } + + @Deprecated public static final class Voices.Companion { + } + +} + diff --git a/firebase-ai/consumer-rules.pro b/firebase-ai/consumer-rules.pro new file mode 100644 index 00000000000..b5225e0c05e --- /dev/null +++ b/firebase-ai/consumer-rules.pro @@ -0,0 +1,24 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-keep class com.google.firebase.ai.type.** { *; } +-keep class com.google.firebase.ai.common.** { *; } diff --git a/firebase-ai/firebase-ai.gradle.kts b/firebase-ai/firebase-ai.gradle.kts new file mode 100644 index 00000000000..de29f44b0a1 --- /dev/null +++ b/firebase-ai/firebase-ai.gradle.kts @@ -0,0 +1,129 @@ +/* + * Copyright 2024 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. + */ + +@file:Suppress("UnstableApiUsage") + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("firebase-library") + id("kotlin-android") + alias(libs.plugins.kotlinx.serialization) +} + +firebaseLibrary { + testLab.enabled = false + publishJavadoc = true + releaseNotes { + name.set("{{firebase_ai}}") + versionName.set("ai") + hasKTX.set(false) + } +} + +android { + val targetSdkVersion: Int by rootProject + + namespace = "com.google.firebase.ai" + compileSdk = 34 + defaultConfig { + minSdk = 21 + consumerProguardFiles("consumer-rules.pro") + multiDexEnabled = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { jvmTarget = "1.8" } + testOptions { + targetSdk = targetSdkVersion + unitTests { + isIncludeAndroidResources = true + isReturnDefaultValues = true + } + } + lint { + targetSdk = targetSdkVersion + baseline = file("lint-baseline.xml") + } + sourceSets { getByName("test").java.srcDirs("src/testUtil") } +} + +// Enable Kotlin "Explicit API Mode". This causes the Kotlin compiler to fail if any +// classes, methods, or properties have implicit `public` visibility. This check helps +// avoid accidentally leaking elements into the public API, requiring that any public +// element be explicitly declared as `public`. +// https://github.com/Kotlin/KEEP/blob/master/proposals/explicit-api-mode.md +// https://chao2zhang.medium.com/explicit-api-mode-for-kotlin-on-android-b8264fdd76d1 +tasks.withType().all { + if (!name.contains("test", ignoreCase = true)) { + if (!kotlinOptions.freeCompilerArgs.contains("-Xexplicit-api=strict")) { + kotlinOptions.freeCompilerArgs += "-Xexplicit-api=strict" + } + } +} + +dependencies { + implementation(libs.ktor.client.okhttp) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.websockets) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.ktor.client.logging) + + api("com.google.firebase:firebase-common:21.0.0") + implementation("com.google.firebase:firebase-components:18.0.0") + implementation("com.google.firebase:firebase-annotations:16.2.0") + implementation("com.google.firebase:firebase-appcheck-interop:17.1.0") + implementation(libs.androidx.annotation) + implementation(libs.kotlinx.serialization.json) + implementation(libs.androidx.core.ktx) + implementation(libs.slf4j.nop) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.coroutines.reactive) + implementation(libs.reactive.streams) + implementation("com.google.guava:listenablefuture:1.0") + implementation("androidx.concurrent:concurrent-futures:1.2.0") + implementation("androidx.concurrent:concurrent-futures-ktx:1.2.0") + implementation("com.google.firebase:firebase-auth-interop:18.0.0") + + testImplementation(libs.kotest.assertions.core) + testImplementation(libs.kotest.assertions) + testImplementation(libs.kotest.assertions.json) + testImplementation(libs.ktor.client.okhttp) + testImplementation(libs.ktor.client.mock) + testImplementation(libs.org.json) + testImplementation(libs.androidx.test.junit) + testImplementation(libs.androidx.test.runner) + testImplementation(libs.junit) + testImplementation(libs.kotlin.coroutines.test) + testImplementation(libs.robolectric) + testImplementation(libs.truth) + testImplementation(libs.mockito.core) + + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.truth) +} diff --git a/firebase-ai/gradle.properties b/firebase-ai/gradle.properties new file mode 100644 index 00000000000..1c7c87996dd --- /dev/null +++ b/firebase-ai/gradle.properties @@ -0,0 +1,16 @@ +# Copyright 2024 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. + +version=16.1.0 +latestReleasedVersion=16.0.0 diff --git a/firebase-ai/lint-baseline.xml b/firebase-ai/lint-baseline.xml new file mode 100644 index 00000000000..3848b6c0e9f --- /dev/null +++ b/firebase-ai/lint-baseline.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/firebase-ai/proguard-rules.pro b/firebase-ai/proguard-rules.pro new file mode 100644 index 00000000000..f1b424510da --- /dev/null +++ b/firebase-ai/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/firebase-ai/src/main/AndroidManifest.xml b/firebase-ai/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..8b668fbb5a4 --- /dev/null +++ b/firebase-ai/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/Chat.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/Chat.kt new file mode 100644 index 00000000000..13599fb1c9a --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/Chat.kt @@ -0,0 +1,224 @@ +/* + * Copyright 2024 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.firebase.ai + +import android.graphics.Bitmap +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.ImagePart +import com.google.firebase.ai.type.InlineDataPart +import com.google.firebase.ai.type.InvalidStateException +import com.google.firebase.ai.type.TextPart +import com.google.firebase.ai.type.content +import java.util.LinkedList +import java.util.concurrent.Semaphore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach + +/** + * Representation of a multi-turn interaction with a model. + * + * Captures and stores the history of communication in memory, and provides it as context with each + * new message. + * + * **Note:** This object is not thread-safe, and calling [sendMessage] multiple times without + * waiting for a response will throw an [InvalidStateException]. + * + * @param model The model to use for the interaction. + * @property history The previous content from the chat that has been successfully sent and received + * from the model. This will be provided to the model for each message sent (as context for the + * discussion). + */ +public class Chat( + private val model: GenerativeModel, + public val history: MutableList = ArrayList() +) { + private var lock = Semaphore(1) + + /** + * Sends a message using the provided [prompt]; automatically providing the existing [history] as + * context. + * + * If successful, the message and response will be added to the [history]. If unsuccessful, + * [history] will remain unchanged. + * + * @param prompt The input that, together with the history, will be given to the model as the + * prompt. + * @throws InvalidStateException if [prompt] is not coming from the 'user' role. + * @throws InvalidStateException if the [Chat] instance has an active request. + */ + public suspend fun sendMessage(prompt: Content): GenerateContentResponse { + prompt.assertComesFromUser() + attemptLock() + try { + val response = model.generateContent(*history.toTypedArray(), prompt) + history.add(prompt) + history.add(response.candidates.first().content) + return response + } finally { + lock.release() + } + } + + /** + * Sends a message using the provided [text prompt][prompt]; automatically providing the existing + * [history] as context. + * + * If successful, the message and response will be added to the [history]. If unsuccessful, + * [history] will remain unchanged. + * + * @param prompt The input that, together with the history, will be given to the model as the + * prompt. + * @throws InvalidStateException if [prompt] is not coming from the 'user' role. + * @throws InvalidStateException if the [Chat] instance has an active request. + */ + public suspend fun sendMessage(prompt: String): GenerateContentResponse { + val content = content { text(prompt) } + return sendMessage(content) + } + + /** + * Sends a message using the existing history of this chat as context and the provided image + * prompt. + * + * If successful, the message and response will be added to the history. If unsuccessful, history + * will remain unchanged. + * + * @param prompt The input that, together with the history, will be given to the model as the + * prompt. + * @throws InvalidStateException if [prompt] is not coming from the 'user' role. + * @throws InvalidStateException if the [Chat] instance has an active request. + */ + public suspend fun sendMessage(prompt: Bitmap): GenerateContentResponse { + val content = content { image(prompt) } + return sendMessage(content) + } + + /** + * Sends a message using the existing history of this chat as context and the provided [Content] + * prompt. + * + * The response from the model is returned as a stream. + * + * If successful, the message and response will be added to the history. If unsuccessful, history + * will remain unchanged. + * + * @param prompt The input that, together with the history, will be given to the model as the + * prompt. + * @throws InvalidStateException if [prompt] is not coming from the 'user' role. + * @throws InvalidStateException if the [Chat] instance has an active request. + */ + public fun sendMessageStream(prompt: Content): Flow { + prompt.assertComesFromUser() + attemptLock() + + val flow = model.generateContentStream(*history.toTypedArray(), prompt) + val bitmaps = LinkedList() + val inlineDataParts = LinkedList() + val text = StringBuilder() + + /** + * TODO: revisit when images and inline data are returned. This will cause issues with how + * things are structured in the response. eg; a text/image/text response will be (incorrectly) + * represented as image/text + */ + return flow + .onEach { + for (part in it.candidates.first().content.parts) { + when (part) { + is TextPart -> text.append(part.text) + is ImagePart -> bitmaps.add(part.image) + is InlineDataPart -> inlineDataParts.add(part) + } + } + } + .onCompletion { + lock.release() + if (it == null) { + val content = + content("model") { + for (bitmap in bitmaps) { + image(bitmap) + } + for (inlineDataPart in inlineDataParts) { + inlineData(inlineDataPart.inlineData, inlineDataPart.mimeType) + } + if (text.isNotBlank()) { + text(text.toString()) + } + } + + history.add(prompt) + history.add(content) + } + } + } + + /** + * Sends a message using the existing history of this chat as context and the provided text + * prompt. + * + * The response from the model is returned as a stream. + * + * If successful, the message and response will be added to the history. If unsuccessful, history + * will remain unchanged. + * + * @param prompt The input(s) that, together with the history, will be given to the model as the + * prompt. + * @throws InvalidStateException if [prompt] is not coming from the 'user' role. + * @throws InvalidStateException if the [Chat] instance has an active request. + */ + public fun sendMessageStream(prompt: String): Flow { + val content = content { text(prompt) } + return sendMessageStream(content) + } + + /** + * Sends a message using the existing history of this chat as context and the provided image + * prompt. + * + * The response from the model is returned as a stream. + * + * If successful, the message and response will be added to the history. If unsuccessful, history + * will remain unchanged. + * + * @param prompt The input that, together with the history, will be given to the model as the + * prompt. + * @throws InvalidStateException if [prompt] is not coming from the 'user' role. + * @throws InvalidStateException if the [Chat] instance has an active request. + */ + public fun sendMessageStream(prompt: Bitmap): Flow { + val content = content { image(prompt) } + return sendMessageStream(content) + } + + private fun Content.assertComesFromUser() { + if (role !in listOf("user", "function")) { + throw InvalidStateException("Chat prompts should come from the 'user' or 'function' role.") + } + } + + private fun attemptLock() { + if (!lock.tryAcquire()) { + throw InvalidStateException( + "This chat instance currently has an ongoing request, please wait for it to complete " + + "before sending more messages" + ) + } + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt new file mode 100644 index 00000000000..86eb8057b1d --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2024 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.firebase.ai + +import android.util.Log +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.GenerationConfig +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.GenerativeBackendEnum +import com.google.firebase.ai.type.ImagenGenerationConfig +import com.google.firebase.ai.type.ImagenSafetySettings +import com.google.firebase.ai.type.InvalidStateException +import com.google.firebase.ai.type.LiveGenerationConfig +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.RequestOptions +import com.google.firebase.ai.type.SafetySetting +import com.google.firebase.ai.type.Tool +import com.google.firebase.ai.type.ToolConfig +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.app +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider +import com.google.firebase.inject.Provider +import kotlin.coroutines.CoroutineContext + +/** Entry point for all _Firebase AI_ functionality. */ +public class FirebaseAI +internal constructor( + private val firebaseApp: FirebaseApp, + private val backend: GenerativeBackend, + @Blocking private val blockingDispatcher: CoroutineContext, + private val appCheckProvider: Provider, + private val internalAuthProvider: Provider, +) { + + /** + * Instantiates a new [GenerativeModel] given the provided parameters. + * + * @param modelName The name of the model to use, for example `"gemini-2.0-flash-exp"`. + * @param generationConfig The configuration parameters to use for content generation. + * @param safetySettings The safety bounds the model will abide to during content generation. + * @param tools A list of [Tool]s the model may use to generate content. + * @param toolConfig The [ToolConfig] that defines how the model handles the tools provided. + * @param systemInstruction [Content] instructions that direct the model to behave a certain way. + * Currently only text content is supported. + * @param requestOptions Configuration options for sending requests to the backend. + * @return The initialized [GenerativeModel] instance. + */ + @JvmOverloads + public fun generativeModel( + modelName: String, + generationConfig: GenerationConfig? = null, + safetySettings: List? = null, + tools: List? = null, + toolConfig: ToolConfig? = null, + systemInstruction: Content? = null, + requestOptions: RequestOptions = RequestOptions(), + ): GenerativeModel { + val modelUri = + when (backend.backend) { + GenerativeBackendEnum.VERTEX_AI -> + "projects/${firebaseApp.options.projectId}/locations/${backend.location}/publishers/google/models/${modelName}" + GenerativeBackendEnum.GOOGLE_AI -> + "projects/${firebaseApp.options.projectId}/models/${modelName}" + } + if (!modelName.startsWith(GEMINI_MODEL_NAME_PREFIX)) { + Log.w( + TAG, + """Unsupported Gemini model "${modelName}"; see + https://firebase.google.com/docs/vertex-ai/models for a list supported Gemini model names. + """ + .trimIndent(), + ) + } + return GenerativeModel( + modelUri, + firebaseApp.options.apiKey, + firebaseApp, + generationConfig, + safetySettings, + tools, + toolConfig, + systemInstruction, + requestOptions, + backend, + appCheckProvider.get(), + internalAuthProvider.get(), + ) + } + + /** + * Instantiates a new [LiveGenerationConfig] given the provided parameters. + * + * @param modelName The name of the model to use, for example `"gemini-2.0-flash-exp"`. + * @param generationConfig The configuration parameters to use for content generation. + * @param tools A list of [Tool]s the model may use to generate content. + * @param systemInstruction [Content] instructions that direct the model to behave a certain way. + * Currently only text content is supported. + * @param requestOptions Configuration options for sending requests to the backend. + * @return The initialized [LiveGenerativeModel] instance. + */ + @JvmOverloads + @PublicPreviewAPI + public fun liveModel( + modelName: String, + generationConfig: LiveGenerationConfig? = null, + tools: List? = null, + systemInstruction: Content? = null, + requestOptions: RequestOptions = RequestOptions(), + ): LiveGenerativeModel { + if (!modelName.startsWith(GEMINI_MODEL_NAME_PREFIX)) { + Log.w( + TAG, + """Unsupported Gemini model "$modelName"; see + https://firebase.google.com/docs/vertex-ai/models for a list supported Gemini model names. + """ + .trimIndent(), + ) + } + return LiveGenerativeModel( + when (backend.backend) { + GenerativeBackendEnum.VERTEX_AI -> + "projects/${firebaseApp.options.projectId}/locations/${backend.location}/publishers/google/models/${modelName}" + GenerativeBackendEnum.GOOGLE_AI -> + throw InvalidStateException("Live Model is not yet available on the Google AI backend") + }, + firebaseApp.options.apiKey, + firebaseApp, + blockingDispatcher, + generationConfig, + tools, + systemInstruction, + backend.location, + requestOptions, + appCheckProvider.get(), + internalAuthProvider.get(), + ) + } + + /** + * Instantiates a new [ImagenModel] given the provided parameters. + * + * @param modelName The name of the model to use, for example `"imagen-3.0-generate-001"`. + * @param generationConfig The configuration parameters to use for image generation. + * @param safetySettings The safety bounds the model will abide by during image generation. + * @param requestOptions Configuration options for sending requests to the backend. + * @return The initialized [ImagenModel] instance. + */ + @JvmOverloads + @PublicPreviewAPI + public fun imagenModel( + modelName: String, + generationConfig: ImagenGenerationConfig? = null, + safetySettings: ImagenSafetySettings? = null, + requestOptions: RequestOptions = RequestOptions(), + ): ImagenModel { + val modelUri = + when (backend.backend) { + GenerativeBackendEnum.VERTEX_AI -> + "projects/${firebaseApp.options.projectId}/locations/${backend.location}/publishers/google/models/${modelName}" + GenerativeBackendEnum.GOOGLE_AI -> + "projects/${firebaseApp.options.projectId}/models/${modelName}" + } + if (!modelName.startsWith(IMAGEN_MODEL_NAME_PREFIX)) { + Log.w( + TAG, + """Unsupported Imagen model "${modelName}"; see + https://firebase.google.com/docs/vertex-ai/models for a list supported Imagen model names. + """ + .trimIndent(), + ) + } + return ImagenModel( + modelUri, + firebaseApp.options.apiKey, + firebaseApp, + generationConfig, + safetySettings, + requestOptions, + appCheckProvider.get(), + internalAuthProvider.get(), + ) + } + + public companion object { + /** The [FirebaseAI] instance for the default [FirebaseApp] using the Google AI Backend. */ + @JvmStatic + public val instance: FirebaseAI + get() = getInstance(backend = GenerativeBackend.googleAI()) + + /** + * Returns the [FirebaseAI] instance for the provided [FirebaseApp] and [backend]. + * + * @param backend the backend reference to make generative AI requests to. + */ + @JvmStatic + @JvmOverloads + public fun getInstance( + app: FirebaseApp = Firebase.app, + backend: GenerativeBackend + ): FirebaseAI { + val multiResourceComponent = app[FirebaseAIMultiResourceComponent::class.java] + return multiResourceComponent.get(backend) + } + + /** The [FirebaseAI] instance for the provided [FirebaseApp] using the Google AI Backend. */ + @JvmStatic + public fun getInstance(app: FirebaseApp): FirebaseAI = + getInstance(app, GenerativeBackend.googleAI()) + + private const val GEMINI_MODEL_NAME_PREFIX = "gemini-" + + private const val IMAGEN_MODEL_NAME_PREFIX = "imagen-" + + private val TAG = FirebaseAI::class.java.simpleName + } +} + +/** The [FirebaseAI] instance for the default [FirebaseApp] using the Google AI Backend. */ +public val Firebase.ai: FirebaseAI + get() = FirebaseAI.instance + +/** + * Returns the [FirebaseAI] instance for the provided [FirebaseApp] and [backend]. + * + * @param backend the backend reference to make generative AI requests to. + */ +public fun Firebase.ai( + app: FirebaseApp = Firebase.app, + backend: GenerativeBackend = GenerativeBackend.googleAI() +): FirebaseAI = FirebaseAI.getInstance(app, backend) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIMultiResourceComponent.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIMultiResourceComponent.kt new file mode 100644 index 00000000000..c0667b1685e --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIMultiResourceComponent.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024 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.firebase.ai + +import androidx.annotation.GuardedBy +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider +import com.google.firebase.inject.Provider +import kotlin.coroutines.CoroutineContext + +/** + * Multi-resource container for Firebase AI. + * + * @hide + */ +internal class FirebaseAIMultiResourceComponent( + private val app: FirebaseApp, + @Blocking val blockingDispatcher: CoroutineContext, + private val appCheckProvider: Provider, + private val internalAuthProvider: Provider, +) { + + @GuardedBy("this") private val instances: MutableMap = mutableMapOf() + + fun get(backend: GenerativeBackend): FirebaseAI = + synchronized(this) { + instances[backend.location] + ?: FirebaseAI( + app, + backend, + blockingDispatcher, + appCheckProvider, + internalAuthProvider, + ) + .also { instances[backend.location] = it } + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIRegistrar.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIRegistrar.kt new file mode 100644 index 00000000000..a8b6b2cb1a3 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIRegistrar.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2024 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.firebase.ai + +import androidx.annotation.Keep +import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider +import com.google.firebase.components.Component +import com.google.firebase.components.ComponentRegistrar +import com.google.firebase.components.Dependency +import com.google.firebase.components.Qualified +import com.google.firebase.components.Qualified.unqualified +import com.google.firebase.platforminfo.LibraryVersionComponent +import kotlinx.coroutines.CoroutineDispatcher + +/** + * [ComponentRegistrar] for setting up [FirebaseAI] and its internal dependencies. + * + * @hide + */ +@Keep +internal class FirebaseAIRegistrar : ComponentRegistrar { + override fun getComponents() = + listOf( + Component.builder(FirebaseAIMultiResourceComponent::class.java) + .name(LIBRARY_NAME) + .add(Dependency.required(firebaseApp)) + .add(Dependency.required(blockingDispatcher)) + .add(Dependency.optionalProvider(appCheckInterop)) + .add(Dependency.optionalProvider(internalAuthProvider)) + .factory { container -> + FirebaseAIMultiResourceComponent( + container[firebaseApp], + container.get(blockingDispatcher), + container.getProvider(appCheckInterop), + container.getProvider(internalAuthProvider) + ) + } + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), + ) + + private companion object { + private const val LIBRARY_NAME = "fire-ai" + + private val firebaseApp = unqualified(FirebaseApp::class.java) + private val appCheckInterop = unqualified(InteropAppCheckTokenProvider::class.java) + private val internalAuthProvider = unqualified(InternalAuthProvider::class.java) + private val blockingDispatcher = + Qualified.qualified(Blocking::class.java, CoroutineDispatcher::class.java) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt new file mode 100644 index 00000000000..1b36998f970 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt @@ -0,0 +1,255 @@ +/* + * Copyright 2024 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.firebase.ai + +import android.graphics.Bitmap +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.common.APIController +import com.google.firebase.ai.common.AppCheckHeaderProvider +import com.google.firebase.ai.common.CountTokensRequest +import com.google.firebase.ai.common.GenerateContentRequest +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.CountTokensResponse +import com.google.firebase.ai.type.FinishReason +import com.google.firebase.ai.type.FirebaseAIException +import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.GenerationConfig +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.GenerativeBackendEnum +import com.google.firebase.ai.type.InvalidStateException +import com.google.firebase.ai.type.PromptBlockedException +import com.google.firebase.ai.type.RequestOptions +import com.google.firebase.ai.type.ResponseStoppedException +import com.google.firebase.ai.type.SafetySetting +import com.google.firebase.ai.type.SerializationException +import com.google.firebase.ai.type.Tool +import com.google.firebase.ai.type.ToolConfig +import com.google.firebase.ai.type.content +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.serialization.ExperimentalSerializationApi + +/** + * Represents a multimodal model (like Gemini), capable of generating content based on various input + * types. + */ +public class GenerativeModel +internal constructor( + private val modelName: String, + private val generationConfig: GenerationConfig? = null, + private val safetySettings: List? = null, + private val tools: List? = null, + private val toolConfig: ToolConfig? = null, + private val systemInstruction: Content? = null, + private val generativeBackend: GenerativeBackend = GenerativeBackend.googleAI(), + private val controller: APIController, +) { + internal constructor( + modelName: String, + apiKey: String, + firebaseApp: FirebaseApp, + generationConfig: GenerationConfig? = null, + safetySettings: List? = null, + tools: List? = null, + toolConfig: ToolConfig? = null, + systemInstruction: Content? = null, + requestOptions: RequestOptions = RequestOptions(), + generativeBackend: GenerativeBackend, + appCheckTokenProvider: InteropAppCheckTokenProvider? = null, + internalAuthProvider: InternalAuthProvider? = null, + ) : this( + modelName, + generationConfig, + safetySettings, + tools, + toolConfig, + systemInstruction, + generativeBackend, + APIController( + apiKey, + modelName, + requestOptions, + "gl-kotlin/${KotlinVersion.CURRENT}-ai fire/${BuildConfig.VERSION_NAME}", + firebaseApp, + AppCheckHeaderProvider(TAG, appCheckTokenProvider, internalAuthProvider), + ), + ) + + /** + * Generates new content from the input [Content] given to the model as a prompt. + * + * @param prompt The input(s) given to the model as a prompt. + * @return The content generated by the model. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public suspend fun generateContent(vararg prompt: Content): GenerateContentResponse = + try { + controller.generateContent(constructRequest(*prompt)).toPublic().validate() + } catch (e: Throwable) { + throw FirebaseAIException.from(e) + } + + /** + * Generates new content as a stream from the input [Content] given to the model as a prompt. + * + * @param prompt The input(s) given to the model as a prompt. + * @return A [Flow] which will emit responses as they are returned by the model. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public fun generateContentStream(vararg prompt: Content): Flow = + controller + .generateContentStream(constructRequest(*prompt)) + .catch { throw FirebaseAIException.from(it) } + .map { it.toPublic().validate() } + + /** + * Generates new content from the text input given to the model as a prompt. + * + * @param prompt The text to be send to the model as a prompt. + * @return The content generated by the model. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public suspend fun generateContent(prompt: String): GenerateContentResponse = + generateContent(content { text(prompt) }) + + /** + * Generates new content as a stream from the text input given to the model as a prompt. + * + * @param prompt The text to be send to the model as a prompt. + * @return A [Flow] which will emit responses as they are returned by the model. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public fun generateContentStream(prompt: String): Flow = + generateContentStream(content { text(prompt) }) + + /** + * Generates new content from the image input given to the model as a prompt. + * + * @param prompt The image to be converted into a single piece of [Content] to send to the model. + * @return A [GenerateContentResponse] after some delay. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public suspend fun generateContent(prompt: Bitmap): GenerateContentResponse = + generateContent(content { image(prompt) }) + + /** + * Generates new content as a stream from the image input given to the model as a prompt. + * + * @param prompt The image to be converted into a single piece of [Content] to send to the model. + * @return A [Flow] which will emit responses as they are returned by the model. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public fun generateContentStream(prompt: Bitmap): Flow = + generateContentStream(content { image(prompt) }) + + /** Creates a [Chat] instance using this model with the optionally provided history. */ + public fun startChat(history: List = emptyList()): Chat = + Chat(this, history.toMutableList()) + + /** + * Counts the number of tokens in a prompt using the model's tokenizer. + * + * @param prompt The input(s) given to the model as a prompt. + * @return The [CountTokensResponse] of running the model's tokenizer on the input. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public suspend fun countTokens(vararg prompt: Content): CountTokensResponse { + try { + return controller.countTokens(constructCountTokensRequest(*prompt)).toPublic() + } catch (e: Throwable) { + throw FirebaseAIException.from(e) + } + } + + /** + * Counts the number of tokens in a text prompt using the model's tokenizer. + * + * @param prompt The text given to the model as a prompt. + * @return The [CountTokensResponse] of running the model's tokenizer on the input. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public suspend fun countTokens(prompt: String): CountTokensResponse { + return countTokens(content { text(prompt) }) + } + + /** + * Counts the number of tokens in an image prompt using the model's tokenizer. + * + * @param prompt The image given to the model as a prompt. + * @return The [CountTokensResponse] of running the model's tokenizer on the input. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public suspend fun countTokens(prompt: Bitmap): CountTokensResponse { + return countTokens(content { image(prompt) }) + } + + @OptIn(ExperimentalSerializationApi::class) + private fun constructRequest(vararg prompt: Content) = + GenerateContentRequest( + modelName, + prompt.map { it.toInternal() }, + safetySettings + ?.also { safetySettingList -> + if ( + generativeBackend.backend == GenerativeBackendEnum.GOOGLE_AI && + safetySettingList.any { it.method != null } + ) { + throw InvalidStateException( + "HarmBlockMethod is unsupported by the Google Developer API" + ) + } + } + ?.map { it.toInternal() }, + generationConfig?.toInternal(), + tools?.map { it.toInternal() }, + toolConfig?.toInternal(), + systemInstruction?.copy(role = "system")?.toInternal(), + ) + + private fun constructCountTokensRequest(vararg prompt: Content) = + when (generativeBackend.backend) { + GenerativeBackendEnum.GOOGLE_AI -> CountTokensRequest.forGoogleAI(constructRequest(*prompt)) + GenerativeBackendEnum.VERTEX_AI -> CountTokensRequest.forVertexAI(constructRequest(*prompt)) + } + + private fun GenerateContentResponse.validate() = apply { + if (candidates.isEmpty() && promptFeedback == null) { + throw SerializationException("Error deserializing response, found no valid fields") + } + promptFeedback?.blockReason?.let { throw PromptBlockedException(this) } + candidates + .mapNotNull { it.finishReason } + .firstOrNull { it != FinishReason.STOP } + ?.let { throw ResponseStoppedException(this) } + } + + private companion object { + private val TAG = GenerativeModel::class.java.simpleName + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt new file mode 100644 index 00000000000..4d88d09b1e1 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2025 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.firebase.ai + +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.common.APIController +import com.google.firebase.ai.common.AppCheckHeaderProvider +import com.google.firebase.ai.common.ContentBlockedException +import com.google.firebase.ai.common.GenerateImageRequest +import com.google.firebase.ai.type.FirebaseAIException +import com.google.firebase.ai.type.ImagenGenerationConfig +import com.google.firebase.ai.type.ImagenGenerationResponse +import com.google.firebase.ai.type.ImagenInlineImage +import com.google.firebase.ai.type.ImagenSafetySettings +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.RequestOptions +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider + +/** + * Represents a generative model (like Imagen), capable of generating images based on various input + * types. + */ +@PublicPreviewAPI +public class ImagenModel +internal constructor( + private val modelName: String, + private val generationConfig: ImagenGenerationConfig? = null, + private val safetySettings: ImagenSafetySettings? = null, + private val controller: APIController, +) { + @JvmOverloads + internal constructor( + modelName: String, + apiKey: String, + firebaseApp: FirebaseApp, + generationConfig: ImagenGenerationConfig? = null, + safetySettings: ImagenSafetySettings? = null, + requestOptions: RequestOptions = RequestOptions(), + appCheckTokenProvider: InteropAppCheckTokenProvider? = null, + internalAuthProvider: InternalAuthProvider? = null, + ) : this( + modelName, + generationConfig, + safetySettings, + APIController( + apiKey, + modelName, + requestOptions, + "gl-kotlin/${KotlinVersion.CURRENT}-ai fire/${BuildConfig.VERSION_NAME}", + firebaseApp, + AppCheckHeaderProvider(TAG, appCheckTokenProvider, internalAuthProvider), + ), + ) + + /** + * Generates an image, returning the result directly to the caller. + * + * @param prompt The input(s) given to the model as a prompt. + */ + public suspend fun generateImages(prompt: String): ImagenGenerationResponse = + try { + controller + .generateImage(constructRequest(prompt, null, generationConfig)) + .validate() + .toPublicInline() + } catch (e: Throwable) { + throw FirebaseAIException.from(e) + } + + private fun constructRequest( + prompt: String, + gcsUri: String?, + config: ImagenGenerationConfig?, + ): GenerateImageRequest { + return GenerateImageRequest( + listOf(GenerateImageRequest.ImagenPrompt(prompt)), + GenerateImageRequest.ImagenParameters( + sampleCount = config?.numberOfImages ?: 1, + includeRaiReason = true, + addWatermark = generationConfig?.addWatermark, + personGeneration = safetySettings?.personFilterLevel?.internalVal, + negativePrompt = config?.negativePrompt, + safetySetting = safetySettings?.safetyFilterLevel?.internalVal, + storageUri = gcsUri, + aspectRatio = config?.aspectRatio?.internalVal, + imageOutputOptions = generationConfig?.imageFormat?.toInternal(), + ), + ) + } + + internal companion object { + private val TAG = ImagenModel::class.java.simpleName + internal const val DEFAULT_FILTERED_ERROR = + "Unable to show generated images. All images were filtered out because they violated Vertex AI's usage guidelines. You will not be charged for blocked images. Try rephrasing the prompt. If you think this was an error, send feedback." + } +} + +@OptIn(PublicPreviewAPI::class) +private fun ImagenGenerationResponse.Internal.validate(): ImagenGenerationResponse.Internal { + if (predictions.none { it.mimeType != null }) { + throw ContentBlockedException( + message = predictions.first { it.raiFilteredReason != null }.raiFilteredReason + ?: ImagenModel.DEFAULT_FILTERED_ERROR + ) + } + return this +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt new file mode 100644 index 00000000000..fe4cae0d187 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2025 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.firebase.ai + +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.common.APIController +import com.google.firebase.ai.common.AppCheckHeaderProvider +import com.google.firebase.ai.common.JSON +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.LiveClientSetupMessage +import com.google.firebase.ai.type.LiveGenerationConfig +import com.google.firebase.ai.type.LiveSession +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.RequestOptions +import com.google.firebase.ai.type.ServiceConnectionHandshakeFailedException +import com.google.firebase.ai.type.Tool +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider +import io.ktor.websocket.Frame +import io.ktor.websocket.close +import io.ktor.websocket.readBytes +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject + +/** + * Represents a multimodal model (like Gemini) capable of real-time content generation based on + * various input types, supporting bidirectional streaming. + */ +@PublicPreviewAPI +public class LiveGenerativeModel +internal constructor( + private val modelName: String, + @Blocking private val blockingDispatcher: CoroutineContext, + private val config: LiveGenerationConfig? = null, + private val tools: List? = null, + private val systemInstruction: Content? = null, + private val location: String, + private val controller: APIController +) { + internal constructor( + modelName: String, + apiKey: String, + firebaseApp: FirebaseApp, + blockingDispatcher: CoroutineContext, + config: LiveGenerationConfig? = null, + tools: List? = null, + systemInstruction: Content? = null, + location: String = "us-central1", + requestOptions: RequestOptions = RequestOptions(), + appCheckTokenProvider: InteropAppCheckTokenProvider? = null, + internalAuthProvider: InternalAuthProvider? = null, + ) : this( + modelName, + blockingDispatcher, + config, + tools, + systemInstruction, + location, + APIController( + apiKey, + modelName, + requestOptions, + "gl-kotlin/${KotlinVersion.CURRENT}-ai fire/${BuildConfig.VERSION_NAME}", + firebaseApp, + AppCheckHeaderProvider(TAG, appCheckTokenProvider, internalAuthProvider), + ), + ) + + /** + * Start a [LiveSession] with the server for bidirectional streaming. + * + * @return A [LiveSession] that you can use to stream messages to and from the server. + * @throws [ServiceConnectionHandshakeFailedException] If the client was not able to establish a + * connection with the server. + */ + @OptIn(ExperimentalSerializationApi::class) + public suspend fun connect(): LiveSession { + val clientMessage = + LiveClientSetupMessage( + modelName, + config?.toInternal(), + tools?.map { it.toInternal() }, + systemInstruction?.toInternal() + ) + .toInternal() + val data: String = Json.encodeToString(clientMessage) + try { + val webSession = controller.getWebSocketSession(location) + webSession.send(Frame.Text(data)) + val receivedJsonStr = webSession.incoming.receive().readBytes().toString(Charsets.UTF_8) + val receivedJson = JSON.parseToJsonElement(receivedJsonStr) + + return if (receivedJson is JsonObject && "setupComplete" in receivedJson) { + LiveSession(session = webSession, blockingDispatcher = blockingDispatcher) + } else { + webSession.close() + throw ServiceConnectionHandshakeFailedException("Unable to connect to the server") + } + } catch (e: ClosedReceiveChannelException) { + throw ServiceConnectionHandshakeFailedException("Channel was closed by the server", e) + } + } + + private companion object { + private val TAG = LiveGenerativeModel::class.java.simpleName + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt new file mode 100644 index 00000000000..34a4b96b7dd --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt @@ -0,0 +1,364 @@ +/* + * Copyright 2024 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.firebase.ai.common + +import android.util.Log +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.common.util.decodeToFlow +import com.google.firebase.ai.common.util.fullModelName +import com.google.firebase.ai.type.CountTokensResponse +import com.google.firebase.ai.type.FinishReason +import com.google.firebase.ai.type.GRpcErrorResponse +import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.ImagenGenerationResponse +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.RequestOptions +import com.google.firebase.ai.type.Response +import com.google.firebase.options +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.websocket.ClientWebSocketSession +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.client.plugins.websocket.webSocketSession +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.preparePost +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsChannel +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.http.withCharset +import io.ktor.serialization.kotlinx.json.json +import io.ktor.utils.io.charsets.Charset +import kotlin.math.max +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json + +@OptIn(ExperimentalSerializationApi::class) +internal val JSON = Json { + ignoreUnknownKeys = true + prettyPrint = false + isLenient = true + explicitNulls = false +} + +/** + * Backend class for interfacing with the Gemini API. + * + * This class handles making HTTP requests to the API and streaming the responses back. + * + * @param httpEngine The HTTP client engine to be used for making requests. Defaults to CIO engine. + * Exposed primarily for DI in tests. + * @property key The API key used for authentication. + * @property model The model to use for generation. + * @property apiClient The value to pass in the `x-goog-api-client` header. + * @property headerProvider A provider that generates extra headers to include in all HTTP requests. + */ +@OptIn(PublicPreviewAPI::class) +internal class APIController +internal constructor( + private val key: String, + model: String, + private val requestOptions: RequestOptions, + httpEngine: HttpClientEngine, + private val apiClient: String, + private val firebaseApp: FirebaseApp, + private val appVersion: Int = 0, + private val googleAppId: String, + private val headerProvider: HeaderProvider?, +) { + + constructor( + key: String, + model: String, + requestOptions: RequestOptions, + apiClient: String, + firebaseApp: FirebaseApp, + headerProvider: HeaderProvider? = null, + ) : this( + key, + model, + requestOptions, + OkHttp.create(), + apiClient, + firebaseApp, + getVersionNumber(firebaseApp), + firebaseApp.options.applicationId, + headerProvider + ) + + private val model = fullModelName(model) + + private val client = + HttpClient(httpEngine) { + install(HttpTimeout) { + requestTimeoutMillis = requestOptions.timeout.inWholeMilliseconds + socketTimeoutMillis = + max(180.seconds.inWholeMilliseconds, requestOptions.timeout.inWholeMilliseconds) + } + install(WebSockets) + install(ContentNegotiation) { json(JSON) } + } + + suspend fun generateContent(request: GenerateContentRequest): GenerateContentResponse.Internal = + try { + client + .post("${requestOptions.endpoint}/${requestOptions.apiVersion}/$model:generateContent") { + applyCommonConfiguration(request) + applyHeaderProvider() + } + .also { validateResponse(it) } + .body() + .validate() + } catch (e: Throwable) { + throw FirebaseCommonAIException.from(e) + } + + suspend fun generateImage(request: GenerateImageRequest): ImagenGenerationResponse.Internal = + try { + client + .post("${requestOptions.endpoint}/${requestOptions.apiVersion}/$model:predict") { + applyCommonConfiguration(request) + applyHeaderProvider() + } + .also { validateResponse(it) } + .body() + } catch (e: Throwable) { + throw FirebaseCommonAIException.from(e) + } + + private fun getBidiEndpoint(location: String): String = + "wss://firebasevertexai.googleapis.com/ws/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent/locations/$location?key=$key" + + suspend fun getWebSocketSession(location: String): ClientWebSocketSession = + client.webSocketSession(getBidiEndpoint(location)) { applyCommonHeaders() } + + fun generateContentStream( + request: GenerateContentRequest + ): Flow = + client + .postStream( + "${requestOptions.endpoint}/${requestOptions.apiVersion}/$model:streamGenerateContent?alt=sse" + ) { + applyCommonConfiguration(request) + } + .map { it.validate() } + .catch { throw FirebaseCommonAIException.from(it) } + + suspend fun countTokens(request: CountTokensRequest): CountTokensResponse.Internal = + try { + client + .post("${requestOptions.endpoint}/${requestOptions.apiVersion}/$model:countTokens") { + applyCommonConfiguration(request) + applyHeaderProvider() + } + .also { validateResponse(it) } + .body() + } catch (e: Throwable) { + throw FirebaseCommonAIException.from(e) + } + + private fun HttpRequestBuilder.applyCommonHeaders() { + contentType(ContentType.Application.Json) + header("x-goog-api-key", key) + header("x-goog-api-client", apiClient) + if (firebaseApp.isDataCollectionDefaultEnabled) { + header("X-Firebase-AppId", googleAppId) + header("X-Firebase-AppVersion", appVersion) + } + } + private fun HttpRequestBuilder.applyCommonConfiguration(request: Request) { + when (request) { + is GenerateContentRequest -> setBody(request) + is CountTokensRequest -> setBody(request) + is GenerateImageRequest -> setBody(request) + } + applyCommonHeaders() + } + + private suspend fun HttpRequestBuilder.applyHeaderProvider() { + if (headerProvider != null) { + try { + withTimeout(headerProvider.timeout) { + for ((tag, value) in headerProvider.generateHeaders()) { + header(tag, value) + } + } + } catch (e: TimeoutCancellationException) { + Log.w(TAG, "HeaderProvided timed out without generating headers, ignoring") + } + } + } + + /** + * Makes a POST request to the specified [url] and returns a [Flow] of deserialized response + * objects of type [R]. The response is expected to be a stream of JSON objects that are parsed in + * real-time as they are received from the server. + * + * This function is intended for internal use within the client that handles streaming responses. + * + * Example usage: + * ``` + * val client: HttpClient = HttpClient(CIO) + * val request: Request = GenerateContentRequest(...) + * val url: String = "http://example.com/stream" + * + * val responses: GenerateContentResponse = client.postStream(url) { + * setBody(request) + * contentType(ContentType.Application.Json) + * } + * responses.collect { + * println("Got a response: $it") + * } + * ``` + * + * @param R The type of the response object. + * @param url The URL to which the POST request will be made. + * @param config An optional [HttpRequestBuilder] callback for request configuration. + * @return A [Flow] of response objects of type [R]. + */ + private inline fun HttpClient.postStream( + url: String, + crossinline config: HttpRequestBuilder.() -> Unit = {}, + ): Flow = channelFlow { + launch(CoroutineName("postStream")) { + preparePost(url) { + applyHeaderProvider() + config() + } + .execute { + validateResponse(it) + + val channel = it.bodyAsChannel() + val flow = JSON.decodeToFlow(channel) + + flow.collect { send(it) } + } + } + } + + companion object { + private val TAG = APIController::class.java.simpleName + + private fun getVersionNumber(app: FirebaseApp): Int { + try { + val context = app.applicationContext + return context.packageManager.getPackageInfo(context.packageName, 0).versionCode + } catch (e: Exception) { + Log.d(TAG, "Error while getting app version: ${e.message}") + return 0 + } + } + } +} + +internal interface HeaderProvider { + val timeout: Duration + + suspend fun generateHeaders(): Map +} + +private suspend fun validateResponse(response: HttpResponse) { + if (response.status == HttpStatusCode.OK) return + + val htmlContentType = ContentType.Text.Html.withCharset(Charset.forName("utf-8")) + if (response.status == HttpStatusCode.NotFound && response.contentType() == htmlContentType) + throw ServerException( + """URL not found. Please verify the location used to create the `FirebaseAI` object + | See https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#available-regions + | for the list of available locations. Raw response: ${response.bodyAsText()}""" + .trimMargin() + ) + val text = response.bodyAsText() + val error = + try { + JSON.decodeFromString(text).error + } catch (e: Throwable) { + throw ServerException("Unexpected Response:\n$text $e") + } + val message = error.message + if (message.contains("API key not valid")) { + throw InvalidAPIKeyException(message) + } + // TODO (b/325117891): Use a better method than string matching. + if (message == "User location is not supported for the API use.") { + throw UnsupportedUserLocationException() + } + if (message.contains("quota")) { + throw QuotaExceededException(message) + } + if (message.contains("The prompt could not be submitted")) { + throw PromptBlockedException(message) + } + getServiceDisabledErrorDetailsOrNull(error)?.let { + val errorMessage = + if (it.metadata?.get("service") == "firebasevertexai.googleapis.com") { + """ + The Firebase AI SDK requires the Vertex AI in Firebase API + (`firebasevertexai.googleapis.com`) to be enabled in your Firebase project. Enable this API + by visiting the Firebase Console at + https://console.firebase.google.com/project/${Firebase.options.projectId}/genai + and clicking "Get started". If you enabled this API recently, wait a few minutes for the + action to propagate to our systems and then retry. + """ + .trimIndent() + } else { + error.message + } + + throw ServiceDisabledException(errorMessage) + } + throw ServerException(message) +} + +private fun getServiceDisabledErrorDetailsOrNull( + error: GRpcErrorResponse.GRpcError +): GRpcErrorResponse.GRpcError.GRpcErrorDetails? { + return error.details?.firstOrNull { + it.reason == "SERVICE_DISABLED" && it.domain == "googleapis.com" + } +} + +private fun GenerateContentResponse.Internal.validate() = apply { + if ((candidates?.isEmpty() != false) && promptFeedback == null) { + throw SerializationException("Error deserializing response, found no valid fields") + } + promptFeedback?.blockReason?.let { throw PromptBlockedException(this) } + candidates + ?.mapNotNull { it.finishReason } + ?.firstOrNull { it != FinishReason.Internal.STOP } + ?.let { throw ResponseStoppedException(this) } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/AppCheckHeaderProvider.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/AppCheckHeaderProvider.kt new file mode 100644 index 00000000000..d5a5ec32305 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/AppCheckHeaderProvider.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2025 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.firebase.ai.common + +import android.util.Log +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.tasks.await + +internal class AppCheckHeaderProvider( + private val logTag: String, + private val appCheckTokenProvider: InteropAppCheckTokenProvider? = null, + private val internalAuthProvider: InternalAuthProvider? = null, +) : HeaderProvider { + override val timeout: Duration + get() = 10.seconds + + override suspend fun generateHeaders(): Map { + val headers = mutableMapOf() + if (appCheckTokenProvider == null) { + Log.w(logTag, "AppCheck not registered, skipping") + } else { + val token = appCheckTokenProvider.getToken(false).await() + + if (token.error != null) { + Log.w(logTag, "Error obtaining AppCheck token", token.error) + } + // The Firebase App Check backend can differentiate between apps without App Check, and + // wrongly configured apps by verifying the value of the token, so it always needs to be + // included. + headers["X-Firebase-AppCheck"] = token.token + } + + if (internalAuthProvider == null) { + Log.w(logTag, "Auth not registered, skipping") + } else { + try { + val token = internalAuthProvider.getAccessToken(false).await() + + headers["Authorization"] = "Firebase ${token.token!!}" + } catch (e: Exception) { + Log.w(logTag, "Error getting Auth token ", e) + } + } + + return headers + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Exceptions.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Exceptions.kt new file mode 100644 index 00000000000..6e2ff67ca4d --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Exceptions.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2024 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.firebase.ai.common + +import com.google.firebase.ai.type.GenerateContentResponse +import io.ktor.serialization.JsonConvertException +import kotlinx.coroutines.TimeoutCancellationException + +/** Parent class for any errors that occur. */ +internal sealed class FirebaseCommonAIException(message: String, cause: Throwable? = null) : + RuntimeException(message, cause) { + companion object { + + /** + * Converts a [Throwable] to a [FirebaseCommonAIException]. + * + * Will populate default messages as expected, and propagate the provided [cause] through the + * resulting exception. + */ + fun from(cause: Throwable): FirebaseCommonAIException = + when (cause) { + is FirebaseCommonAIException -> cause + is JsonConvertException, + is kotlinx.serialization.SerializationException -> + SerializationException( + "Something went wrong while trying to deserialize a response from the server.", + cause, + ) + is TimeoutCancellationException -> + RequestTimeoutException("The request failed to complete in the allotted time.") + else -> UnknownException("Something unexpected happened.", cause) + } + } +} + +/** Something went wrong while trying to deserialize a response from the server. */ +internal class SerializationException(message: String, cause: Throwable? = null) : + FirebaseCommonAIException(message, cause) + +/** The server responded with a non 200 response code. */ +internal class ServerException(message: String, cause: Throwable? = null) : + FirebaseCommonAIException(message, cause) + +/** The server responded that the API Key is no valid. */ +internal class InvalidAPIKeyException(message: String, cause: Throwable? = null) : + FirebaseCommonAIException(message, cause) + +/** + * A request was blocked for some reason. + * + * See the [response's][response] `promptFeedback.blockReason` for more information. + * + * @property response the full server response for the request. + */ +internal class PromptBlockedException +internal constructor( + val response: GenerateContentResponse.Internal?, + cause: Throwable? = null, + message: String? = null, +) : + FirebaseCommonAIException( + "Prompt was blocked: ${response?.promptFeedback?.blockReason?.name?: message}", + cause, + ) { + internal constructor(message: String, cause: Throwable? = null) : this(null, cause, message) +} + +/** + * The user's location (region) is not supported by the API. + * + * See the Google documentation for a + * [list of regions](https://ai.google.dev/available_regions#available_regions) (countries and + * territories) where the API is available. + */ +internal class UnsupportedUserLocationException(cause: Throwable? = null) : + FirebaseCommonAIException("User location is not supported for the API use.", cause) + +/** + * Some form of state occurred that shouldn't have. + * + * Usually indicative of consumer error. + */ +internal class InvalidStateException(message: String, cause: Throwable? = null) : + FirebaseCommonAIException(message, cause) + +/** + * A request was stopped during generation for some reason. + * + * @property response the full server response for the request + */ +internal class ResponseStoppedException( + val response: GenerateContentResponse.Internal, + cause: Throwable? = null +) : + FirebaseCommonAIException( + "Content generation stopped. Reason: ${response.candidates?.first()?.finishReason?.name}", + cause, + ) + +/** + * A request took too long to complete. + * + * Usually occurs due to a user specified [timeout][RequestOptions.timeout]. + */ +internal class RequestTimeoutException(message: String, cause: Throwable? = null) : + FirebaseCommonAIException(message, cause) + +/** The quota for this API key is depleted, retry this request at a later time. */ +internal class QuotaExceededException(message: String, cause: Throwable? = null) : + FirebaseCommonAIException(message, cause) + +/** The service is not enabled for this project. Visit the Firebase Console to enable it. */ +internal class ServiceDisabledException(message: String, cause: Throwable? = null) : + FirebaseCommonAIException(message, cause) + +/** Catch all case for exceptions not explicitly expected. */ +internal class UnknownException(message: String, cause: Throwable? = null) : + FirebaseCommonAIException(message, cause) + +internal class ContentBlockedException(message: String, cause: Throwable? = null) : + FirebaseCommonAIException(message, cause) + +internal fun makeMissingCaseException( + source: String, + ordinal: Int +): com.google.firebase.ai.type.SerializationException { + return com.google.firebase.ai.type.SerializationException( + """ + |Missing case for a $source: $ordinal + |This error indicates that one of the `toInternal` conversions needs updating. + |If you're a developer seeing this exception, please file an issue on our GitHub repo: + |https://github.com/firebase/firebase-android-sdk + """ + .trimMargin() + ) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt new file mode 100644 index 00000000000..ebc3db7f282 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024 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. + */ +@file:OptIn(ExperimentalSerializationApi::class) + +package com.google.firebase.ai.common + +import com.google.firebase.ai.common.util.fullModelName +import com.google.firebase.ai.common.util.trimmedModelName +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.GenerationConfig +import com.google.firebase.ai.type.ImagenImageFormat +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.SafetySetting +import com.google.firebase.ai.type.Tool +import com.google.firebase.ai.type.ToolConfig +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +internal interface Request + +@Serializable +internal data class GenerateContentRequest( + val model: String? = null, + val contents: List, + @SerialName("safety_settings") val safetySettings: List? = null, + @SerialName("generation_config") val generationConfig: GenerationConfig.Internal? = null, + val tools: List? = null, + @SerialName("tool_config") var toolConfig: ToolConfig.Internal? = null, + @SerialName("system_instruction") val systemInstruction: Content.Internal? = null, +) : Request + +@Serializable +internal data class CountTokensRequest( + val generateContentRequest: GenerateContentRequest? = null, + val model: String? = null, + val contents: List? = null, + val tools: List? = null, + @SerialName("system_instruction") val systemInstruction: Content.Internal? = null, + val generationConfig: GenerationConfig.Internal? = null +) : Request { + companion object { + + fun forGoogleAI(generateContentRequest: GenerateContentRequest) = + CountTokensRequest( + generateContentRequest = + generateContentRequest.model?.let { + generateContentRequest.copy(model = fullModelName(trimmedModelName(it))) + } + ?: generateContentRequest + ) + + fun forVertexAI(generateContentRequest: GenerateContentRequest) = + CountTokensRequest( + model = generateContentRequest.model?.let { fullModelName(it) }, + contents = generateContentRequest.contents, + tools = generateContentRequest.tools, + systemInstruction = generateContentRequest.systemInstruction, + generationConfig = generateContentRequest.generationConfig, + ) + } +} + +@Serializable +internal data class GenerateImageRequest( + val instances: List, + val parameters: ImagenParameters, +) : Request { + @Serializable internal data class ImagenPrompt(val prompt: String) + + @OptIn(PublicPreviewAPI::class) + @Serializable + internal data class ImagenParameters( + val sampleCount: Int, + val includeRaiReason: Boolean, + val storageUri: String?, + val negativePrompt: String?, + val aspectRatio: String?, + val safetySetting: String?, + val personGeneration: String?, + val addWatermark: Boolean?, + val imageOutputOptions: ImagenImageFormat.Internal?, + ) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/android.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/android.kt new file mode 100644 index 00000000000..4d7a1e46097 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/android.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2025 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.firebase.ai.common.util + +import android.media.AudioRecord +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.yield + +/** + * The minimum buffer size for this instance. + * + * The same as calling [AudioRecord.getMinBufferSize], except the params are pre-populated. + */ +internal val AudioRecord.minBufferSize: Int + get() = AudioRecord.getMinBufferSize(sampleRate, channelConfiguration, audioFormat) + +/** + * Reads from this [AudioRecord] and returns the data in a flow. + * + * Will yield when this instance is not recording. + */ +internal fun AudioRecord.readAsFlow() = flow { + val buffer = ByteArray(minBufferSize) + + while (true) { + if (recordingState != AudioRecord.RECORDSTATE_RECORDING) { + yield() + continue + } + + val bytesRead = read(buffer, 0, buffer.size) + if (bytesRead > 0) { + emit(buffer.copyOf(bytesRead)) + } + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/kotlin.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/kotlin.kt new file mode 100644 index 00000000000..f9b3add3cc4 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/kotlin.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2023 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.firebase.ai.common.util + +import java.io.ByteArrayOutputStream +import java.lang.reflect.Field +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.fold + +/** + * Removes the last character from the [StringBuilder]. + * + * If the StringBuilder is empty, calling this function will throw an [IndexOutOfBoundsException]. + * + * @return The [StringBuilder] used to make the call, for optional chaining. + * @throws IndexOutOfBoundsException if the StringBuilder is empty. + */ +internal fun StringBuilder.removeLast(): StringBuilder = + if (isEmpty()) throw IndexOutOfBoundsException("StringBuilder is empty.") + else deleteCharAt(length - 1) + +/** + * A variant of [getAnnotation][Field.getAnnotation] that provides implicit Kotlin support. + * + * Syntax sugar for: + * ``` + * getAnnotation(T::class.java) + * ``` + */ +internal inline fun Field.getAnnotation() = getAnnotation(T::class.java) + +/** + * Collects bytes from this flow and doesn't emit them back until [minSize] is reached. + * + * For example: + * ``` + * val byteArr = flowOf(byteArrayOf(1), byteArrayOf(2, 3, 4), byteArrayOf(5, 6, 7, 8)) + * val expectedResult = listOf(byteArrayOf(1, 2, 3, 4), byteArrayOf( 5, 6, 7, 8)) + * + * byteArr.accumulateUntil(4).toList() shouldContainExactly expectedResult + * ``` + * + * @param minSize The minimum about of bytes the array should have before being sent down-stream + * @param emitLeftOvers If the flow completes and there are bytes left over that don't meet the + * [minSize], send them anyways. + */ +internal fun Flow.accumulateUntil( + minSize: Int, + emitLeftOvers: Boolean = false +): Flow = flow { + val remaining = + fold(ByteArrayOutputStream()) { buffer, it -> + buffer.apply { + write(it, 0, it.size) + if (size() >= minSize) { + emit(toByteArray()) + reset() + } + } + } + + if (emitLeftOvers && remaining.size() > 0) { + emit(remaining.toByteArray()) + } +} + +/** + * Create a [Job] that is a child of the [currentCoroutineContext], if any. + * + * This is useful when you want a coroutine scope to be canceled when its parent scope is canceled, + * and you don't have full control over the parent scope, but you don't want the cancellation of the + * child to impact the parent. + * + * If the parent coroutine context does not have a job, an empty one will be created. + */ +internal suspend inline fun childJob() = Job(currentCoroutineContext()[Job] ?: Job()) + +/** + * A constant value pointing to a cancelled [CoroutineScope]. + * + * Useful when you want to initialize a mutable [CoroutineScope] in a canceled state. + */ +internal val CancelledCoroutineScope = CoroutineScope(EmptyCoroutineContext).apply { cancel() } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/ktor.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/ktor.kt new file mode 100644 index 00000000000..1084ed56323 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/ktor.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024 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. + */ + +@file:Suppress("DEPRECATION") // a replacement for our purposes has not been published yet + +package com.google.firebase.ai.common.util + +import io.ktor.utils.io.ByteChannel +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.close +import io.ktor.utils.io.readUTF8Line +import io.ktor.utils.io.writeFully +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json + +/** + * Suspends and processes each line read from the [ByteReadChannel] until the channel is closed for + * read. + * + * This extension function facilitates processing the stream of lines in a manner that takes into + * account EOF/empty strings- and avoids calling [block] as such. + * + * Example usage: + * ``` + * val channel: ByteReadChannel = ByteReadChannel("Hello, World!") + * channel.onEachLine { + * println("Received line: $it") + * } + * ``` + * + * @param block A suspending function to process each line. + */ +internal suspend fun ByteReadChannel.onEachLine(block: suspend (String) -> Unit) { + while (!isClosedForRead) { + awaitContent() + val line = readUTF8Line()?.takeUnless { it.isEmpty() } ?: continue + block(line) + } +} + +/** + * Decodes a stream of JSON elements from the given [ByteReadChannel] into a [Flow] of objects of + * type [T]. + * + * This function takes in a stream of events, each with a set of named parts. Parts are separated by + * an HTTP \r\n newline, events are separated by a double HTTP \r\n\r\n newline. This function + * assumes every event will only contain a named "data" part with a JSON object. Each data JSON is + * decoded into an instance of [T] and emitted as it is read from the channel. + * + * Example usage: + * ``` + * val json = Json { ignoreUnknownKeys = true } // Create a Json instance with any configurations + * val channel: ByteReadChannel = ByteReadChannel("data: {\"name\":\"Alice\"}\r\n\r\ndata: {\"name\":\"Bob\"}]") + * + * json.decodeToFlow(channel).collect { person -> + * println(person.name) + * } + * ``` + * + * @param T The type of objects to decode from the JSON stream. + * @param channel The [ByteReadChannel] from which the JSON stream will be read. + * @return A [Flow] of objects of type [T]. + * @throws SerializationException in case of any decoding-specific error + * @throws IllegalArgumentException if the decoded input is not a valid instance of [T] + */ +internal inline fun Json.decodeToFlow(channel: ByteReadChannel): Flow = channelFlow { + channel.onEachLine { + val data = it.removePrefix("data:") + send(decodeFromString(data)) + } +} + +/** + * Writes the provided [bytes] to the channel and closes it. + * + * Just a wrapper around [writeFully] that closes the channel after writing is complete. + * + * @param bytes the data to send through the channel + */ +internal suspend fun ByteChannel.send(bytes: ByteArray) { + writeFully(bytes) + close() +} + +/** String separator used in SSE communication to signal the end of a message. */ +internal const val SSE_SEPARATOR = "\r\n\r\n" diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/serialization.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/serialization.kt new file mode 100644 index 00000000000..91490da4126 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/serialization.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2024 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.firebase.ai.common.util + +import android.util.Log +import com.google.firebase.ai.common.SerializationException +import kotlin.reflect.KClass +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * Serializer for enums that defaults to the first ordinal on unknown types. + * + * Convention is that the first enum be named `UNKNOWN`, but any name is valid. + * + * When an unknown enum value is found, the enum itself will be logged to stderr with a message + * about opening an issue on GitHub regarding the new enum value. + */ +internal class FirstOrdinalSerializer>(private val enumClass: KClass) : + KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("FirstOrdinalSerializer") { + for (enumValue in enumClass.enumValues()) { + element(enumValue.toString()) + } + } + + override fun deserialize(decoder: Decoder): T { + val name = decoder.decodeString() + val values = enumClass.enumValues() + + return values.firstOrNull { it.serialName == name } + ?: values.first().also { printWarning(name) } + } + + private fun printWarning(name: String) { + Log.e( + "FirstOrdinalSerializer", + """ + |Unknown enum value found: $name" + |This usually means the backend was updated, and the SDK needs to be updated to match it. + |Check if there's a new version for the SDK, otherwise please open an issue on our + |GitHub to bring it to our attention: + |https://github.com/google/google-ai-android + """ + .trimMargin(), + ) + } + + override fun serialize(encoder: Encoder, value: T) { + encoder.encodeString(value.serialName) + } +} + +/** + * Provides the name to be used in serialization for this enum value. + * + * By default an enum is serialized to its [name][Enum.name], and can be overwritten by providing a + * [SerialName] annotation. + */ +internal val > T.serialName: String + get() = declaringJavaClass.getField(name).getAnnotation()?.value ?: name + +/** + * Variant of [kotlin.enumValues] that provides support for [KClass] instances of enums. + * + * @throws SerializationException if the class is not a valid enum. Beyond runtime emily magic, this + * shouldn't really be possible. + */ +internal fun > KClass.enumValues(): Array = + java.enumConstants ?: throw SerializationException("$simpleName is not a valid enum type.") diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/util.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/util.kt new file mode 100644 index 00000000000..1fc71f6c4ff --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/util.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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.firebase.ai.common.util + +/** + * Ensures the model name provided has a `models/` prefix + * + * Models must be prepended with the `models/` prefix when communicating with the backend. + */ +internal fun fullModelName(name: String): String = + name.takeIf { it.contains("/") } ?: "models/$name" + +internal fun trimmedModelName(name: String): String = name.split("/").last() diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ChatFutures.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ChatFutures.kt new file mode 100644 index 00000000000..2973cf78624 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ChatFutures.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2023 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.firebase.ai.java + +import androidx.concurrent.futures.SuspendToFutureAdapter +import com.google.common.util.concurrent.ListenableFuture +import com.google.firebase.ai.Chat +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.InvalidStateException +import kotlinx.coroutines.reactive.asPublisher +import org.reactivestreams.Publisher + +/** + * Wrapper class providing Java compatible methods for [Chat]. + * + * @see [Chat] + */ +public abstract class ChatFutures internal constructor() { + + /** + * Sends a message using the existing history of this chat as context and the provided [Content] + * prompt. + * + * If successful, the message and response will be added to the history. If unsuccessful, history + * will remain unchanged. + * + * @param prompt The input(s) that, together with the history, will be given to the model as the + * prompt. + * @throws InvalidStateException if [prompt] is not coming from the 'user' role + * @throws InvalidStateException if the [Chat] instance has an active request + */ + public abstract fun sendMessage(prompt: Content): ListenableFuture + + /** + * Sends a message using the existing history of this chat as context and the provided [Content] + * prompt. + * + * The response from the model is returned as a stream. + * + * If successful, the message and response will be added to the history. If unsuccessful, history + * will remain unchanged. + * + * @param prompt The input(s) that, together with the history, will be given to the model as the + * prompt. + * @throws InvalidStateException if [prompt] is not coming from the 'user' role + * @throws InvalidStateException if the [Chat] instance has an active request + */ + public abstract fun sendMessageStream(prompt: Content): Publisher + + /** Returns the [Chat] object wrapped by this object. */ + public abstract fun getChat(): Chat + + private class FuturesImpl(private val chat: Chat) : ChatFutures() { + override fun sendMessage(prompt: Content): ListenableFuture = + SuspendToFutureAdapter.launchFuture { chat.sendMessage(prompt) } + + override fun sendMessageStream(prompt: Content): Publisher = + chat.sendMessageStream(prompt).asPublisher() + + override fun getChat(): Chat = chat + } + + public companion object { + + /** @return a [ChatFutures] created around the provided [Chat] */ + @JvmStatic public fun from(chat: Chat): ChatFutures = FuturesImpl(chat) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt new file mode 100644 index 00000000000..57a531c1cd8 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2023 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.firebase.ai.java + +import androidx.concurrent.futures.SuspendToFutureAdapter +import com.google.common.util.concurrent.ListenableFuture +import com.google.firebase.ai.GenerativeModel +import com.google.firebase.ai.java.ChatFutures.Companion.from +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.CountTokensResponse +import com.google.firebase.ai.type.FirebaseAIException +import com.google.firebase.ai.type.GenerateContentResponse +import kotlinx.coroutines.reactive.asPublisher +import org.reactivestreams.Publisher + +/** + * Wrapper class providing Java compatible methods for [GenerativeModel]. + * + * @see [GenerativeModel] + */ +public abstract class GenerativeModelFutures internal constructor() { + + /** + * Generates new content from the input [Content] given to the model as a prompt. + * + * @param prompt The input(s) given to the model as a prompt. + * @return The content generated by the model. + * @throws [FirebaseAIException] if the request failed. + */ + public abstract fun generateContent( + vararg prompt: Content + ): ListenableFuture + + /** + * Generates new content as a stream from the input [Content] given to the model as a prompt. + * + * @param prompt The input(s) given to the model as a prompt. + * @return A [Publisher] which will emit responses as they are returned by the model. + * @throws [FirebaseAIException] if the request failed. + */ + public abstract fun generateContentStream( + vararg prompt: Content + ): Publisher + + /** + * Counts the number of tokens in a prompt using the model's tokenizer. + * + * @param prompt The input(s) given to the model as a prompt. + * @return The [CountTokensResponse] of running the model's tokenizer on the input. + * @throws [FirebaseAIException] if the request failed. + */ + public abstract fun countTokens(vararg prompt: Content): ListenableFuture + + /** + * Creates a [ChatFutures] instance which internally tracks the ongoing conversation with the + * model. + */ + public abstract fun startChat(): ChatFutures + + /** + * Creates a [ChatFutures] instance, initialized using the optionally provided [history]. + * + * @param history A list of previous interactions with the model to use as a starting point + */ + public abstract fun startChat(history: List): ChatFutures + + /** Returns the [GenerativeModel] object wrapped by this object. */ + public abstract fun getGenerativeModel(): GenerativeModel + + private class FuturesImpl(private val model: GenerativeModel) : GenerativeModelFutures() { + override fun generateContent( + vararg prompt: Content + ): ListenableFuture = + SuspendToFutureAdapter.launchFuture { model.generateContent(*prompt) } + + override fun generateContentStream(vararg prompt: Content): Publisher = + model.generateContentStream(*prompt).asPublisher() + + override fun countTokens(vararg prompt: Content): ListenableFuture = + SuspendToFutureAdapter.launchFuture { model.countTokens(*prompt) } + + override fun startChat(): ChatFutures = startChat(emptyList()) + + override fun startChat(history: List): ChatFutures = from(model.startChat(history)) + + override fun getGenerativeModel(): GenerativeModel = model + } + + public companion object { + + /** @return a [GenerativeModelFutures] created around the provided [GenerativeModel] */ + @JvmStatic public fun from(model: GenerativeModel): GenerativeModelFutures = FuturesImpl(model) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ImagenModelFutures.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ImagenModelFutures.kt new file mode 100644 index 00000000000..99d42d32732 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ImagenModelFutures.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2025 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.firebase.ai.java + +import androidx.concurrent.futures.SuspendToFutureAdapter +import com.google.common.util.concurrent.ListenableFuture +import com.google.firebase.ai.ImagenModel +import com.google.firebase.ai.type.ImagenGenerationResponse +import com.google.firebase.ai.type.ImagenInlineImage +import com.google.firebase.ai.type.PublicPreviewAPI + +/** + * Wrapper class providing Java compatible methods for [ImagenModel]. + * + * @see [ImagenModel] + */ +@PublicPreviewAPI +public abstract class ImagenModelFutures internal constructor() { + /** + * Generates an image, returning the result directly to the caller. + * + * @param prompt The main text prompt from which the image is generated. + */ + public abstract fun generateImages( + prompt: String, + ): ListenableFuture> + + /** Returns the [ImagenModel] object wrapped by this object. */ + public abstract fun getImageModel(): ImagenModel + + private class FuturesImpl(private val model: ImagenModel) : ImagenModelFutures() { + override fun generateImages( + prompt: String, + ): ListenableFuture> = + SuspendToFutureAdapter.launchFuture { model.generateImages(prompt) } + + override fun getImageModel(): ImagenModel = model + } + + public companion object { + + /** @return a [ImagenModelFutures] created around the provided [ImagenModel] */ + @JvmStatic public fun from(model: ImagenModel): ImagenModelFutures = FuturesImpl(model) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveModelFutures.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveModelFutures.kt new file mode 100644 index 00000000000..9eb222007a0 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveModelFutures.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2025 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.firebase.ai.java + +import androidx.concurrent.futures.SuspendToFutureAdapter +import com.google.common.util.concurrent.ListenableFuture +import com.google.firebase.ai.LiveGenerativeModel +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.ServiceConnectionHandshakeFailedException + +/** + * Wrapper class providing Java compatible methods for [LiveGenerativeModel]. + * + * @see [LiveGenerativeModel] + */ +@PublicPreviewAPI +public abstract class LiveModelFutures internal constructor() { + + /** + * Start a [LiveSessionFutures] with the server for bidirectional streaming. + * @return A [LiveSessionFutures] that you can use to stream messages to and from the server. + * @throws [ServiceConnectionHandshakeFailedException] If the client was not able to establish a + * connection with the server. + */ + public abstract fun connect(): ListenableFuture + + private class FuturesImpl(private val model: LiveGenerativeModel) : LiveModelFutures() { + override fun connect(): ListenableFuture { + return SuspendToFutureAdapter.launchFuture { LiveSessionFutures.from(model.connect()) } + } + } + + public companion object { + + /** @return a [LiveModelFutures] created around the provided [LiveGenerativeModel] */ + @JvmStatic public fun from(model: LiveGenerativeModel): LiveModelFutures = FuturesImpl(model) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt new file mode 100644 index 00000000000..1efa2dfedfc --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2025 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.firebase.ai.java + +import android.Manifest.permission.RECORD_AUDIO +import androidx.annotation.RequiresPermission +import androidx.concurrent.futures.SuspendToFutureAdapter +import com.google.common.util.concurrent.ListenableFuture +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.FunctionCallPart +import com.google.firebase.ai.type.FunctionResponsePart +import com.google.firebase.ai.type.LiveServerMessage +import com.google.firebase.ai.type.LiveSession +import com.google.firebase.ai.type.MediaData +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.SessionAlreadyReceivingException +import io.ktor.websocket.close +import kotlinx.coroutines.reactive.asPublisher +import org.reactivestreams.Publisher + +/** + * Wrapper class providing Java compatible methods for [LiveSession]. + * + * @see [LiveSession] + */ +@PublicPreviewAPI +public abstract class LiveSessionFutures internal constructor() { + + /** + * Starts an audio conversation with the model, which can only be stopped using + * [stopAudioConversation] or [close]. + * + * @param functionCallHandler A callback function that is invoked whenever the model receives a + * function call. + */ + public abstract fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? + ): ListenableFuture + + /** + * Starts an audio conversation with the model, which can only be stopped using + * [stopAudioConversation]. + */ + @RequiresPermission(RECORD_AUDIO) + public abstract fun startAudioConversation(): ListenableFuture + + /** + * Stops the audio conversation with the Gemini Server. + * + * This only needs to be called after a previous call to [startAudioConversation]. + * + * If there is no audio conversation currently active, this function does nothing. + */ + @RequiresPermission(RECORD_AUDIO) + public abstract fun stopAudioConversation(): ListenableFuture + + /** + * Stops receiving from the model. + * + * If this function is called during an ongoing audio conversation, the model's response will not + * be received, and no audio will be played; the live session object will no longer receive data + * from the server. + * + * To resume receiving data, you must either handle it directly using [receive], or indirectly by + * using [startAudioConversation]. + * + * @see close + */ + // TODO(b/410059569): Remove when fixed + public abstract fun stopReceiving() + + /** + * Sends function calling responses to the model. + * + * @param functionList The list of [FunctionResponsePart] instances indicating the function + * response from the client. + */ + public abstract fun sendFunctionResponse( + functionList: List + ): ListenableFuture + + /** + * Streams client data to the model. + * + * Calling this after [startAudioConversation] will play the response audio immediately. + * + * @param mediaChunks The list of [MediaData] instances representing the media data to be sent. + */ + public abstract fun sendMediaStream(mediaChunks: List): ListenableFuture + + /** + * Sends [data][Content] to the model. + * + * Calling this after [startAudioConversation] will play the response audio immediately. + * + * @param content Client [Content] to be sent to the model. + */ + public abstract fun send(content: Content): ListenableFuture + + /** + * Sends text to the model. + * + * Calling this after [startAudioConversation] will play the response audio immediately. + * + * @param text Text to be sent to the model. + */ + public abstract fun send(text: String): ListenableFuture + + /** + * Closes the client session. + * + * Once a [LiveSession] is closed, it can not be reopened; you'll need to start a new + * [LiveSession]. + * + * @see stopReceiving + */ + public abstract fun close(): ListenableFuture + + /** + * Receives responses from the model for both streaming and standard requests. + * + * Call [close] to stop receiving responses from the model. + * + * @return A [Publisher] which will emit [LiveServerMessage] from the model. + * + * @throws [SessionAlreadyReceivingException] when the session is already receiving. + * @see stopReceiving + */ + public abstract fun receive(): Publisher + + private class FuturesImpl(private val session: LiveSession) : LiveSessionFutures() { + + override fun receive(): Publisher = session.receive().asPublisher() + + override fun close(): ListenableFuture = + SuspendToFutureAdapter.launchFuture { session.close() } + + override fun send(text: String) = SuspendToFutureAdapter.launchFuture { session.send(text) } + + override fun send(content: Content) = + SuspendToFutureAdapter.launchFuture { session.send(content) } + + override fun sendFunctionResponse(functionList: List) = + SuspendToFutureAdapter.launchFuture { session.sendFunctionResponse(functionList) } + + override fun sendMediaStream(mediaChunks: List) = + SuspendToFutureAdapter.launchFuture { session.sendMediaStream(mediaChunks) } + + @RequiresPermission(RECORD_AUDIO) + override fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? + ) = SuspendToFutureAdapter.launchFuture { session.startAudioConversation(functionCallHandler) } + + @RequiresPermission(RECORD_AUDIO) + override fun startAudioConversation() = + SuspendToFutureAdapter.launchFuture { session.startAudioConversation() } + + override fun stopAudioConversation() = + SuspendToFutureAdapter.launchFuture { session.stopAudioConversation() } + + override fun stopReceiving() = session.stopReceiving() + } + + public companion object { + + /** @return a [LiveSessionFutures] created around the provided [LiveSession] */ + @JvmStatic public fun from(session: LiveSession): LiveSessionFutures = FuturesImpl(session) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/AudioHelper.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/AudioHelper.kt new file mode 100644 index 00000000000..4db66ae6c3e --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/AudioHelper.kt @@ -0,0 +1,213 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import android.Manifest +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioRecord +import android.media.AudioTrack +import android.media.MediaRecorder +import android.media.audiofx.AcousticEchoCanceler +import android.util.Log +import androidx.annotation.RequiresPermission +import com.google.firebase.ai.common.util.readAsFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +/** + * Helper class for recording audio and playing back a separate audio track at the same time. + * + * @see AudioHelper.build + * @see LiveSession.startAudioConversation + */ +@PublicPreviewAPI +internal class AudioHelper( + /** Record for recording the System microphone. */ + private val recorder: AudioRecord, + /** Track for playing back what the model says. */ + private val playbackTrack: AudioTrack, +) { + private var released: Boolean = false + + /** + * Release the system resources on the recorder and playback track. + * + * Once an [AudioHelper] has been "released", it can _not_ be used again. + * + * This method can safely be called multiple times, as it won't do anything if this instance has + * already been released. + */ + fun release() { + if (released) return + released = true + + recorder.release() + playbackTrack.release() + } + + /** + * Play the provided audio data on the playback track. + * + * Does nothing if this [AudioHelper] has been [released][release]. + * + * @throws IllegalStateException If the playback track was not properly initialized. + * @throws IllegalArgumentException If the playback data is invalid. + * @throws RuntimeException If we fail to play the audio data for some unknown reason. + */ + fun playAudio(data: ByteArray) { + if (released) return + if (data.isEmpty()) return + + if (playbackTrack.playState == AudioTrack.PLAYSTATE_STOPPED) playbackTrack.play() + + val result = playbackTrack.write(data, 0, data.size) + if (result > 0) return + if (result == 0) { + Log.w( + TAG, + "Failed to write any audio bytes to the playback track. The audio track may have been stopped or paused." + ) + return + } + + // ERROR_INVALID_OPERATION and ERROR_BAD_VALUE should never occur + when (result) { + AudioTrack.ERROR_INVALID_OPERATION -> + throw IllegalStateException("The playback track was not properly initialized.") + AudioTrack.ERROR_BAD_VALUE -> + throw IllegalArgumentException("Playback data is somehow invalid.") + AudioTrack.ERROR_DEAD_OBJECT -> { + Log.w(TAG, "Attempted to playback some audio, but the track has been released.") + release() // to ensure `released` is set and `record` is released too + } + AudioTrack.ERROR -> + throw RuntimeException("Failed to play the audio data for some unknown reason.") + } + } + + /** + * Pause the recording of the microphone, if it's recording. + * + * Does nothing if this [AudioHelper] has been [released][release]. + * + * @see resumeRecording + * + * @throws IllegalStateException If the playback track was not properly initialized. + */ + fun pauseRecording() { + if (released || recorder.recordingState == AudioRecord.RECORDSTATE_STOPPED) return + + try { + recorder.stop() + } catch (e: IllegalStateException) { + release() + throw IllegalStateException("The playback track was not properly initialized.") + } + } + + /** + * Resumes the recording of the microphone, if it's not already running. + * + * Does nothing if this [AudioHelper] has been [released][release]. + * + * @see pauseRecording + */ + fun resumeRecording() { + if (released || recorder.recordingState == AudioRecord.RECORDSTATE_RECORDING) return + + recorder.startRecording() + } + + /** + * Start perpetually recording the system microphone, and return the bytes read in a flow. + * + * Returns an empty flow if this [AudioHelper] has been [released][release]. + */ + fun listenToRecording(): Flow { + if (released) return emptyFlow() + + resumeRecording() + + return recorder.readAsFlow() + } + + companion object { + private val TAG = AudioHelper::class.simpleName + + /** + * Creates an instance of [AudioHelper] with the track and record initialized. + * + * A separate build method is necessary so that we can properly propagate the required manifest + * permission, and throw exceptions when needed. + * + * It also makes it easier to read, since the long initialization is separate from the + * constructor. + */ + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + fun build(): AudioHelper { + val playbackTrack = + AudioTrack( + AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION).build(), + AudioFormat.Builder() + .setSampleRate(24000) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .build(), + AudioTrack.getMinBufferSize( + 24000, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT + ), + AudioTrack.MODE_STREAM, + AudioManager.AUDIO_SESSION_ID_GENERATE + ) + + val bufferSize = + AudioRecord.getMinBufferSize( + 16000, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + + if (bufferSize <= 0) + throw AudioRecordInitializationFailedException( + "Audio Record buffer size is invalid ($bufferSize)" + ) + + val recorder = + AudioRecord( + MediaRecorder.AudioSource.VOICE_COMMUNICATION, + 16000, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT, + bufferSize + ) + if (recorder.state != AudioRecord.STATE_INITIALIZED) + throw AudioRecordInitializationFailedException( + "Audio Record initialization has failed. State: ${recorder.state}" + ) + + if (AcousticEchoCanceler.isAvailable()) { + AcousticEchoCanceler.create(recorder.audioSessionId)?.enabled = true + } + + return AudioHelper(recorder, playbackTrack) + } + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt new file mode 100644 index 00000000000..d5fc51f21c0 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt @@ -0,0 +1,319 @@ +/* + * Copyright 2023 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. + */ + +@file:OptIn(ExperimentalSerializationApi::class) + +package com.google.firebase.ai.type + +import com.google.firebase.ai.common.util.FirstOrdinalSerializer +import java.util.Calendar +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames + +/** + * A `Candidate` represents a single response generated by the model for a given request. + * + * @property content The actual content generated by the model. + * @property safetyRatings A list of [SafetyRating]s describing the generated content. + * @property citationMetadata Metadata about the sources used to generate this content. + * @property finishReason The reason the model stopped generating content, if it exist. + */ +public class Candidate +internal constructor( + public val content: Content, + public val safetyRatings: List, + public val citationMetadata: CitationMetadata?, + public val finishReason: FinishReason? +) { + + @Serializable + internal data class Internal( + val content: Content.Internal? = null, + val finishReason: FinishReason.Internal? = null, + val safetyRatings: List? = null, + val citationMetadata: CitationMetadata.Internal? = null, + val groundingMetadata: GroundingMetadata? = null, + ) { + internal fun toPublic(): Candidate { + val safetyRatings = safetyRatings?.mapNotNull { it.toPublic() }.orEmpty() + val citations = citationMetadata?.toPublic() + val finishReason = finishReason?.toPublic() + + return Candidate( + this.content?.toPublic() ?: content("model") {}, + safetyRatings, + citations, + finishReason + ) + } + + @Serializable + internal data class GroundingMetadata( + @SerialName("web_search_queries") val webSearchQueries: List?, + @SerialName("search_entry_point") val searchEntryPoint: SearchEntryPoint?, + @SerialName("retrieval_queries") val retrievalQueries: List?, + @SerialName("grounding_attribution") val groundingAttribution: List?, + ) { + + @Serializable + internal data class SearchEntryPoint( + @SerialName("rendered_content") val renderedContent: String?, + @SerialName("sdk_blob") val sdkBlob: String?, + ) + + @Serializable + internal data class GroundingAttribution( + val segment: Segment, + @SerialName("confidence_score") val confidenceScore: Float?, + ) { + + @Serializable + internal data class Segment( + @SerialName("start_index") val startIndex: Int, + @SerialName("end_index") val endIndex: Int, + ) + } + } + } +} + +/** + * An assessment of the potential harm of some generated content. + * + * The rating will be restricted to a particular [category]. + * + * @property category The category of harm being assessed (e.g., Hate speech). + * @property probability The likelihood of the content causing harm. + * @property probabilityScore A numerical score representing the probability of harm, between 0 and + * 1. + * @property blocked Indicates whether the content was blocked due to safety concerns. + * @property severity The severity of the potential harm. + * @property severityScore A numerical score representing the severity of harm. + */ +public class SafetyRating +internal constructor( + public val category: HarmCategory, + public val probability: HarmProbability, + public val probabilityScore: Float = 0f, + public val blocked: Boolean? = null, + public val severity: HarmSeverity? = null, + public val severityScore: Float? = null +) { + + @Serializable + internal data class Internal + @JvmOverloads + constructor( + val category: HarmCategory.Internal? = null, + val probability: HarmProbability.Internal? = null, + val blocked: Boolean? = null, // TODO(): any reason not to default to false? + val probabilityScore: Float? = null, + val severity: HarmSeverity.Internal? = null, + val severityScore: Float? = null, + ) { + + internal fun toPublic() = + // Due to a bug in the backend, it's possible that we receive + // an invalid `SafetyRating` value, without either category or + // probability. We return null in those cases to enable + // filtering by the higher level types. + if (category == null || probability == null) { + null + } else { + SafetyRating( + category = category.toPublic(), + probability = probability.toPublic(), + probabilityScore = probabilityScore ?: 0f, + blocked = blocked, + severity = severity?.toPublic(), + severityScore = severityScore + ) + } + } +} + +/** + * A collection of source attributions for a piece of content. + * + * @property citations A list of individual cited sources and the parts of the content to which they + * apply. + */ +public class CitationMetadata internal constructor(public val citations: List) { + + @Serializable + internal data class Internal + @OptIn(ExperimentalSerializationApi::class) + internal constructor(@JsonNames("citations") val citationSources: List) { + + internal fun toPublic() = CitationMetadata(citationSources.map { it.toPublic() }) + } +} + +/** + * Represents a citation of content from an external source within the model's output. + * + * When the language model generates text that includes content from another source, it should + * provide a citation to properly attribute the original source. This class encapsulates the + * metadata associated with that citation. + * + * @property title The title of the cited source, if available. + * @property startIndex The (inclusive) starting index within the model output where the cited + * content begins. + * @property endIndex The (exclusive) ending index within the model output where the cited content + * ends. + * @property uri The URI of the cited source, if available. + * @property license The license under which the cited content is distributed under, if available. + * @property publicationDate The date of publication of the cited source, if available. + */ +public class Citation +internal constructor( + public val title: String? = null, + public val startIndex: Int = 0, + public val endIndex: Int, + public val uri: String? = null, + public val license: String? = null, + public val publicationDate: Calendar? = null +) { + + @Serializable + internal data class Internal( + val title: String? = null, + val startIndex: Int = 0, + val endIndex: Int, + val uri: String? = null, + val license: String? = null, + val publicationDate: Date? = null, + ) { + + internal fun toPublic(): Citation { + val publicationDateAsCalendar = + publicationDate?.let { + val calendar = Calendar.getInstance() + // Internal `Date.year` uses 0 to represent not specified. We use 1 as default. + val year = if (it.year == null || it.year < 1) 1 else it.year + // Internal `Date.month` uses 0 to represent not specified, or is 1-12 as months. The + // month as + // expected by [Calendar] is 0-based, so we subtract 1 or use 0 as default. + val month = if (it.month == null || it.month < 1) 0 else it.month - 1 + // Internal `Date.day` uses 0 to represent not specified. We use 1 as default. + val day = if (it.day == null || it.day < 1) 1 else it.day + calendar.set(year, month, day) + calendar + } + return Citation( + title = title, + startIndex = startIndex, + endIndex = endIndex, + uri = uri, + license = license, + publicationDate = publicationDateAsCalendar + ) + } + + @Serializable + internal data class Date( + /** Year of the date. Must be between 1 and 9999, or 0 for no year. */ + val year: Int? = null, + /** 1-based index for month. Must be from 1 to 12, or 0 to specify a year without a month. */ + val month: Int? = null, + /** + * Day of a month. Must be from 1 to 31 and valid for the year and month, or 0 to specify a + * year by itself or a year and month where the day isn't significant. + */ + val day: Int? = null, + ) + } +} + +/** + * Represents the reason why the model stopped generating content. + * + * @property name The name of the finish reason. + * @property ordinal The ordinal value of the finish reason. + */ +public class FinishReason private constructor(public val name: String, public val ordinal: Int) { + + @Serializable(Internal.Serializer::class) + internal enum class Internal { + UNKNOWN, + @SerialName("FINISH_REASON_UNSPECIFIED") UNSPECIFIED, + STOP, + MAX_TOKENS, + SAFETY, + RECITATION, + OTHER, + BLOCKLIST, + PROHIBITED_CONTENT, + SPII, + MALFORMED_FUNCTION_CALL; + + internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class) + + internal fun toPublic() = + when (this) { + MAX_TOKENS -> FinishReason.MAX_TOKENS + RECITATION -> FinishReason.RECITATION + SAFETY -> FinishReason.SAFETY + STOP -> FinishReason.STOP + OTHER -> FinishReason.OTHER + BLOCKLIST -> FinishReason.BLOCKLIST + PROHIBITED_CONTENT -> FinishReason.PROHIBITED_CONTENT + SPII -> FinishReason.SPII + MALFORMED_FUNCTION_CALL -> FinishReason.MALFORMED_FUNCTION_CALL + else -> FinishReason.UNKNOWN + } + } + public companion object { + /** A new and not yet supported value. */ + @JvmField public val UNKNOWN: FinishReason = FinishReason("UNKNOWN", 0) + + /** Model finished successfully and stopped. */ + @JvmField public val STOP: FinishReason = FinishReason("STOP", 1) + + /** Model hit the token limit. */ + @JvmField public val MAX_TOKENS: FinishReason = FinishReason("MAX_TOKENS", 2) + + /** [SafetySetting] prevented the model from outputting content. */ + @JvmField public val SAFETY: FinishReason = FinishReason("SAFETY", 3) + + /** + * The token generation was stopped because the response was flagged for unauthorized citations. + */ + @JvmField public val RECITATION: FinishReason = FinishReason("RECITATION", 4) + + /** Model stopped for another reason. */ + @JvmField public val OTHER: FinishReason = FinishReason("OTHER", 5) + + /** Token generation stopped because the content contains forbidden terms. */ + @JvmField public val BLOCKLIST: FinishReason = FinishReason("BLOCKLIST", 6) + + /** Token generation stopped for potentially containing prohibited content. */ + @JvmField public val PROHIBITED_CONTENT: FinishReason = FinishReason("PROHIBITED_CONTENT", 7) + + /** + * Token generation stopped because the content potentially contains Sensitive Personally + * Identifiable Information (SPII). + */ + @JvmField public val SPII: FinishReason = FinishReason("SPII", 8) + + /** The function call generated by the model is invalid. */ + @JvmField + public val MALFORMED_FUNCTION_CALL: FinishReason = FinishReason("MALFORMED_FUNCTION_CALL", 9) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Content.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Content.kt new file mode 100644 index 00000000000..4e9f1a860db --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Content.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2023 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.firebase.ai.type + +import android.graphics.Bitmap +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable + +/** + * Represents content sent to and received from the model. + * + * `Content` is composed of a one or more heterogeneous parts that can be represent data in + * different formats, like text or images. + * + * @param role The producer of the content. Must be either `"user"` or `"model"`. By default, it's + * `"user"`. + * @param parts An ordered list of [Part] that constitute this content. + */ +public class Content +@JvmOverloads +constructor(public val role: String? = "user", public val parts: List) { + + /** Returns a copy of this object, with the provided parameters overwriting the originals. */ + public fun copy(role: String? = this.role, parts: List = this.parts): Content { + return Content(role, parts) + } + + /** Builder class to facilitate constructing complex [Content] objects. */ + public class Builder { + + /** The producer of the content. Must be either 'user' or 'model'. By default, it's "user". */ + @JvmField public var role: String? = "user" + + /** + * The mutable list of [Part]s comprising the [Content]. + * + * Prefer using the provided helper methods over modifying this list directly. + */ + @JvmField public var parts: MutableList = arrayListOf() + + public fun setRole(role: String?): Content.Builder = apply { this.role = role } + public fun setParts(parts: MutableList): Content.Builder = apply { this.parts = parts } + + /** Adds a new [Part] to [parts]. */ + @JvmName("addPart") + public fun part(data: T): Content.Builder = apply { parts.add(data) } + + /** Adds a new [TextPart] with the provided [text] to [parts]. */ + @JvmName("addText") public fun text(text: String): Content.Builder = part(TextPart(text)) + + /** + * Adds a new [InlineDataPart] with the provided [bytes], which should be interpreted by the + * model based on the [mimeType], to [parts]. + */ + @JvmName("addInlineData") + public fun inlineData(bytes: ByteArray, mimeType: String): Content.Builder = + part(InlineDataPart(bytes, mimeType)) + + /** Adds a new [ImagePart] with the provided [image] to [parts]. */ + @JvmName("addImage") public fun image(image: Bitmap): Content.Builder = part(ImagePart(image)) + + /** Adds a new [FileDataPart] with the provided [uri] and [mimeType] to [parts]. */ + @JvmName("addFileData") + public fun fileData(uri: String, mimeType: String): Content.Builder = + part(FileDataPart(uri, mimeType)) + + /** Returns a new [Content] using the defined [role] and [parts]. */ + public fun build(): Content = Content(role, parts) + } + + @OptIn(ExperimentalSerializationApi::class) + internal fun toInternal() = Internal(this.role ?: "user", this.parts.map { it.toInternal() }) + + @ExperimentalSerializationApi + @Serializable + internal data class Internal( + @EncodeDefault val role: String? = "user", + val parts: List + ) { + internal fun toPublic(): Content { + val returnedParts = + parts.map { it.toPublic() }.filterNot { it is TextPart && it.text.isEmpty() } + // If all returned parts were text and empty, we coalesce them into a single one-character + // string + // part so the backend doesn't fail if we send this back as part of a multi-turn interaction. + return Content(role, returnedParts.ifEmpty { listOf(TextPart(" ")) }) + } + } +} + +/** + * Function to build a new [Content] instances in a DSL-like manner. + * + * Contains a collection of text, image, and binary parts. + * + * Example usage: + * ``` + * content("user") { + * text("Example string") + * ) + * ``` + */ +public fun content(role: String? = "user", init: Content.Builder.() -> Unit): Content { + val builder = Content.Builder() + builder.role = role + builder.init() + return builder.build() +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ContentModality.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ContentModality.kt new file mode 100644 index 00000000000..bfdf8831a43 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ContentModality.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import com.google.firebase.ai.common.util.FirstOrdinalSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Content part modality. */ +public class ContentModality private constructor(public val ordinal: Int) { + + @Serializable(Internal.Serializer::class) + internal enum class Internal { + @SerialName("MODALITY_UNSPECIFIED") UNSPECIFIED, + TEXT, + IMAGE, + VIDEO, + AUDIO, + DOCUMENT; + + internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class) + + internal fun toPublic() = + when (this) { + TEXT -> ContentModality.TEXT + IMAGE -> ContentModality.IMAGE + VIDEO -> ContentModality.VIDEO + AUDIO -> ContentModality.AUDIO + DOCUMENT -> ContentModality.DOCUMENT + else -> ContentModality.UNSPECIFIED + } + } + + internal fun toInternal() = + when (this) { + TEXT -> "TEXT" + IMAGE -> "IMAGE" + VIDEO -> "VIDEO" + AUDIO -> "AUDIO" + DOCUMENT -> "DOCUMENT" + else -> "UNSPECIFIED" + } + public companion object { + /** Unspecified modality. */ + @JvmField public val UNSPECIFIED: ContentModality = ContentModality(0) + + /** Plain text. */ + @JvmField public val TEXT: ContentModality = ContentModality(1) + + /** Image. */ + @JvmField public val IMAGE: ContentModality = ContentModality(2) + + /** Video. */ + @JvmField public val VIDEO: ContentModality = ContentModality(3) + + /** Audio. */ + @JvmField public val AUDIO: ContentModality = ContentModality(4) + + /** Document, e.g. PDF. */ + @JvmField public val DOCUMENT: ContentModality = ContentModality(5) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/CountTokensResponse.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/CountTokensResponse.kt new file mode 100644 index 00000000000..955f7bf941a --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/CountTokensResponse.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2023 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.firebase.ai.type + +import kotlinx.serialization.Serializable + +/** + * The model's response to a count tokens request. + * + * **Important:** The counters in this class do not include billable image, video or other non-text + * input. See [Vertex AI pricing](https://cloud.google.com/vertex-ai/generative-ai/pricing) for + * details. + * + * @property totalTokens The total number of tokens in the input given to the model as a prompt. + * @property totalBillableCharacters The total number of billable characters in the text input given + * to the model as a prompt. **Important:** this property does not include billable image, video or + * other non-text input. See + * [Vertex AI pricing](https://cloud.google.com/vertex-ai/generative-ai/pricing) for details. + * @property promptTokensDetails The breakdown, by modality, of how many tokens are consumed by the + * prompt. + */ +public class CountTokensResponse( + public val totalTokens: Int, + public val totalBillableCharacters: Int? = null, + public val promptTokensDetails: List = emptyList(), +) { + public operator fun component1(): Int = totalTokens + + public operator fun component2(): Int? = totalBillableCharacters + + public operator fun component3(): List? = promptTokensDetails + + @Serializable + internal data class Internal( + val totalTokens: Int, + val totalBillableCharacters: Int? = null, + val promptTokensDetails: List? = null + ) : Response { + + internal fun toPublic(): CountTokensResponse { + return CountTokensResponse( + totalTokens, + totalBillableCharacters ?: 0, + promptTokensDetails?.map { it.toPublic() } ?: emptyList() + ) + } + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt new file mode 100644 index 00000000000..57a27f241a0 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt @@ -0,0 +1,224 @@ +/* + * Copyright 2023 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.firebase.ai.type + +import com.google.firebase.ai.FirebaseAI +import com.google.firebase.ai.common.FirebaseCommonAIException +import kotlinx.coroutines.TimeoutCancellationException + +/** Parent class for any errors that occur from the [FirebaseAI] SDK. */ +public abstract class FirebaseAIException +internal constructor(message: String, cause: Throwable? = null) : RuntimeException(message, cause) { + + internal companion object { + + /** + * Converts a [Throwable] to a [FirebaseAIException]. + * + * Will populate default messages as expected, and propagate the provided [cause] through the + * resulting exception. + */ + internal fun from(cause: Throwable): FirebaseAIException = + when (cause) { + is FirebaseAIException -> cause + is FirebaseCommonAIException -> + when (cause) { + is com.google.firebase.ai.common.SerializationException -> + SerializationException(cause.message ?: "", cause.cause) + is com.google.firebase.ai.common.ServerException -> + ServerException(cause.message ?: "", cause.cause) + is com.google.firebase.ai.common.InvalidAPIKeyException -> + InvalidAPIKeyException(cause.message ?: "") + is com.google.firebase.ai.common.PromptBlockedException -> + PromptBlockedException(cause.response?.toPublic(), cause.cause) + is com.google.firebase.ai.common.UnsupportedUserLocationException -> + UnsupportedUserLocationException(cause.cause) + is com.google.firebase.ai.common.InvalidStateException -> + InvalidStateException(cause.message ?: "", cause) + is com.google.firebase.ai.common.ResponseStoppedException -> + ResponseStoppedException(cause.response.toPublic(), cause.cause) + is com.google.firebase.ai.common.RequestTimeoutException -> + RequestTimeoutException(cause.message ?: "", cause.cause) + is com.google.firebase.ai.common.ServiceDisabledException -> + ServiceDisabledException(cause.message ?: "", cause.cause) + is com.google.firebase.ai.common.UnknownException -> + UnknownException(cause.message ?: "", cause.cause) + is com.google.firebase.ai.common.ContentBlockedException -> + ContentBlockedException(cause.message ?: "", cause.cause) + is com.google.firebase.ai.common.QuotaExceededException -> + QuotaExceededException(cause.message ?: "", cause.cause) + else -> UnknownException(cause.message ?: "", cause) + } + is TimeoutCancellationException -> + RequestTimeoutException("The request failed to complete in the allotted time.") + else -> UnknownException("Something unexpected happened.", cause) + } + + /** + * Catch any exception thrown in the [callback] block and rethrow it as a [FirebaseAIException]. + * + * Will return whatever the [callback] returns as well. + * + * @see catch + */ + internal suspend fun catchAsync(callback: suspend () -> T): T { + try { + return callback() + } catch (e: Exception) { + throw from(e) + } + } + + /** + * Catch any exception thrown in the [callback] block and rethrow it as a [FirebaseAIException]. + * + * Will return whatever the [callback] returns as well. + * + * @see catchAsync + */ + internal fun catch(callback: () -> T): T { + try { + return callback() + } catch (e: Exception) { + throw from(e) + } + } + } +} + +/** Something went wrong while trying to deserialize a response from the server. */ +public class SerializationException +internal constructor(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + +/** The server responded with a non 200 response code. */ +public class ServerException internal constructor(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + +/** The provided API Key is not valid. */ +public class InvalidAPIKeyException +internal constructor(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + +/** + * A request was blocked. + * + * See the [response's][response] `promptFeedback.blockReason` for more information. + * + * @property response The full server response. + */ +public class PromptBlockedException +internal constructor( + public val response: GenerateContentResponse?, + cause: Throwable? = null, + message: String? = null, +) : + FirebaseAIException( + "Prompt was blocked: ${response?.promptFeedback?.blockReason?.name?: message}", + cause, + ) { + internal constructor(message: String, cause: Throwable? = null) : this(null, cause, message) +} + +public class ContentBlockedException +internal constructor(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + +/** + * The user's location (region) is not supported by the API. + * + * See the documentation for a + * [list of regions](https://firebase.google.com/docs/vertex-ai/locations?platform=android#available-locations) + * (countries and territories) where the API is available. + */ +// TODO(rlazo): Add secondary constructor to pass through the message? +public class UnsupportedUserLocationException internal constructor(cause: Throwable? = null) : + FirebaseAIException("User location is not supported for the API use.", cause) + +/** + * Some form of state occurred that shouldn't have. + * + * Usually indicative of consumer error. + */ +public class InvalidStateException internal constructor(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + +/** + * A request was stopped during generation for some reason. + * + * @property response The full server response. + */ +public class ResponseStoppedException +internal constructor(public val response: GenerateContentResponse, cause: Throwable? = null) : + FirebaseAIException( + "Content generation stopped. Reason: ${response.candidates.first().finishReason?.name}", + cause, + ) + +/** + * A request took too long to complete. + * + * Usually occurs due to a user specified [timeout][RequestOptions.timeout]. + */ +public class RequestTimeoutException +internal constructor(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + +/** + * The specified Vertex AI location is invalid. + * + * For a list of valid locations, see + * [Vertex AI locations.](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#available-regions) + */ +public class InvalidLocationException +internal constructor(location: String, cause: Throwable? = null) : + FirebaseAIException("Invalid location \"${location}\"", cause) + +/** + * The service is not enabled for this Firebase project. Learn how to enable the required services + * in the + * [Firebase documentation.](https://firebase.google.com/docs/vertex-ai/faq-and-troubleshooting#required-apis) + */ +public class ServiceDisabledException +internal constructor(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + +/** + * The request has hit a quota limit. Learn more about quotas in the + * [Firebase documentation.](https://firebase.google.com/docs/vertex-ai/quotas) + */ +public class QuotaExceededException +internal constructor(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + +/** Streaming session already receiving. */ +public class SessionAlreadyReceivingException : + FirebaseAIException( + "This session is already receiving. Please call stopReceiving() before calling this again." + ) + +/** Audio record initialization failures for audio streaming */ +public class AudioRecordInitializationFailedException(message: String) : + FirebaseAIException(message) + +/** Handshake failed with the server */ +public class ServiceConnectionHandshakeFailedException(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + +/** Catch all case for exceptions not explicitly expected. */ +public class UnknownException internal constructor(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionCallingConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionCallingConfig.kt new file mode 100644 index 00000000000..f854de85fd8 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionCallingConfig.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024 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.firebase.ai.type + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * The configuration that specifies the function calling behavior. + * + * See the static methods in the `companion object` for the list of available behaviors. + */ +public class FunctionCallingConfig +internal constructor( + internal val mode: Mode, + internal val allowedFunctionNames: List? = null +) { + + /** Configuration for dictating when the model should call the attached function. */ + internal enum class Mode { + /** + * The default behavior for function calling. The model calls functions to answer queries at its + * discretion + */ + AUTO, + + /** The model always predicts a provided function call to answer every query. */ + ANY, + + /** + * The model will never predict a function call to answer a query. This can also be achieved by + * not passing any tools to the model. + */ + NONE, + } + + @Serializable + internal data class Internal( + val mode: Mode, + @SerialName("allowed_function_names") val allowedFunctionNames: List? = null + ) { + @Serializable + enum class Mode { + @SerialName("MODE_UNSPECIFIED") UNSPECIFIED, + AUTO, + ANY, + NONE, + } + } + + public companion object { + /** + * The default behavior for function calling. The model calls functions to answer queries at its + * discretion. + */ + @JvmStatic public fun auto(): FunctionCallingConfig = FunctionCallingConfig(Mode.AUTO) + + /** The model always predicts a provided function call to answer every query. */ + @JvmStatic + @JvmOverloads + public fun any(allowedFunctionNames: List? = null): FunctionCallingConfig = + FunctionCallingConfig(Mode.ANY, allowedFunctionNames) + + /** + * The model will never predict a function call to answer a query. This can also be achieved by + * not passing any tools to the model. + */ + @JvmStatic public fun none(): FunctionCallingConfig = FunctionCallingConfig(Mode.NONE) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionDeclaration.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionDeclaration.kt new file mode 100644 index 00000000000..2b73d5ccfb1 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionDeclaration.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2024 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.firebase.ai.type + +import kotlinx.serialization.Serializable + +/** + * Defines a function that the model can use as a tool. + * + * When generating responses, the model might need external information or require the application + * to perform an action. `FunctionDeclaration` provides the necessary information for the model to + * create a [FunctionCallPart], which instructs the client to execute the corresponding function. + * The client then sends the result back to the model as a [FunctionResponsePart]. + * + * For example + * + * ``` + * val getExchangeRate = FunctionDeclaration( + * name = "getExchangeRate", + * description = "Get the exchange rate for currencies between countries.", + * parameters = mapOf( + * "currencyFrom" to Schema.str("The currency to convert from."), + * "currencyTo" to Schema.str("The currency to convert to.") + * ) + * ) + * ``` + * + * See the + * [Use the Gemini API for function calling](https://firebase.google.com/docs/vertex-ai/function-calling?platform=android) + * guide for more information on function calling. + * + * @param name The name of the function. + * @param description The description of what the function does and its output. To improve the + * effectiveness of the model, the description should be clear and detailed. + * @param parameters The map of parameters names to their [Schema] the function accepts as + * arguments. + * @param optionalParameters The list of parameter names that the model can omit when invoking this + * function. + * @see Schema + */ +public class FunctionDeclaration( + internal val name: String, + internal val description: String, + internal val parameters: Map, + internal val optionalParameters: List = emptyList(), +) { + internal val schema: Schema = + Schema.obj(properties = parameters, optionalProperties = optionalParameters, nullable = false) + + internal fun toInternal() = Internal(name, description, schema.toInternal()) + + @Serializable + internal data class Internal( + val name: String, + val description: String, + val parameters: Schema.Internal + ) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt new file mode 100644 index 00000000000..be2b50f3be4 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2023 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.firebase.ai.type + +import kotlinx.serialization.Serializable + +/** + * A response from the model. + * + * @property candidates The list of [Candidate] responses generated by the model. + * @property promptFeedback Feedback about the prompt send to the model to generate this response. + * When streaming, it's only populated in the first response. + * @property usageMetadata Information about the number of tokens in the prompt and in the response. + */ +public class GenerateContentResponse( + public val candidates: List, + public val promptFeedback: PromptFeedback?, + public val usageMetadata: UsageMetadata?, +) { + /** + * Convenience field representing all the text parts in the response as a single string, if they + * exists. + */ + public val text: String? by lazy { + candidates.first().content.parts.filterIsInstance().joinToString(" ") { it.text } + } + + /** Convenience field to list all the [FunctionCallPart]s in the response, if they exist. */ + public val functionCalls: List by lazy { + candidates.first().content.parts.filterIsInstance() + } + + /** + * Convenience field representing all the [InlineDataPart]s in the first candidate, if they exist. + * + * This also includes any [ImagePart], but they will be represented as [InlineDataPart] instead. + */ + public val inlineDataParts: List by lazy { + candidates.first().content.parts.let { parts -> + parts.filterIsInstance().map { it.toInlineDataPart() } + + parts.filterIsInstance() + } + } + + @Serializable + internal data class Internal( + val candidates: List? = null, + val promptFeedback: PromptFeedback.Internal? = null, + val usageMetadata: UsageMetadata.Internal? = null, + ) : Response { + internal fun toPublic(): GenerateContentResponse { + return GenerateContentResponse( + candidates?.map { it.toPublic() }.orEmpty(), + promptFeedback?.toPublic(), + usageMetadata?.toPublic() + ) + } + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerationConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerationConfig.kt new file mode 100644 index 00000000000..1c2d2680bb1 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerationConfig.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2023 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.firebase.ai.type + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Configuration parameters to use for content generation. + * + * @property temperature A parameter controlling the degree of randomness in token selection. A + * temperature of 0 means that the highest probability tokens are always selected. In this case, + * responses for a given prompt are mostly deterministic, but a small amount of variation is still + * possible. + * + * @property topK The `topK` parameter changes how the model selects tokens for output. A `topK` of + * 1 means the selected token is the most probable among all the tokens in the model's vocabulary, + * while a `topK` of 3 means that the next token is selected from among the 3 most probable using + * the `temperature`. For each token selection step, the `topK` tokens with the highest + * probabilities are sampled. Tokens are then further filtered based on `topP` with the final token + * selected using `temperature` sampling. Defaults to 40 if unspecified. + * + * @property topP The `topP` parameter changes how the model selects tokens for output. Tokens are + * selected from the most to least probable until the sum of their probabilities equals the `topP` + * value. For example, if tokens A, B, and C have probabilities of 0.3, 0.2, and 0.1 respectively + * and the topP value is 0.5, then the model will select either A or B as the next token by using + * the `temperature` and exclude C as a candidate. Defaults to 0.95 if unset. + * + * @property candidateCount The maximum number of generated response messages to return. This value + * must be between [1, 8], inclusive. If unset, this will default to 1. + * + * - Note: Only unique candidates are returned. Higher temperatures are more likely to produce + * unique candidates. Setting `temperature` to 0 will always produce exactly one candidate + * regardless of the `candidateCount`. + * + * @property presencePenalty Positive penalties. + * + * @property frequencyPenalty Frequency penalties. + * + * @property maxOutputTokens Specifies the maximum number of tokens that can be generated in the + * response. The number of tokens per word varies depending on the language outputted. Defaults to 0 + * (unbounded). + * + * @property stopSequences A set of up to 5 `String`s that will stop output generation. If + * specified, the API will stop at the first appearance of a stop sequence. The stop sequence will + * not be included as part of the response. + * + * @property responseMimeType Output response MIME type of the generated candidate text (IANA + * standard). + * + * Supported MIME types depend on the model used, but could include: + * - `text/plain`: Text output; the default behavior if unspecified. + * - `application/json`: JSON response in the candidates. + * + * @property responseSchema Output schema of the generated candidate text. If set, a compatible + * [responseMimeType] must also be set. + * + * Compatible MIME types: + * - `application/json`: Schema for JSON response. + * + * Refer to the + * [Control generated output](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output) + * guide for more details. + * + * @property responseModalities The format of data in which the model should respond with. + */ +public class GenerationConfig +private constructor( + internal val temperature: Float?, + internal val topK: Int?, + internal val topP: Float?, + internal val candidateCount: Int?, + internal val maxOutputTokens: Int?, + internal val presencePenalty: Float?, + internal val frequencyPenalty: Float?, + internal val stopSequences: List?, + internal val responseMimeType: String?, + internal val responseSchema: Schema?, + internal val responseModalities: List?, +) { + + /** + * Builder for creating a [GenerationConfig]. + * + * Mainly intended for Java interop. Kotlin consumers should use [generationConfig] for a more + * idiomatic experience. + * + * @property temperature See [GenerationConfig.temperature]. + * + * @property topK See [GenerationConfig.topK]. + * + * @property topP See [GenerationConfig.topP]. + * + * @property presencePenalty See [GenerationConfig.presencePenalty] + * + * @property frequencyPenalty See [GenerationConfig.frequencyPenalty] + * + * @property candidateCount See [GenerationConfig.candidateCount]. + * + * @property maxOutputTokens See [GenerationConfig.maxOutputTokens]. + * + * @property stopSequences See [GenerationConfig.stopSequences]. + * + * @property responseMimeType See [GenerationConfig.responseMimeType]. + * + * @property responseSchema See [GenerationConfig.responseSchema]. + * + * @property responseModalities See [GenerationConfig.responseModalities]. + * + * @see [generationConfig] + */ + public class Builder { + @JvmField public var temperature: Float? = null + @JvmField public var topK: Int? = null + @JvmField public var topP: Float? = null + @JvmField public var candidateCount: Int? = null + @JvmField public var maxOutputTokens: Int? = null + @JvmField public var presencePenalty: Float? = null + @JvmField public var frequencyPenalty: Float? = null + @JvmField public var stopSequences: List? = null + @JvmField public var responseMimeType: String? = null + @JvmField public var responseSchema: Schema? = null + @JvmField public var responseModalities: List? = null + + public fun setTemperature(temperature: Float?): Builder = apply { + this.temperature = temperature + } + public fun setTopK(topK: Int?): Builder = apply { this.topK = topK } + public fun setTopP(topP: Float?): Builder = apply { this.topP = topP } + public fun setCandidateCount(candidateCount: Int?): Builder = apply { + this.candidateCount = candidateCount + } + public fun setMaxOutputTokens(maxOutputTokens: Int?): Builder = apply { + this.maxOutputTokens = maxOutputTokens + } + public fun setPresencePenalty(presencePenalty: Float?): Builder = apply { + this.presencePenalty = presencePenalty + } + public fun setFrequencyPenalty(frequencyPenalty: Float?): Builder = apply { + this.frequencyPenalty = frequencyPenalty + } + public fun setStopSequences(stopSequences: List?): Builder = apply { + this.stopSequences = stopSequences + } + public fun setResponseMimeType(responseMimeType: String?): Builder = apply { + this.responseMimeType = responseMimeType + } + public fun setResponseSchema(responseSchema: Schema?): Builder = apply { + this.responseSchema = responseSchema + } + public fun setResponseModalities(responseModalities: List?): Builder = apply { + this.responseModalities = responseModalities + } + + /** Create a new [GenerationConfig] with the attached arguments. */ + public fun build(): GenerationConfig = + GenerationConfig( + temperature = temperature, + topK = topK, + topP = topP, + candidateCount = candidateCount, + maxOutputTokens = maxOutputTokens, + stopSequences = stopSequences, + presencePenalty = presencePenalty, + frequencyPenalty = frequencyPenalty, + responseMimeType = responseMimeType, + responseSchema = responseSchema, + responseModalities = responseModalities + ) + } + + internal fun toInternal() = + Internal( + temperature = temperature, + topP = topP, + topK = topK, + candidateCount = candidateCount, + maxOutputTokens = maxOutputTokens, + stopSequences = stopSequences, + frequencyPenalty = frequencyPenalty, + presencePenalty = presencePenalty, + responseMimeType = responseMimeType, + responseSchema = responseSchema?.toInternal(), + responseModalities = responseModalities?.map { it.toInternal() } + ) + + @Serializable + internal data class Internal( + val temperature: Float?, + @SerialName("top_p") val topP: Float?, + @SerialName("top_k") val topK: Int?, + @SerialName("candidate_count") val candidateCount: Int?, + @SerialName("max_output_tokens") val maxOutputTokens: Int?, + @SerialName("stop_sequences") val stopSequences: List?, + @SerialName("response_mime_type") val responseMimeType: String? = null, + @SerialName("presence_penalty") val presencePenalty: Float? = null, + @SerialName("frequency_penalty") val frequencyPenalty: Float? = null, + @SerialName("response_schema") val responseSchema: Schema.Internal? = null, + @SerialName("response_modalities") val responseModalities: List? = null + ) + + public companion object { + + /** + * Alternative casing for [GenerationConfig.Builder]: + * ``` + * val config = GenerationConfig.builder() + * ``` + */ + public fun builder(): Builder = Builder() + } +} + +/** + * Helper method to construct a [GenerationConfig] in a DSL-like manner. + * + * Example Usage: + * ``` + * generationConfig { + * temperature = 0.75f + * topP = 0.5f + * topK = 30 + * candidateCount = 4 + * maxOutputTokens = 300 + * stopSequences = listOf("in conclusion", "-----", "do you need") + * } + * ``` + */ +public fun generationConfig(init: GenerationConfig.Builder.() -> Unit): GenerationConfig { + val builder = GenerationConfig.builder() + builder.init() + return builder.build() +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerativeBackend.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerativeBackend.kt new file mode 100644 index 00000000000..9598266fa68 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerativeBackend.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2025 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.firebase.ai.type + +/** Represents a reference to a backend for generative AI. */ +public class GenerativeBackend +internal constructor(internal val location: String, internal val backend: GenerativeBackendEnum) { + public companion object { + + /** References the Google Developer API backend. */ + @JvmStatic + public fun googleAI(): GenerativeBackend = + GenerativeBackend("", GenerativeBackendEnum.GOOGLE_AI) + + /** + * References the VertexAI Enterprise backend. + * + * @param location passes a valid cloud server location, defaults to "us-central1" + */ + @JvmStatic + @JvmOverloads + public fun vertexAI(location: String = "us-central1"): GenerativeBackend { + if (location.isBlank() || location.contains("/")) { + throw InvalidLocationException(location) + } + return GenerativeBackend(location, GenerativeBackendEnum.VERTEX_AI) + } + } +} + +internal enum class GenerativeBackendEnum { + GOOGLE_AI, + VERTEX_AI, +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockMethod.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockMethod.kt new file mode 100644 index 00000000000..a65d153bc53 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockMethod.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024 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.firebase.ai.type + +import com.google.firebase.ai.common.makeMissingCaseException +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Specifies how the block method computes the score that will be compared against the + * [HarmBlockThreshold] in [SafetySetting]. + */ +public class HarmBlockMethod private constructor(public val ordinal: Int) { + internal fun toInternal() = + when (this) { + SEVERITY -> Internal.SEVERITY + PROBABILITY -> Internal.PROBABILITY + else -> throw makeMissingCaseException("HarmBlockMethod", ordinal) + } + + @Serializable + internal enum class Internal { + @SerialName("HARM_BLOCK_METHOD_UNSPECIFIED") UNSPECIFIED, + SEVERITY, + PROBABILITY, + } + public companion object { + /** + * The harm block method uses both probability and severity scores. See [HarmSeverity] and + * [HarmProbability]. + */ + @JvmField public val SEVERITY: HarmBlockMethod = HarmBlockMethod(0) + + /** The harm block method uses the probability score. See [HarmProbability]. */ + @JvmField public val PROBABILITY: HarmBlockMethod = HarmBlockMethod(1) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockThreshold.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockThreshold.kt new file mode 100644 index 00000000000..93ebfde5da9 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockThreshold.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2023 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.firebase.ai.type + +import com.google.firebase.ai.common.makeMissingCaseException +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Represents the threshold for a [HarmCategory] to be allowed by [SafetySetting]. */ +public class HarmBlockThreshold private constructor(public val ordinal: Int) { + + internal fun toInternal() = + when (this) { + OFF -> Internal.OFF + NONE -> Internal.BLOCK_NONE + ONLY_HIGH -> Internal.BLOCK_ONLY_HIGH + MEDIUM_AND_ABOVE -> Internal.BLOCK_MEDIUM_AND_ABOVE + LOW_AND_ABOVE -> Internal.BLOCK_LOW_AND_ABOVE + else -> throw makeMissingCaseException("HarmBlockThreshold", ordinal) + } + + @Serializable + internal enum class Internal { + @SerialName("HARM_BLOCK_THRESHOLD_UNSPECIFIED") UNSPECIFIED, + BLOCK_LOW_AND_ABOVE, + BLOCK_MEDIUM_AND_ABOVE, + BLOCK_ONLY_HIGH, + BLOCK_NONE, + OFF + } + + public companion object { + /** Content with negligible harm is allowed. */ + @JvmField public val LOW_AND_ABOVE: HarmBlockThreshold = HarmBlockThreshold(0) + + /** Content with negligible to low harm is allowed. */ + @JvmField public val MEDIUM_AND_ABOVE: HarmBlockThreshold = HarmBlockThreshold(1) + + /** Content with negligible to medium harm is allowed. */ + @JvmField public val ONLY_HIGH: HarmBlockThreshold = HarmBlockThreshold(2) + + /** All content is allowed regardless of harm. */ + @JvmField public val NONE: HarmBlockThreshold = HarmBlockThreshold(3) + + /** + * All content is allowed regardless of harm. + * + * The same as [NONE], but metadata when the corresponding [HarmCategory] occurs will **NOT** be + * present in the response. + */ + @JvmField public val OFF: HarmBlockThreshold = HarmBlockThreshold(4) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmCategory.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmCategory.kt new file mode 100644 index 00000000000..5144058ab44 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmCategory.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2023 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.firebase.ai.type + +import com.google.firebase.ai.common.makeMissingCaseException +import com.google.firebase.ai.common.util.FirstOrdinalSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Category for a given harm rating. */ +public class HarmCategory private constructor(public val ordinal: Int) { + internal fun toInternal() = + when (this) { + HARASSMENT -> Internal.HARASSMENT + HATE_SPEECH -> Internal.HATE_SPEECH + SEXUALLY_EXPLICIT -> Internal.SEXUALLY_EXPLICIT + DANGEROUS_CONTENT -> Internal.DANGEROUS_CONTENT + CIVIC_INTEGRITY -> Internal.CIVIC_INTEGRITY + UNKNOWN -> Internal.UNKNOWN + else -> throw makeMissingCaseException("HarmCategory", ordinal) + } + @Serializable(Internal.Serializer::class) + internal enum class Internal { + UNKNOWN, + @SerialName("HARM_CATEGORY_HARASSMENT") HARASSMENT, + @SerialName("HARM_CATEGORY_HATE_SPEECH") HATE_SPEECH, + @SerialName("HARM_CATEGORY_SEXUALLY_EXPLICIT") SEXUALLY_EXPLICIT, + @SerialName("HARM_CATEGORY_DANGEROUS_CONTENT") DANGEROUS_CONTENT, + @SerialName("HARM_CATEGORY_CIVIC_INTEGRITY") CIVIC_INTEGRITY; + + internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class) + + internal fun toPublic() = + when (this) { + HARASSMENT -> HarmCategory.HARASSMENT + HATE_SPEECH -> HarmCategory.HATE_SPEECH + SEXUALLY_EXPLICIT -> HarmCategory.SEXUALLY_EXPLICIT + DANGEROUS_CONTENT -> HarmCategory.DANGEROUS_CONTENT + CIVIC_INTEGRITY -> HarmCategory.CIVIC_INTEGRITY + else -> HarmCategory.UNKNOWN + } + } + public companion object { + /** A new and not yet supported value. */ + @JvmField public val UNKNOWN: HarmCategory = HarmCategory(0) + + /** Harassment content. */ + @JvmField public val HARASSMENT: HarmCategory = HarmCategory(1) + + /** Hate speech and content. */ + @JvmField public val HATE_SPEECH: HarmCategory = HarmCategory(2) + + /** Sexually explicit content. */ + @JvmField public val SEXUALLY_EXPLICIT: HarmCategory = HarmCategory(3) + + /** Dangerous content. */ + @JvmField public val DANGEROUS_CONTENT: HarmCategory = HarmCategory(4) + + /** Content that may be used to harm civic integrity. */ + @JvmField public val CIVIC_INTEGRITY: HarmCategory = HarmCategory(5) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmProbability.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmProbability.kt new file mode 100644 index 00000000000..e586b94d5b5 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmProbability.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2023 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.firebase.ai.type + +import com.google.firebase.ai.common.util.FirstOrdinalSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Represents the probability that some [HarmCategory] is applicable in a [SafetyRating]. */ +public class HarmProbability private constructor(public val ordinal: Int) { + @Serializable(Internal.Serializer::class) + internal enum class Internal { + UNKNOWN, + @SerialName("HARM_PROBABILITY_UNSPECIFIED") UNSPECIFIED, + NEGLIGIBLE, + LOW, + MEDIUM, + HIGH; + + internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class) + + internal fun toPublic() = + when (this) { + HIGH -> HarmProbability.HIGH + MEDIUM -> HarmProbability.MEDIUM + LOW -> HarmProbability.LOW + NEGLIGIBLE -> HarmProbability.NEGLIGIBLE + else -> HarmProbability.UNKNOWN + } + } + public companion object { + /** A new and not yet supported value. */ + @JvmField public val UNKNOWN: HarmProbability = HarmProbability(0) + + /** Probability for harm is negligible. */ + @JvmField public val NEGLIGIBLE: HarmProbability = HarmProbability(1) + + /** Probability for harm is low. */ + @JvmField public val LOW: HarmProbability = HarmProbability(2) + + /** Probability for harm is medium. */ + @JvmField public val MEDIUM: HarmProbability = HarmProbability(3) + + /** Probability for harm is high. */ + @JvmField public val HIGH: HarmProbability = HarmProbability(4) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmSeverity.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmSeverity.kt new file mode 100644 index 00000000000..36fa5d1b8c1 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmSeverity.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2024 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.firebase.ai.type + +import com.google.firebase.ai.common.util.FirstOrdinalSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Represents the severity of a [HarmCategory] being applicable in a [SafetyRating]. */ +public class HarmSeverity private constructor(public val ordinal: Int) { + @Serializable(Internal.Serializer::class) + internal enum class Internal { + UNKNOWN, + @SerialName("HARM_SEVERITY_UNSPECIFIED") UNSPECIFIED, + @SerialName("HARM_SEVERITY_NEGLIGIBLE") NEGLIGIBLE, + @SerialName("HARM_SEVERITY_LOW") LOW, + @SerialName("HARM_SEVERITY_MEDIUM") MEDIUM, + @SerialName("HARM_SEVERITY_HIGH") HIGH; + + internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class) + + internal fun toPublic() = + when (this) { + HIGH -> HarmSeverity.HIGH + MEDIUM -> HarmSeverity.MEDIUM + LOW -> HarmSeverity.LOW + NEGLIGIBLE -> HarmSeverity.NEGLIGIBLE + else -> HarmSeverity.UNKNOWN + } + } + public companion object { + /** A new and not yet supported value. */ + @JvmField public val UNKNOWN: HarmSeverity = HarmSeverity(0) + + /** Severity for harm is negligible. */ + @JvmField public val NEGLIGIBLE: HarmSeverity = HarmSeverity(1) + + /** Low level of harm severity. */ + @JvmField public val LOW: HarmSeverity = HarmSeverity(2) + + /** Medium level of harm severity. */ + @JvmField public val MEDIUM: HarmSeverity = HarmSeverity(3) + + /** High level of harm severity. */ + @JvmField public val HIGH: HarmSeverity = HarmSeverity(4) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenAspectRatio.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenAspectRatio.kt new file mode 100644 index 00000000000..10a8b4fed84 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenAspectRatio.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 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.firebase.ai.type + +/** Represents the aspect ratio that the generated image should conform to. */ +@PublicPreviewAPI +public class ImagenAspectRatio private constructor(internal val internalVal: String) { + public companion object { + /** A square image, useful for icons, profile pictures, etc. */ + @JvmField public val SQUARE_1x1: ImagenAspectRatio = ImagenAspectRatio("1:1") + /** A portrait image in 3:4, the aspect ratio of older TVs. */ + @JvmField public val PORTRAIT_3x4: ImagenAspectRatio = ImagenAspectRatio("3:4") + /** A landscape image in 4:3, the aspect ratio of older TVs. */ + @JvmField public val LANDSCAPE_4x3: ImagenAspectRatio = ImagenAspectRatio("4:3") + /** A portrait image in 9:16, the aspect ratio of modern monitors and phone screens. */ + @JvmField public val PORTRAIT_9x16: ImagenAspectRatio = ImagenAspectRatio("9:16") + /** A landscape image in 16:9, the aspect ratio of modern monitors and phone screens. */ + @JvmField public val LANDSCAPE_16x9: ImagenAspectRatio = ImagenAspectRatio("16:9") + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGCSImage.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGCSImage.kt new file mode 100644 index 00000000000..6e2466da496 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGCSImage.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 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.firebase.ai.type + +/** + * Represents an Imagen-generated image that is contained in Google Cloud Storage. + * + * @param gcsUri Contains the `gs://` URI for the image. + * @param mimeType Contains the MIME type of the image (for example, `"image/png"`). + */ +@PublicPreviewAPI +internal class ImagenGCSImage +internal constructor(public val gcsUri: String, public val mimeType: String) {} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationConfig.kt new file mode 100644 index 00000000000..b59ed4d0e44 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationConfig.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2025 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.firebase.ai.type + +/** + * Contains extra settings to configure image generation. + * + * @param negativePrompt This string contains things that should be explicitly excluded from + * generated images. + * @param numberOfImages How many images should be generated. + * @param aspectRatio The aspect ratio of the generated images. + * @param imageFormat The file format/compression of the generated images. + * @param addWatermark Adds an invisible watermark to mark the image as AI generated. + */ +import kotlin.jvm.JvmField + +@PublicPreviewAPI +public class ImagenGenerationConfig( + public val negativePrompt: String? = null, + public val numberOfImages: Int? = 1, + public val aspectRatio: ImagenAspectRatio? = null, + public val imageFormat: ImagenImageFormat? = null, + public val addWatermark: Boolean? = null, +) { + /** + * Builder for creating a [ImagenGenerationConfig]. + * + * This is mainly intended for Java interop. For Kotlin, use [imagenGenerationConfig] for a more + * idiomatic experience. + */ + public class Builder { + @JvmField public var negativePrompt: String? = null + @JvmField public var numberOfImages: Int? = 1 + @JvmField public var aspectRatio: ImagenAspectRatio? = null + @JvmField public var imageFormat: ImagenImageFormat? = null + @JvmField public var addWatermark: Boolean? = null + + /** See [ImagenGenerationConfig.negativePrompt]. */ + public fun setNegativePrompt(negativePrompt: String): Builder = apply { + this.negativePrompt = negativePrompt + } + + /** See [ImagenGenerationConfig.numberOfImages]. */ + public fun setNumberOfImages(numberOfImages: Int): Builder = apply { + this.numberOfImages = numberOfImages + } + + /** See [ImagenGenerationConfig.aspectRatio]. */ + public fun setAspectRatio(aspectRatio: ImagenAspectRatio): Builder = apply { + this.aspectRatio = aspectRatio + } + + /** See [ImagenGenerationConfig.imageFormat]. */ + public fun setImageFormat(imageFormat: ImagenImageFormat): Builder = apply { + this.imageFormat = imageFormat + } + + /** See [ImagenGenerationConfig.addWatermark]. */ + public fun setAddWatermark(addWatermark: Boolean): Builder = apply { + this.addWatermark = addWatermark + } + + /** + * Alternative casing for [ImagenGenerationConfig.Builder]: + * ``` + * val config = GenerationConfig.builder() + * ``` + */ + public fun build(): ImagenGenerationConfig = + ImagenGenerationConfig( + negativePrompt = negativePrompt, + numberOfImages = numberOfImages, + aspectRatio = aspectRatio, + imageFormat = imageFormat, + addWatermark = addWatermark, + ) + } + + public companion object { + public fun builder(): Builder = Builder() + } +} + +/** + * Helper method to construct a [ImagenGenerationConfig] in a DSL-like manner. + * + * Example Usage: + * ``` + * imagenGenerationConfig { + * negativePrompt = "People, black and white, painting" + * numberOfImages = 1 + * aspectRatio = ImagenAspecRatio.SQUARE_1x1 + * imageFormat = ImagenImageFormat.png() + * addWatermark = false + * } + * ``` + */ +@PublicPreviewAPI +public fun imagenGenerationConfig( + init: ImagenGenerationConfig.Builder.() -> Unit +): ImagenGenerationConfig { + val builder = ImagenGenerationConfig.builder() + builder.init() + return builder.build() +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationResponse.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationResponse.kt new file mode 100644 index 00000000000..67f13cff199 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationResponse.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import android.util.Base64 +import com.google.firebase.ai.ImagenModel +import kotlinx.serialization.Serializable + +/** + * Represents a response from a call to [ImagenModel.generateImages] + * + * @param images contains the generated images + * @param filteredReason if fewer images were generated than were requested, this field will contain + * the reason they were filtered out. + */ +@PublicPreviewAPI +public class ImagenGenerationResponse +internal constructor(public val images: List, public val filteredReason: String?) { + + @Serializable + internal data class Internal(val predictions: List) { + internal fun toPublicGCS() = + ImagenGenerationResponse( + images = predictions.filter { it.mimeType != null }.map { it.toPublicGCS() }, + null, + ) + + internal fun toPublicInline() = + ImagenGenerationResponse( + images = predictions.filter { it.mimeType != null }.map { it.toPublicInline() }, + null, + ) + } + + @Serializable + internal data class ImagenImageResponse( + val bytesBase64Encoded: String? = null, + val gcsUri: String? = null, + val mimeType: String? = null, + val raiFilteredReason: String? = null, + ) { + internal fun toPublicInline() = + ImagenInlineImage(Base64.decode(bytesBase64Encoded!!, Base64.NO_WRAP), mimeType!!) + + internal fun toPublicGCS() = ImagenGCSImage(gcsUri!!, mimeType!!) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenImageFormat.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenImageFormat.kt new file mode 100644 index 00000000000..014763fd54c --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenImageFormat.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import kotlinx.serialization.Serializable + +/** + * Represents the format an image should be returned in. + * + * @param mimeType A string (like `"image/jpeg"`) specifying the encoding MIME type of the image. + * @param compressionQuality an int (1-100) representing the quality of the image; a lower number + * means the image is permitted to be lower quality to reduce size. This parameter is not relevant + * for every MIME type. + */ +@PublicPreviewAPI +public class ImagenImageFormat +private constructor(public val mimeType: String, public val compressionQuality: Int?) { + + internal fun toInternal() = Internal(mimeType, compressionQuality) + + @Serializable internal data class Internal(val mimeType: String, val compressionQuality: Int?) + + public companion object { + /** + * An [ImagenImageFormat] representing a JPEG image. + * + * @param compressionQuality an int (1-100) representing the quality of the image; a lower + * number means the image is permitted to be lower quality to reduce size. + */ + @JvmStatic + public fun jpeg(compressionQuality: Int? = null): ImagenImageFormat { + return ImagenImageFormat("image/jpeg", compressionQuality) + } + + /** An [ImagenImageFormat] representing a PNG image */ + @JvmStatic + public fun png(): ImagenImageFormat { + return ImagenImageFormat("image/png", null) + } + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenInlineImage.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenInlineImage.kt new file mode 100644 index 00000000000..5fa1d0e183b --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenInlineImage.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import android.graphics.Bitmap +import android.graphics.BitmapFactory + +/** + * Represents an Imagen-generated image that is returned as inline data. + * + * @property data The raw image bytes in JPEG or PNG format, as specified by [mimeType]. + * @property mimeType The IANA standard MIME type of the image data; either `"image/png"` or + * `"image/jpeg"`; to request a different format, see [ImagenGenerationConfig.imageFormat]. + */ +@PublicPreviewAPI +public class ImagenInlineImage +internal constructor(public val data: ByteArray, public val mimeType: String) { + + /** + * Returns the image as an Android OS native [Bitmap] so that it can be saved or sent to the UI. + */ + public fun asBitmap(): Bitmap { + return BitmapFactory.decodeByteArray(data, 0, data.size) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenPersonFilterLevel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenPersonFilterLevel.kt new file mode 100644 index 00000000000..5daa354bbbd --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenPersonFilterLevel.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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.firebase.ai.type + +/** A filter used to prevent images from containing depictions of children or people. */ +@PublicPreviewAPI +public class ImagenPersonFilterLevel private constructor(internal val internalVal: String) { + public companion object { + /** + * Allow generation of images containing people of all ages. + * + * > Important: Generation of images containing people or faces may require your use case to be + * reviewed and approved by Cloud support; see the + * [Responsible AI and usage + * guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen) + * for more details. + */ + @JvmField public val ALLOW_ALL: ImagenPersonFilterLevel = ImagenPersonFilterLevel("allow_all") + /** + * Allow generation of images containing adults only; images of children are filtered out. + * + * > Important: Generation of images containing people or faces may require your use case to be + * reviewed and approved by Cloud support; see the + * [Responsible AI and usage + * guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen) + * for more details. + */ + @JvmField + public val ALLOW_ADULT: ImagenPersonFilterLevel = ImagenPersonFilterLevel("allow_adult") + /** + * Disallow generation of images containing people or faces; images of people are filtered out. + */ + @JvmField public val BLOCK_ALL: ImagenPersonFilterLevel = ImagenPersonFilterLevel("dont_allow") + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetyFilterLevel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetyFilterLevel.kt new file mode 100644 index 00000000000..90872d5a15d --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetyFilterLevel.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 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.firebase.ai.type + +/** Used for safety filtering. */ +@PublicPreviewAPI +public class ImagenSafetyFilterLevel private constructor(internal val internalVal: String) { + public companion object { + /** Strongest filtering level, most strict blocking. */ + @JvmField + public val BLOCK_LOW_AND_ABOVE: ImagenSafetyFilterLevel = + ImagenSafetyFilterLevel("block_low_and_above") + /** Block some problematic prompts and responses. */ + @JvmField + public val BLOCK_MEDIUM_AND_ABOVE: ImagenSafetyFilterLevel = + ImagenSafetyFilterLevel("block_medium_and_above") + /** + * Reduces the number of requests blocked due to safety filters. May increase objectionable + * content generated by the Imagen model. + */ + @JvmField + public val BLOCK_ONLY_HIGH: ImagenSafetyFilterLevel = ImagenSafetyFilterLevel("block_only_high") + /** Turns off all optional safety filters. */ + @JvmField public val BLOCK_NONE: ImagenSafetyFilterLevel = ImagenSafetyFilterLevel("block_none") + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetySettings.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetySettings.kt new file mode 100644 index 00000000000..7496fc6fcff --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetySettings.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 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.firebase.ai.type + +/** + * A configuration for filtering unsafe content or images containing people. + * + * @param safetyFilterLevel Used to filter unsafe content. + * @param personFilterLevel Used to filter images containing people. + */ +@PublicPreviewAPI +public class ImagenSafetySettings( + internal val safetyFilterLevel: ImagenSafetyFilterLevel, + internal val personFilterLevel: ImagenPersonFilterLevel, +) {} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveClientSetupMessage.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveClientSetupMessage.kt new file mode 100644 index 00000000000..36e06b184e8 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveClientSetupMessage.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable + +/** + * First message in a live session. + * + * Contains configuration that will be used for the duration of the session. + */ +@OptIn(ExperimentalSerializationApi::class) +@PublicPreviewAPI +internal class LiveClientSetupMessage( + val model: String, + // Some config options are supported in generateContent but not in bidi and vise versa; so bidi + // needs its own config class + val generationConfig: LiveGenerationConfig.Internal?, + val tools: List?, + val systemInstruction: Content.Internal? +) { + @Serializable + internal class Internal(val setup: LiveClientSetup) { + @Serializable + internal data class LiveClientSetup( + val model: String, + val generationConfig: LiveGenerationConfig.Internal?, + val tools: List?, + val systemInstruction: Content.Internal? + ) + } + + fun toInternal() = + Internal(Internal.LiveClientSetup(model, generationConfig, tools, systemInstruction)) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveGenerationConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveGenerationConfig.kt new file mode 100644 index 00000000000..3ded9338f9b --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveGenerationConfig.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Configuration parameters to use for live content generation. + * + * @property temperature A parameter controlling the degree of randomness in token selection. A + * temperature of 0 means that the highest probability tokens are always selected. In this case, + * responses for a given prompt are mostly deterministic, but a small amount of variation is still + * possible. + * + * @property topK The `topK` parameter changes how the model selects tokens for output. A `topK` of + * 1 means the selected token is the most probable among all the tokens in the model's vocabulary, + * while a `topK` of 3 means that the next token is selected from among the 3 most probable using + * the `temperature`. For each token selection step, the `topK` tokens with the highest + * probabilities are sampled. Tokens are then further filtered based on `topP` with the final token + * selected using `temperature` sampling. Defaults to 40 if unspecified. + * + * @property topP The `topP` parameter changes how the model selects tokens for output. Tokens are + * selected from the most to least probable until the sum of their probabilities equals the `topP` + * value. For example, if tokens A, B, and C have probabilities of 0.3, 0.2, and 0.1 respectively + * and the topP value is 0.5, then the model will select either A or B as the next token by using + * the `temperature` and exclude C as a candidate. Defaults to 0.95 if unset. + * + * @property candidateCount The maximum number of generated response messages to return. This value + * must be between [1, 8], inclusive. If unset, this will default to 1. + * + * - Note: Only unique candidates are returned. Higher temperatures are more likely to produce + * unique candidates. Setting `temperature` to 0 will always produce exactly one candidate + * regardless of the `candidateCount`. + * + * @property presencePenalty Positive penalties. + * + * @property frequencyPenalty Frequency penalties. + * + * @property maxOutputTokens Specifies the maximum number of tokens that can be generated in the + * response. The number of tokens per word varies depending on the language outputted. Defaults to 0 + * (unbounded). + * + * @property responseModality Specifies the format of the data in which the server responds to + * requests + * + * @property speechConfig Specifies the voice configuration of the audio response from the server. + * + * Refer to the + * [Control generated output](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output) + * guide for more details. + */ +@PublicPreviewAPI +public class LiveGenerationConfig +private constructor( + internal val temperature: Float?, + internal val topK: Int?, + internal val topP: Float?, + internal val candidateCount: Int?, + internal val maxOutputTokens: Int?, + internal val presencePenalty: Float?, + internal val frequencyPenalty: Float?, + internal val responseModality: ResponseModality?, + internal val speechConfig: SpeechConfig? +) { + + /** + * Builder for creating a [LiveGenerationConfig]. + * + * Mainly intended for Java interop. Kotlin consumers should use [liveGenerationConfig] for a more + * idiomatic experience. + * + * @property temperature See [LiveGenerationConfig.temperature]. + * + * @property topK See [LiveGenerationConfig.topK]. + * + * @property topP See [LiveGenerationConfig.topP]. + * + * @property presencePenalty See [LiveGenerationConfig.presencePenalty] + * + * @property frequencyPenalty See [LiveGenerationConfig.frequencyPenalty] + * + * @property candidateCount See [LiveGenerationConfig.candidateCount]. + * + * @property maxOutputTokens See [LiveGenerationConfig.maxOutputTokens]. + * + * @property responseModality See [LiveGenerationConfig.responseModality] + * + * @property speechConfig See [LiveGenerationConfig.speechConfig] + */ + public class Builder { + @JvmField public var temperature: Float? = null + @JvmField public var topK: Int? = null + @JvmField public var topP: Float? = null + @JvmField public var candidateCount: Int? = null + @JvmField public var maxOutputTokens: Int? = null + @JvmField public var presencePenalty: Float? = null + @JvmField public var frequencyPenalty: Float? = null + @JvmField public var responseModality: ResponseModality? = null + @JvmField public var speechConfig: SpeechConfig? = null + + public fun setTemperature(temperature: Float?): Builder = apply { + this.temperature = temperature + } + public fun setTopK(topK: Int?): Builder = apply { this.topK = topK } + public fun setTopP(topP: Float?): Builder = apply { this.topP = topP } + public fun setCandidateCount(candidateCount: Int?): Builder = apply { + this.candidateCount = candidateCount + } + public fun setMaxOutputTokens(maxOutputTokens: Int?): Builder = apply { + this.maxOutputTokens = maxOutputTokens + } + public fun setPresencePenalty(presencePenalty: Float?): Builder = apply { + this.presencePenalty = presencePenalty + } + public fun setFrequencyPenalty(frequencyPenalty: Float?): Builder = apply { + this.frequencyPenalty = frequencyPenalty + } + public fun setResponseModality(responseModality: ResponseModality?): Builder = apply { + this.responseModality = responseModality + } + public fun setSpeechConfig(speechConfig: SpeechConfig?): Builder = apply { + this.speechConfig = speechConfig + } + + /** Create a new [LiveGenerationConfig] with the attached arguments. */ + public fun build(): LiveGenerationConfig = + LiveGenerationConfig( + temperature = temperature, + topK = topK, + topP = topP, + candidateCount = candidateCount, + maxOutputTokens = maxOutputTokens, + presencePenalty = presencePenalty, + frequencyPenalty = frequencyPenalty, + speechConfig = speechConfig, + responseModality = responseModality + ) + } + + internal fun toInternal(): Internal { + return Internal( + temperature = temperature, + topP = topP, + topK = topK, + candidateCount = candidateCount, + maxOutputTokens = maxOutputTokens, + frequencyPenalty = frequencyPenalty, + presencePenalty = presencePenalty, + speechConfig = speechConfig?.toInternal(), + responseModalities = + if (responseModality != null) listOf(responseModality.toInternal()) else null + ) + } + + @Serializable + internal data class Internal( + val temperature: Float?, + @SerialName("top_p") val topP: Float?, + @SerialName("top_k") val topK: Int?, + @SerialName("candidate_count") val candidateCount: Int?, + @SerialName("max_output_tokens") val maxOutputTokens: Int?, + @SerialName("presence_penalty") val presencePenalty: Float? = null, + @SerialName("frequency_penalty") val frequencyPenalty: Float? = null, + @SerialName("speech_config") val speechConfig: SpeechConfig.Internal? = null, + @SerialName("response_modalities") val responseModalities: List? = null + ) + + public companion object { + + /** + * Alternative casing for [LiveGenerationConfig.Builder]: + * ``` + * val config = LiveGenerationConfig.builder() + * ``` + */ + public fun builder(): Builder = Builder() + } +} + +/** + * Helper method to construct a [LiveGenerationConfig] in a DSL-like manner. + * + * Example Usage: + * ``` + * liveGenerationConfig { + * temperature = 0.75f + * topP = 0.5f + * topK = 30 + * candidateCount = 4 + * maxOutputTokens = 300 + * ... + * } + * ``` + */ +@OptIn(PublicPreviewAPI::class) +public fun liveGenerationConfig( + init: LiveGenerationConfig.Builder.() -> Unit +): LiveGenerationConfig { + val builder = LiveGenerationConfig.builder() + builder.init() + return builder.build() +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveServerMessage.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveServerMessage.kt new file mode 100644 index 00000000000..5ab520af474 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveServerMessage.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject + +/** + * Parent interface for responses from the model during live interactions. + * + * @see LiveServerContent + * @see LiveServerToolCall + * @see LiveServerToolCallCancellation + * @see LiveServerSetupComplete + */ +@PublicPreviewAPI public interface LiveServerMessage + +/** + * Incremental server update generated by the model in response to client messages. + * + * Content is generated as quickly as possible, and not in realtime. You may choose to buffer and + * play it out in realtime. + */ +@PublicPreviewAPI +public class LiveServerContent( + /** + * The content that the model has generated as part of the current conversation with the user. + * + * This can be `null` if there is no content. + */ + public val content: Content?, + + /** + * The model was interrupted by the client while generating data. + * + * An interruption occurs when the client sends a message while the model is actively sending + * data. + */ + public val interrupted: Boolean, + + /** + * The model has finished sending data in the current turn. + * + * Generation will only start in response to additional client messages. + * + * Can be set alongside [content], indicating that the [content] is the last in the turn. + * + * @see generationComplete + */ + public val turnComplete: Boolean, + + /** + * The model has finished _generating_ data for the current turn. + * + * For realtime playback, there will be a delay between when the model finishes generating content + * and the client has finished playing back the generated content. [generationComplete] indicates + * that the model is done generating data, while [turnComplete] indicates the model is waiting for + * additional client messages. Sending a message during this delay may cause an [interrupted] + * message to be sent. + * + * Note that if the model was [interrupted], this will not be set. The model will go from + * [interrupted] -> [turnComplete]. + */ + public val generationComplete: Boolean, +) : LiveServerMessage { + @OptIn(ExperimentalSerializationApi::class) + @Serializable + internal data class Internal( + val modelTurn: Content.Internal? = null, + val interrupted: Boolean = false, + val turnComplete: Boolean = false, + val generationComplete: Boolean = false + ) + @Serializable + internal data class InternalWrapper(val serverContent: Internal) : InternalLiveServerMessage { + @OptIn(ExperimentalSerializationApi::class) + override fun toPublic() = + LiveServerContent( + serverContent.modelTurn?.toPublic(), + serverContent.interrupted, + serverContent.turnComplete, + serverContent.generationComplete + ) + } +} + +/** The model is ready to receive client messages. */ +@PublicPreviewAPI +public class LiveServerSetupComplete : LiveServerMessage { + @Serializable + internal data class Internal(val setupComplete: JsonObject) : InternalLiveServerMessage { + override fun toPublic() = LiveServerSetupComplete() + } +} + +/** + * Request for the client to execute the provided [functionCalls]. + * + * The client should return matching [FunctionResponsePart], where the `id` fields correspond to + * individual [FunctionCallPart]s. + * + * @property functionCalls A list of [FunctionCallPart] to run and return responses for. + */ +@PublicPreviewAPI +public class LiveServerToolCall(public val functionCalls: List) : + LiveServerMessage { + @Serializable + internal data class Internal( + val functionCalls: List = emptyList() + ) + @Serializable + internal data class InternalWrapper(val toolCall: Internal) : InternalLiveServerMessage { + override fun toPublic() = + LiveServerToolCall( + toolCall.functionCalls.map { functionCall -> + FunctionCallPart( + name = functionCall.name, + args = functionCall.args.orEmpty().mapValues { it.value ?: JsonNull } + ) + } + ) + } +} + +/** + * Notification for the client to cancel a previous function call from [LiveServerToolCall]. + * + * You do not need to send [FunctionResponsePart]s for the cancelled [FunctionCallPart]s. + * + * @property functionIds A list of `id`s matching the `id` provided in a previous + * [LiveServerToolCall], where only the provided `id`s should be cancelled. + */ +@PublicPreviewAPI +public class LiveServerToolCallCancellation(public val functionIds: List) : + LiveServerMessage { + @Serializable internal data class Internal(val functionIds: List = emptyList()) + @Serializable + internal data class InternalWrapper(val toolCallCancellation: Internal) : + InternalLiveServerMessage { + override fun toPublic() = LiveServerToolCallCancellation(toolCallCancellation.functionIds) + } +} + +@PublicPreviewAPI +@Serializable(LiveServerMessageSerializer::class) +internal sealed interface InternalLiveServerMessage { + fun toPublic(): LiveServerMessage +} + +@OptIn(PublicPreviewAPI::class) +internal object LiveServerMessageSerializer : + JsonContentPolymorphicSerializer(InternalLiveServerMessage::class) { + @OptIn(PublicPreviewAPI::class) + override fun selectDeserializer( + element: JsonElement + ): DeserializationStrategy { + val jsonObject = element.jsonObject + return when { + "serverContent" in jsonObject -> LiveServerContent.InternalWrapper.serializer() + "setupComplete" in jsonObject -> LiveServerSetupComplete.Internal.serializer() + "toolCall" in jsonObject -> LiveServerToolCall.InternalWrapper.serializer() + "toolCallCancellation" in jsonObject -> + LiveServerToolCallCancellation.InternalWrapper.serializer() + else -> + throw SerializationException( + "The given subclass of LiveServerMessage (${javaClass.simpleName}) is not supported in the serialization yet." + ) + } + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt new file mode 100644 index 00000000000..1f84c18a53b --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt @@ -0,0 +1,446 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import android.Manifest.permission.RECORD_AUDIO +import android.media.AudioFormat +import android.media.AudioTrack +import android.util.Log +import androidx.annotation.RequiresPermission +import com.google.firebase.ai.common.JSON +import com.google.firebase.ai.common.util.CancelledCoroutineScope +import com.google.firebase.ai.common.util.accumulateUntil +import com.google.firebase.ai.common.util.childJob +import com.google.firebase.annotations.concurrent.Blocking +import io.ktor.client.plugins.websocket.ClientWebSocketSession +import io.ktor.websocket.Frame +import io.ktor.websocket.close +import io.ktor.websocket.readBytes +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +/** Represents a live WebSocket session capable of streaming content to and from the server. */ +@PublicPreviewAPI +@OptIn(ExperimentalSerializationApi::class) +public class LiveSession +internal constructor( + private val session: ClientWebSocketSession, + @Blocking private val blockingDispatcher: CoroutineContext, + private var audioHelper: AudioHelper? = null +) { + /** + * Coroutine scope that we batch data on for [startAudioConversation]. + * + * Makes it easy to stop all the work with [stopAudioConversation] by just cancelling the scope. + */ + private var scope = CancelledCoroutineScope + + /** + * Playback audio data sent from the model. + * + * Effectively, this is what the model is saying. + */ + private val playBackQueue = ConcurrentLinkedQueue() + + /** + * Toggled whenever [receive] and [stopReceiving] are called. + * + * Used to ensure only one flow is consuming the playback at once. + */ + private val startedReceiving = AtomicBoolean(false) + + /** + * Starts an audio conversation with the model, which can only be stopped using + * [stopAudioConversation] or [close]. + * + * @param functionCallHandler A callback function that is invoked whenever the model receives a + * function call. The [FunctionResponsePart] that the callback function returns will be + * automatically sent to the model. + */ + @RequiresPermission(RECORD_AUDIO) + public suspend fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null + ) { + FirebaseAIException.catchAsync { + if (scope.isActive) { + Log.w( + TAG, + "startAudioConversation called after the recording has already started. " + + "Call stopAudioConversation to close the previous connection." + ) + return@catchAsync + } + + scope = CoroutineScope(blockingDispatcher + childJob()) + audioHelper = AudioHelper.build() + + recordUserAudio() + processModelResponses(functionCallHandler) + listenForModelPlayback() + } + } + + /** + * Stops the audio conversation with the model. + * + * This only needs to be called after a previous call to [startAudioConversation]. + * + * If there is no audio conversation currently active, this function does nothing. + */ + public fun stopAudioConversation() { + FirebaseAIException.catch { + if (!startedReceiving.getAndSet(false)) return@catch + + scope.cancel() + playBackQueue.clear() + + audioHelper?.release() + audioHelper = null + } + } + + /** + * Receives responses from the model for both streaming and standard requests. + * + * Call [close] to stop receiving responses from the model. + * + * @return A [Flow] which will emit [LiveServerMessage] from the model. + * + * @throws [SessionAlreadyReceivingException] when the session is already receiving. + * @see stopReceiving + */ + public fun receive(): Flow { + return FirebaseAIException.catch { + if (startedReceiving.getAndSet(true)) { + throw SessionAlreadyReceivingException() + } + + // TODO(b/410059569): Remove when fixed + flow { + while (true) { + val response = session.incoming.tryReceive() + if (response.isClosed || !startedReceiving.get()) break + + response + .getOrNull() + ?.let { + JSON.decodeFromString( + it.readBytes().toString(Charsets.UTF_8) + ) + } + ?.let { emit(it.toPublic()) } + + yield() + } + } + .onCompletion { stopAudioConversation() } + .catch { throw FirebaseAIException.from(it) } + + // TODO(b/410059569): Add back when fixed + } + } + + /** + * Stops receiving from the model. + * + * If this function is called during an ongoing audio conversation, the model's response will not + * be received, and no audio will be played; the live session object will no longer receive data + * from the server. + * + * To resume receiving data, you must either handle it directly using [receive], or indirectly by + * using [startAudioConversation]. + * + * @see close + */ + // TODO(b/410059569): Remove when fixed + public fun stopReceiving() { + FirebaseAIException.catch { + if (!startedReceiving.getAndSet(false)) return@catch + + scope.cancel() + playBackQueue.clear() + + audioHelper?.release() + audioHelper = null + } + } + + /** + * Sends function calling responses to the model. + * + * **NOTE:** If you're using [startAudioConversation], the method will handle sending function + * responses to the model for you. You do _not_ need to call this method in that case. + * + * @param functionList The list of [FunctionResponsePart] instances indicating the function + * response from the client. + */ + public suspend fun sendFunctionResponse(functionList: List) { + FirebaseAIException.catchAsync { + val jsonString = + Json.encodeToString( + BidiGenerateContentToolResponseSetup(functionList.map { it.toInternalFunctionCall() }) + .toInternal() + ) + session.send(Frame.Text(jsonString)) + } + } + + /** + * Streams client data to the model. + * + * Calling this after [startAudioConversation] will play the response audio immediately. + * + * @param mediaChunks The list of [MediaData] instances representing the media data to be sent. + */ + public suspend fun sendMediaStream( + mediaChunks: List, + ) { + FirebaseAIException.catchAsync { + val jsonString = + Json.encodeToString( + BidiGenerateContentRealtimeInputSetup(mediaChunks.map { (it.toInternal()) }).toInternal() + ) + session.send(Frame.Text(jsonString)) + } + } + + /** + * Sends [data][Content] to the model. + * + * Calling this after [startAudioConversation] will play the response audio immediately. + * + * @param content Client [Content] to be sent to the model. + */ + public suspend fun send(content: Content) { + FirebaseAIException.catchAsync { + val jsonString = + Json.encodeToString( + BidiGenerateContentClientContentSetup(listOf(content.toInternal()), true).toInternal() + ) + session.send(Frame.Text(jsonString)) + } + } + + /** + * Sends text to the model. + * + * Calling this after [startAudioConversation] will play the response audio immediately. + * + * @param text Text to be sent to the model. + */ + public suspend fun send(text: String) { + FirebaseAIException.catchAsync { send(Content.Builder().text(text).build()) } + } + + /** + * Closes the client session. + * + * Once a [LiveSession] is closed, it can not be reopened; you'll need to start a new + * [LiveSession]. + * + * @see stopReceiving + */ + public suspend fun close() { + FirebaseAIException.catchAsync { + session.close() + stopAudioConversation() + } + } + + /** Listen to the user's microphone and send the data to the model. */ + private fun recordUserAudio() { + // Buffer the recording so we can keep recording while data is sent to the server + audioHelper + ?.listenToRecording() + ?.buffer(UNLIMITED) + ?.accumulateUntil(MIN_BUFFER_SIZE) + ?.onEach { sendMediaStream(listOf(MediaData(it, "audio/pcm"))) } + ?.catch { throw FirebaseAIException.from(it) } + ?.launchIn(scope) + } + + /** + * Processes responses from the model during an audio conversation. + * + * Audio messages are added to [playBackQueue]. + * + * Launched asynchronously on [scope]. + * + * @param functionCallHandler A callback function that is invoked whenever the server receives a + * function call. + */ + private fun processModelResponses( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? + ) { + receive() + .onEach { + when (it) { + is LiveServerToolCall -> { + if (it.functionCalls.isEmpty()) { + Log.w( + TAG, + "The model sent a tool call request, but it was missing functions to call." + ) + } else if (functionCallHandler != null) { + // It's fine to suspend here since you can't have a function call running concurrently + // with an audio response + sendFunctionResponse(it.functionCalls.map(functionCallHandler).toList()) + } else { + Log.w( + TAG, + "Function calls were present in the response, but a functionCallHandler was not provided." + ) + } + } + is LiveServerToolCallCancellation -> { + Log.w( + TAG, + "The model sent a tool cancellation request, but tool cancellation is not supported when using startAudioConversation()." + ) + } + is LiveServerContent -> { + if (it.interrupted) { + playBackQueue.clear() + } else { + val audioParts = it.content?.parts?.filterIsInstance().orEmpty() + for (part in audioParts) { + playBackQueue.add(part.inlineData) + } + } + } + is LiveServerSetupComplete -> { + // we should only get this message when we initially `connect` in LiveGenerativeModel + Log.w( + TAG, + "The model sent LiveServerSetupComplete after the connection was established." + ) + } + } + } + .launchIn(scope) + } + + /** + * Listens for playback data from the model and plays the audio. + * + * Polls [playBackQueue] for data, and calls [AudioHelper.playAudio] when data is received. + * + * Launched asynchronously on [scope]. + */ + private fun listenForModelPlayback() { + scope.launch { + while (isActive) { + val playbackData = playBackQueue.poll() + if (playbackData == null) { + // The model playback queue is complete, so we can continue recording + // TODO(b/408223520): Conditionally resume when param is added + audioHelper?.resumeRecording() + yield() + } else { + /** + * We pause the recording while the model is speaking to avoid interrupting it because of + * no echo cancellation + */ + // TODO(b/408223520): Conditionally pause when param is added + audioHelper?.pauseRecording() + + audioHelper?.playAudio(playbackData) + } + } + } + } + + /** + * Incremental update of the current conversation delivered from the client. + * + * Effectively, a message from the client to the model. + */ + internal class BidiGenerateContentClientContentSetup( + val turns: List, + val turnComplete: Boolean + ) { + @Serializable + internal class Internal(val clientContent: BidiGenerateContentClientContent) { + @Serializable + internal data class BidiGenerateContentClientContent( + val turns: List, + val turnComplete: Boolean + ) + } + + fun toInternal() = Internal(Internal.BidiGenerateContentClientContent(turns, turnComplete)) + } + + /** Client generated responses to a [LiveServerToolCall]. */ + internal class BidiGenerateContentToolResponseSetup( + val functionResponses: List + ) { + @Serializable + internal data class Internal(val toolResponse: BidiGenerateContentToolResponse) { + @Serializable + internal data class BidiGenerateContentToolResponse( + val functionResponses: List + ) + } + + fun toInternal() = Internal(Internal.BidiGenerateContentToolResponse(functionResponses)) + } + + /** + * User input that is sent to the model in real time. + * + * End of turn is derived from user activity (eg; end of speech). + */ + internal class BidiGenerateContentRealtimeInputSetup(val mediaChunks: List) { + @Serializable + internal class Internal(val realtimeInput: BidiGenerateContentRealtimeInput) { + @Serializable + internal data class BidiGenerateContentRealtimeInput( + val mediaChunks: List + ) + } + fun toInternal() = Internal(Internal.BidiGenerateContentRealtimeInput(mediaChunks)) + } + + private companion object { + val TAG = LiveSession::class.java.simpleName + val MIN_BUFFER_SIZE = + AudioTrack.getMinBufferSize( + 24000, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/MediaData.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/MediaData.kt new file mode 100644 index 00000000000..1262027989d --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/MediaData.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import android.util.Base64 +import kotlinx.serialization.Serializable + +/** + * Represents the media data to be sent to the server + * + * @param data Byte array representing the data to be sent. + * @param mimeType an IANA standard MIME type. For supported MIME type values see the + * [Firebase documentation](https://firebase.google.com/docs/vertex-ai/input-file-requirements). + */ +@PublicPreviewAPI +public class MediaData(public val data: ByteArray, public val mimeType: String) { + @Serializable + internal class Internal( + val data: String, + val mimeType: String, + ) + + internal fun toInternal(): Internal { + return Internal(Base64.encodeToString(data, BASE_64_FLAGS), mimeType) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ModalityTokenCount.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ModalityTokenCount.kt new file mode 100644 index 00000000000..0e959ac4b3f --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ModalityTokenCount.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import kotlinx.serialization.Serializable + +/** + * Represents token counting info for a single modality. + * + * @property modality The modality associated with this token count. + * @property tokenCount The number of tokens counted. + */ +public class ModalityTokenCount +private constructor(public val modality: ContentModality, public val tokenCount: Int) { + + public operator fun component1(): ContentModality = modality + + public operator fun component2(): Int = tokenCount + + @Serializable + internal data class Internal( + val modality: ContentModality.Internal, + val tokenCount: Int? = null + ) { + internal fun toPublic() = ModalityTokenCount(modality.toPublic(), tokenCount ?: 0) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt new file mode 100644 index 00000000000..bcc7e14b657 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt @@ -0,0 +1,252 @@ +/* + * Copyright 2023 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.firebase.ai.type + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import java.io.ByteArrayOutputStream +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.json.JSONObject + +/** Interface representing data sent to and received from requests. */ +public interface Part {} + +/** Represents text or string based data sent to and received from requests. */ +public class TextPart(public val text: String) : Part { + + @Serializable internal data class Internal(val text: String) : InternalPart +} + +/** + * Represents image data sent to and received from requests. The image is converted client-side to + * JPEG encoding at 80% quality before being sent to the server. + * + * @param image [Bitmap] to convert into a [Part] + */ +public class ImagePart(public val image: Bitmap) : Part { + + internal fun toInlineDataPart() = + InlineDataPart( + android.util.Base64.decode(encodeBitmapToBase64Jpeg(image), BASE_64_FLAGS), + "image/jpeg" + ) +} + +/** + * Represents binary data with an associated MIME type sent to and received from requests. + * + * @param inlineData the binary data as a [ByteArray] + * @param mimeType an IANA standard MIME type. For supported values, see the + * [Vertex AI documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/send-multimodal-prompts#media_requirements) + */ +public class InlineDataPart(public val inlineData: ByteArray, public val mimeType: String) : Part { + + @Serializable + internal data class Internal(@SerialName("inlineData") val inlineData: InlineData) : + InternalPart { + + @Serializable + internal data class InlineData(@SerialName("mimeType") val mimeType: String, val data: Base64) + } +} + +/** + * Represents function call name and params received from requests. + * + * @param name the name of the function to call + * @param args the function parameters and values as a [Map] + * @param id Unique id of the function call. If present, the returned [FunctionResponsePart] should + * have a matching `id` field. + */ +public class FunctionCallPart +@JvmOverloads +constructor( + public val name: String, + public val args: Map, + public val id: String? = null +) : Part { + + @Serializable + internal data class Internal(val functionCall: FunctionCall) : InternalPart { + + @Serializable + internal data class FunctionCall( + val name: String, + val args: Map? = null, + val id: String? = null + ) + } +} + +/** + * Represents function call output to be returned to the model when it requests a function call. + * + * @param name The name of the called function. + * @param response The response produced by the function as a [JSONObject]. + * @param id Matching `id` for a [FunctionCallPart], if one was provided. + */ +public class FunctionResponsePart +@JvmOverloads +constructor( + public val name: String, + public val response: JsonObject, + public val id: String? = null +) : Part { + + @Serializable + internal data class Internal(val functionResponse: FunctionResponse) : InternalPart { + + @Serializable + internal data class FunctionResponse( + val name: String, + val response: JsonObject, + val id: String? = null + ) + } + + internal fun toInternalFunctionCall(): Internal.FunctionResponse { + return Internal.FunctionResponse(name, response, id) + } +} + +/** + * Represents file data stored in Cloud Storage for Firebase, referenced by URI. + * + * @param uri The `"gs://"`-prefixed URI of the file in Cloud Storage for Firebase, for example, + * `"gs://bucket-name/path/image.jpg"` + * @param mimeType an IANA standard MIME type. For supported MIME type values see the + * [Firebase documentation](https://firebase.google.com/docs/vertex-ai/input-file-requirements). + */ +public class FileDataPart(public val uri: String, public val mimeType: String) : Part { + + @Serializable + internal data class Internal(@SerialName("file_data") val fileData: FileData) : InternalPart { + + @Serializable + internal data class FileData( + @SerialName("mime_type") val mimeType: String, + @SerialName("file_uri") val fileUri: String, + ) + } +} + +/** Returns the part as a [String] if it represents text, and null otherwise */ +public fun Part.asTextOrNull(): String? = (this as? TextPart)?.text + +/** Returns the part as a [Bitmap] if it represents an image, and null otherwise */ +public fun Part.asImageOrNull(): Bitmap? = (this as? ImagePart)?.image + +/** Returns the part as a [InlineDataPart] if it represents inline data, and null otherwise */ +public fun Part.asInlineDataPartOrNull(): InlineDataPart? = this as? InlineDataPart + +/** Returns the part as a [FileDataPart] if it represents a file, and null otherwise */ +public fun Part.asFileDataOrNull(): FileDataPart? = this as? FileDataPart + +internal typealias Base64 = String + +internal const val BASE_64_FLAGS = android.util.Base64.NO_WRAP + +@Serializable(PartSerializer::class) internal sealed interface InternalPart + +internal object PartSerializer : + JsonContentPolymorphicSerializer(InternalPart::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + val jsonObject = element.jsonObject + return when { + "text" in jsonObject -> TextPart.Internal.serializer() + "functionCall" in jsonObject -> FunctionCallPart.Internal.serializer() + "functionResponse" in jsonObject -> FunctionResponsePart.Internal.serializer() + "inlineData" in jsonObject -> InlineDataPart.Internal.serializer() + "fileData" in jsonObject -> FileDataPart.Internal.serializer() + else -> throw SerializationException("Unknown Part type") + } + } +} + +internal fun Part.toInternal(): InternalPart { + return when (this) { + is TextPart -> TextPart.Internal(text) + is ImagePart -> + InlineDataPart.Internal( + InlineDataPart.Internal.InlineData("image/jpeg", encodeBitmapToBase64Jpeg(image)) + ) + is InlineDataPart -> + InlineDataPart.Internal( + InlineDataPart.Internal.InlineData( + mimeType, + android.util.Base64.encodeToString(inlineData, BASE_64_FLAGS) + ) + ) + is FunctionCallPart -> + FunctionCallPart.Internal(FunctionCallPart.Internal.FunctionCall(name, args, id)) + is FunctionResponsePart -> + FunctionResponsePart.Internal( + FunctionResponsePart.Internal.FunctionResponse(name, response, id) + ) + is FileDataPart -> + FileDataPart.Internal(FileDataPart.Internal.FileData(mimeType = mimeType, fileUri = uri)) + else -> + throw com.google.firebase.ai.type.SerializationException( + "The given subclass of Part (${javaClass.simpleName}) is not supported in the serialization yet." + ) + } +} + +private fun encodeBitmapToBase64Jpeg(input: Bitmap): String { + ByteArrayOutputStream().let { + input.compress(Bitmap.CompressFormat.JPEG, 80, it) + return android.util.Base64.encodeToString(it.toByteArray(), BASE_64_FLAGS) + } +} + +internal fun InternalPart.toPublic(): Part { + return when (this) { + is TextPart.Internal -> TextPart(text) + is InlineDataPart.Internal -> { + val data = android.util.Base64.decode(inlineData.data, BASE_64_FLAGS) + if (inlineData.mimeType.contains("image")) { + ImagePart(decodeBitmapFromImage(data)) + } else { + InlineDataPart(data, inlineData.mimeType) + } + } + is FunctionCallPart.Internal -> + FunctionCallPart( + functionCall.name, + functionCall.args.orEmpty().mapValues { it.value ?: JsonNull }, + functionCall.id + ) + is FunctionResponsePart.Internal -> + FunctionResponsePart(functionResponse.name, functionResponse.response, functionResponse.id) + is FileDataPart.Internal -> FileDataPart(fileData.mimeType, fileData.fileUri) + else -> + throw com.google.firebase.ai.type.SerializationException( + "Unsupported part type \"${javaClass.simpleName}\" provided. This model may not be supported by this SDK." + ) + } +} + +private fun decodeBitmapFromImage(input: ByteArray) = + BitmapFactory.decodeByteArray(input, 0, input.size) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PromptFeedback.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PromptFeedback.kt new file mode 100644 index 00000000000..5f9840263eb --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PromptFeedback.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2023 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.firebase.ai.type + +import com.google.firebase.ai.common.util.FirstOrdinalSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Feedback on the prompt provided in the request. + * + * @param blockReason The reason that content was blocked, if at all. + * @param safetyRatings A list of relevant [SafetyRating]. + * @param blockReasonMessage A message describing the reason that content was blocked, if any. + */ +public class PromptFeedback( + public val blockReason: BlockReason?, + public val safetyRatings: List, + public val blockReasonMessage: String? +) { + + @Serializable + internal data class Internal( + val blockReason: BlockReason.Internal? = null, + val safetyRatings: List? = null, + val blockReasonMessage: String? = null, + ) { + + internal fun toPublic(): PromptFeedback { + val safetyRatings = safetyRatings?.mapNotNull { it.toPublic() }.orEmpty() + return PromptFeedback(blockReason?.toPublic(), safetyRatings, blockReasonMessage) + } + } +} + +/** Describes why content was blocked. */ +public class BlockReason private constructor(public val name: String, public val ordinal: Int) { + + @Serializable(Internal.Serializer::class) + internal enum class Internal { + UNKNOWN, + @SerialName("BLOCKED_REASON_UNSPECIFIED") UNSPECIFIED, + SAFETY, + OTHER, + BLOCKLIST, + PROHIBITED_CONTENT; + + internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class) + + internal fun toPublic() = + when (this) { + SAFETY -> BlockReason.SAFETY + OTHER -> BlockReason.OTHER + BLOCKLIST -> BlockReason.BLOCKLIST + PROHIBITED_CONTENT -> BlockReason.PROHIBITED_CONTENT + else -> BlockReason.UNKNOWN + } + } + public companion object { + /** A new and not yet supported value. */ + @JvmField public val UNKNOWN: BlockReason = BlockReason("UNKNOWN", 0) + + /** Content was blocked for violating provided [SafetySetting]. */ + @JvmField public val SAFETY: BlockReason = BlockReason("SAFETY", 1) + + /** Content was blocked for another reason. */ + @JvmField public val OTHER: BlockReason = BlockReason("OTHER", 2) + + /** Content was blocked for another reason. */ + @JvmField public val BLOCKLIST: BlockReason = BlockReason("BLOCKLIST", 3) + + /** Candidates blocked due to the terms which are included from the terminology blocklist. */ + @JvmField public val PROHIBITED_CONTENT: BlockReason = BlockReason("PROHIBITED_CONTENT", 4) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt new file mode 100644 index 00000000000..bc4a53cc8eb --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 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.firebase.ai.type + +@Retention(AnnotationRetention.BINARY) +@RequiresOptIn( + level = RequiresOptIn.Level.ERROR, + message = + "This API is part of an experimental public preview and may change in " + + "backwards-incompatible ways without notice.", +) +public annotation class PublicPreviewAPI() diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/RequestOptions.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/RequestOptions.kt new file mode 100644 index 00000000000..dc4211e7222 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/RequestOptions.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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.firebase.ai.type + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +/** Configurable options unique to how requests to the backend are performed. */ +public class RequestOptions +internal constructor( + internal val timeout: Duration, + internal val endpoint: String = "https://firebasevertexai.googleapis.com", + internal val apiVersion: String = "v1beta", +) { + + /** + * Constructor for RequestOptions. + * + * @param timeoutInMillis the maximum amount of time, in milliseconds, for a request to take, from + * the first request to first response. + */ + @JvmOverloads + public constructor( + timeoutInMillis: Long = 180.seconds.inWholeMilliseconds, + ) : this( + timeout = timeoutInMillis.toDuration(DurationUnit.MILLISECONDS), + ) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ResponseModality.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ResponseModality.kt new file mode 100644 index 00000000000..4c1586227a2 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ResponseModality.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import com.google.firebase.ai.common.util.FirstOrdinalSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable + +/** Represents the type of content present in a response (e.g., text, image, audio). */ +public class ResponseModality private constructor(public val ordinal: Int) { + + @Serializable(Internal.Serializer::class) + internal enum class Internal { + TEXT, + IMAGE, + AUDIO; + + internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class) + + internal fun toPublic() = + when (this) { + TEXT -> ResponseModality.TEXT + IMAGE -> ResponseModality.IMAGE + else -> ResponseModality.AUDIO + } + } + + internal fun toInternal() = + when (this) { + TEXT -> "TEXT" + IMAGE -> "IMAGE" + else -> "AUDIO" + } + public companion object { + + /** Represents a plain text response modality. */ + @JvmField public val TEXT: ResponseModality = ResponseModality(1) + + /** Represents an image response modality. */ + @JvmField public val IMAGE: ResponseModality = ResponseModality(2) + + /** Represents an audio response modality. */ + @JvmField public val AUDIO: ResponseModality = ResponseModality(4) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/SafetySetting.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/SafetySetting.kt new file mode 100644 index 00000000000..107e0623c57 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/SafetySetting.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023 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.firebase.ai.type + +import kotlinx.serialization.Serializable + +/** + * A configuration for a [HarmBlockThreshold] of some [HarmCategory] allowed and blocked in + * responses. + * + * @param harmCategory The relevant [HarmCategory]. + * @param threshold The threshold form harm allowable. + * @param method Specify if the threshold is used for probability or severity score, if not + * specified it will default to [HarmBlockMethod.PROBABILITY]. + */ +public class SafetySetting( + internal val harmCategory: HarmCategory, + internal val threshold: HarmBlockThreshold, + internal val method: HarmBlockMethod? = null, +) { + internal fun toInternal() = + Internal(harmCategory.toInternal(), threshold.toInternal(), method?.toInternal()) + + @Serializable + internal data class Internal( + val category: HarmCategory.Internal, + val threshold: HarmBlockThreshold.Internal, + val method: HarmBlockMethod.Internal? = null, + ) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Schema.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Schema.kt new file mode 100644 index 00000000000..9eaa4590aad --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Schema.kt @@ -0,0 +1,266 @@ +/* + * Copyright 2024 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.firebase.ai.type + +import kotlinx.serialization.Serializable + +public abstract class StringFormat private constructor(internal val value: String) { + public class Custom(value: String) : StringFormat(value) +} + +/** + * Definition of a data type. + * + * These types can be objects, but also primitives and arrays. Represents a select subset of an + * [OpenAPI 3.0 schema object](https://spec.openapis.org/oas/v3.0.3#schema). + * + * **Note:** While optional, including a `description` field in your `Schema` is strongly + * encouraged. The more information the model has about what it's expected to generate, the better + * the results. + */ +public class Schema +internal constructor( + public val type: String, + public val description: String? = null, + public val format: String? = null, + public val nullable: Boolean? = null, + public val enum: List? = null, + public val properties: Map? = null, + public val required: List? = null, + public val items: Schema? = null, +) { + + public companion object { + /** + * Returns a [Schema] representing a boolean value. + * + * @param description An optional description of what the boolean should contain or represent. + * @param nullable Indicates whether the value can be `null`. Defaults to `false`. + */ + @JvmStatic + @JvmOverloads + public fun boolean(description: String? = null, nullable: Boolean = false): Schema = + Schema( + description = description, + nullable = nullable, + type = "BOOLEAN", + ) + + /** + * Returns a [Schema] for a 32-bit signed integer number. + * + * **Important:** This [Schema] provides a hint to the model that it should generate a 32-bit + * integer, but only guarantees that the value will be an integer. Therefore it's *possible* + * that decoding it as an `Int` variable (or `int` in Java) could overflow. + * + * @param description An optional description of what the integer should contain or represent. + * @param nullable Indicates whether the value can be `null`. Defaults to `false`. + */ + @JvmStatic + @JvmName("numInt") + @JvmOverloads + public fun integer(description: String? = null, nullable: Boolean = false): Schema = + Schema( + description = description, + format = "int32", + nullable = nullable, + type = "INTEGER", + ) + + /** + * Returns a [Schema] for a 64-bit signed integer number. + * + * @param description An optional description of what the number should contain or represent. + * @param nullable Indicates whether the value can be `null`. Defaults to `false`. + */ + @JvmStatic + @JvmName("numLong") + @JvmOverloads + public fun long(description: String? = null, nullable: Boolean = false): Schema = + Schema( + description = description, + nullable = nullable, + type = "INTEGER", + ) + + /** + * Returns a [Schema] for a double-precision floating-point number. + * + * @param description An optional description of what the number should contain or represent. + * @param nullable Indicates whether the value can be `null`. Defaults to `false`. + */ + @JvmStatic + @JvmName("numDouble") + @JvmOverloads + public fun double(description: String? = null, nullable: Boolean = false): Schema = + Schema(description = description, nullable = nullable, type = "NUMBER") + + /** + * Returns a [Schema] for a single-precision floating-point number. + * + * **Important:** This [Schema] provides a hint to the model that it should generate a + * single-precision floating-point number, but only guarantees that the value will be a number. + * Therefore it's *possible* that decoding it as a `Float` variable (or `float` in Java) could + * overflow. + * + * @param description An optional description of what the number should contain or represent. + * @param nullable Indicates whether the value can be `null`. Defaults to `false`. + */ + @JvmStatic + @JvmName("numFloat") + @JvmOverloads + public fun float(description: String? = null, nullable: Boolean = false): Schema = + Schema(description = description, nullable = nullable, type = "NUMBER", format = "float") + + /** + * Returns a [Schema] for a string. + * + * @param description An optional description of what the string should contain or represent. + * @param nullable Indicates whether the value can be `null`. Defaults to `false`. + * @param format An optional pattern that values need to adhere to. + */ + @JvmStatic + @JvmName("str") + @JvmOverloads + public fun string( + description: String? = null, + nullable: Boolean = false, + format: StringFormat? = null + ): Schema = + Schema( + description = description, + format = format?.value, + nullable = nullable, + type = "STRING" + ) + + /** + * Returns a [Schema] for a complex data type. + * + * This schema instructs the model to produce data of type object, which has keys of type + * `String` and values of type [Schema]. + * + * **Example:** A `city` could be represented with the following object `Schema`. + * ``` + * Schema.obj(mapOf( + * "name" to Schema.string(), + * "population" to Schema.integer() + * )) + * ``` + * + * @param properties The map of the object's property names to their [Schema]s. + * @param optionalProperties The list of optional properties. They must correspond to the keys + * provided in the `properties` map. By default it's empty, signaling the model that all + * properties are to be included. + * @param description An optional description of what the object represents. + * @param nullable Indicates whether the value can be `null`. Defaults to `false`. + */ + @JvmStatic + @JvmOverloads + public fun obj( + properties: Map, + optionalProperties: List = emptyList(), + description: String? = null, + nullable: Boolean = false, + ): Schema { + if (!properties.keys.containsAll(optionalProperties)) { + throw IllegalArgumentException( + "All optional properties must be present in properties. Missing: ${optionalProperties.minus(properties.keys)}" + ) + } + return Schema( + description = description, + nullable = nullable, + properties = properties, + required = properties.keys.minus(optionalProperties.toSet()).toList(), + type = "OBJECT", + ) + } + + /** + * Returns a [Schema] for an array. + * + * @param items The [Schema] of the elements stored in the array. + * @param description An optional description of what the array represents. + * @param nullable Indicates whether the value can be `null`. Defaults to `false`. + */ + @JvmStatic + @JvmOverloads + public fun array( + items: Schema, + description: String? = null, + nullable: Boolean = false + ): Schema = + Schema( + description = description, + nullable = nullable, + items = items, + type = "ARRAY", + ) + + /** + * Returns a [Schema] for an enumeration. + * + * For example, the cardinal directions can be represented as: + * + * ``` + * Schema.enumeration(listOf("north", "east", "south", "west"), "Cardinal directions") + * ``` + * + * @param values The list of valid values for this enumeration + * @param description The description of what the parameter should contain or represent + * @param nullable Indicates whether the value can be `null`. Defaults to `false`. + */ + @JvmStatic + @JvmOverloads + public fun enumeration( + values: List, + description: String? = null, + nullable: Boolean = false + ): Schema = + Schema( + description = description, + format = "enum", + nullable = nullable, + enum = values, + type = "STRING", + ) + } + + internal fun toInternal(): Internal = + Internal( + type, + description, + format, + nullable, + enum, + properties?.mapValues { it.value.toInternal() }, + required, + items?.toInternal(), + ) + @Serializable + internal data class Internal( + val type: String, + val description: String? = null, + val format: String? = null, + val nullable: Boolean? = false, + val enum: List? = null, + val properties: Map? = null, + val required: List? = null, + val items: Internal? = null, + ) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/SpeechConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/SpeechConfig.kt new file mode 100644 index 00000000000..12de21caff3 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/SpeechConfig.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Speech configuration class for setting up the voice of the server's response. */ +@PublicPreviewAPI +public class SpeechConfig( + /** The voice to be used for the server's speech response. */ + public val voice: Voice +) { + + @Serializable + internal data class Internal(@SerialName("voice_config") val voiceConfig: VoiceConfigInternal) { + @Serializable + internal data class VoiceConfigInternal( + @SerialName("prebuilt_voice_config") val prebuiltVoiceConfig: Voice.Internal, + ) + } + + internal fun toInternal(): Internal { + return Internal(Internal.VoiceConfigInternal(prebuiltVoiceConfig = voice.toInternal())) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt new file mode 100644 index 00000000000..83391166bd4 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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.firebase.ai.type + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** + * Contains a set of function declarations that the model has access to. These can be used to gather + * information, or complete tasks + * + * @param functionDeclarations The set of functions that this tool allows the model access to + */ +public class Tool +internal constructor(internal val functionDeclarations: List?) { + internal fun toInternal() = Internal(functionDeclarations?.map { it.toInternal() } ?: emptyList()) + @Serializable + internal data class Internal( + val functionDeclarations: List? = null, + // This is a json object because it is not possible to make a data class with no parameters. + val codeExecution: JsonObject? = null, + ) + public companion object { + + /** + * Creates a [Tool] instance that provides the model with access to the [functionDeclarations]. + * + * @param functionDeclarations The list of functions that this tool allows the model access to. + */ + @JvmStatic + public fun functionDeclarations(functionDeclarations: List): Tool { + return Tool(functionDeclarations) + } + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ToolConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ToolConfig.kt new file mode 100644 index 00000000000..43d20ec3fd6 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ToolConfig.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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.firebase.ai.type + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Contains configuration for the function calling tools of the model. This can be used to change + * when the model can predict function calls. + * + * @param functionCallingConfig The config for function calling + */ +public class ToolConfig(internal val functionCallingConfig: FunctionCallingConfig?) { + + internal fun toInternal() = + Internal( + functionCallingConfig?.let { + FunctionCallingConfig.Internal( + when (it.mode) { + FunctionCallingConfig.Mode.ANY -> FunctionCallingConfig.Internal.Mode.ANY + FunctionCallingConfig.Mode.AUTO -> FunctionCallingConfig.Internal.Mode.AUTO + FunctionCallingConfig.Mode.NONE -> FunctionCallingConfig.Internal.Mode.NONE + }, + it.allowedFunctionNames + ) + } + ) + + @Serializable + internal data class Internal( + @SerialName("function_calling_config") + val functionCallingConfig: FunctionCallingConfig.Internal? + ) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Type.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Type.kt new file mode 100644 index 00000000000..76d7623e0c6 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Type.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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.firebase.ai.type + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import org.json.JSONObject + +internal sealed interface Response + +@Serializable +internal data class GRpcErrorResponse(val error: GRpcError) : Response { + + @Serializable + internal data class GRpcError( + val code: Int, + val message: String, + val details: List? = null + ) { + + @Serializable + internal data class GRpcErrorDetails( + val reason: String? = null, + val domain: String? = null, + val metadata: Map? = null + ) + } +} + +internal fun JSONObject.toInternal() = Json.decodeFromString(toString()) + +internal fun JsonObject.toPublic() = JSONObject(toString()) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/UsageMetadata.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/UsageMetadata.kt new file mode 100644 index 00000000000..1b858a1e6cd --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/UsageMetadata.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024 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.firebase.ai.type + +import kotlinx.serialization.Serializable + +/** + * Usage metadata about response(s). + * + * @param promptTokenCount Number of tokens in the request. + * @param candidatesTokenCount Number of tokens in the response(s). + * @param totalTokenCount Total number of tokens. + * @param promptTokensDetails The breakdown, by modality, of how many tokens are consumed by the + * prompt. + * @param candidatesTokensDetails The breakdown, by modality, of how many tokens are consumed by the + * candidates. + */ +public class UsageMetadata( + public val promptTokenCount: Int, + public val candidatesTokenCount: Int?, + public val totalTokenCount: Int, + public val promptTokensDetails: List, + public val candidatesTokensDetails: List, +) { + + @Serializable + internal data class Internal( + val promptTokenCount: Int? = null, + val candidatesTokenCount: Int? = null, + val totalTokenCount: Int? = null, + val promptTokensDetails: List? = null, + val candidatesTokensDetails: List? = null, + ) { + + internal fun toPublic(): UsageMetadata = + UsageMetadata( + promptTokenCount ?: 0, + candidatesTokenCount ?: 0, + totalTokenCount ?: 0, + promptTokensDetails = promptTokensDetails?.map { it.toPublic() } ?: emptyList(), + candidatesTokensDetails = candidatesTokensDetails?.map { it.toPublic() } ?: emptyList() + ) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voice.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voice.kt new file mode 100644 index 00000000000..7053fc986cf --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voice.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Various voices supported by the server. The list of all voices can be found + * [here](https://cloud.google.com/text-to-speech/docs/chirp3-hd) + */ +@PublicPreviewAPI +public class Voice public constructor(public val voiceName: String) { + + @Serializable internal data class Internal(@SerialName("voice_name") val voiceName: String) + + internal fun toInternal(): Internal { + return Internal(this.voiceName) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voices.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voices.kt new file mode 100644 index 00000000000..d5e1f738dc2 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voices.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Various voices supported by the server */ +@Deprecated("Please use the Voice class instead.", ReplaceWith("Voice")) +@PublicPreviewAPI +public class Voices private constructor(public val ordinal: Int) { + + @Serializable internal data class Internal(@SerialName("voice_name") val voiceName: String) + + @Serializable + internal enum class InternalEnum { + CHARON, + AOEDE, + FENRIR, + KORE, + PUCK; + internal fun toPublic() = + when (this) { + CHARON -> Voices.CHARON + AOEDE -> Voices.AOEDE + FENRIR -> Voices.FENRIR + KORE -> Voices.KORE + else -> Voices.PUCK + } + } + + internal fun toInternal(): Internal { + return when (this) { + CHARON -> Internal(InternalEnum.CHARON.name) + AOEDE -> Internal(InternalEnum.AOEDE.name) + FENRIR -> Internal(InternalEnum.FENRIR.name) + KORE -> Internal(InternalEnum.KORE.name) + else -> Internal(InternalEnum.PUCK.name) + } + } + + public companion object { + /** + * Unspecified voice. + * + * Will use the default voice of the model. + */ + @JvmField public val UNSPECIFIED: Voices = Voices(0) + + /** Represents the Charon voice. */ + @JvmField public val CHARON: Voices = Voices(1) + + /** Represents the Aoede voice. */ + @JvmField public val AOEDE: Voices = Voices(2) + + /** Represents the Fenrir voice. */ + @JvmField public val FENRIR: Voices = Voices(3) + + /** Represents the Kore voice. */ + @JvmField public val KORE: Voices = Voices(4) + + /** Represents the Puck voice. */ + @JvmField public val PUCK: Voices = Voices(5) + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIStreamingSnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIStreamingSnapshotTests.kt new file mode 100644 index 00000000000..967254a096c --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIStreamingSnapshotTests.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2025 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.firebase.ai + +import com.google.firebase.ai.type.BlockReason +import com.google.firebase.ai.type.FinishReason +import com.google.firebase.ai.type.PromptBlockedException +import com.google.firebase.ai.type.ResponseStoppedException +import com.google.firebase.ai.type.ServerException +import com.google.firebase.ai.util.goldenDevAPIStreamingFile +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.ktor.http.HttpStatusCode +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.withTimeout +import org.junit.Test + +internal class DevAPIStreamingSnapshotTests { + private val testTimeout = 5.seconds + + @Test + fun `short reply`() = + goldenDevAPIStreamingFile("streaming-success-basic-reply-short.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.isEmpty() shouldBe false + responseList.last().candidates.first().apply { + finishReason shouldBe FinishReason.STOP + content.parts.isEmpty() shouldBe false + } + } + } + + @Test + fun `long reply`() = + goldenDevAPIStreamingFile("streaming-success-basic-reply-long.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.isEmpty() shouldBe false + responseList.last().candidates.first().apply { + finishReason shouldBe FinishReason.STOP + content.parts.isEmpty() shouldBe false + } + } + } + + @Test + fun `prompt blocked for safety`() = + goldenDevAPIStreamingFile("streaming-failure-prompt-blocked-safety.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val exception = shouldThrow { responses.collect() } + exception.response?.promptFeedback?.blockReason shouldBe BlockReason.SAFETY + } + } + + @Test + fun `citation parsed correctly`() = + goldenDevAPIStreamingFile("streaming-success-citations.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.any { + it.candidates.any { it.citationMetadata?.citations?.isNotEmpty() ?: false } + } shouldBe true + } + } + + @Test + fun `stopped for recitation`() = + goldenDevAPIStreamingFile("streaming-failure-recitation-no-content.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val exception = shouldThrow { responses.collect() } + exception.response.candidates.first().finishReason shouldBe FinishReason.RECITATION + } + } + + @Test + fun `image rejected`() = + goldenDevAPIStreamingFile("streaming-failure-image-rejected.txt", HttpStatusCode.BadRequest) { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { shouldThrow { responses.collect() } } + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt new file mode 100644 index 00000000000..91a263f8c66 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2025 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.firebase.ai + +import com.google.firebase.ai.type.FinishReason +import com.google.firebase.ai.type.InvalidAPIKeyException +import com.google.firebase.ai.type.ResponseStoppedException +import com.google.firebase.ai.type.ServerException +import com.google.firebase.ai.util.goldenDevAPIUnaryFile +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.ktor.http.HttpStatusCode +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.withTimeout +import org.junit.Test + +internal class DevAPIUnarySnapshotTests { + private val testTimeout = 5.seconds + + @Test + fun `short reply`() = + goldenDevAPIUnaryFile("unary-success-basic-reply-short.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().finishReason shouldBe FinishReason.STOP + response.candidates.first().content.parts.isEmpty() shouldBe false + } + } + + @Test + fun `long reply`() = + goldenDevAPIUnaryFile("unary-success-basic-reply-long.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().finishReason shouldBe FinishReason.STOP + response.candidates.first().content.parts.isEmpty() shouldBe false + } + } + + @Test + fun `citation returns correctly`() = + goldenDevAPIUnaryFile("unary-success-citations.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().citationMetadata?.citations?.size shouldBe 4 + response.candidates.first().citationMetadata?.citations?.forEach { + it.startIndex shouldNotBe null + it.endIndex shouldNotBe null + } + } + } + + @Test + fun `response blocked for safety`() = + goldenDevAPIUnaryFile("unary-failure-finish-reason-safety.txt") { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } should + { + it.response.candidates[0].finishReason shouldBe FinishReason.SAFETY + } + } + } + + @Test + fun `invalid api key`() = + goldenDevAPIUnaryFile("unary-failure-api-key.json", HttpStatusCode.BadRequest) { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } + } + } + @Test + fun `unknown model`() = + goldenDevAPIUnaryFile("unary-failure-unknown-model.json", HttpStatusCode.NotFound) { + withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt new file mode 100644 index 00000000000..8301f48d968 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2024 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.firebase.ai + +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.common.APIController +import com.google.firebase.ai.common.JSON +import com.google.firebase.ai.common.util.doBlocking +import com.google.firebase.ai.type.Candidate +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.RequestOptions +import com.google.firebase.ai.type.ServerException +import com.google.firebase.ai.type.TextPart +import com.google.firebase.ai.type.content +import io.kotest.assertions.json.shouldContainJsonKey +import io.kotest.assertions.json.shouldContainJsonKeyValue +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.types.shouldBeInstanceOf +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.content.TextContent +import io.ktor.http.headersOf +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito + +internal class GenerativeModelTesting { + private val TEST_CLIENT_ID = "test" + private val TEST_APP_ID = "1:android:12345" + private val TEST_VERSION = 1 + + private var mockFirebaseApp: FirebaseApp = Mockito.mock() + + @Before + fun setup() { + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + } + + @Test + fun `system calling in request`() = doBlocking { + val mockEngine = MockEngine { + respond( + generateContentResponseAsJsonString("text response"), + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + val apiController = + APIController( + "super_cool_test_key", + "gemini-1.5-flash", + RequestOptions(timeout = 5.seconds, endpoint = "https://my.custom.endpoint"), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + val generativeModel = + GenerativeModel( + "gemini-1.5-flash", + systemInstruction = content { text("system instruction") }, + controller = apiController + ) + + withTimeout(5.seconds) { generativeModel.generateContent("my test prompt") } + + mockEngine.requestHistory.shouldNotBeEmpty() + + val request = mockEngine.requestHistory.first().body + request.shouldBeInstanceOf() + + request.text.let { + it shouldContainJsonKey "system_instruction" + it.shouldContainJsonKeyValue("$.system_instruction.role", "system") + it.shouldContainJsonKeyValue("$.system_instruction.parts[0].text", "system instruction") + } + } + + @Test + fun `exception thrown when using invalid location`() = doBlocking { + val mockEngine = MockEngine { + respond( + """ + + Error 404 (Not Found)!!1 + """ + .trimIndent(), + HttpStatusCode.NotFound, + headersOf(HttpHeaders.ContentType, "text/html; charset=utf-8") + ) + } + + val apiController = + APIController( + "super_cool_test_key", + "gemini-1.5-flash", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + // Creating the + val generativeModel = + GenerativeModel( + "projects/PROJECTID/locations/INVALID_LOCATION/publishers/google/models/gemini-1.5-flash", + controller = apiController + ) + + val exception = + shouldThrow { + withTimeout(5.seconds) { generativeModel.generateContent("my test prompt") } + } + + // Let's not be too strict on the wording to avoid breaking the test unnecessarily. + exception.message shouldContain "location" + } + + @OptIn(ExperimentalSerializationApi::class) + private fun generateContentResponseAsJsonString(text: String): String { + return JSON.encodeToString( + GenerateContentResponse.Internal( + listOf(Candidate.Internal(Content.Internal(parts = listOf(TextPart.Internal(text))))) + ) + ) + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/SchemaTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/SchemaTests.kt new file mode 100644 index 00000000000..f9bdf8c835f --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/SchemaTests.kt @@ -0,0 +1,221 @@ +/* + * Copyright 2024 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.firebase.ai + +import com.google.firebase.ai.type.Schema +import com.google.firebase.ai.type.StringFormat +import io.kotest.assertions.json.shouldEqualJson +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.junit.Test + +internal class SchemaTests { + @Test + fun `basic schema declaration`() { + val schemaDeclaration = + Schema.array( + Schema.obj( + mapOf( + "name" to Schema.string(), + "country" to Schema.string(), + "population" to Schema.integer(), + "coordinates" to + Schema.obj( + mapOf( + "latitude" to Schema.double(), + "longitude" to Schema.double(), + ) + ), + "hemisphere" to + Schema.obj( + mapOf( + "latitudinal" to Schema.enumeration(listOf("N", "S")), + "longitudinal" to Schema.enumeration(listOf("E", "W")), + ) + ), + "elevation" to Schema.double(), + "isCapital" to Schema.boolean(), + "foundingDate" to Schema.string(nullable = true, format = StringFormat.Custom("date")), + ), + optionalProperties = listOf("population") + ) + ) + + val expectedJson = + """ + { + "type": "ARRAY", + "items": { + "type": "OBJECT", + "properties": { + "name": {"type": "STRING"}, + "country": {"type": "STRING"}, + "population": {"type": "INTEGER", "format": "int32"}, + "coordinates": { + "type": "OBJECT", + "properties": { + "latitude": {"type": "NUMBER"}, + "longitude": {"type": "NUMBER"} + }, + "required": ["latitude","longitude"] + }, + "hemisphere": { + "type": "OBJECT", + "properties": { + "latitudinal": {"type": "STRING","format": "enum","enum": ["N","S"]}, + "longitudinal": {"type": "STRING","format": "enum","enum": ["E","W"]} + }, + "required": ["latitudinal","longitudinal"] + }, + "elevation": {"type": "NUMBER"}, + "isCapital": {"type": "BOOLEAN"}, + "foundingDate": {"type": "STRING","format": "date","nullable": true} + }, + "required": [ + "name","country","coordinates","hemisphere","elevation", + "isCapital","foundingDate"] + } + } + """ + .trimIndent() + + Json.encodeToString(schemaDeclaration.toInternal()).shouldEqualJson(expectedJson) + } + + @Test + fun `full schema declaration`() { + val schemaDeclaration = + Schema.array( + Schema.obj( + description = "generic description", + nullable = true, + properties = + mapOf( + "name" to Schema.string(description = null, nullable = false, format = null), + "country" to + Schema.string( + description = "country name", + nullable = true, + format = StringFormat.Custom("custom format") + ), + "population" to Schema.long(description = "population count", nullable = true), + "coordinates" to + Schema.obj( + description = "coordinates", + nullable = true, + properties = + mapOf( + "latitude" to Schema.double(description = "latitude", nullable = false), + "longitude" to Schema.double(description = "longitude", nullable = false), + ) + ), + "hemisphere" to + Schema.obj( + description = "hemisphere", + nullable = false, + properties = + mapOf( + "latitudinal" to + Schema.enumeration( + listOf("N", "S"), + description = "latitudinal", + nullable = true + ), + "longitudinal" to + Schema.enumeration( + listOf("E", "W"), + description = "longitudinal", + nullable = true + ), + ), + ), + "elevation" to Schema.float(description = "elevation", nullable = false), + "isCapital" to + Schema.boolean( + description = "True if the city is the capital of the country", + nullable = false + ), + "foundingDate" to + Schema.string( + description = "Founding date", + nullable = true, + format = StringFormat.Custom("date") + ), + ) + ) + ) + + val expectedJson = + """ + { + "type": "ARRAY", + "items": { + "type": "OBJECT", + "description": "generic description", + "nullable": true, + "properties": { + "name": {"type": "STRING"}, + "country": {"type": "STRING", "description": "country name", "format": "custom format", "nullable": true}, + "population": {"type": "INTEGER", "description": "population count", "nullable": true}, + "coordinates": { + "type": "OBJECT", + "description": "coordinates", + "nullable": true, + "properties": { + "latitude": {"type": "NUMBER", "description": "latitude"}, + "longitude": {"type": "NUMBER", "description": "longitude"} + }, + "required": ["latitude","longitude"] + }, + "hemisphere": { + "type": "OBJECT", + "description": "hemisphere", + "properties": { + "latitudinal": { + "type": "STRING", + "description": "latitudinal", + "format": "enum", + "nullable": true, + "enum": ["N","S"] + }, + "longitudinal": { + "type": "STRING", + "description": "longitudinal", + "format": "enum", + "nullable": true, + "enum": ["E","W"] + } + }, + "required": ["latitudinal","longitudinal"] + }, + "elevation": {"type": "NUMBER", "description": "elevation", "format": "float"}, + "isCapital": {"type": "BOOLEAN", "description": "True if the city is the capital of the country"}, + "foundingDate": {"type": "STRING", "description": "Founding date", "format": "date", "nullable": true} + }, + "required": [ + "name","country","population","coordinates","hemisphere", + "elevation","isCapital","foundingDate" + ] + } + } + + """ + .trimIndent() + + Json.encodeToString(schemaDeclaration.toInternal()).shouldEqualJson(expectedJson) + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt new file mode 100644 index 00000000000..d00f75c2714 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2025 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.firebase.ai + +import com.google.firebase.ai.common.util.descriptorToJson +import com.google.firebase.ai.type.Candidate +import com.google.firebase.ai.type.CountTokensResponse +import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.ModalityTokenCount +import com.google.firebase.ai.type.Schema +import io.kotest.assertions.json.shouldEqualJson +import org.junit.Test + +internal class SerializationTests { + @Test + fun `test countTokensResponse serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "CountTokensResponse", + "type": "object", + "properties": { + "totalTokens": { + "type": "integer" + }, + "totalBillableCharacters": { + "type": "integer" + }, + "promptTokensDetails": { + "type": "array", + "items": { + "${'$'}ref": "ModalityTokenCount" + } + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(CountTokensResponse.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test modalityTokenCount serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "ModalityTokenCount", + "type": "object", + "properties": { + "modality": { + "type": "string", + "enum": [ + "UNSPECIFIED", + "TEXT", + "IMAGE", + "VIDEO", + "AUDIO", + "DOCUMENT" + ] + }, + "tokenCount": { + "type": "integer" + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(ModalityTokenCount.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test GenerateContentResponse serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "GenerateContentResponse", + "type": "object", + "properties": { + "candidates": { + "type": "array", + "items": { + "${'$'}ref": "Candidate" + } + }, + "promptFeedback": { + "${'$'}ref": "PromptFeedback" + }, + "usageMetadata": { + "${'$'}ref": "UsageMetadata" + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(GenerateContentResponse.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test Candidate serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "Candidate", + "type": "object", + "properties": { + "content": { + "${'$'}ref": "Content" + }, + "finishReason": { + "type": "string", + "enum": [ + "UNKNOWN", + "UNSPECIFIED", + "STOP", + "MAX_TOKENS", + "SAFETY", + "RECITATION", + "OTHER", + "BLOCKLIST", + "PROHIBITED_CONTENT", + "SPII", + "MALFORMED_FUNCTION_CALL" + ] + }, + "safetyRatings": { + "type": "array", + "items": { + "${'$'}ref": "SafetyRating" + } + }, + "citationMetadata": { + "${'$'}ref": "CitationMetadata" + }, + "groundingMetadata": { + "${'$'}ref": "GroundingMetadata" + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(Candidate.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test Schema serialization as Json`() { + /** + * Unlike the actual schema in the background, we don't represent "type" as an enum, but rather + * as a string. This is because we restrict what values can be used (using helper methods, + * rather than type). + */ + val expectedJsonAsString = + """ + { + "id": "Schema", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "format": { + "type": "string" + }, + "description": { + "type": "string" + }, + "nullable": { + "type": "boolean" + }, + "items": { + "${'$'}ref": "Schema" + }, + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "${'$'}ref": "Schema" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(Schema.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIStreamingSnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIStreamingSnapshotTests.kt new file mode 100644 index 00000000000..e6331401fde --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIStreamingSnapshotTests.kt @@ -0,0 +1,246 @@ +/* + * Copyright 2024 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.firebase.ai + +import com.google.firebase.ai.type.BlockReason +import com.google.firebase.ai.type.FinishReason +import com.google.firebase.ai.type.HarmCategory +import com.google.firebase.ai.type.InvalidAPIKeyException +import com.google.firebase.ai.type.PromptBlockedException +import com.google.firebase.ai.type.ResponseStoppedException +import com.google.firebase.ai.type.SerializationException +import com.google.firebase.ai.type.ServerException +import com.google.firebase.ai.type.TextPart +import com.google.firebase.ai.util.goldenVertexStreamingFile +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.ktor.http.HttpStatusCode +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.withTimeout +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class VertexAIStreamingSnapshotTests { + private val testTimeout = 5.seconds + + @Test + fun `short reply`() = + goldenVertexStreamingFile("streaming-success-basic-reply-short.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.isEmpty() shouldBe false + responseList.last().candidates.first().apply { + finishReason shouldBe FinishReason.STOP + content.parts.isEmpty() shouldBe false + safetyRatings.isEmpty() shouldBe false + } + } + } + + @Test + fun `long reply`() = + goldenVertexStreamingFile("streaming-success-basic-reply-long.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.isEmpty() shouldBe false + responseList.last().candidates.first().apply { + finishReason shouldBe FinishReason.STOP + content.parts.isEmpty() shouldBe false + } + } + } + + @Test + fun `unknown enum in safety ratings`() = + goldenVertexStreamingFile("streaming-success-unknown-safety-enum.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + + responseList.isEmpty() shouldBe false + responseList.any { + it.candidates.any { it.safetyRatings.any { it.category == HarmCategory.UNKNOWN } } + } shouldBe true + } + } + + @Test + fun `invalid safety ratings during image generation`() = + goldenVertexStreamingFile("streaming-success-image-invalid-safety-ratings.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + + responseList.isEmpty() shouldBe false + } + } + + @Test + fun `unknown enum in finish reason`() = + goldenVertexStreamingFile("streaming-failure-unknown-finish-enum.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val exception = shouldThrow { responses.collect() } + exception.response.candidates.first().finishReason shouldBe FinishReason.UNKNOWN + } + } + + @Test + fun `quotes escaped`() = + goldenVertexStreamingFile("streaming-success-quotes-escaped.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + + responseList.isEmpty() shouldBe false + val part = responseList.first().candidates.first().content.parts.first() as? TextPart + part.shouldNotBeNull() + part.text shouldContain "\"" + } + } + + @Test + fun `prompt blocked for safety`() = + goldenVertexStreamingFile("streaming-failure-prompt-blocked-safety.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val exception = shouldThrow { responses.collect() } + exception.response?.promptFeedback?.blockReason shouldBe BlockReason.SAFETY + } + } + + @Test + fun `prompt blocked for safety with message`() = + goldenVertexStreamingFile("streaming-failure-prompt-blocked-safety-with-message.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val exception = shouldThrow { responses.collect() } + exception.response?.promptFeedback?.blockReason shouldBe BlockReason.SAFETY + exception.response?.promptFeedback?.blockReasonMessage shouldBe "Reasons" + } + } + + @Test + fun `empty content`() = + goldenVertexStreamingFile("streaming-failure-empty-content.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { shouldThrow { responses.collect() } } + } + + @Test + fun `http errors`() = + goldenVertexStreamingFile( + "streaming-failure-http-error.txt", + HttpStatusCode.PreconditionFailed + ) { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { shouldThrow { responses.collect() } } + } + + @Test + fun `stopped for safety`() = + goldenVertexStreamingFile("streaming-failure-finish-reason-safety.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val exception = shouldThrow { responses.collect() } + exception.response.candidates.first().finishReason shouldBe FinishReason.SAFETY + } + } + + @Test + fun `citation parsed correctly`() = + goldenVertexStreamingFile("streaming-success-citations.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.any { + it.candidates.any { it.citationMetadata?.citations?.isNotEmpty() ?: false } + } shouldBe true + } + } + + @Test + fun `stopped for recitation`() = + goldenVertexStreamingFile("streaming-failure-recitation-no-content.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val exception = shouldThrow { responses.collect() } + exception.response.candidates.first().finishReason shouldBe FinishReason.RECITATION + } + } + + @Test + fun `image rejected`() = + goldenVertexStreamingFile("streaming-failure-image-rejected.txt", HttpStatusCode.BadRequest) { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { shouldThrow { responses.collect() } } + } + + @Test + fun `unknown model`() = + goldenVertexStreamingFile("streaming-failure-unknown-model.txt", HttpStatusCode.NotFound) { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { shouldThrow { responses.collect() } } + } + + @Test + fun `invalid api key`() = + goldenVertexStreamingFile("streaming-failure-api-key.txt", HttpStatusCode.BadRequest) { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { shouldThrow { responses.collect() } } + } + + @Test + fun `invalid json`() = + goldenVertexStreamingFile("streaming-failure-invalid-json.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { shouldThrow { responses.collect() } } + } + + @Test + fun `malformed content`() = + goldenVertexStreamingFile("streaming-failure-malformed-content.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { shouldThrow { responses.collect() } } + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIUnarySnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIUnarySnapshotTests.kt new file mode 100644 index 00000000000..ca1d279d288 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIUnarySnapshotTests.kt @@ -0,0 +1,592 @@ +/* + * Copyright 2024 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.firebase.ai + +import com.google.firebase.ai.type.BlockReason +import com.google.firebase.ai.type.ContentBlockedException +import com.google.firebase.ai.type.ContentModality +import com.google.firebase.ai.type.FinishReason +import com.google.firebase.ai.type.FunctionCallPart +import com.google.firebase.ai.type.HarmCategory +import com.google.firebase.ai.type.HarmProbability +import com.google.firebase.ai.type.HarmSeverity +import com.google.firebase.ai.type.InvalidAPIKeyException +import com.google.firebase.ai.type.PromptBlockedException +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.QuotaExceededException +import com.google.firebase.ai.type.ResponseStoppedException +import com.google.firebase.ai.type.SerializationException +import com.google.firebase.ai.type.ServerException +import com.google.firebase.ai.type.ServiceDisabledException +import com.google.firebase.ai.type.TextPart +import com.google.firebase.ai.type.UnsupportedUserLocationException +import com.google.firebase.ai.util.goldenVertexUnaryFile +import com.google.firebase.ai.util.shouldNotBeNullOrEmpty +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.inspectors.forAtLeastOne +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotBeEmpty +import io.kotest.matchers.types.shouldBeInstanceOf +import io.ktor.http.HttpStatusCode +import java.util.Calendar +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.json.JSONArray +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@OptIn(PublicPreviewAPI::class) +@RunWith(RobolectricTestRunner::class) +internal class VertexAIUnarySnapshotTests { + private val testTimeout = 5.seconds + + @Test + fun `short reply`() = + goldenVertexUnaryFile("unary-success-basic-reply-short.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().finishReason shouldBe FinishReason.STOP + response.candidates.first().content.parts.isEmpty() shouldBe false + response.candidates.first().safetyRatings.isEmpty() shouldBe false + } + } + + @Test + fun `long reply`() = + goldenVertexUnaryFile("unary-success-basic-reply-long.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().finishReason shouldBe FinishReason.STOP + response.candidates.first().content.parts.isEmpty() shouldBe false + response.candidates.first().safetyRatings.isEmpty() shouldBe false + } + } + + @Test + fun `response with detailed token-based usageMetadata`() = + goldenVertexUnaryFile("unary-success-basic-response-long-usage-metadata.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().finishReason shouldBe FinishReason.STOP + response.candidates.first().content.parts.isEmpty() shouldBe false + response.usageMetadata shouldNotBe null + response.usageMetadata?.apply { + totalTokenCount shouldBe 1913 + candidatesTokenCount shouldBe 76 + promptTokensDetails?.forAtLeastOne { + it.modality shouldBe ContentModality.IMAGE + it.tokenCount shouldBe 1806 + } + candidatesTokensDetails?.forAtLeastOne { + it.modality shouldBe ContentModality.TEXT + it.tokenCount shouldBe 76 + } + } + } + } + + @Test + fun `unknown enum in safety ratings`() = + goldenVertexUnaryFile("unary-success-unknown-enum-safety-ratings.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + val candidate = response.candidates.first() + candidate.safetyRatings.any { it.category == HarmCategory.UNKNOWN } shouldBe true + response.promptFeedback?.safetyRatings?.any { it.category == HarmCategory.UNKNOWN } shouldBe + true + } + } + + @Test + fun `invalid safety ratings during image generation`() = + goldenVertexUnaryFile("unary-success-image-invalid-safety-ratings.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + } + } + + @Test + fun `unknown enum in finish reason`() = + goldenVertexUnaryFile("unary-failure-unknown-enum-finish-reason.json") { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } should + { + it.response.candidates.first().finishReason shouldBe FinishReason.UNKNOWN + } + } + } + + @Test + fun `unknown enum in block reason`() = + goldenVertexUnaryFile("unary-failure-unknown-enum-prompt-blocked.json") { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } should + { + it.response?.promptFeedback?.blockReason shouldBe BlockReason.UNKNOWN + } + } + } + + @Test + fun `quotes escaped`() = + goldenVertexUnaryFile("unary-success-quote-reply.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().content.parts.isEmpty() shouldBe false + val part = response.candidates.first().content.parts.first() as TextPart + part.text shouldContain "\"" + } + } + + @Test + fun `safetyRatings missing`() = + goldenVertexUnaryFile("unary-success-missing-safety-ratings.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().content.parts.isEmpty() shouldBe false + response.candidates.first().safetyRatings.isEmpty() shouldBe true + response.promptFeedback?.safetyRatings?.isEmpty() shouldBe true + } + } + + @Test + fun `safetyRatings including severity`() = + goldenVertexUnaryFile("unary-success-including-severity.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().safetyRatings.isEmpty() shouldBe false + response.candidates.first().safetyRatings.all { + it.probability == HarmProbability.NEGLIGIBLE + } shouldBe true + response.candidates.first().safetyRatings.all { + it.severity == HarmSeverity.NEGLIGIBLE + } shouldBe true + response.candidates.first().safetyRatings.all { it.severityScore != null } shouldBe true + } + } + + @Test + fun `function call has no arguments field`() = + goldenVertexUnaryFile("unary-success-function-call-empty-arguments.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + val content = response.candidates.shouldNotBeNullOrEmpty().first().content + content.shouldNotBeNull() + val callPart = content.parts.shouldNotBeNullOrEmpty().first() as FunctionCallPart + + callPart.name shouldBe "current_time" + callPart.args shouldBe emptyMap() + } + } + + @Test + fun `prompt blocked for safety`() = + goldenVertexUnaryFile("unary-failure-prompt-blocked-safety.json") { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } should + { + it.response?.promptFeedback?.blockReason shouldBe BlockReason.SAFETY + } + } + } + + @Test + fun `prompt blocked for safety with message`() = + goldenVertexUnaryFile("unary-failure-prompt-blocked-safety-with-message.json") { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } should + { + it.response?.promptFeedback?.blockReason shouldBe BlockReason.SAFETY + it.response?.promptFeedback?.blockReasonMessage shouldContain "Reasons" + } + } + } + + @Test + fun `empty content`() = + goldenVertexUnaryFile("unary-failure-empty-content.json") { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } + } + } + + @Test + fun `http error`() = + goldenVertexUnaryFile("unary-failure-http-error.json", HttpStatusCode.PreconditionFailed) { + withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } + } + + @Test + fun `user location error`() = + goldenVertexUnaryFile( + "unary-failure-unsupported-user-location.json", + HttpStatusCode.PreconditionFailed, + ) { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } + } + } + + @Test + fun `stopped for safety`() = + goldenVertexUnaryFile("unary-failure-finish-reason-safety.json") { + withTimeout(testTimeout) { + val exception = shouldThrow { model.generateContent("prompt") } + exception.response.candidates.first().finishReason shouldBe FinishReason.SAFETY + exception.response.candidates.first().safetyRatings.forAtLeastOne { + it.category shouldBe HarmCategory.HARASSMENT + it.probability shouldBe HarmProbability.LOW + it.severity shouldBe HarmSeverity.LOW + } + } + } + + @Test + fun `quota exceeded`() = + goldenVertexUnaryFile("unary-failure-quota-exceeded.json", HttpStatusCode.BadRequest) { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } + } + } + + @Test + fun `stopped for safety with no content`() = + goldenVertexUnaryFile("unary-failure-finish-reason-safety-no-content.json") { + withTimeout(testTimeout) { + val exception = shouldThrow { model.generateContent("prompt") } + exception.response.candidates.first().finishReason shouldBe FinishReason.SAFETY + } + } + + @Test + fun `citation returns correctly`() = + goldenVertexUnaryFile("unary-success-citations.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().citationMetadata?.citations?.size shouldBe 3 + response.candidates.first().citationMetadata?.citations?.forAtLeastOne { + it.publicationDate?.get(Calendar.YEAR) shouldBe 2019 + it.publicationDate?.get(Calendar.DAY_OF_MONTH) shouldBe 10 + } + } + } + + @Test + fun `citation returns correctly with missing license and startIndex`() = + goldenVertexUnaryFile("unary-success-citations-nolicense.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().citationMetadata?.citations?.isEmpty() shouldBe false + // Verify the values in the citation source + val firstCitation = response.candidates.first().citationMetadata?.citations?.first() + if (firstCitation != null) { + with(firstCitation) { + license shouldBe null + startIndex shouldBe 0 + } + } + } + } + + @Test + fun `response includes usage metadata`() = + goldenVertexUnaryFile("unary-success-usage-metadata.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().finishReason shouldBe FinishReason.STOP + response.usageMetadata shouldNotBe null + response.usageMetadata?.totalTokenCount shouldBe 363 + response.usageMetadata?.promptTokensDetails?.isEmpty() shouldBe true + } + } + + @Test + fun `response includes partial usage metadata`() = + goldenVertexUnaryFile("unary-success-partial-usage-metadata.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().finishReason shouldBe FinishReason.STOP + response.usageMetadata shouldNotBe null + response.usageMetadata?.promptTokenCount shouldBe 6 + response.usageMetadata?.totalTokenCount shouldBe 0 + } + } + + @Test + fun `properly translates json text`() = + goldenVertexUnaryFile("unary-success-constraint-decoding-json.json") { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + with(response.candidates.first().content.parts.first().shouldBeInstanceOf()) { + shouldNotBeNull() + val jsonArr = JSONArray(text) + jsonArr.length() shouldBe 3 + for (i in 0 until jsonArr.length()) { + with(jsonArr.getJSONObject(i)) { + shouldNotBeNull() + getString("name").shouldNotBeEmpty() + getJSONArray("colors").length() shouldBe 5 + } + } + } + } + + @Test + fun `invalid response`() = + goldenVertexUnaryFile("unary-failure-invalid-response.json") { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } + } + } + + @Test + fun `malformed content`() = + goldenVertexUnaryFile("unary-failure-malformed-content.json") { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } + } + } + + @Test + fun `invalid api key`() = + goldenVertexUnaryFile("unary-failure-api-key.json", HttpStatusCode.BadRequest) { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } + } + } + + @Test + fun `image rejected`() = + goldenVertexUnaryFile("unary-failure-image-rejected.json", HttpStatusCode.BadRequest) { + withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } + } + + @Test + fun `unknown model`() = + goldenVertexUnaryFile("unary-failure-unknown-model.json", HttpStatusCode.NotFound) { + withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } + } + + @Test + fun `service disabled`() = + goldenVertexUnaryFile( + "unary-failure-firebaseml-api-not-enabled.json", + HttpStatusCode.Forbidden + ) { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } + } + } + + @Test + fun `function call contains null param`() = + goldenVertexUnaryFile("unary-success-function-call-null.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + val callPart = (response.candidates.first().content.parts.first() as FunctionCallPart) + + callPart.args["season"] shouldBe JsonPrimitive(null) + } + } + + @Test + fun `function call contains json literal`() = + goldenVertexUnaryFile("unary-success-function-call-json-literal.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + val content = response.candidates.shouldNotBeNullOrEmpty().first().content + val callPart = + content.let { + it.shouldNotBeNull() + it.parts.shouldNotBeEmpty() + it.parts.first().shouldBeInstanceOf() + } + + callPart.args["current"] shouldBe JsonPrimitive(true) + } + } + + @Test + fun `function call with complex json literal parses correctly`() = + goldenVertexUnaryFile("unary-success-function-call-complex-json-literal.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + val content = response.candidates.shouldNotBeNullOrEmpty().first().content + val callPart = + content.let { + it.shouldNotBeNull() + it.parts.shouldNotBeEmpty() + it.parts.first().shouldBeInstanceOf() + } + + callPart.args["current"] shouldBe JsonPrimitive(true) + callPart.args["testObject"]!!.jsonObject["testProperty"]!!.jsonPrimitive.content shouldBe + "string property" + } + } + + @Test + fun `function call contains no arguments`() = + goldenVertexUnaryFile("unary-success-function-call-no-arguments.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + val callPart = response.functionCalls.shouldNotBeEmpty().first() + + callPart.name shouldBe "current_time" + callPart.args.isEmpty() shouldBe true + } + } + + @Test + fun `function call contains arguments`() = + goldenVertexUnaryFile("unary-success-function-call-with-arguments.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + val callPart = response.functionCalls.shouldNotBeEmpty().first() + + callPart.name shouldBe "sum" + callPart.args["x"] shouldBe JsonPrimitive(4) + callPart.args["y"] shouldBe JsonPrimitive(5) + } + } + + @Test + fun `function call with parallel calls`() = + goldenVertexUnaryFile("unary-success-function-call-parallel-calls.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + val callList = response.functionCalls + + callList.size shouldBe 3 + callList.forEach { + it.name shouldBe "sum" + it.args.size shouldBe 2 + } + } + } + + @Test + fun `function call with mixed content`() = + goldenVertexUnaryFile("unary-success-function-call-mixed-content.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + val callList = response.functionCalls + + response.text shouldBe "The sum of [1, 2, 3] is" + callList.size shouldBe 2 + callList.forEach { it.args.size shouldBe 2 } + } + } + + @Test + fun `countTokens succeeds`() = + goldenVertexUnaryFile("unary-success-total-tokens.json") { + withTimeout(testTimeout) { + val response = model.countTokens("prompt") + + response.totalTokens shouldBe 6 + response.totalBillableCharacters shouldBe 16 + response.promptTokensDetails.isEmpty() shouldBe true + } + } + + @Test + fun `countTokens with modality fields returned`() = + goldenVertexUnaryFile("unary-success-detailed-token-response.json") { + withTimeout(testTimeout) { + val response = model.countTokens("prompt") + + response.totalTokens shouldBe 1837 + response.totalBillableCharacters shouldBe 117 + response.promptTokensDetails shouldNotBe null + response.promptTokensDetails?.forAtLeastOne { + it.modality shouldBe ContentModality.IMAGE + it.tokenCount shouldBe 1806 + } + } + } + + @Test + fun `countTokens succeeds with no billable characters`() = + goldenVertexUnaryFile("unary-success-no-billable-characters.json") { + withTimeout(testTimeout) { + val response = model.countTokens("prompt") + + response.totalTokens shouldBe 258 + response.totalBillableCharacters shouldBe 0 + } + } + + @Test + fun `countTokens fails with model not found`() = + goldenVertexUnaryFile("unary-failure-model-not-found.json", HttpStatusCode.NotFound) { + withTimeout(testTimeout) { shouldThrow { model.countTokens("prompt") } } + } + + @Test + fun `generateImages should throw when all images filtered`() = + goldenVertexUnaryFile("unary-failure-generate-images-all-filtered.json") { + withTimeout(testTimeout) { + shouldThrow { imagenModel.generateImages("prompt") } + } + } + + @Test + fun `generateImages should throw when prompt blocked`() = + goldenVertexUnaryFile( + "unary-failure-generate-images-prompt-blocked.json", + HttpStatusCode.BadRequest, + ) { + withTimeout(testTimeout) { + shouldThrow { imagenModel.generateImages("prompt") } + } + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt new file mode 100644 index 00000000000..b1ae69c25b2 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt @@ -0,0 +1,435 @@ +/* + * Copyright 2024 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.firebase.ai.common + +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.BuildConfig +import com.google.firebase.ai.common.util.commonTest +import com.google.firebase.ai.common.util.createResponses +import com.google.firebase.ai.common.util.doBlocking +import com.google.firebase.ai.common.util.prepareStreamingResponse +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.CountTokensResponse +import com.google.firebase.ai.type.FunctionCallingConfig +import com.google.firebase.ai.type.RequestOptions +import com.google.firebase.ai.type.TextPart +import com.google.firebase.ai.type.Tool +import com.google.firebase.ai.type.ToolConfig +import io.kotest.assertions.json.shouldContainJsonKey +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.content.TextContent +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.ktor.utils.io.ByteChannel +import io.ktor.utils.io.close +import io.ktor.utils.io.writeFully +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.delay +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.JsonObject +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.Mockito + +private val TEST_CLIENT_ID = "genai-android/test" + +private val TEST_APP_ID = "1:android:12345" + +private val TEST_VERSION = 1 + +internal class APIControllerTests { + private val testTimeout = 5.seconds + + @Test + fun `(generateContentStream) emits responses as they come in`() = commonTest { + val response = createResponses("The", " world", " is", " a", " beautiful", " place!") + val bytes = prepareStreamingResponse(response) + + bytes.forEach { channel.writeFully(it) } + val responses = apiController.generateContentStream(textGenerateContentRequest("test")) + + withTimeout(testTimeout) { + responses.collect { + it.candidates?.isEmpty() shouldBe false + channel.close() + } + } + } + + @Test + fun `(generateContent) respects a custom timeout`() = + commonTest(requestOptions = RequestOptions(2.seconds)) { + shouldThrow { + withTimeout(testTimeout) { + apiController.generateContent(textGenerateContentRequest("test")) + } + } + } +} + +@OptIn(ExperimentalSerializationApi::class) +internal class RequestFormatTests { + + private val mockFirebaseApp = Mockito.mock() + + @Before + fun setup() { + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + } + + @Test + fun `using default endpoint`() = doBlocking { + val channel = ByteChannel(autoFlush = true) + val mockEngine = MockEngine { + respond(channel, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + prepareStreamingResponse(createResponses("Random")).forEach { channel.writeFully(it) } + val controller = + APIController( + "super_cool_test_key", + "gemini-pro-1.5", + RequestOptions(), + mockEngine, + "genai-android/${BuildConfig.VERSION_NAME}", + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + withTimeout(5.seconds) { + controller.generateContentStream(textGenerateContentRequest("cats")).collect { + it.candidates?.isEmpty() shouldBe false + channel.close() + } + } + + mockEngine.requestHistory.first().url.host shouldBe "firebasevertexai.googleapis.com" + } + + @Test + fun `using custom endpoint`() = doBlocking { + val channel = ByteChannel(autoFlush = true) + val mockEngine = MockEngine { + respond(channel, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + prepareStreamingResponse(createResponses("Random")).forEach { channel.writeFully(it) } + val controller = + APIController( + "super_cool_test_key", + "gemini-pro-1.5", + RequestOptions(timeout = 5.seconds, endpoint = "https://my.custom.endpoint"), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + withTimeout(5.seconds) { + controller.generateContentStream(textGenerateContentRequest("cats")).collect { + it.candidates?.isEmpty() shouldBe false + channel.close() + } + } + + mockEngine.requestHistory.first().url.host shouldBe "my.custom.endpoint" + } + + @Test + fun `client id header is set correctly in the request`() = doBlocking { + val response = JSON.encodeToString(CountTokensResponse.Internal(totalTokens = 10)) + val mockEngine = MockEngine { + respond(response, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + + val controller = + APIController( + "super_cool_test_key", + "gemini-pro-1.5", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + withTimeout(5.seconds) { controller.countTokens(textCountTokenRequest("cats")) } + + mockEngine.requestHistory.first().headers["x-goog-api-client"] shouldBe TEST_CLIENT_ID + } + + @Test + fun `ml monitoring header is set correctly if data collection is enabled`() = doBlocking { + val response = JSON.encodeToString(CountTokensResponse.Internal(totalTokens = 10)) + val mockEngine = MockEngine { + respond(response, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(true) + + val controller = + APIController( + "super_cool_test_key", + "gemini-pro-1.5", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + withTimeout(5.seconds) { controller.countTokens(textCountTokenRequest("cats")) } + + mockEngine.requestHistory.first().headers["X-Firebase-AppId"] shouldBe TEST_APP_ID + mockEngine.requestHistory.first().headers["X-Firebase-AppVersion"] shouldBe + TEST_VERSION.toString() + } + + @Test + fun `ToolConfig serialization contains correct keys`() = doBlocking { + val channel = ByteChannel(autoFlush = true) + val mockEngine = MockEngine { + respond(channel, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + prepareStreamingResponse(createResponses("Random")).forEach { channel.writeFully(it) } + + val controller = + APIController( + "super_cool_test_key", + "gemini-pro-1.5", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + withTimeout(5.seconds) { + controller + .generateContentStream( + GenerateContentRequest( + model = "unused", + contents = listOf(Content.Internal(parts = listOf(TextPart.Internal("Arbitrary")))), + toolConfig = + ToolConfig.Internal( + FunctionCallingConfig.Internal( + mode = FunctionCallingConfig.Internal.Mode.ANY, + allowedFunctionNames = listOf("allowedFunctionName") + ) + ) + ), + ) + .collect { channel.close() } + } + + val requestBodyAsText = (mockEngine.requestHistory.first().body as TextContent).text + + requestBodyAsText shouldContainJsonKey "tool_config.function_calling_config.mode" + requestBodyAsText shouldContainJsonKey + "tool_config.function_calling_config.allowed_function_names" + } + + @Test + fun `headers from HeaderProvider are added to the request`() = doBlocking { + val response = JSON.encodeToString(CountTokensResponse.Internal(totalTokens = 10)) + val mockEngine = MockEngine { + respond(response, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + + val testHeaderProvider = + object : HeaderProvider { + override val timeout: Duration + get() = 5.seconds + + override suspend fun generateHeaders(): Map = + mapOf("header1" to "value1", "header2" to "value2") + } + + val controller = + APIController( + "super_cool_test_key", + "gemini-pro-1.5", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + testHeaderProvider, + ) + + withTimeout(5.seconds) { controller.countTokens(textCountTokenRequest("cats")) } + + mockEngine.requestHistory.first().headers["header1"] shouldBe "value1" + mockEngine.requestHistory.first().headers["header2"] shouldBe "value2" + } + + @Test + fun `headers from HeaderProvider are ignored if timeout`() = doBlocking { + val response = JSON.encodeToString(CountTokensResponse.Internal(totalTokens = 10)) + val mockEngine = MockEngine { + respond(response, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + + val testHeaderProvider = + object : HeaderProvider { + override val timeout: Duration + get() = 5.milliseconds + + override suspend fun generateHeaders(): Map { + delay(10.milliseconds) + return mapOf("header1" to "value1") + } + } + + val controller = + APIController( + "super_cool_test_key", + "gemini-pro-1.5", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + testHeaderProvider, + ) + + withTimeout(5.seconds) { controller.countTokens(textCountTokenRequest("cats")) } + + mockEngine.requestHistory.first().headers.contains("header1") shouldBe false + } + + @Test + fun `code execution tool serialization contains correct keys`() = doBlocking { + val channel = ByteChannel(autoFlush = true) + val mockEngine = MockEngine { + respond(channel, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + prepareStreamingResponse(createResponses("Random")).forEach { channel.writeFully(it) } + + val controller = + APIController( + "super_cool_test_key", + "gemini-pro-1.5", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + withTimeout(5.seconds) { + controller + .generateContentStream( + GenerateContentRequest( + model = "unused", + contents = listOf(Content.Internal(parts = listOf(TextPart.Internal("Arbitrary")))), + tools = listOf(Tool.Internal(codeExecution = JsonObject(emptyMap()))), + ) + ) + .collect { channel.close() } + } + + val requestBodyAsText = (mockEngine.requestHistory.first().body as TextContent).text + + requestBodyAsText shouldContainJsonKey "tools[0].codeExecution" + } +} + +@RunWith(Parameterized::class) +internal class ModelNamingTests(private val modelName: String, private val actualName: String) { + private val mockFirebaseApp = Mockito.mock() + + @Before + fun setup() { + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + } + + @Test + fun `request should include right model name`() = doBlocking { + val channel = ByteChannel(autoFlush = true) + val mockEngine = MockEngine { + respond(channel, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + prepareStreamingResponse(createResponses("Random")).forEach { channel.writeFully(it) } + val controller = + APIController( + "super_cool_test_key", + modelName, + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + withTimeout(5.seconds) { + controller.generateContentStream(textGenerateContentRequest("cats")).collect { + it.candidates?.isEmpty() shouldBe false + channel.close() + } + } + + mockEngine.requestHistory.first().url.encodedPath shouldContain actualName + } + + companion object { + @JvmStatic + @Parameterized.Parameters + fun data() = + listOf( + arrayOf("gemini-pro", "models/gemini-pro"), + arrayOf("x/gemini-pro", "x/gemini-pro"), + arrayOf("models/gemini-pro", "models/gemini-pro"), + arrayOf("/modelname", "/modelname"), + arrayOf("modifiedNaming/mymodel", "modifiedNaming/mymodel"), + ) + } +} + +internal fun textGenerateContentRequest(prompt: String) = + GenerateContentRequest( + model = "unused", + contents = listOf(Content.Internal(parts = listOf(TextPart.Internal(prompt)))), + ) + +internal fun textCountTokenRequest(prompt: String) = + CountTokensRequest(generateContentRequest = textGenerateContentRequest(prompt)) diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/EnumUpdateTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/EnumUpdateTests.kt new file mode 100644 index 00000000000..dea94317743 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/EnumUpdateTests.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2024 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.firebase.ai.common + +import com.google.firebase.ai.type.HarmBlockMethod +import com.google.firebase.ai.type.HarmBlockThreshold +import com.google.firebase.ai.type.HarmCategory +import org.junit.Test + +/** + * Fetches all the `@JvmStatic` properties of a class that are instances of the class itself. + * + * For example, given the following class: + * ```kt + * public class HarmCategory private constructor(public val ordinal: Int) { + * public companion object { + * @JvmField public val UNKNOWN: HarmCategory = HarmCategory(0) + * @JvmField public val HARASSMENT: HarmCategory = HarmCategory(1) + * } + * } + * ``` + * This function will yield: + * ```kt + * [UNKNOWN, HARASSMENT] + * ``` + */ +internal inline fun getEnumValues(): List { + return T::class + .java + .declaredFields + .filter { it.type == T::class.java } + .mapNotNull { it.get(null) as? T } +} + +/** + * Ensures that whenever any of our "pseudo-enums" are updated, that the conversion layer is also + * updated. + */ +internal class EnumUpdateTests { + @Test + fun `HarmCategory#toInternal() covers all values`() { + val values = getEnumValues() + values.forEach { it.toInternal() } + } + + @Test + fun `HarmBlockMethod#toInternal() covers all values`() { + val values = getEnumValues() + values.forEach { it.toInternal() } + } + + @Test + fun `HarmBlockThreshold#toInternal() covers all values`() { + val values = getEnumValues() + values.forEach { it.toInternal() } + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/util/descriptorToJson.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/descriptorToJson.kt new file mode 100644 index 00000000000..797c4587665 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/descriptorToJson.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2025 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.firebase.ai.common.util + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.descriptors.elementDescriptors +import kotlinx.serialization.descriptors.elementNames +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject + +/** + * Returns a [JsonObject] representing the classes in the hierarchy of a serialization [descriptor]. + * + * The format of the JSON object is similar to that of a Discovery Document, but restricted to these + * fields: + * - id + * - type + * - properties + * - items + * - $ref + * + * @param descriptor The [SerialDescriptor] to process. + */ +@OptIn(ExperimentalSerializationApi::class) +internal fun descriptorToJson(descriptor: SerialDescriptor): JsonObject { + return buildJsonObject { + put("id", simpleNameFromSerialName(descriptor.serialName)) + put("type", typeNameFromKind(descriptor.kind)) + if (descriptor.kind != StructureKind.CLASS) { + throw UnsupportedOperationException("Only classes can be serialized to JSON for now.") + } + // For top-level enums, add them directly. + if (descriptor.serialName == "FirstOrdinalSerializer") { + addEnumDescription(descriptor) + } else { + addObjectProperties(descriptor) + } + } +} + +@OptIn(ExperimentalSerializationApi::class) +internal fun JsonObjectBuilder.addListDescription(descriptor: SerialDescriptor) = + putJsonObject("items") { + val itemDescriptor = descriptor.elementDescriptors.first() + val nestedIsPrimitive = (descriptor.elementsCount == 1 && itemDescriptor.kind is PrimitiveKind) + if (nestedIsPrimitive) { + put("type", typeNameFromKind(itemDescriptor.kind)) + } else { + put("\$ref", simpleNameFromSerialName(itemDescriptor.serialName)) + } + } + +@OptIn(ExperimentalSerializationApi::class) +internal fun JsonObjectBuilder.addEnumDescription(descriptor: SerialDescriptor): JsonElement? { + put("type", typeNameFromKind(SerialKind.ENUM)) + return put("enum", JsonArray(descriptor.elementNames.map { JsonPrimitive(it) })) +} + +@OptIn(ExperimentalSerializationApi::class) +internal fun JsonObjectBuilder.addObjectProperties(descriptor: SerialDescriptor): JsonElement? { + return putJsonObject("properties") { + for (i in 0 until descriptor.elementsCount) { + val elementDescriptor = descriptor.getElementDescriptor(i) + val elementName = descriptor.getElementName(i) + putJsonObject(elementName) { + when (elementDescriptor.kind) { + StructureKind.LIST -> { + put("type", typeNameFromKind(elementDescriptor.kind)) + addListDescription(elementDescriptor) + } + StructureKind.CLASS -> { + if (elementDescriptor.serialName.startsWith("FirstOrdinalSerializer")) { + addEnumDescription(elementDescriptor) + } else { + put("\$ref", simpleNameFromSerialName(elementDescriptor.serialName)) + } + } + StructureKind.MAP -> { + put("type", typeNameFromKind(elementDescriptor.kind)) + putJsonObject("additionalProperties") { + put( + "\$ref", + simpleNameFromSerialName(elementDescriptor.getElementDescriptor(1).serialName) + ) + } + } + else -> { + put("type", typeNameFromKind(elementDescriptor.kind)) + } + } + } + } + } +} + +@OptIn(ExperimentalSerializationApi::class) +internal fun typeNameFromKind(kind: SerialKind): String { + return when (kind) { + PrimitiveKind.BOOLEAN -> "boolean" + PrimitiveKind.BYTE -> "integer" + PrimitiveKind.CHAR -> "string" + PrimitiveKind.DOUBLE -> "number" + PrimitiveKind.FLOAT -> "number" + PrimitiveKind.INT -> "integer" + PrimitiveKind.LONG -> "integer" + PrimitiveKind.SHORT -> "integer" + PrimitiveKind.STRING -> "string" + StructureKind.CLASS -> "object" + StructureKind.LIST -> "array" + SerialKind.ENUM -> "string" + StructureKind.MAP -> "object" + /* Only add new cases if they show up in actual test scenarios. */ + else -> TODO() + } +} + +/** + * Extracts the name expected for a class from its serial name. + * + * Our serialization classes are nested within the public-facing classes, and that's the name we + * want in the json output. There are two class names + * + * - `com.google.firebase.ai.type.Content.Internal` for regular scenarios + * - `com.google.firebase.ai.type.Content.Internal.SomeClass` for nested classes in the serializer. + * + * For the later time we need the second to last component, for the former we need the last + * component. + * + * Additionally, given that types can be nullable, we need to strip the `?` from the end of the + * name. + */ +internal fun simpleNameFromSerialName(serialName: String): String = + serialName + .split(".") + .let { + if (it.last().startsWith("Internal")) { + it[it.size - 2] + } else { + it.last() + } + } + .replace("?", "") diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/util/kotlin.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/kotlin.kt new file mode 100644 index 00000000000..5187607cc3b --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/kotlin.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 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.firebase.ai.common.util + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking + +/** + * Runs the given [block] using [runBlocking] on the current thread for side effect. + * + * Using this function is like [runBlocking] with default context (which runs the given block on the + * calling thread) but forces the return type to be `Unit`, which is helpful when implementing + * suspending tests as expression functions: + * ``` + * @Test + * fun myTest() = doBlocking {...} + * ``` + */ +internal fun doBlocking(block: suspend CoroutineScope.() -> Unit) { + runBlocking(block = block) +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt new file mode 100644 index 00000000000..6cc501cedd5 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2024 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. + */ + +@file:Suppress("DEPRECATION") // a replacement for our purposes has not been published yet + +package com.google.firebase.ai.common.util + +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.common.APIController +import com.google.firebase.ai.common.JSON +import com.google.firebase.ai.type.Candidate +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.RequestOptions +import com.google.firebase.ai.type.TextPart +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.ktor.utils.io.ByteChannel +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString +import org.mockito.Mockito + +private val TEST_CLIENT_ID = "genai-android/test" +private val TEST_APP_ID = "1:android:12345" +private val TEST_VERSION = 1 + +internal fun prepareStreamingResponse( + response: List +): List = response.map { "data: ${JSON.encodeToString(it)}$SSE_SEPARATOR".toByteArray() } + +@OptIn(ExperimentalSerializationApi::class) +internal fun createResponses(vararg text: String): List { + val candidates = + text.map { Candidate.Internal(Content.Internal(parts = listOf(TextPart.Internal(it)))) } + + return candidates.map { GenerateContentResponse.Internal(candidates = listOf(it)) } +} + +/** + * Wrapper around common instances needed in tests. + * + * @param channel A [ByteChannel] for sending responses through the mock HTTP engine + * @param apiController A [APIController] that consumes the [channel] + * @see commonTest + * @see send + */ +internal data class CommonTestScope(val channel: ByteChannel, val apiController: APIController) + +/** A test that runs under a [CommonTestScope]. */ +internal typealias CommonTest = suspend CommonTestScope.() -> Unit + +/** + * Common test block for providing a [CommonTestScope] during tests. + * + * Example usage: + * ``` + * @Test + * fun `(generateContent) generates a proper response`() = commonTest { + * val request = createRequest("say something nice") + * val response = createResponse("The world is a beautiful place!") + * + * channel.send(prepareResponse(response)) + * + * withTimeout(testTimeout) { + * val data = controller.generateContent(request) + * data.candidates.shouldNotBeEmpty() + * } + * } + * ``` + * + * @param status An optional [HttpStatusCode] to return as a response + * @param requestOptions Optional [RequestOptions] to utilize in the underlying controller + * @param block The test contents themselves, with the [CommonTestScope] implicitly provided + * @see CommonTestScope + */ +internal fun commonTest( + status: HttpStatusCode = HttpStatusCode.OK, + requestOptions: RequestOptions = RequestOptions(), + block: CommonTest, +) = doBlocking { + val mockFirebaseApp = Mockito.mock() + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + + val channel = ByteChannel(autoFlush = true) + val apiController = + APIController( + "super_cool_test_key", + "gemini-pro", + requestOptions, + MockEngine { + respond(channel, status, headersOf(HttpHeaders.ContentType, "application/json")) + }, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + CommonTestScope(channel, apiController).block() +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/FunctionDeclarationTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/FunctionDeclarationTest.kt new file mode 100644 index 00000000000..7719044b498 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/FunctionDeclarationTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2025 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.firebase.ai.type + +import io.kotest.assertions.json.shouldEqualJson +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.junit.Test + +internal class FunctionDeclarationTest { + + @Test + fun `Basic FunctionDeclaration with name, description and parameters`() { + val functionDeclaration = + FunctionDeclaration( + name = "isUserAGoat", + description = "Determine if the user is subject to teleportations.", + parameters = mapOf("userID" to Schema.string("ID of the User making the call")) + ) + + val expectedJson = + """ + { + "name": "isUserAGoat", + "description": "Determine if the user is subject to teleportations.", + "parameters": { + "type": "OBJECT", + "properties": { + "userID": { + "type": "STRING", + "description": "ID of the User making the call" + } + }, + "required": [ + "userID" + ] + } + } + """ + .trimIndent() + + Json.encodeToString(functionDeclaration.toInternal()).shouldEqualJson(expectedJson) + } + + @Test + fun `FunctionDeclaration with optional parameters`() { + val functionDeclaration = + FunctionDeclaration( + name = "isUserAGoat", + description = "Determine if the user is subject to teleportations.", + parameters = + mapOf( + "userID" to Schema.string("ID of the user making the call"), + "userName" to Schema.string("Name of the user making the call") + ), + optionalParameters = listOf("userName") + ) + + val expectedJson = + """ + { + "name": "isUserAGoat", + "description": "Determine if the user is subject to teleportations.", + "parameters": { + "type": "OBJECT", + "properties": { + "userID": { + "type": "STRING", + "description": "ID of the user making the call" + }, + "userName": { + "type": "STRING", + "description": "Name of the user making the call" + } + }, + "required": [ + "userID" + ] + } + } + """ + .trimIndent() + + Json.encodeToString(functionDeclaration.toInternal()).shouldEqualJson(expectedJson) + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/util/kotlin.kt b/firebase-ai/src/test/java/com/google/firebase/ai/util/kotlin.kt new file mode 100644 index 00000000000..6726923f7bf --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/util/kotlin.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 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.firebase.ai.util + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking + +/** + * Runs the given [block] using [runBlocking] on the current thread for side effect. + * + * Using this function is like [runBlocking] with default context (which runs the given block on the + * calling thread) but forces the return type to be `Unit`, which is helpful when implementing + * suspending tests as expression functions: + * ``` + * @Test + * fun myTest() = doBlocking {...} + * ``` + */ +internal fun doBlocking(block: suspend CoroutineScope.() -> Unit) { + runBlocking(block = block) +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt new file mode 100644 index 00000000000..393a2a16adc --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt @@ -0,0 +1,276 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(PublicPreviewAPI::class) + +package com.google.firebase.ai.util + +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.GenerativeModel +import com.google.firebase.ai.ImagenModel +import com.google.firebase.ai.common.APIController +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.RequestOptions +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.ktor.utils.io.ByteChannel +import io.ktor.utils.io.close +import io.ktor.utils.io.writeFully +import java.io.File +import kotlinx.coroutines.launch +import org.mockito.Mockito + +private val TEST_CLIENT_ID = "firebase-ai-android/test" +private val TEST_APP_ID = "1:android:12345" +private val TEST_VERSION = 1 + +/** String separator used in SSE communication to signal the end of a message. */ +internal const val SSE_SEPARATOR = "\r\n\r\n" + +/** + * Writes the provided [bytes] to the channel and closes it. + * + * Just a wrapper around [writeFully] that closes the channel after writing is complete. + * + * @param bytes the data to send through the channel + */ +internal suspend fun ByteChannel.send(bytes: ByteArray) { + writeFully(bytes) + close() +} + +/** + * Wrapper around common instances needed in tests. + * + * @param channel A [ByteChannel] for sending responses through the mock HTTP engine + * @param apiController A [APIController] that consumes the [channel] + * @see commonTest + * @see send + */ +internal data class CommonTestScope( + val channel: ByteChannel, + val model: GenerativeModel, + val imagenModel: ImagenModel, +) + +/** A test that runs under a [CommonTestScope]. */ +internal typealias CommonTest = suspend CommonTestScope.() -> Unit + +/** + * Common test block for providing a [CommonTestScope] during tests. + * + * Example usage: + * ``` + * @Test + * fun `(generateContent) generates a proper response`() = commonTest { + * val request = createRequest("say something nice") + * val response = createResponse("The world is a beautiful place!") + * + * channel.send(prepareResponse(response)) + * + * withTimeout(testTimeout) { + * val data = controller.generateContent(request) + * data.candidates.shouldNotBeEmpty() + * } + * } + * ``` + * + * @param status An optional [HttpStatusCode] to return as a response + * @param requestOptions Optional [RequestOptions] to utilize in the underlying controller + * @param block The test contents themselves, with the [CommonTestScope] implicitly provided + * @see CommonTestScope + */ +internal fun commonTest( + status: HttpStatusCode = HttpStatusCode.OK, + requestOptions: RequestOptions = RequestOptions(), + backend: GenerativeBackend = GenerativeBackend.vertexAI(), + block: CommonTest, +) = doBlocking { + val channel = ByteChannel(autoFlush = true) + val mockFirebaseApp = Mockito.mock() + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + + val apiController = + APIController( + "super_cool_test_key", + "gemini-pro", + requestOptions, + MockEngine { + respond(channel, status, headersOf(HttpHeaders.ContentType, "application/json")) + }, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + val model = + GenerativeModel("cool-model-name", generativeBackend = backend, controller = apiController) + val imagenModel = ImagenModel("cooler-model-name", controller = apiController) + CommonTestScope(channel, model, imagenModel).block() +} + +/** + * A variant of [commonTest] for performing *streaming-based* snapshot tests. + * + * Loads the *Golden File* and automatically parses the messages from it; providing it to the + * channel. + * + * @param name The name of the *Golden File* to load + * @param httpStatusCode An optional [HttpStatusCode] to return as a response + * @param block The test contents themselves, with a [CommonTestScope] implicitly provided + * @see goldenVertexUnaryFile + */ +internal fun goldenStreamingFile( + name: String, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + backend: GenerativeBackend = GenerativeBackend.vertexAI(), + block: CommonTest, +) = doBlocking { + val goldenFile = loadGoldenFile(name) + val messages = goldenFile.readLines().filter { it.isNotBlank() } + + commonTest(httpStatusCode, backend = backend) { + launch { + for (message in messages) { + channel.writeFully("$message$SSE_SEPARATOR".toByteArray()) + } + channel.close() + } + + block() + } +} + +/** + * A variant of [goldenStreamingFile] for testing vertexAI + * + * Loads the *Golden File* and automatically parses the messages from it; providing it to the + * channel. + * + * @param name The name of the *Golden File* to load + * @param httpStatusCode An optional [HttpStatusCode] to return as a response + * @param block The test contents themselves, with a [CommonTestScope] implicitly provided + * @see goldenStreamingFile + */ +internal fun goldenVertexStreamingFile( + name: String, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + block: CommonTest, +) = goldenStreamingFile("vertexai/$name", httpStatusCode, block = block) + +/** + * A variant of [goldenStreamingFile] for testing the developer api + * + * Loads the *Golden File* and automatically parses the messages from it; providing it to the + * channel. + * + * @param name The name of the *Golden File* to load + * @param httpStatusCode An optional [HttpStatusCode] to return as a response + * @param block The test contents themselves, with a [CommonTestScope] implicitly provided + * @see goldenStreamingFile + */ +internal fun goldenDevAPIStreamingFile( + name: String, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + block: CommonTest, +) = goldenStreamingFile("googleai/$name", httpStatusCode, GenerativeBackend.googleAI(), block) + +/** + * A variant of [commonTest] for performing snapshot tests. + * + * Loads the *Golden File* and automatically provides it to the channel. + * + * @param name The name of the *Golden File* to load + * @param httpStatusCode An optional [HttpStatusCode] to return as a response + * @param block The test contents themselves, with a [CommonTestScope] implicitly provided + * @see goldenStreamingFile + */ +internal fun goldenUnaryFile( + name: String, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + backend: GenerativeBackend = GenerativeBackend.vertexAI(), + block: CommonTest, +) = doBlocking { + commonTest(httpStatusCode, backend = backend) { + val goldenFile = loadGoldenFile(name) + val message = goldenFile.readText() + + launch { channel.send(message.toByteArray()) } + + block() + } +} + +/** + * A variant of [goldenUnaryFile] for vertexai tests Loads the *Golden File* and automatically + * provides it to the channel. + * + * @param name The name of the *Golden File* to load + * @param httpStatusCode An optional [HttpStatusCode] to return as a response + * @param block The test contents themselves, with a [CommonTestScope] implicitly provided + * @see goldenUnaryFile + */ +internal fun goldenVertexUnaryFile( + name: String, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + block: CommonTest, +) = goldenUnaryFile("vertexai/$name", httpStatusCode, block = block) + +/** + * A variant of [goldenUnaryFile] for developer api tests Loads the *Golden File* and automatically + * provides it to the channel. + * + * @param name The name of the *Golden File* to load + * @param httpStatusCode An optional [HttpStatusCode] to return as a response + * @param block The test contents themselves, with a [CommonTestScope] implicitly provided + * @see goldenUnaryFile + */ +internal fun goldenDevAPIUnaryFile( + name: String, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + block: CommonTest, +) = goldenUnaryFile("googleai/$name", httpStatusCode, GenerativeBackend.googleAI(), block) + +/** + * Loads a *Golden File* from the resource directory. + * + * Expects golden files to live under `golden-files` in the resource files. + * + * @see goldenUnaryFile + */ +internal fun loadGoldenFile(path: String): File = + loadResourceFile("vertexai-sdk-test-data/mock-responses/$path") + +/** Loads a file from the test resources directory. */ +internal fun loadResourceFile(path: String) = File("src/test/resources/$path") + +/** + * Ensures that a collection is neither null or empty. + * + * Syntax sugar for [shouldNotBeNull] and [shouldNotBeEmpty]. + */ +inline fun Collection?.shouldNotBeNullOrEmpty(): Collection { + shouldNotBeNull() + shouldNotBeEmpty() + return this +} diff --git a/firebase-ai/src/test/resources/README.md b/firebase-ai/src/test/resources/README.md new file mode 100644 index 00000000000..372846e739d --- /dev/null +++ b/firebase-ai/src/test/resources/README.md @@ -0,0 +1,2 @@ +Mock response files should be cloned into this directory to run unit tests. See +the Firebase AI [README](../../..#running-tests) for instructions. \ No newline at end of file diff --git a/firebase-ai/src/testUtil/java/com/google/firebase/ai/JavaCompileTests.java b/firebase-ai/src/testUtil/java/com/google/firebase/ai/JavaCompileTests.java new file mode 100644 index 00000000000..559c4ac8a04 --- /dev/null +++ b/firebase-ai/src/testUtil/java/com/google/firebase/ai/JavaCompileTests.java @@ -0,0 +1,379 @@ +/* + * Copyright 2025 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 java.com.google.firebase.ai; + +import android.graphics.Bitmap; +import androidx.annotation.Nullable; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.firebase.ai.FirebaseAI; +import com.google.firebase.ai.GenerativeModel; +import com.google.firebase.ai.LiveGenerativeModel; +import com.google.firebase.ai.java.ChatFutures; +import com.google.firebase.ai.java.GenerativeModelFutures; +import com.google.firebase.ai.java.LiveModelFutures; +import com.google.firebase.ai.java.LiveSessionFutures; +import com.google.firebase.ai.type.BlockReason; +import com.google.firebase.ai.type.Candidate; +import com.google.firebase.ai.type.Citation; +import com.google.firebase.ai.type.CitationMetadata; +import com.google.firebase.ai.type.Content; +import com.google.firebase.ai.type.ContentModality; +import com.google.firebase.ai.type.CountTokensResponse; +import com.google.firebase.ai.type.FileDataPart; +import com.google.firebase.ai.type.FinishReason; +import com.google.firebase.ai.type.FunctionCallPart; +import com.google.firebase.ai.type.FunctionResponsePart; +import com.google.firebase.ai.type.GenerateContentResponse; +import com.google.firebase.ai.type.GenerationConfig; +import com.google.firebase.ai.type.HarmCategory; +import com.google.firebase.ai.type.HarmProbability; +import com.google.firebase.ai.type.HarmSeverity; +import com.google.firebase.ai.type.ImagePart; +import com.google.firebase.ai.type.InlineDataPart; +import com.google.firebase.ai.type.LiveGenerationConfig; +import com.google.firebase.ai.type.LiveServerContent; +import com.google.firebase.ai.type.LiveServerMessage; +import com.google.firebase.ai.type.LiveServerSetupComplete; +import com.google.firebase.ai.type.LiveServerToolCall; +import com.google.firebase.ai.type.LiveServerToolCallCancellation; +import com.google.firebase.ai.type.MediaData; +import com.google.firebase.ai.type.ModalityTokenCount; +import com.google.firebase.ai.type.Part; +import com.google.firebase.ai.type.PromptFeedback; +import com.google.firebase.ai.type.PublicPreviewAPI; +import com.google.firebase.ai.type.ResponseModality; +import com.google.firebase.ai.type.SafetyRating; +import com.google.firebase.ai.type.Schema; +import com.google.firebase.ai.type.SpeechConfig; +import com.google.firebase.ai.type.TextPart; +import com.google.firebase.ai.type.UsageMetadata; +import com.google.firebase.ai.type.Voice; +import com.google.firebase.concurrent.FirebaseExecutors; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import kotlin.OptIn; +import kotlinx.serialization.json.JsonElement; +import kotlinx.serialization.json.JsonNull; +import kotlinx.serialization.json.JsonObject; +import org.junit.Assert; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * Tests in this file exist to be compiled, not invoked + */ +@OptIn(markerClass = PublicPreviewAPI.class) +public class JavaCompileTests { + + public void initializeJava() throws Exception { + FirebaseAI vertex = FirebaseAI.getInstance(); + GenerativeModel model = vertex.generativeModel("fake-model-name", getConfig()); + LiveGenerativeModel live = vertex.liveModel("fake-model-name", getLiveConfig()); + GenerativeModelFutures futures = GenerativeModelFutures.from(model); + LiveModelFutures liveFutures = LiveModelFutures.from(live); + testFutures(futures); + testLiveFutures(liveFutures); + } + + private GenerationConfig getConfig() { + return new GenerationConfig.Builder() + .setTopK(10) + .setTopP(11.0F) + .setTemperature(32.0F) + .setCandidateCount(1) + .setMaxOutputTokens(0xCAFEBABE) + .setFrequencyPenalty(1.0F) + .setPresencePenalty(2.0F) + .setStopSequences(List.of("foo", "bar")) + .setResponseMimeType("image/jxl") + .setResponseModalities(List.of(ResponseModality.TEXT, ResponseModality.TEXT)) + .setResponseSchema(getSchema()) + .build(); + } + + private Schema getSchema() { + return Schema.obj( + Map.of( + "foo", Schema.numInt(), + "bar", Schema.numInt("Some integer"), + "baz", Schema.numInt("Some integer", false), + "qux", Schema.numDouble(), + "quux", Schema.numFloat("Some floating point number"), + "xyzzy", Schema.array(Schema.numInt(), "A list of integers"), + "fee", Schema.numLong(), + "ber", + Schema.obj( + Map.of( + "bez", Schema.array(Schema.numDouble("Nullable double", true)), + "qez", Schema.enumeration(List.of("A", "B", "C"), "One of 3 letters"), + "qeez", Schema.str("A funny string"))))); + } + + private LiveGenerationConfig getLiveConfig() { + return new LiveGenerationConfig.Builder() + .setTopK(10) + .setTopP(11.0F) + .setTemperature(32.0F) + .setCandidateCount(1) + .setMaxOutputTokens(0xCAFEBABE) + .setFrequencyPenalty(1.0F) + .setPresencePenalty(2.0F) + .setResponseModality(ResponseModality.AUDIO) + .setSpeechConfig(new SpeechConfig(new Voice("AOEDE"))) + .build(); + } + + private void testFutures(GenerativeModelFutures futures) throws Exception { + Content content = + new Content.Builder() + .setParts(new ArrayList<>()) + .addText("Fake prompt") + .addFileData("fakeuri", "image/png") + .addInlineData(new byte[] {}, "text/json") + .addImage(Bitmap.createBitmap(0, 0, Bitmap.Config.HARDWARE)) + .addPart(new FunctionCallPart("fakeFunction", Map.of("fakeArg", JsonNull.INSTANCE))) + .setRole("user") + .build(); + Executor executor = FirebaseExecutors.directExecutor(); + ListenableFuture countResponse = futures.countTokens(content); + validateCountTokensResponse(countResponse.get()); + ListenableFuture generateResponse = futures.generateContent(content); + validateGenerateContentResponse(generateResponse.get()); + ChatFutures chat = futures.startChat(); + ListenableFuture future = chat.sendMessage(content); + future.addListener( + () -> { + try { + validateGenerateContentResponse(future.get()); + } catch (Exception e) { + // Ignore + } + }, + executor); + Publisher responsePublisher = futures.generateContentStream(content); + responsePublisher.subscribe( + new Subscriber() { + private boolean complete = false; + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(GenerateContentResponse response) { + Assert.assertFalse(complete); + validateGenerateContentResponse(response); + } + + @Override + public void onError(Throwable t) { + // Ignore + } + + @Override + public void onComplete() { + complete = true; + } + }); + } + + public void validateCountTokensResponse(CountTokensResponse response) { + int tokens = response.getTotalTokens(); + Integer billable = response.getTotalBillableCharacters(); + Assert.assertEquals(tokens, response.component1()); + Assert.assertEquals(billable, response.component2()); + Assert.assertEquals(response.getPromptTokensDetails(), response.component3()); + for (ModalityTokenCount count : response.getPromptTokensDetails()) { + ContentModality modality = count.getModality(); + int tokenCount = count.getTokenCount(); + } + } + + public void validateGenerateContentResponse(GenerateContentResponse response) { + List candidates = response.getCandidates(); + if (candidates.size() == 1 + && candidates.get(0).getContent().getParts().stream() + .anyMatch(p -> p instanceof TextPart && !((TextPart) p).getText().isEmpty())) { + String text = response.getText(); + Assert.assertNotNull(text); + Assert.assertFalse(text.isBlank()); + } + validateCandidates(candidates); + validateFunctionCalls(response.getFunctionCalls()); + validatePromptFeedback(response.getPromptFeedback()); + validateUsageMetadata(response.getUsageMetadata()); + } + + public void validateCandidates(List candidates) { + for (Candidate candidate : candidates) { + validateCitationMetadata(candidate.getCitationMetadata()); + FinishReason reason = candidate.getFinishReason(); + validateSafetyRatings(candidate.getSafetyRatings()); + validateCitationMetadata(candidate.getCitationMetadata()); + validateContent(candidate.getContent()); + } + } + + public void validateContent(@Nullable Content content) { + if (content == null) { + return; + } + String role = content.getRole(); + for (Part part : content.getParts()) { + if (part instanceof TextPart) { + String text = ((TextPart) part).getText(); + } else if (part instanceof ImagePart) { + Bitmap bitmap = ((ImagePart) part).getImage(); + } else if (part instanceof InlineDataPart) { + String mime = ((InlineDataPart) part).getMimeType(); + byte[] data = ((InlineDataPart) part).getInlineData(); + } else if (part instanceof FileDataPart) { + String mime = ((FileDataPart) part).getMimeType(); + String uri = ((FileDataPart) part).getUri(); + } + } + } + + public void validateCitationMetadata(CitationMetadata metadata) { + if (metadata != null) { + for (Citation citation : metadata.getCitations()) { + String uri = citation.getUri(); + String license = citation.getLicense(); + Calendar calendar = citation.getPublicationDate(); + int startIndex = citation.getStartIndex(); + int endIndex = citation.getEndIndex(); + Assert.assertTrue(startIndex <= endIndex); + } + } + } + + public void validateFunctionCalls(List parts) { + if (parts != null) { + for (FunctionCallPart part : parts) { + String functionName = part.getName(); + Map args = part.getArgs(); + Assert.assertFalse(functionName.isBlank()); + } + } + } + + public void validatePromptFeedback(PromptFeedback feedback) { + if (feedback != null) { + String message = feedback.getBlockReasonMessage(); + BlockReason reason = feedback.getBlockReason(); + validateSafetyRatings(feedback.getSafetyRatings()); + } + } + + public void validateSafetyRatings(List ratings) { + for (SafetyRating rating : ratings) { + Boolean blocked = rating.getBlocked(); + HarmCategory category = rating.getCategory(); + HarmProbability probability = rating.getProbability(); + float score = rating.getProbabilityScore(); + HarmSeverity severity = rating.getSeverity(); + Float severityScore = rating.getSeverityScore(); + if (severity != null) { + Assert.assertNotNull(severityScore); + } + } + } + + public void validateUsageMetadata(UsageMetadata metadata) { + if (metadata != null) { + int totalTokens = metadata.getTotalTokenCount(); + int promptTokenCount = metadata.getPromptTokenCount(); + for (ModalityTokenCount count : metadata.getPromptTokensDetails()) { + ContentModality modality = count.getModality(); + int tokenCount = count.getTokenCount(); + } + Integer candidatesTokenCount = metadata.getCandidatesTokenCount(); + for (ModalityTokenCount count : metadata.getCandidatesTokensDetails()) { + ContentModality modality = count.getModality(); + int tokenCount = count.getTokenCount(); + } + } + } + + private void testLiveFutures(LiveModelFutures futures) throws Exception { + LiveSessionFutures session = futures.connect().get(); + session + .receive() + .subscribe( + new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(LiveServerMessage message) { + validateLiveContentResponse(message); + } + + @Override + public void onError(Throwable t) { + // Ignore + } + + @Override + public void onComplete() { + // Also ignore + } + }); + + session.send("Fake message"); + session.send(new Content.Builder().addText("Fake message").build()); + + byte[] bytes = new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE}; + session.sendMediaStream(List.of(new MediaData(bytes, "image/jxl"))); + + FunctionResponsePart functionResponse = + new FunctionResponsePart("myFunction", new JsonObject(Map.of())); + session.sendFunctionResponse(List.of(functionResponse, functionResponse)); + + session.startAudioConversation(part -> functionResponse); + session.startAudioConversation(); + session.stopAudioConversation(); + session.stopReceiving(); + session.close(); + } + + private void validateLiveContentResponse(LiveServerMessage message) { + if (message instanceof LiveServerContent) { + LiveServerContent content = (LiveServerContent) message; + validateContent(content.getContent()); + boolean complete = content.getGenerationComplete(); + boolean interrupted = content.getInterrupted(); + boolean turnComplete = content.getTurnComplete(); + } else if (message instanceof LiveServerSetupComplete) { + LiveServerSetupComplete setup = (LiveServerSetupComplete) message; + // No methods + } else if (message instanceof LiveServerToolCall) { + LiveServerToolCall call = (LiveServerToolCall) message; + validateFunctionCalls(call.getFunctionCalls()); + } else if (message instanceof LiveServerToolCallCancellation) { + LiveServerToolCallCancellation cancel = (LiveServerToolCallCancellation) message; + List functions = cancel.getFunctionIds(); + } + } +} diff --git a/firebase-ai/update_responses.sh b/firebase-ai/update_responses.sh new file mode 100755 index 00000000000..7d6ea18e0ee --- /dev/null +++ b/firebase-ai/update_responses.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Copyright 2024 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. + +# This script replaces mock response files for Vertex AI unit tests with a fresh +# clone of the shared repository of Vertex AI test data. + +RESPONSES_VERSION='v13.*' # The major version of mock responses to use +REPO_NAME="vertexai-sdk-test-data" +REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git" + +set -x + +cd "$(dirname "$0")/src/test/resources" || exit +rm -rf "$REPO_NAME" +git clone "$REPO_LINK" --quiet || exit +cd "$REPO_NAME" || exit + +# Find and checkout latest tag matching major version +TAG=$(git tag -l "$RESPONSES_VERSION" --sort=v:refname | tail -n1) +if [ -z "$TAG" ]; then + echo "Error: No tag matching '$RESPONSES_VERSION' found in $REPO_NAME" + exit +fi +git checkout "$TAG" --quiet diff --git a/firebase-appdistribution-api/CHANGELOG.md b/firebase-appdistribution-api/CHANGELOG.md index 52fc8534ef5..44afcf8054e 100644 --- a/firebase-appdistribution-api/CHANGELOG.md +++ b/firebase-appdistribution-api/CHANGELOG.md @@ -1,6 +1,16 @@ # Unreleased +# 16.0.0-beta15 +* [unchanged] Updated to accommodate the release of the updated + [appdistro] library. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-appdistribution-api` library. The Kotlin extensions library has no additional +updates. + # 16.0.0-beta14 * [unchanged] Updated to accommodate the release of the updated [appdistro] library. diff --git a/firebase-appdistribution-api/gradle.properties b/firebase-appdistribution-api/gradle.properties index 43fb0b20a81..a39a1d388f4 100644 --- a/firebase-appdistribution-api/gradle.properties +++ b/firebase-appdistribution-api/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=16.0.0-beta15 -latestReleasedVersion=16.0.0-beta14 +version=16.0.0-beta16 +latestReleasedVersion=16.0.0-beta15 diff --git a/firebase-appdistribution/CHANGELOG.md b/firebase-appdistribution/CHANGELOG.md index 6aa5cbfad70..c7ef8338657 100644 --- a/firebase-appdistribution/CHANGELOG.md +++ b/firebase-appdistribution/CHANGELOG.md @@ -1,6 +1,9 @@ # Unreleased +# 16.0.0-beta15 +* [fixed] Added custom tab support for more browsers [#6692] + # 16.0.0-beta14 * [changed] Internal improvements to testing on Android 14 diff --git a/firebase-appdistribution/gradle.properties b/firebase-appdistribution/gradle.properties index d02dfc98183..5dcfcbcc66c 100644 --- a/firebase-appdistribution/gradle.properties +++ b/firebase-appdistribution/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=16.0.0-beta15 -latestReleasedVersion=16.0.0-beta14 +version=16.0.0-beta16 +latestReleasedVersion=16.0.0-beta15 diff --git a/firebase-appdistribution/src/main/AndroidManifest.xml b/firebase-appdistribution/src/main/AndroidManifest.xml index ef91581edc3..452fe856e6c 100644 --- a/firebase-appdistribution/src/main/AndroidManifest.xml +++ b/firebase-appdistribution/src/main/AndroidManifest.xml @@ -21,6 +21,12 @@ + + + + + + resolveInfos = - context.getPackageManager().queryIntentServices(customTabIntent, 0); - return resolveInfos != null && !resolveInfos.isEmpty(); + String packageName = CustomTabsClient.getPackageName(context, Collections.emptyList()); + return packageName != null; } } diff --git a/firebase-config/CHANGELOG.md b/firebase-config/CHANGELOG.md index 93a7da1867e..fc00b486a87 100644 --- a/firebase-config/CHANGELOG.md +++ b/firebase-config/CHANGELOG.md @@ -1,6 +1,26 @@ # Unreleased +# 22.1.2 +* [fixed] Fixed `NetworkOnMainThreadException` on Android versions below 8 by disconnecting + `HttpURLConnection` only on API levels 26 and higher. GitHub Issue [#6934] + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-config` library. The Kotlin extensions library has no additional +updates. + +# 22.1.1 +* [fixed] Fixed an issue where the connection to the real-time Remote Config backend could remain +open in the background. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-config` library. The Kotlin extensions library has no additional +updates. + # 22.1.0 * [feature] Added support for custom signal targeting in Remote Config. Use `setCustomSignals` API for setting custom signals and use them to build custom targeting conditions in Remote Config. diff --git a/firebase-config/bandwagoner/src/main/AndroidManifest.xml b/firebase-config/bandwagoner/src/main/AndroidManifest.xml index 409b510c785..01252cf60bb 100644 --- a/firebase-config/bandwagoner/src/main/AndroidManifest.xml +++ b/firebase-config/bandwagoner/src/main/AndroidManifest.xml @@ -17,9 +17,8 @@ + android:versionCode="1" + android:versionName="3.0.0"> @@ -30,22 +29,22 @@ - - - - - - - - - - - + android:name=".MainApplication" + android:label="Bandwagoner" + android:theme="@style/LightNoActionBarTheme"> + + + + + + + + + + + diff --git a/firebase-config/gradle.properties b/firebase-config/gradle.properties index 26e0ad751e6..02671b7fc28 100644 --- a/firebase-config/gradle.properties +++ b/firebase-config/gradle.properties @@ -14,7 +14,7 @@ # limitations under the License. # -version=22.1.1 -latestReleasedVersion=22.1.0 +version=22.1.3 +latestReleasedVersion=22.1.2 android.enableUnitTestBinaryResources=true diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java index 808892e7521..abd09dd0330 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -656,16 +656,17 @@ private Task setDefaultsWithStringsMapAsync(Map defaultsSt * Asynchronously changes the custom signals for this {@link FirebaseRemoteConfig} instance. * *

Custom signals are subject to limits on the size of key/value pairs and the total - * number of signals. Any calls that exceed these limits will be discarded. + * number of signals. Any calls that exceed these limits will be discarded. See Custom + * Signal Limits. * * @param customSignals The custom signals to set for this instance. - *

    + *
      *
    • New keys will add new key-value pairs in the custom signals. *
    • Existing keys with new values will update the corresponding signals. *
    • Setting a key's value to {@code null} will remove the associated signal. - *
+ * */ - // TODO(b/385028620): Add link to documentation about custom signal limits. @NonNull public Task setCustomSignals(@NonNull CustomSignals customSignals) { return Tasks.call( diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java index 81016602532..a93b1dc5784 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java @@ -54,6 +54,7 @@ public class ConfigAutoFetch { private final ConfigUpdateListener retryCallback; private final ScheduledExecutorService scheduledExecutorService; private final Random random; + private boolean isInBackground; public ConfigAutoFetch( HttpURLConnection httpURLConnection, @@ -69,6 +70,7 @@ public ConfigAutoFetch( this.retryCallback = retryCallback; this.scheduledExecutorService = scheduledExecutorService; this.random = new Random(); + this.isInBackground = false; } private synchronized void propagateErrors(FirebaseRemoteConfigException exception) { @@ -87,6 +89,10 @@ private synchronized boolean isEventListenersEmpty() { return this.eventListeners.isEmpty(); } + public void setIsInBackground(boolean isInBackground) { + this.isInBackground = isInBackground; + } + private String parseAndValidateConfigUpdateMessage(String message) { int left = message.indexOf('{'); int right = message.lastIndexOf('}'); @@ -105,15 +111,29 @@ public void listenForNotifications() { return; } + // Maintain a reference to the InputStream to guarantee its closure upon completion or in case + // of an exception. + InputStream inputStream = null; try { - InputStream inputStream = httpURLConnection.getInputStream(); + inputStream = httpURLConnection.getInputStream(); handleNotifications(inputStream); - inputStream.close(); } catch (IOException ex) { - // Stream was interrupted due to a transient issue and the system will retry the connection. - Log.d(TAG, "Stream was cancelled due to an exception. Retrying the connection...", ex); + // If the real-time connection is at an unexpected lifecycle state when the app is + // backgrounded, it's expected closing the httpURLConnection will throw an exception. + if (!isInBackground) { + // Otherwise, the real-time server connection was closed due to a transient issue. + Log.d(TAG, "Real-time connection was closed due to an exception.", ex); + } } finally { - httpURLConnection.disconnect(); + if (inputStream != null) { + try { + // Only need to close the InputStream, ConfigRealtimeHttpClient will disconnect + // HttpUrlConnection + inputStream.close(); + } catch (IOException ex) { + Log.d(TAG, "Exception thrown when closing connection stream. Retrying connection...", ex); + } + } } } @@ -186,7 +206,6 @@ private void handleNotifications(InputStream inputStream) throws IOException { } reader.close(); - inputStream.close(); } private void autoFetch(int remainingAttempts, long targetVersion) { diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHandler.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHandler.java index 5ed1135dfc7..e340ef0b8c0 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHandler.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHandler.java @@ -91,7 +91,7 @@ public synchronized ConfigUpdateListenerRegistration addRealtimeConfigUpdateList } public synchronized void setBackgroundState(boolean isInBackground) { - configRealtimeHttpClient.setRealtimeBackgroundState(isInBackground); + configRealtimeHttpClient.setIsInBackground(isInBackground); if (!isInBackground) { beginRealtime(); } diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java index 2c1c44480e2..7be3ef97136 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java @@ -22,6 +22,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; +import android.os.Build; import android.util.Log; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; @@ -100,6 +101,10 @@ public class ConfigRealtimeHttpClient { /** Flag to indicate whether or not the app is in the background or not. */ private boolean isInBackground; + // The HttpUrlConnection and auto-fetcher for this client. Only one of each exist at a time. + private HttpURLConnection httpURLConnection; + private ConfigAutoFetch configAutoFetch; + private final int ORIGINAL_RETRIES = 8; private final ScheduledExecutorService scheduledExecutorService; private final ConfigFetchHandler configFetchHandler; @@ -111,6 +116,7 @@ public class ConfigRealtimeHttpClient { private final Random random; private final Clock clock; private final ConfigSharedPrefsClient sharedPrefsClient; + private final Object backgroundLock; public ConfigRealtimeHttpClient( FirebaseApp firebaseApp, @@ -145,6 +151,7 @@ public ConfigRealtimeHttpClient( this.sharedPrefsClient = sharedPrefsClient; this.isRealtimeDisabled = false; this.isInBackground = false; + this.backgroundLock = new Object(); } private static String extractProjectNumberFromAppId(String gmpAppId) { @@ -391,14 +398,47 @@ public void run() { } } - void setRealtimeBackgroundState(boolean backgroundState) { - isInBackground = backgroundState; + public void setIsInBackground(boolean isInBackground) { + // Make changes in synchronized block so only one thread sets the background state and calls + // disconnect. + synchronized (backgroundLock) { + this.isInBackground = isInBackground; + + // Propagate to ConfigAutoFetch as well. + if (configAutoFetch != null) { + configAutoFetch.setIsInBackground(isInBackground); + } + // Close the connection if the app is in the background and there is an active + // HttpUrlConnection. + // This is now only done on Android versions >= O (API 26) because + // on older versions, background detection callbacks run on the main thread, which + // could lead to a NetworkOnMainThreadException when disconnecting the connection. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (isInBackground && httpURLConnection != null) { + httpURLConnection.disconnect(); + } + } + } } private synchronized void resetRetryCount() { httpRetriesRemaining = ORIGINAL_RETRIES; } + /** + * The check and set http connection method are combined so that when canMakeHttpStreamConnection + * returns true, the same thread can mark isHttpConnectionIsRunning as true to prevent a race + * condition with another thread. + */ + private synchronized boolean checkAndSetHttpConnectionFlagIfNotRunning() { + boolean canMakeConnection = canMakeHttpStreamConnection(); + if (canMakeConnection) { + setIsHttpConnectionRunning(true); + } + + return canMakeConnection; + } + private synchronized void setIsHttpConnectionRunning(boolean connectionRunning) { isHttpConnectionRunning = connectionRunning; } @@ -469,7 +509,7 @@ private String parseForbiddenErrorResponseMessage(InputStream inputStream) { */ @SuppressLint({"VisibleForTests", "DefaultLocale"}) public void beginRealtimeHttpStream() { - if (!canMakeHttpStreamConnection()) { + if (!checkAndSetHttpConnectionFlagIfNotRunning()) { return; } @@ -489,17 +529,21 @@ public void beginRealtimeHttpStream() { this.scheduledExecutorService, (completedHttpUrlConnectionTask) -> { Integer responseCode = null; - HttpURLConnection httpURLConnection = null; + // Get references to InputStream and ErrorStream before listening on the stream so + // that they can be closed without getting them from HttpUrlConnection. + InputStream inputStream = null; + InputStream errorStream = null; try { // If HTTP connection task failed throw exception to move to the catch block. if (!httpURLConnectionTask.isSuccessful()) { throw new IOException(httpURLConnectionTask.getException()); } - setIsHttpConnectionRunning(true); // Get HTTP connection and check response code. httpURLConnection = httpURLConnectionTask.getResult(); + inputStream = httpURLConnection.getInputStream(); + errorStream = httpURLConnection.getErrorStream(); responseCode = httpURLConnection.getResponseCode(); // If the connection returned a 200 response code, start listening for messages. @@ -509,23 +553,32 @@ public void beginRealtimeHttpStream() { sharedPrefsClient.resetRealtimeBackoff(); // Start listening for realtime notifications. - ConfigAutoFetch configAutoFetch = startAutoFetch(httpURLConnection); + configAutoFetch = startAutoFetch(httpURLConnection); configAutoFetch.listenForNotifications(); } } catch (IOException e) { - // Stream could not be open due to a transient issue and the system will retry the - // connection - // without user intervention. - Log.d( - TAG, - "Exception connecting to real-time RC backend. Retrying the connection...", - e); + if (isInBackground) { + // It's possible the app was backgrounded while the connection was open, which + // threw an exception trying to read the response. No real error here, so treat + // this as a success, even if we haven't read a 200 response code yet. + resetRetryCount(); + } else { + // If it's not in the background, there might have been a transient error so the + // client will retry the connection. + Log.d( + TAG, + "Exception connecting to real-time RC backend. Retrying the connection...", + e); + } } finally { - closeRealtimeHttpStream(httpURLConnection); + // Close HTTP connection and associated streams. + closeRealtimeHttpConnection(inputStream, errorStream); setIsHttpConnectionRunning(false); + // Update backoff metadata if the connection failed in the foreground. boolean connectionFailed = - responseCode == null || isStatusCodeRetryable(responseCode); + !isInBackground + && (responseCode == null || isStatusCodeRetryable(responseCode)); if (connectionFailed) { updateBackoffMetadataWithLastFailedStreamConnectionTime( new Date(clock.currentTimeMillis())); @@ -556,24 +609,34 @@ public void beginRealtimeHttpStream() { } } + // Reset parameters. + httpURLConnection = null; + configAutoFetch = null; + return Tasks.forResult(null); }); } - // Pauses Http stream listening - public void closeRealtimeHttpStream(HttpURLConnection httpURLConnection) { - if (httpURLConnection != null) { - httpURLConnection.disconnect(); - - // Explicitly close the input stream due to a bug in the Android okhttp implementation. - // See github.com/firebase/firebase-android-sdk/pull/808. + private void closeHttpConnectionInputStream(InputStream inputStream) { + if (inputStream != null) { try { - httpURLConnection.getInputStream().close(); - if (httpURLConnection.getErrorStream() != null) { - httpURLConnection.getErrorStream().close(); - } - } catch (IOException e) { + inputStream.close(); + } catch (IOException ex) { + Log.d(TAG, "Error closing connection stream.", ex); } } } + + // Pauses Http stream listening by disconnecting the HttpUrlConnection and underlying InputStream + // and ErrorStream if they exist. + @VisibleForTesting + public void closeRealtimeHttpConnection(InputStream inputStream, InputStream errorStream) { + // Disconnect only if the connection is not null and in the foreground. + if (httpURLConnection != null && !isInBackground) { + httpURLConnection.disconnect(); + } + + closeHttpConnectionInputStream(inputStream); + closeHttpConnectionInputStream(errorStream); + } } diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index fffc439dc2b..9e8f65c767e 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -80,6 +81,7 @@ import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -350,6 +352,7 @@ public void onError(@NonNull FirebaseRemoteConfigException error) { listeners, mockRetryListener, scheduledExecutorService); + configAutoFetch.setIsInBackground(false); realtimeSharedPrefsClient = new ConfigSharedPrefsClient( context.getSharedPreferences("test_file", Context.MODE_PRIVATE)); @@ -1274,17 +1277,18 @@ public void realtime_client_removeListener_success() { @Test public void realtime_stream_listen_and_end_connection() throws Exception { - when(mockHttpURLConnection.getInputStream()) - .thenReturn( - new ByteArrayInputStream( - "{ \"latestTemplateVersionNumber\": 1 }".getBytes(StandardCharsets.UTF_8))); + InputStream inputStream = + new ByteArrayInputStream( + "{ \"latestTemplateVersionNumber\": 1 }".getBytes(StandardCharsets.UTF_8)); + InputStream inputStreamSpy = spy(inputStream); + when(mockHttpURLConnection.getInputStream()).thenReturn(inputStreamSpy); when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L); when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber( ConfigFetchHandler.FetchType.REALTIME, 1)) .thenReturn(Tasks.forResult(realtimeFetchedContainerResponse)); configAutoFetch.listenForNotifications(); - verify(mockHttpURLConnection).disconnect(); + verify(inputStreamSpy, times(2)).close(); } @Test @@ -1308,7 +1312,7 @@ public void realtime_redirectStatusCode_noRetries() throws Exception { .createRealtimeConnection(); doNothing() .when(configRealtimeHttpClientSpy) - .closeRealtimeHttpStream(any(HttpURLConnection.class)); + .closeRealtimeHttpConnection(any(InputStream.class), any(InputStream.class)); when(mockHttpURLConnection.getResponseCode()).thenReturn(301); configRealtimeHttpClientSpy.beginRealtimeHttpStream(); @@ -1329,7 +1333,7 @@ public void realtime_okStatusCode_startAutofetchAndRetries() throws Exception { doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds(); doNothing() .when(configRealtimeHttpClientSpy) - .closeRealtimeHttpStream(any(HttpURLConnection.class)); + .closeRealtimeHttpConnection(any(InputStream.class), any(InputStream.class)); when(mockHttpURLConnection.getResponseCode()).thenReturn(200); configRealtimeHttpClientSpy.beginRealtimeHttpStream(); @@ -1348,7 +1352,7 @@ public void realtime_badGatewayStatusCode_noAutofetchButRetries() throws Excepti doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds(); doNothing() .when(configRealtimeHttpClientSpy) - .closeRealtimeHttpStream(any(HttpURLConnection.class)); + .closeRealtimeHttpConnection(any(InputStream.class), any(InputStream.class)); when(mockHttpURLConnection.getResponseCode()).thenReturn(502); configRealtimeHttpClientSpy.beginRealtimeHttpStream(); @@ -1367,7 +1371,7 @@ public void realtime_retryableStatusCode_increasesConfigMetadataFailedStreams() doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds(); doNothing() .when(configRealtimeHttpClientSpy) - .closeRealtimeHttpStream(any(HttpURLConnection.class)); + .closeRealtimeHttpConnection(any(InputStream.class), any(InputStream.class)); when(mockHttpURLConnection.getResponseCode()).thenReturn(502); int failedStreams = configRealtimeHttpClientSpy.getNumberOfFailedStreams(); @@ -1386,7 +1390,7 @@ public void realtime_retryableStatusCode_increasesConfigMetadataBackoffDate() th doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds(); doNothing() .when(configRealtimeHttpClientSpy) - .closeRealtimeHttpStream(any(HttpURLConnection.class)); + .closeRealtimeHttpConnection(any(InputStream.class), any(InputStream.class)); when(mockHttpURLConnection.getResponseCode()).thenReturn(502); Date backoffDate = configRealtimeHttpClientSpy.getBackoffEndTime(); @@ -1407,7 +1411,7 @@ public void realtime_successfulStatusCode_doesNotIncreaseConfigMetadataFailedStr doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds(); doNothing() .when(configRealtimeHttpClientSpy) - .closeRealtimeHttpStream(any(HttpURLConnection.class)); + .closeRealtimeHttpConnection(any(InputStream.class), any(InputStream.class)); when(mockHttpURLConnection.getResponseCode()).thenReturn(200); int failedStreams = configRealtimeHttpClientSpy.getNumberOfFailedStreams(); @@ -1428,7 +1432,7 @@ public void realtime_successfulStatusCode_doesNotIncreaseConfigMetadataBackoffDa doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds(); doNothing() .when(configRealtimeHttpClientSpy) - .closeRealtimeHttpStream(any(HttpURLConnection.class)); + .closeRealtimeHttpConnection(any(InputStream.class), any(InputStream.class)); when(mockHttpURLConnection.getResponseCode()).thenReturn(200); Date backoffDate = configRealtimeHttpClientSpy.getBackoffEndTime(); @@ -1446,7 +1450,7 @@ public void realtime_forbiddenStatusCode_returnsStreamError() throws Exception { .createRealtimeConnection(); doNothing() .when(configRealtimeHttpClientSpy) - .closeRealtimeHttpStream(any(HttpURLConnection.class)); + .closeRealtimeHttpConnection(any(InputStream.class), any(InputStream.class)); when(mockHttpURLConnection.getErrorStream()) .thenReturn( new ByteArrayInputStream(FORBIDDEN_ERROR_MESSAGE.getBytes(StandardCharsets.UTF_8))); @@ -1469,7 +1473,7 @@ public void realtime_exceptionThrown_noAutofetchButRetries() throws Exception { doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds(); doNothing() .when(configRealtimeHttpClientSpy) - .closeRealtimeHttpStream(any(HttpURLConnection.class)); + .closeRealtimeHttpConnection(any(InputStream.class), any(InputStream.class)); configRealtimeHttpClientSpy.beginRealtimeHttpStream(); flushScheduledTasks(); @@ -1511,6 +1515,25 @@ public void realtime_stream_listen_and_failsafe_disabled() throws Exception { verify(mockFetchHandler).getTemplateVersionNumber(); } + @Test + public void realtime_stream_listen_backgrounded_disconnects() throws Exception { + ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient); + doReturn(Tasks.forResult(mockHttpURLConnection)) + .when(configRealtimeHttpClientSpy) + .createRealtimeConnection(); + doReturn(mockConfigAutoFetch).when(configRealtimeHttpClientSpy).startAutoFetch(any()); + doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds(); + doNothing() + .when(configRealtimeHttpClientSpy) + .closeRealtimeHttpConnection(any(InputStream.class), any(InputStream.class)); + when(mockHttpURLConnection.getResponseCode()).thenReturn(200); + configRealtimeHttpClientSpy.beginRealtimeHttpStream(); + configRealtimeHttpClientSpy.setIsInBackground(true); + flushScheduledTasks(); + + verify(mockHttpURLConnection, times(1)).disconnect(); + } + @Test public void realtimeStreamListen_andUnableToParseMessage() throws Exception { when(mockHttpURLConnection.getResponseCode()).thenReturn(200); @@ -1530,15 +1553,28 @@ public void realtimeStreamListen_andUnableToParseMessage() throws Exception { @Test public void realtime_stream_listen_get_inputstream_fail() throws Exception { + InputStream inputStream = mock(InputStream.class); when(mockHttpURLConnection.getResponseCode()).thenReturn(200); - when(mockHttpURLConnection.getInputStream()).thenThrow(IOException.class); + when(mockHttpURLConnection.getInputStream()).thenReturn(inputStream); + when(inputStream.read()).thenThrow(IOException.class); when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L); when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber( ConfigFetchHandler.FetchType.REALTIME, 1)) .thenReturn(Tasks.forResult(realtimeFetchedContainerResponse)); configAutoFetch.listenForNotifications(); - verify(mockHttpURLConnection).disconnect(); + verify(inputStream).close(); + } + + @Test + public void realtime_stream_listen_get_inputstream_exception_handling() throws Exception { + InputStream inputStream = mock(InputStream.class); + when(mockHttpURLConnection.getResponseCode()).thenReturn(200); + when(mockHttpURLConnection.getInputStream()).thenThrow(IOException.class); + configAutoFetch.listenForNotifications(); + + verify(mockHttpURLConnection, times(1)).getInputStream(); + verify(inputStream, never()).close(); } @Test diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md index b5b8f7868d4..38387afdd12 100644 --- a/firebase-crashlytics-ndk/CHANGELOG.md +++ b/firebase-crashlytics-ndk/CHANGELOG.md @@ -1,6 +1,16 @@ # Unreleased +* [changed] Updated `firebase-crashlytics` dependency to v19.4.4 +# 19.4.3 +* [changed] Updated internal Crashpad version to commit `21a20e`. + +# 19.4.2 +* [changed] Updated `firebase-crashlytics` dependency to v19.4.2 + +# 19.4.1 +* [changed] Updated `firebase-crashlytics` dependency to v19.4.1 + # 19.3.0 * [changed] Updated `firebase-crashlytics` dependency to v19.3.0 diff --git a/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle b/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle index aafc02f489c..4cf82e95069 100644 --- a/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle +++ b/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle @@ -37,7 +37,7 @@ android { timeOutInMs 60 * 1000 } namespace "com.google.firebase.crashlytics.ndk" - ndkVersion "25.1.8937393" + ndkVersion "27.2.12479018" compileSdkVersion project.compileSdkVersion defaultConfig { minSdkVersion project.minSdkVersion diff --git a/firebase-crashlytics-ndk/gradle.properties b/firebase-crashlytics-ndk/gradle.properties index 5ab96e1d760..7ef5196b4b5 100644 --- a/firebase-crashlytics-ndk/gradle.properties +++ b/firebase-crashlytics-ndk/gradle.properties @@ -1,2 +1,2 @@ -version=19.4.1 -latestReleasedVersion=19.4.0 +version=19.4.4 +latestReleasedVersion=19.4.3 diff --git a/firebase-crashlytics-ndk/src/main/jni/Application.mk b/firebase-crashlytics-ndk/src/main/jni/Application.mk index af0b6b867d1..affe25cf47c 100644 --- a/firebase-crashlytics-ndk/src/main/jni/Application.mk +++ b/firebase-crashlytics-ndk/src/main/jni/Application.mk @@ -1,3 +1,3 @@ APP_ABI := arm64-v8a armeabi-v7a x86_64 x86 APP_STL := c++_static -APP_PLATFORM := android-16 +APP_PLATFORM := android-21 diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_client/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_client/Android.mk index f08db85fed0..db8e082111c 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_client/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_client/Android.mk @@ -17,7 +17,7 @@ LOCAL_CPPFLAGS := \ -Wall \ -Os \ -flto \ - -std=c++17 \ + -std=c++20 \ LOCAL_SRC_FILES := \ $(THIRD_PARTY_PATH)/crashpad/client/annotation.cc \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_compat/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_compat/Android.mk index ddec4ff67a6..e3157757580 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_compat/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_compat/Android.mk @@ -21,7 +21,7 @@ LOCAL_EXPORT_C_INCLUDES := \ LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ -Wall \ - -std=c++17 \ + -std=c++20 \ -Os \ -flto \ -fvisibility=hidden \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_handler_lib/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_handler_lib/Android.mk index 94928ae917d..4b5f4e31b09 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_handler_lib/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_handler_lib/Android.mk @@ -13,7 +13,7 @@ LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ -DCRASHPAD_ZLIB_SOURCE_SYSTEM \ -Wall \ - -std=c++17 \ + -std=c++20 \ -Os \ -flto \ -fvisibility=hidden \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_minidump/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_minidump/Android.mk index 3a717888749..8c5309bf003 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_minidump/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_minidump/Android.mk @@ -8,7 +8,7 @@ LOCAL_MODULE := crashpad_minidump LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(THIRD_PARTY_PATH)/crashpad LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -flto \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_snapshot/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_snapshot/Android.mk index de07187cf21..60f2eda169b 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_snapshot/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_snapshot/Android.mk @@ -8,7 +8,7 @@ LOCAL_MODULE := crashpad_snapshot LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(THIRD_PARTY_PATH)/crashpad LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -flto \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_tool_support/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_tool_support/Android.mk index 11c2e0c51e2..b7b1281bae6 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_tool_support/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_tool_support/Android.mk @@ -9,7 +9,7 @@ LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(THIRD_PARTY_PATH)/crashpad LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -flto \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk index 56d1a65ea5b..0b75dc36e53 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk @@ -18,7 +18,7 @@ LOCAL_CPPFLAGS := \ -DZLIB_CONST \ -DCRASHPAD_ZLIB_SOURCE_SYSTEM \ -DCRASHPAD_LSS_SOURCE_EXTERNAL \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -flto \ @@ -44,6 +44,7 @@ LOCAL_SRC_FILES := \ $(THIRD_PARTY_PATH)/crashpad/util/linux/exception_handler_protocol.cc \ $(THIRD_PARTY_PATH)/crashpad/util/linux/initial_signal_dispositions.cc \ $(THIRD_PARTY_PATH)/crashpad/util/linux/memory_map.cc \ + $(THIRD_PARTY_PATH)/crashpad/util/linux/pac_helper.cc \ $(THIRD_PARTY_PATH)/crashpad/util/linux/proc_stat_reader.cc \ $(THIRD_PARTY_PATH)/crashpad/util/linux/proc_task_reader.cc \ $(THIRD_PARTY_PATH)/crashpad/util/linux/ptrace_broker.cc \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk index 62cab7de0d4..a2a5b656c47 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk @@ -10,7 +10,7 @@ LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/$(THIRD_PARTY_PATH)/mini_chromium LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -flto \ @@ -25,6 +25,7 @@ LOCAL_SRC_FILES := \ $(THIRD_PARTY_PATH)/mini_chromium/base/posix/safe_strerror.cc \ $(THIRD_PARTY_PATH)/mini_chromium/base/process/memory.cc \ $(THIRD_PARTY_PATH)/mini_chromium/base/rand_util.cc \ + $(THIRD_PARTY_PATH)/mini_chromium/base/strings/pattern.cc \ $(THIRD_PARTY_PATH)/mini_chromium/base/strings/string_number_conversions.cc \ $(THIRD_PARTY_PATH)/mini_chromium/base/strings/string_util.cc \ $(THIRD_PARTY_PATH)/mini_chromium/base/strings/stringprintf.cc \ diff --git a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-common/Android.mk b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-common/Android.mk index fcd45bd0b4f..bf322f3a873 100644 --- a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-common/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-common/Android.mk @@ -18,7 +18,7 @@ LOCAL_C_INCLUDES := \ LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include LOCAL_CPPFLAGS := \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -s \ diff --git a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-handler/Android.mk b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-handler/Android.mk index ac89324387b..1dc59d2d7cc 100644 --- a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-handler/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-handler/Android.mk @@ -12,7 +12,7 @@ LOCAL_C_INCLUDES := \ $(LOCAL_PATH)/../libcrashlytics-common/include \ LOCAL_CPPFLAGS := \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -s \ diff --git a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-trampoline/Android.mk b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-trampoline/Android.mk index 2eb43b255fb..46114835893 100644 --- a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-trampoline/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-trampoline/Android.mk @@ -9,7 +9,7 @@ endif LOCAL_MODULE := crashlytics-trampoline LOCAL_C_INCLUDES := $(LOCAL_PATH)/include LOCAL_CPPFLAGS := \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -s \ diff --git a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics/Android.mk b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics/Android.mk index 12c9f2088ce..d62658e9112 100644 --- a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics/Android.mk @@ -16,7 +16,7 @@ LOCAL_C_INCLUDES := \ $(LOCAL_PATH)/$(THIRD_PARTY_PATH)/mini_chromium \ LOCAL_CPPFLAGS := \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -s \ diff --git a/firebase-crashlytics-ndk/src/third_party/crashpad b/firebase-crashlytics-ndk/src/third_party/crashpad index c902f6b1c9e..21a20ef8adf 160000 --- a/firebase-crashlytics-ndk/src/third_party/crashpad +++ b/firebase-crashlytics-ndk/src/third_party/crashpad @@ -1 +1 @@ -Subproject commit c902f6b1c9e43224181969110b83e0053b2ddd3c +Subproject commit 21a20ef8adf3949de8dd65758a16f83aab344b3c diff --git a/firebase-crashlytics-ndk/src/third_party/lss b/firebase-crashlytics-ndk/src/third_party/lss index 9719c1e1e67..ed31caa60f2 160000 --- a/firebase-crashlytics-ndk/src/third_party/lss +++ b/firebase-crashlytics-ndk/src/third_party/lss @@ -1 +1 @@ -Subproject commit 9719c1e1e676814c456b55f5f070eabad6709d31 +Subproject commit ed31caa60f20a4f6569883b2d752ef7522de51e0 diff --git a/firebase-crashlytics-ndk/src/third_party/mini_chromium b/firebase-crashlytics-ndk/src/third_party/mini_chromium index 4332ddb6963..7477036e238 160000 --- a/firebase-crashlytics-ndk/src/third_party/mini_chromium +++ b/firebase-crashlytics-ndk/src/third_party/mini_chromium @@ -1 +1 @@ -Subproject commit 4332ddb6963750e1106efdcece6d6e2de6dc6430 +Subproject commit 7477036e238e54f220bed206f71036db8064dd34 diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index 7086b0b0c9d..aace588a1fe 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -1,5 +1,36 @@ # Unreleased +* [fixed] Fixed more strict mode violations +# 19.4.3 +* [fixed] Fixed UnbufferedIoViolation strict mode violation [#6822] + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-crashlytics` library. The Kotlin extensions library has no additional +updates. + +# 19.4.2 +* [changed] Internal changes to read version control info more efficiently [#6754] +* [fixed] Fixed NoSuchMethodError when getting process info on Android 13 on some devices [#6720] +* [changed] Updated `firebase-sessions` dependency to v2.1.0 + * [changed] Add warning for known issue [b/328687152](https://issuetracker.google.com/328687152) [#6755] + * [changed] Updated datastore dependency to v1.1.3 to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8) [#6688] + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-crashlytics` library. The Kotlin extensions library has no additional +updates. + +# 19.4.1 +* [changed] Updated `firebase-sessions` dependency to v2.0.9 + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-crashlytics` library. The Kotlin extensions library has no additional +updates. # 19.4.0 * [feature] Added an overload for `recordException` that allows logging additional custom @@ -324,10 +355,10 @@ updates. # 18.2.10 * [fixed] Fixed a bug that could prevent unhandled exceptions from being - propogated to the default handler when the network is unavailable. + propagated to the default handler when the network is unavailable. * [changed] Internal changes to support on-demand fatal crash reporting for Flutter apps. -* [fixed] Fixed a bug that prevented [crashlytics] from initalizing on some +* [fixed] Fixed a bug that prevented [crashlytics] from initializing on some devices in some cases. (#3269) diff --git a/firebase-crashlytics/gradle.properties b/firebase-crashlytics/gradle.properties index 5ab96e1d760..7ef5196b4b5 100644 --- a/firebase-crashlytics/gradle.properties +++ b/firebase-crashlytics/gradle.properties @@ -1,2 +1,2 @@ -version=19.4.1 -latestReleasedVersion=19.4.0 +version=19.4.4 +latestReleasedVersion=19.4.3 diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt index 636b975ab1d..74d3793e215 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt @@ -23,7 +23,7 @@ private constructor( private val builder: CustomKeysAndValues.Builder, ) { @Deprecated( - "Do not construct this directly. Use [setCustomKeys] instead. To be removed in the next major release." + "Do not construct this directly. Use `setCustomKeys` instead. To be removed in the next major release." ) constructor(crashlytics: FirebaseCrashlytics) : this(crashlytics, CustomKeysAndValues.Builder()) diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt index 49fd2fafd18..172ebaaf477 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt @@ -29,6 +29,8 @@ import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session. * @hide */ internal object ProcessDetailsProvider { + // TODO(mrober): Merge this with [com.google.firebase.sessions.ProcessDetailsProvider]. + /** Gets the details for all of this app's running processes. */ fun getAppProcessDetails(context: Context): List { val appUid = context.applicationInfo.uid @@ -70,7 +72,7 @@ internal object ProcessDetailsProvider { processName: String, pid: Int = 0, importance: Int = 0, - isDefaultProcess: Boolean = false + isDefaultProcess: Boolean = false, ) = ProcessDetails.builder() .setProcessName(processName) @@ -81,7 +83,7 @@ internal object ProcessDetailsProvider { /** Gets the app's current process name. If the API is not available, returns an empty string. */ private fun getProcessName(): String = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { Process.myProcessName() } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { Application.getProcessName() ?: "" diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java index b29863f66c5..d76ccbc2133 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java @@ -69,6 +69,8 @@ public class CommonUtils { "com.google.firebase.crashlytics.build_ids_arch"; static final String BUILD_IDS_BUILD_ID_RESOURCE_NAME = "com.google.firebase.crashlytics.build_ids_build_id"; + static final String VERSION_CONTROL_INFO_RESOURCE_NAME = + "com.google.firebase.crashlytics.version_control_info"; // TODO: Maybe move this method into a more appropriate class. public static SharedPreferences getSharedPrefs(Context context) { @@ -137,8 +139,9 @@ static Architecture getValue() { public static String streamToString(InputStream is) { // Previous code was running into this: http://code.google.com/p/android/issues/detail?id=14562 // on Android 2.3.3. The below code below does not exhibit that problem. - final java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; + try (final java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A")) { + return s.hasNext() ? s.next() : ""; + } } public static String sha1(String source) { @@ -525,6 +528,15 @@ public static List getBuildIdInfo(Context context) { return buildIdInfoList; } + @Nullable + public static String getVersionControlInfo(Context context) { + int id = getResourcesIdentifier(context, VERSION_CONTROL_INFO_RESOURCE_NAME, "string"); + if (id == 0) { + return null; + } + return context.getResources().getString(id); + } + public static void closeQuietly(Closeable closeable) { if (closeable != null) { try { diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java index b55a26678d4..5a1322fc9b6 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java @@ -48,6 +48,7 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -77,6 +78,8 @@ class CrashlyticsController { private static final String VERSION_CONTROL_INFO_FILE = "version-control-info.textproto"; private static final String META_INF_FOLDER = "META-INF/"; + private static final Charset UTF_8 = Charset.forName("UTF-8"); + private final Context context; private final DataCollectionArbiter dataCollectionArbiter; private final CrashlyticsFileMarker crashMarker; @@ -628,13 +631,23 @@ void saveVersionControlInfo() { } String getVersionControlInfo() throws IOException { - InputStream is = getResourceAsStream(META_INF_FOLDER + VERSION_CONTROL_INFO_FILE); - if (is == null) { - return null; + // Attempt to read from an Android string resource + String versionControlInfo = CommonUtils.getVersionControlInfo(context); + if (versionControlInfo != null) { + Logger.getLogger().d("Read version control info from string resource"); + return Base64.encodeToString(versionControlInfo.getBytes(UTF_8), 0); + } + + // Fallback to reading the file + try (InputStream is = getResourceAsStream(META_INF_FOLDER + VERSION_CONTROL_INFO_FILE)) { + if (is != null) { + Logger.getLogger().d("Read version control info from file"); + return Base64.encodeToString(readResource(is), 0); + } } - Logger.getLogger().d("Read version control info"); - return Base64.encodeToString(readResource(is), 0); + Logger.getLogger().i("No version control information found"); + return null; } private InputStream getResourceAsStream(String resource) { @@ -644,25 +657,19 @@ private InputStream getResourceAsStream(String resource) { return null; } - InputStream is = classLoader.getResourceAsStream(resource); - if (is == null) { - Logger.getLogger().i("No version control information found"); - return null; - } - - return is; + return classLoader.getResourceAsStream(resource); } private static byte[] readResource(InputStream is) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int length; - while ((length = is.read(buffer)) != -1) { - out.write(buffer, 0, length); + while ((length = is.read(buffer)) != -1) { + out.write(buffer, 0, length); + } + return out.toByteArray(); } - - return out.toByteArray(); } private void finalizePreviousNativeSession(String previousSessionId) { diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java index 5abc282d556..50533be05b1 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java @@ -36,6 +36,7 @@ import com.google.firebase.crashlytics.internal.send.DataTransportCrashlyticsReportSender; import com.google.firebase.crashlytics.internal.settings.SettingsProvider; import com.google.firebase.crashlytics.internal.stacktrace.StackTraceTrimmingStrategy; +import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -427,13 +428,15 @@ private static CrashlyticsReport.ApplicationExitInfo convertApplicationExitInfo( @VisibleForTesting @RequiresApi(api = Build.VERSION_CODES.KITKAT) public static String convertInputStreamToString(InputStream inputStream) throws IOException { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - byte[] bytes = new byte[DEFAULT_BUFFER_SIZE]; - int length; - while ((length = inputStream.read(bytes)) != -1) { - byteArrayOutputStream.write(bytes, 0, length); + try (BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + byte[] bytes = new byte[DEFAULT_BUFFER_SIZE]; + int length; + while ((length = bufferedInputStream.read(bytes)) != -1) { + byteArrayOutputStream.write(bytes, 0, length); + } + return byteArrayOutputStream.toString(StandardCharsets.UTF_8.name()); } - return byteArrayOutputStream.toString(StandardCharsets.UTF_8.name()); } /** Finds the first ANR ApplicationExitInfo within the session. */ diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index fa16a7ed32d..57be7a46be0 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -1,5 +1,26 @@ # Unreleased +* [fixed] Fixed occasional `NullPointerException` when registering with + FirebaseAuth, leading to erroneous UNAUTHENTICATED exceptions. + ([#7001](https://github.com/firebase/firebase-android-sdk/pull/7001)) +# 16.0.2 +* [changed] Improved code robustness related to state management in + `FirebaseDataConnect` objects. + ([#6861](https://github.com/firebase/firebase-android-sdk/pull/6861)) + +# 16.0.1 +* [changed] Internal improvements. + +# 16.0.0 +* [changed] DataConnectOperationException added, enabling support for partial + errors; that is, any data that was received and/or was able to be decoded is + now available via the "response" property of the exception thrown when a + query or mutation is executed. + ([#6794](https://github.com/firebase/firebase-android-sdk/pull/6794)) + +# 16.0.0-beta05 +* [changed] Changed gRPC proto package to v1 (was v1beta). + ([#6729](https://github.com/firebase/firebase-android-sdk/pull/6729)) # 16.0.0-beta04 * [changed] `FirebaseDataConnect.logLevel` type changed from `LogLevel` to diff --git a/firebase-dataconnect/api.txt b/firebase-dataconnect/api.txt index 19fb52985f5..d919cc593db 100644 --- a/firebase-dataconnect/api.txt +++ b/firebase-dataconnect/api.txt @@ -42,6 +42,47 @@ package com.google.firebase.dataconnect { ctor public DataConnectException(String message, Throwable? cause = null); } + public class DataConnectOperationException extends com.google.firebase.dataconnect.DataConnectException { + ctor public DataConnectOperationException(String message, Throwable? cause = null, com.google.firebase.dataconnect.DataConnectOperationFailureResponse response); + method public final com.google.firebase.dataconnect.DataConnectOperationFailureResponse getResponse(); + property public final com.google.firebase.dataconnect.DataConnectOperationFailureResponse response; + } + + public interface DataConnectOperationFailureResponse { + method public Data? getData(); + method public java.util.List getErrors(); + method public java.util.Map? getRawData(); + method public String toString(); + property public abstract Data? data; + property public abstract java.util.List errors; + property public abstract java.util.Map? rawData; + } + + public static interface DataConnectOperationFailureResponse.ErrorInfo { + method public boolean equals(Object? other); + method public String getMessage(); + method public java.util.List getPath(); + method public int hashCode(); + method public String toString(); + property public abstract String message; + property public abstract java.util.List path; + } + + public sealed interface DataConnectPathSegment { + } + + @kotlin.jvm.JvmInline public static final value class DataConnectPathSegment.Field implements com.google.firebase.dataconnect.DataConnectPathSegment { + ctor public DataConnectPathSegment.Field(String field); + method public String getField(); + property public final String field; + } + + @kotlin.jvm.JvmInline public static final value class DataConnectPathSegment.ListIndex implements com.google.firebase.dataconnect.DataConnectPathSegment { + ctor public DataConnectPathSegment.ListIndex(int index); + method public int getIndex(); + property public final int index; + } + public final class DataConnectSettings { ctor public DataConnectSettings(String host = "firebasedataconnect.googleapis.com", boolean sslEnabled = true); method public String getHost(); diff --git a/firebase-dataconnect/ci/README.md b/firebase-dataconnect/ci/README.md new file mode 100644 index 00000000000..8dd8c015eb9 --- /dev/null +++ b/firebase-dataconnect/ci/README.md @@ -0,0 +1,22 @@ +# Firebase Data Connect Android SDK Continuous Integration Scripts + +These scripts are used by GitHub Actions. + +There are GitHub Actions workflows that verify code formatting, lint checks, type annotations, +and running unit tests of code in this directory. Although they are not "required" checks, it +is requested to wait for these checks to pass. See `dataconnect.yaml`. + +The minimum required Python version (at the time of writing, April 2025) is 3.13. +See `pyproject.toml` for the most up-to-date requirement. + +Before running the scripts, install the required dependencies by running: + +``` +pip install -r requirements.txt +``` + +Then, run all of these presubmit checks by running the following command: + +``` +ruff check --fix && ruff format && pyright && pytest && echo 'SUCCESS!!!!!!!!!!!!!!!' +``` diff --git a/firebase-dataconnect/ci/calculate_github_issue_for_commenting.py b/firebase-dataconnect/ci/calculate_github_issue_for_commenting.py new file mode 100644 index 00000000000..2f71eea86ea --- /dev/null +++ b/firebase-dataconnect/ci/calculate_github_issue_for_commenting.py @@ -0,0 +1,148 @@ +# Copyright 2025 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. + +from __future__ import annotations + +import argparse +import logging +import pathlib +import re +import typing + +from util import fetch_pr_info, pr_number_from_github_ref + + +def main() -> None: + args = parse_args() + logging.basicConfig(format="%(message)s", level=logging.INFO) + + github_issue = calculate_github_issue( + github_event_name=args.github_event_name, + github_issue_for_scheduled_run=args.github_issue_for_scheduled_run, + github_ref=args.github_ref, + github_repository=args.github_repository, + pr_body_github_issue_key=args.pr_body_github_issue_key, + ) + + issue_file_text = "" if github_issue is None else str(github_issue) + logging.info("Writing '%s' to %s", issue_file_text, args.issue_output_file) + args.issue_output_file.write_text(issue_file_text, encoding="utf8", errors="replace") + + +def calculate_github_issue( + github_event_name: str, + github_issue_for_scheduled_run: int, + github_ref: str, + github_repository: str, + pr_body_github_issue_key: str, +) -> int | None: + if github_event_name == "schedule": + logging.info( + "GitHub Event name is: %s; using GitHub Issue: %s", + github_event_name, + github_issue_for_scheduled_run, + ) + return github_issue_for_scheduled_run + + logging.info("Extracting PR number from string: %s", github_ref) + pr_number = pr_number_from_github_ref(github_ref) + if pr_number is None: + logging.info("No PR number extracted") + return None + typing.assert_type(pr_number, int) + + logging.info("PR number extracted: %s", pr_number) + logging.info("Loading body text of PR: %s", pr_number) + pr_info = fetch_pr_info( + pr_number=pr_number, + github_repository=github_repository, + ) + + logging.info("Looking for GitHub Issue key in PR body text: %s=NNNN", pr_body_github_issue_key) + github_issue = github_issue_from_pr_body( + pr_body=pr_info.body, + issue_key=pr_body_github_issue_key, + ) + + if github_issue is None: + logging.info("No GitHub Issue key found in PR body") + return None + typing.assert_type(github_issue, int) + + logging.info("Found GitHub Issue key in PR body: %s", github_issue) + return github_issue + + +def github_issue_from_pr_body(pr_body: str, issue_key: str) -> int | None: + expr = re.compile(r"\s*" + re.escape(issue_key) + r"\s*=\s*(\d+)\s*") + for line in pr_body.splitlines(): + match = expr.fullmatch(line.strip()) + if match: + return int(match.group(1)) + return None + + +class ParsedArgs(typing.Protocol): + issue_output_file: pathlib.Path + github_ref: str + github_repository: str + github_event_name: str + pr_body_github_issue_key: str + github_issue_for_scheduled_run: int + + +def parse_args() -> ParsedArgs: + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument( + "--issue-output-file", + required=True, + help="The file to which to write the calculated issue number" + "if no issue number was found, then an empty file will be written", + ) + arg_parser.add_argument( + "--github-ref", + required=True, + help="The value of ${{ github.ref }} in the workflow", + ) + arg_parser.add_argument( + "--github-repository", + required=True, + help="The value of ${{ github.repository }} in the workflow", + ) + arg_parser.add_argument( + "--github-event-name", + required=True, + help="The value of ${{ github.event_name }} in the workflow", + ) + arg_parser.add_argument( + "--pr-body-github-issue-key", + required=True, + help="The string to search for in a Pull Request body to determine the GitHub Issue number " + "for commenting. For example, if the value is 'foobar' then this script searched a PR " + "body for a line of the form 'foobar=NNNN' where 'NNNN' is the GitHub issue number", + ) + arg_parser.add_argument( + "--github-issue-for-scheduled-run", + type=int, + required=True, + help="The GitHub Issue number to use for commenting when --github-event-name is 'schedule'", + ) + + parse_result = arg_parser.parse_args() + parse_result.issue_output_file = pathlib.Path(parse_result.issue_output_file) + return typing.cast("ParsedArgs", parse_result) + + +if __name__ == "__main__": + main() diff --git a/firebase-dataconnect/ci/calculate_github_issue_for_commenting_test.py b/firebase-dataconnect/ci/calculate_github_issue_for_commenting_test.py new file mode 100644 index 00000000000..a7e1df7da03 --- /dev/null +++ b/firebase-dataconnect/ci/calculate_github_issue_for_commenting_test.py @@ -0,0 +1,162 @@ +# Copyright 2025 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. + +from __future__ import annotations + +import hypothesis +import hypothesis.strategies as st +import pytest + +import calculate_github_issue_for_commenting as sut + + +class Test_pr_number_from_github_ref: + @hypothesis.given(number=st.integers(min_value=0, max_value=10000)) + def test_returns_number_from_valid_github_ref(self, number: int) -> None: + github_ref = f"refs/pull/{number}/merge" + assert sut.pr_number_from_github_ref(github_ref) == number + + @hypothesis.given(number=st.integers(min_value=0, max_value=10000)) + def test_ignores_leading_zeroes(self, number: int) -> None: + github_ref = f"refs/pull/0{number}/merge" + assert sut.pr_number_from_github_ref(github_ref) == number + + @hypothesis.given(invalid_github_ref=st.text()) + def test_returns_none_on_random_input(self, invalid_github_ref: str) -> None: + assert sut.pr_number_from_github_ref(invalid_github_ref) is None + + @pytest.mark.parametrize( + "invalid_number", + [ + "", + "-1", + "123a", + "a123", + "12a34", + "1.2", + pytest.param( + "1234", + marks=pytest.mark.xfail( + reason="make sure that the test would otherwise pass on valid int values", + strict=True, + ), + ), + ], + ) + def test_returns_none_on_invalid_number(self, invalid_number: str) -> None: + invalid_github_ref = f"refs/pull/{invalid_number}/merge" + assert sut.pr_number_from_github_ref(invalid_github_ref) is None + + @pytest.mark.parametrize( + "malformed_ref", + [ + "", + "refs", + "refs/", + "refs/pull", + "refs/pull/", + "refs/pull/1234", + "refs/pull/1234/", + "Refs/pull/1234/merge", + "refs/Pull/1234/merge", + "refs/pull/1234/Merge", + "Arefs/pull/1234/merge", + "refs/pull/1234/mergeZ", + " refs/pull/1234/merge", + "refs/pull/1234/merge ", + pytest.param( + "refs/pull/1234/merge", + marks=pytest.mark.xfail( + reason="make sure that the test would otherwise pass on valid ref", + strict=True, + ), + ), + ], + ) + def test_returns_none_on_malformed_ref(self, malformed_ref: str) -> None: + assert sut.pr_number_from_github_ref(malformed_ref) is None + + +class Test_github_issue_from_pr_body: + @hypothesis.given(number=st.integers(min_value=0, max_value=10000)) + def test_returns_number(self, number: int) -> None: + text = f"zzyzx={number}" + assert sut.github_issue_from_pr_body(text, "zzyzx") == number + + @hypothesis.given(number=st.integers(min_value=0, max_value=10000)) + def test_ignores_leading_zeroes(self, number: int) -> None: + text = f"zzyzx=0{number}" + assert sut.github_issue_from_pr_body(text, "zzyzx") == number + + @hypothesis.given(number=st.integers(min_value=0, max_value=10000)) + def test_ignores_whitespace(self, number: int) -> None: + text = f" zzyzx = {number} " + assert sut.github_issue_from_pr_body(text, "zzyzx") == number + + @hypothesis.given( + number1=st.integers(min_value=0, max_value=10000), + number2=st.integers(min_value=0, max_value=10000), + ) + def test_does_not_ignore_whitespace_in_key(self, number1: int, number2: int) -> None: + text = f"zzyzx={number1}\n z z y z x = {number2} " + assert sut.github_issue_from_pr_body(text, "z z y z x") == number2 + + @hypothesis.given( + number1=st.integers(min_value=0, max_value=10000), + number2=st.integers(min_value=0, max_value=10000), + ) + def test_returns_first_number_ignoring_second(self, number1: int, number2: int) -> None: + text = f"zzyzx={number1}\nzzyzx={number2}" + assert sut.github_issue_from_pr_body(text, "zzyzx") == number1 + + @hypothesis.given(number=st.integers(min_value=0, max_value=10000)) + def test_returns_first_valid_number_ignoring_invalid(self, number: int) -> None: + text = f"zzyzx=12X34\nzzyzx={number}" + assert sut.github_issue_from_pr_body(text, "zzyzx") == number + + @hypothesis.given(number=st.integers(min_value=0, max_value=10000)) + def test_returns_number_amidst_other_lines(self, number: int) -> None: + text = f"line 1\nline 2\nzzyzx={number}\nline 3" + assert sut.github_issue_from_pr_body(text, "zzyzx") == number + + @hypothesis.given(number=st.integers(min_value=0, max_value=10000)) + def test_returns_escapes_regex_special_chars_in_key(self, number: int) -> None: + text = f"*+={number}" + assert sut.github_issue_from_pr_body(text, "*+") == number + + @pytest.mark.parametrize( + "text", + [ + "", + "asdf", + "zzyzx=", + "=zzyzx", + "zzyzx=a", + "zzyzx=-1", + "zzyzx=a123", + "zzyzx=123a", + "zzyzx=1.2", + "a zzyzx=1234", + "zzyzx=1234 a", + pytest.param( + "zzyzx=1234", + marks=pytest.mark.xfail( + reason="make sure that the test would otherwise pass on valid text", + strict=True, + ), + ), + ], + ) + def test_returns_none_when_key_not_found_or_cannot_parse_int(self, text: str) -> None: + assert sut.github_issue_from_pr_body(text, "zzyzx") is None diff --git a/firebase-dataconnect/ci/logcat_error_report.py b/firebase-dataconnect/ci/logcat_error_report.py new file mode 100644 index 00000000000..fe316fd8a11 --- /dev/null +++ b/firebase-dataconnect/ci/logcat_error_report.py @@ -0,0 +1,175 @@ +# Copyright 2025 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. + +from __future__ import annotations + +import argparse +import dataclasses +import logging +import pathlib +import re +import tempfile +import typing + +if typing.TYPE_CHECKING: + from _typeshed import SupportsWrite + +TEST_STARTED_TOKEN = "TestRunner: started:" # noqa: S105 +TEST_STARTED_PATTERN = r"(\W|^)" + re.escape(TEST_STARTED_TOKEN) + r"\s+(?P.*\S)" +TEST_FAILED_TOKEN = "TestRunner: failed:" # noqa: S105 +TEST_FAILED_PATTERN = r"(\W|^)" + re.escape(TEST_FAILED_TOKEN) + r"\s+(?P.*\S)" +TEST_FINISHED_TOKEN = "TestRunner: finished:" # noqa: S105 +TEST_FINISHED_PATTERN = r"(\W|^)" + re.escape(TEST_FINISHED_TOKEN) + r"\s+(?P.*\S)" + + +@dataclasses.dataclass +class TestResult: + test_name: str + output_file: pathlib.Path + passed: bool + + +def main() -> None: + args = parse_args() + logging.basicConfig(format="%(message)s", level=args.log_level) + + if args.work_dir is None: + work_temp_dir = tempfile.TemporaryDirectory("dd9rh9apdf") + work_dir = pathlib.Path(work_temp_dir.name) + logging.debug("Using temporary directory as work directory: %s", work_dir) + else: + work_temp_dir = None + work_dir = args.work_dir + logging.debug("Using specified directory as work directory: %s", work_dir) + work_dir.mkdir(parents=True, exist_ok=True) + + logging.info("Extracting test failures from %s", args.logcat_file) + test_results: list[TestResult] = [] + cur_test_result: TestResult | None = None + cur_test_result_output_file: SupportsWrite[str] | None = None + + with args.logcat_file.open("rt", encoding="utf8", errors="ignore") as logcat_file_handle: + for line in logcat_file_handle: + test_started_match = TEST_STARTED_TOKEN in line and re.search(TEST_STARTED_PATTERN, line) + if test_started_match: + test_name = test_started_match.group("name") + logging.debug('Found "Test Started" logcat line for test: %s', test_name) + if cur_test_result_output_file is not None: + cur_test_result_output_file.close() + test_output_file = work_dir / f"{len(test_results)}.txt" + cur_test_result = TestResult(test_name=test_name, output_file=test_output_file, passed=True) + test_results.append(cur_test_result) + cur_test_result_output_file = test_output_file.open("wt", encoding="utf8", errors="replace") + + if cur_test_result_output_file is not None: + cur_test_result_output_file.write(line) + + test_failed_match = TEST_FAILED_TOKEN in line and re.search(TEST_FAILED_PATTERN, line) + if test_failed_match: + test_name = test_failed_match.group("name") + logging.warning("FAILED TEST: %s", test_name) + if cur_test_result is None: + logging.warning( + "WARNING: failed test reported without matching test started: %s", test_name + ) + else: + cur_test_result.passed = False + + test_finished_match = TEST_FINISHED_TOKEN in line and re.search(TEST_FINISHED_PATTERN, line) + if test_finished_match: + test_name = test_finished_match.group("name") + logging.debug('Found "Test Finished" logcat line for test: %s', test_name) + if cur_test_result_output_file is not None: + cur_test_result_output_file.close() + cur_test_result_output_file = None + cur_test_result = None + + if cur_test_result_output_file is not None: + cur_test_result_output_file.close() + del cur_test_result_output_file + + passed_tests = [test_result for test_result in test_results if test_result.passed] + failed_tests = [test_result for test_result in test_results if not test_result.passed] + print_line( + f"Found results for {len(test_results)} tests: " + f"{len(passed_tests)} passed, {len(failed_tests)} failed" + ) + + if len(failed_tests) > 0: + fail_number = 0 + for failed_test_result in failed_tests: + fail_number += 1 + print_line("") + print_line(f"Failure {fail_number}/{len(failed_tests)}: {failed_test_result.test_name}:") + try: + with failed_test_result.output_file.open( + "rt", encoding="utf8", errors="ignore" + ) as test_output_file: + for line in test_output_file: + print_line(line.rstrip()) + except OSError: + logging.warning("WARNING: reading file failed: %s", failed_test_result.output_file) + continue + + if work_temp_dir is not None: + logging.debug("Cleaning up temporary directory: %s", work_dir) + del work_dir + del work_temp_dir + + +def print_line(line: str) -> None: + print(line) # noqa: T201 + + +class ParsedArgs(typing.Protocol): + logcat_file: pathlib.Path + log_level: int + work_dir: pathlib.Path | None + + +def parse_args() -> ParsedArgs: + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument( + "--logcat-file", + required=True, + help="The text file containing the logcat logs to scan.", + ) + arg_parser.add_argument( + "--work-dir", + default=None, + help="The directory into which to write temporary files; " + "if not specified, use a temporary directory that is deleted " + "when this script completes; this is primarily intended for " + "developers of this script to use in testing and debugging", + ) + arg_parser.add_argument( + "--verbose", + action="store_const", + dest="log_level", + default=logging.INFO, + const=logging.DEBUG, + help="Include debug logging output", + ) + + parse_result = arg_parser.parse_args() + + parse_result.logcat_file = pathlib.Path(parse_result.logcat_file) + parse_result.work_dir = ( + None if parse_result.work_dir is None else pathlib.Path(parse_result.work_dir) + ) + return typing.cast("ParsedArgs", parse_result) + + +if __name__ == "__main__": + main() diff --git a/firebase-dataconnect/ci/logcat_error_report_test.py b/firebase-dataconnect/ci/logcat_error_report_test.py new file mode 100644 index 00000000000..59030ec7c4e --- /dev/null +++ b/firebase-dataconnect/ci/logcat_error_report_test.py @@ -0,0 +1,149 @@ +# Copyright 2025 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. + +from __future__ import annotations + +import re + +import pytest + +import logcat_error_report as sut + + +class TestRegularExpressionPatterns: + @pytest.mark.parametrize( + "string", + [ + "", + "XTestRunner: started: fooTest1234", + "TestRunner: started:fooTest1234", + pytest.param( + "TestRunner: started: fooTest1234", + marks=pytest.mark.xfail( + reason="make sure that the test would otherwise pass on match", + strict=True, + ), + ), + ], + ) + def test_test_started_pattern_no_match(self, string: str) -> None: + assert re.search(sut.TEST_STARTED_PATTERN, string) is None + + @pytest.mark.parametrize( + ("string", "expected_name"), + [ + ("TestRunner: started: fooTest1234", "fooTest1234"), + (" TestRunner: started: fooTest1234", "fooTest1234"), + ("TestRunner: started: fooTest1234", "fooTest1234"), + ("TestRunner: started: fooTest1234 ", "fooTest1234"), + ("TestRunner: started: fooTest1234(abc.123)", "fooTest1234(abc.123)"), + ("TestRunner: started: a $ 2 ^ %% . ", "a $ 2 ^ %% ."), + pytest.param( + "i do not match the pattern", + None, + marks=pytest.mark.xfail( + reason="make sure that the test would otherwise pass on match", + strict=True, + ), + ), + ], + ) + def test_test_started_pattern_match(self, string: str, expected_name: str) -> None: + match = re.search(sut.TEST_STARTED_PATTERN, string) + assert match is not None + assert match.group("name") == expected_name + + @pytest.mark.parametrize( + "string", + [ + "", + "XTestRunner: finished: fooTest1234", + "TestRunner: finished:fooTest1234", + pytest.param( + "TestRunner: finished: fooTest1234", + marks=pytest.mark.xfail( + reason="make sure that the test would otherwise pass on match", + strict=True, + ), + ), + ], + ) + def test_test_finished_pattern_no_match(self, string: str) -> None: + assert re.search(sut.TEST_FINISHED_PATTERN, string) is None + + @pytest.mark.parametrize( + ("string", "expected_name"), + [ + ("TestRunner: finished: fooTest1234", "fooTest1234"), + (" TestRunner: finished: fooTest1234", "fooTest1234"), + ("TestRunner: finished: fooTest1234", "fooTest1234"), + ("TestRunner: finished: fooTest1234 ", "fooTest1234"), + ("TestRunner: finished: fooTest1234(abc.123)", "fooTest1234(abc.123)"), + ("TestRunner: finished: a $ 2 ^ %% . ", "a $ 2 ^ %% ."), + pytest.param( + "i do not match the pattern", + None, + marks=pytest.mark.xfail( + reason="make sure that the test would otherwise pass on match", + strict=True, + ), + ), + ], + ) + def test_test_finished_pattern_match(self, string: str, expected_name: str) -> None: + match = re.search(sut.TEST_FINISHED_PATTERN, string) + assert match is not None + assert match.group("name") == expected_name + + @pytest.mark.parametrize( + "string", + [ + "", + "XTestRunner: failed: fooTest1234", + "TestRunner: failed:fooTest1234", + pytest.param( + "TestRunner: failed: fooTest1234", + marks=pytest.mark.xfail( + reason="make sure that the test would otherwise pass on match", + strict=True, + ), + ), + ], + ) + def test_test_failed_pattern_no_match(self, string: str) -> None: + assert re.search(sut.TEST_FAILED_PATTERN, string) is None + + @pytest.mark.parametrize( + ("string", "expected_name"), + [ + ("TestRunner: failed: fooTest1234", "fooTest1234"), + (" TestRunner: failed: fooTest1234", "fooTest1234"), + ("TestRunner: failed: fooTest1234", "fooTest1234"), + ("TestRunner: failed: fooTest1234 ", "fooTest1234"), + ("TestRunner: failed: fooTest1234(abc.123)", "fooTest1234(abc.123)"), + ("TestRunner: failed: a $ 2 ^ %% . ", "a $ 2 ^ %% ."), + pytest.param( + "i do not match the pattern", + None, + marks=pytest.mark.xfail( + reason="make sure that the test would otherwise pass on match", + strict=True, + ), + ), + ], + ) + def test_test_failed_pattern_match(self, string: str, expected_name: str) -> None: + match = re.search(sut.TEST_FAILED_PATTERN, string) + assert match is not None + assert match.group("name") == expected_name diff --git a/firebase-dataconnect/ci/post_comment_for_job_results.py b/firebase-dataconnect/ci/post_comment_for_job_results.py new file mode 100644 index 00000000000..738ac2c9b9d --- /dev/null +++ b/firebase-dataconnect/ci/post_comment_for_job_results.py @@ -0,0 +1,229 @@ +# Copyright 2025 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. + +from __future__ import annotations + +import argparse +import dataclasses +import logging +import pathlib +import subprocess +import tempfile +import typing + +from util import fetch_pr_info, pr_number_from_github_ref + +if typing.TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + +def main() -> None: + args = parse_args() + logging.basicConfig(format="%(message)s", level=logging.INFO) + + message_lines = tuple(generate_message_lines(args)) + + issue_url = f"{args.github_repository_html_url}/issues/{args.github_issue}" + logging.info("Posting the following comment to GitHub Issue %s", issue_url) + for line in message_lines: + logging.info(line) + + message_bytes = "\n".join(message_lines).encode("utf8", errors="replace") + with tempfile.TemporaryDirectory() as tempdir_path: + message_file = pathlib.Path(tempdir_path) / "message_text.txt" + message_file.write_bytes(message_bytes) + post_github_issue_comment( + issue_number=args.github_issue, + body_file=message_file, + github_repository=args.github_repository, + ) + + +def generate_message_lines(data: ParsedArgs) -> Iterable[str]: + logging.info("Extracting PR number from string: %s", data.github_ref) + pr_number = pr_number_from_github_ref(data.github_ref) + pr_title: str | None + if pr_number is None: + logging.info("No PR number extracted") + pr_title = None + else: + pr_info = fetch_pr_info( + pr_number=pr_number, + github_repository=data.github_repository, + ) + pr_title = pr_info.title + + if pr_number is not None: + yield ( + f"Posting from Pull Request {data.github_repository_html_url}/pull/{pr_number} ({pr_title})" + ) + + yield f"Result of workflow '{data.github_workflow}' at {data.github_sha}:" + + for job_result in data.job_results: + result_symbol = "✅" if job_result.result == "success" else "❌" + yield f" - {job_result.job_id}: {result_symbol} {job_result.result}" + + yield "" + yield f"{data.github_repository_html_url}/actions/runs/{data.github_run_id}" + + yield "" + yield ( + f"event_name=`{data.github_event_name}` " + f"run_id=`{data.github_run_id}` " + f"run_number=`{data.github_run_number}` " + f"run_attempt=`{data.github_run_attempt}`" + ) + + +def post_github_issue_comment( + issue_number: int, body_file: pathlib.Path, github_repository: str +) -> None: + gh_args = post_issue_comment_gh_args( + issue_number=issue_number, body_file=body_file, github_repository=github_repository + ) + gh_args = tuple(gh_args) + logging.info("Running command: %s", subprocess.list2cmdline(gh_args)) + subprocess.check_call(gh_args) # noqa: S603 + + +def post_issue_comment_gh_args( + issue_number: int, body_file: pathlib.Path, github_repository: str +) -> Iterable[str]: + yield "gh" + yield "issue" + + yield "comment" + yield str(issue_number) + yield "--body-file" + yield str(body_file) + yield "-R" + yield github_repository + + +@dataclasses.dataclass(frozen=True) +class JobResult: + job_id: str + result: str + + @classmethod + def parse(cls, s: str) -> JobResult: + colon_index = s.find(":") + if colon_index < 0: + raise ParseError( + "no colon (:) character found in job result specification, " + "which is required to delimit the job ID from the job result" + ) + job_id = s[:colon_index] + job_result = s[colon_index + 1 :] + return cls(job_id=job_id, result=job_result) + + +class ParsedArgs(typing.Protocol): + job_results: Sequence[JobResult] + github_issue: int + github_repository: str + github_event_name: str + github_ref: str + github_workflow: str + github_sha: str + github_repository_html_url: str + github_run_id: str + github_run_number: str + github_run_attempt: str + + +class ParseError(Exception): + pass + + +def parse_args() -> ParsedArgs: + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument( + "job_results", + nargs="+", + help="The results of the jobs in question, of the form " + "'job-id:${{ needs.job-id.result }}' where 'job-id' is the id of the corresponding job " + "in the 'needs' section of the job.", + ) + arg_parser.add_argument( + "--github-issue", + type=int, + required=True, + help="The GitHub Issue number to which to post a comment", + ) + arg_parser.add_argument( + "--github-repository", + required=True, + help="The value of ${{ github.repository }} in the workflow", + ) + arg_parser.add_argument( + "--github-event-name", + required=True, + help="The value of ${{ github.event_name }} in the workflow", + ) + arg_parser.add_argument( + "--github-ref", + required=True, + help="The value of ${{ github.ref }} in the workflow", + ) + arg_parser.add_argument( + "--github-workflow", + required=True, + help="The value of ${{ github.workflow }} in the workflow", + ) + arg_parser.add_argument( + "--github-sha", + required=True, + help="The value of ${{ github.sha }} in the workflow", + ) + arg_parser.add_argument( + "--github-repository-html-url", + required=True, + help="The value of ${{ github.event.repository.html_url }} in the workflow", + ) + arg_parser.add_argument( + "--github-run-id", + required=True, + help="The value of ${{ github.run_id }} in the workflow", + ) + arg_parser.add_argument( + "--github-run-number", + required=True, + help="The value of ${{ github.run_number }} in the workflow", + ) + arg_parser.add_argument( + "--github-run-attempt", + required=True, + help="The value of ${{ github.run_attempt }} in the workflow", + ) + + parse_result = arg_parser.parse_args() + + job_results: list[JobResult] = [] + for job_result_str in parse_result.job_results: + try: + job_result = JobResult.parse(job_result_str) + except ParseError as e: + arg_parser.error(f"invalid job result specification: {job_result_str} ({e})") + typing.assert_never("the line above should have raised an exception") + else: + job_results.append(job_result) + parse_result.job_results = tuple(job_results) + + return typing.cast("ParsedArgs", parse_result) + + +if __name__ == "__main__": + main() diff --git a/firebase-dataconnect/ci/pyproject.toml b/firebase-dataconnect/ci/pyproject.toml new file mode 100644 index 00000000000..71800e3a086 --- /dev/null +++ b/firebase-dataconnect/ci/pyproject.toml @@ -0,0 +1,48 @@ +[project] +name = "Firebase Data Connect Android SDK Continuous Integration Tools" +requires-python = ">= 3.13" + +[tool.pytest.ini_options] +addopts = "--strict-markers" + +[tool.pyright] +include = ["**/*.py"] +typeCheckingMode = "strict" + +[tool.ruff] +line-length = 100 +indent-width = 2 + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "C901", # function is too complex + "COM812", # missing-trailing-comma + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in `__init__` + "D203", # incorrect-blank-line-before-class + "D211", # no-blank-line-before-class + "D212", # multi-line-summary-second-line + "E501", # Line too long (will be fixed by the formatter) + "EM101", # Exception must not use a string literal, assign to variable first + "LOG015", # root-logger-call + "PLR0912", # Too many branches + "PLR0915", # Too many statements + "TRY003", # Avoid specifying long messages outside the exception class +] + +[tool.ruff.lint.per-file-ignores] +"*_test.py" = [ + "N801", # invalid-class-name + "S101", # Use of `assert` detected +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +docstring-code-format = true diff --git a/firebase-dataconnect/ci/requirements.txt b/firebase-dataconnect/ci/requirements.txt new file mode 100644 index 00000000000..3440d3b19f8 --- /dev/null +++ b/firebase-dataconnect/ci/requirements.txt @@ -0,0 +1,11 @@ +attrs==25.3.0 +hypothesis==6.131.0 +iniconfig==2.1.0 +nodeenv==1.9.1 +packaging==24.2 +pluggy==1.5.0 +pyright==1.1.399 +pytest==8.3.5 +ruff==0.11.5 +sortedcontainers==2.4.0 +typing_extensions==4.13.2 diff --git a/firebase-dataconnect/ci/util.py b/firebase-dataconnect/ci/util.py new file mode 100644 index 00000000000..9f71bdcb226 --- /dev/null +++ b/firebase-dataconnect/ci/util.py @@ -0,0 +1,60 @@ +# Copyright 2025 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. + +from __future__ import annotations + +import dataclasses +import json +import logging +import re +import subprocess +import typing + +if typing.TYPE_CHECKING: + from collections.abc import Iterable + + +@dataclasses.dataclass(frozen=True) +class GitHubPrInfo: + title: str + body: str + + +def fetch_pr_info(pr_number: int, github_repository: str) -> GitHubPrInfo: + gh_args = _fetch_pr_gh_args(pr_number=pr_number, github_repository=github_repository) + gh_args = tuple(gh_args) + logging.info("Running command: %s", subprocess.list2cmdline(gh_args)) + output_str = subprocess.check_output(gh_args, encoding="utf8", errors="replace") # noqa: S603 + logging.info("%s", output_str.strip()) + output = json.loads(output_str) + return GitHubPrInfo( + title=output["title"], + body=output["body"], + ) + + +def _fetch_pr_gh_args(pr_number: int, github_repository: str) -> Iterable[str]: + yield "gh" + yield "issue" + yield "view" + yield str(pr_number) + yield "--json" + yield "title,body" + yield "-R" + yield github_repository + + +def pr_number_from_github_ref(github_ref: str) -> int | None: + match = re.fullmatch("refs/pull/([0-9]+)/merge", github_ref) + return int(match.group(1)) if match else None diff --git a/firebase-dataconnect/demo/build.gradle.kts b/firebase-dataconnect/demo/build.gradle.kts index 78465a0df41..7c00d7d0445 100644 --- a/firebase-dataconnect/demo/build.gradle.kts +++ b/firebase-dataconnect/demo/build.gradle.kts @@ -19,12 +19,12 @@ import java.nio.charset.StandardCharsets plugins { // Use whichever versions of these dependencies suit your application. - // The versions shown here were the latest versions as of December 03, 2024. + // The versions shown here were the latest versions as of May 09, 2025. // Note, however, that the version of kotlin("plugin.serialization") _must_, // in general, match the version of kotlin("android"). - id("com.android.application") version "8.7.3" + id("com.android.application") version "8.9.2" id("com.google.gms.google-services") version "4.4.2" - val kotlinVersion = "2.1.0" + val kotlinVersion = "2.1.10" kotlin("android") version kotlinVersion kotlin("plugin.serialization") version kotlinVersion @@ -35,19 +35,19 @@ plugins { dependencies { // Use whichever versions of these dependencies suit your application. - // The versions shown here were the latest versions as of December 03, 2024. - implementation("com.google.firebase:firebase-dataconnect:16.0.0-beta03") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3") + // The versions shown here were the latest versions as of May 09, 2025. + implementation("com.google.firebase:firebase-dataconnect:16.0.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.0") implementation("androidx.appcompat:appcompat:1.7.0") - implementation("androidx.activity:activity-ktx:1.9.3") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") + implementation("androidx.activity:activity-ktx:1.10.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0") implementation("com.google.android.material:material:1.12.0") // The following code in this "dependencies" block can be omitted from customer // facing documentation as it is an implementation detail of this application. - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") implementation("io.kotest:kotest-property:5.9.1") implementation("io.kotest.extensions:kotest-property-arbs:2.1.2") } diff --git a/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml b/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml index 341a3fc587a..3a718496328 100644 --- a/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml +++ b/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml @@ -1,4 +1,4 @@ -specVersion: v1beta +specVersion: v1 serviceId: srv3ar8skbsza location: us-central1 schema: diff --git a/firebase-dataconnect/demo/src/main/kotlin/com/google/firebase/dataconnect/minimaldemo/MyApplication.kt b/firebase-dataconnect/demo/src/main/kotlin/com/google/firebase/dataconnect/minimaldemo/MyApplication.kt index eb70e8af475..1b6360efb58 100644 --- a/firebase-dataconnect/demo/src/main/kotlin/com/google/firebase/dataconnect/minimaldemo/MyApplication.kt +++ b/firebase-dataconnect/demo/src/main/kotlin/com/google/firebase/dataconnect/minimaldemo/MyApplication.kt @@ -61,7 +61,7 @@ class MyApplication : Application() { } ) - private val initialLogLevel = FirebaseDataConnect.logLevel + private val initialLogLevel = FirebaseDataConnect.logLevel.value private val connectorMutex = Mutex() private var connector: Ctry3q3tp6kzxConnector? = null @@ -70,7 +70,7 @@ class MyApplication : Application() { coroutineScope.launch { if (getDataConnectDebugLoggingEnabled()) { - FirebaseDataConnect.logLevel = LogLevel.DEBUG + FirebaseDataConnect.logLevel.value = LogLevel.DEBUG } } } @@ -102,7 +102,7 @@ class MyApplication : Application() { getSharedPreferences().all[SharedPrefsKeys.IS_DATA_CONNECT_LOGGING_ENABLED] as? Boolean ?: false suspend fun setDataConnectDebugLoggingEnabled(enabled: Boolean) { - FirebaseDataConnect.logLevel = if (enabled) LogLevel.DEBUG else initialLogLevel + FirebaseDataConnect.logLevel.value = if (enabled) LogLevel.DEBUG else initialLogLevel editSharedPreferences { putBoolean(SharedPrefsKeys.IS_DATA_CONNECT_LOGGING_ENABLED, enabled) } } diff --git a/firebase-dataconnect/emulator/dataconnect/connector/person/person_ops.gql b/firebase-dataconnect/emulator/dataconnect/connector/person/person_ops.gql index ffc8281e0fd..db53ca04dd2 100644 --- a/firebase-dataconnect/emulator/dataconnect/connector/person/person_ops.gql +++ b/firebase-dataconnect/emulator/dataconnect/connector/person/person_ops.gql @@ -88,3 +88,20 @@ query getPersonAuth($id: String!) @auth(level: USER_ANON) { age } } + +query getPersonWithPartialFailure($id: String!) @auth(level: PUBLIC) { + person1: person(id: $id) { name } + person2: person(id: $id) @check(expr: "false", message: "c8azjdwz2x") { name } +} + +mutation createPersonWithPartialFailure($id: String!, $name: String!) @auth(level: PUBLIC) { + person1: person_insert(data: { id: $id, name: $name }) + query @redact { person(id: $id) { id @check(expr: "false", message: "ecxpjy4qfy") } } + person2: person_insert(data: { id_expr: "uuidV4()", name: $name }) +} + +mutation createPersonWithPartialFailureInTransaction($id: String!, $name: String!) @auth(level: PUBLIC) @transaction { + person1: person_insert(data: { id: $id, name: $name }) + person2: person_insert(data: { id_expr: "uuidV4()", name: $name }) @check(expr: "false", message: "te36b3zkvn") +} + diff --git a/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml b/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml index a17c5213bc0..e66af00a793 100644 --- a/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml +++ b/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml @@ -1,4 +1,4 @@ -specVersion: "v1beta" +specVersion: "v1" serviceId: "sid2ehn9ct8te" location: "us-central1" schema: diff --git a/firebase-dataconnect/emulator/emulator.sh b/firebase-dataconnect/emulator/emulator.sh index 68ccf3331a7..c534aaacd12 100755 --- a/firebase-dataconnect/emulator/emulator.sh +++ b/firebase-dataconnect/emulator/emulator.sh @@ -16,9 +16,8 @@ set -euo pipefail -echo "[$0] PID=$$" - -readonly SELF_DIR="$(dirname "$0")" +export FIREBASE_DATACONNECT_POSTGRESQL_STRING='postgresql://postgres:postgres@localhost:5432?sslmode=disable' +echo "[$0] export FIREBASE_DATACONNECT_POSTGRESQL_STRING='$FIREBASE_DATACONNECT_POSTGRESQL_STRING'" readonly FIREBASE_ARGS=( firebase diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts index 87ffe572916..914985ac18d 100644 --- a/firebase-dataconnect/firebase-dataconnect.gradle.kts +++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts @@ -28,7 +28,6 @@ firebaseLibrary { libraryGroup = "dataconnect" testLab.enabled = false publishJavadoc = false - previewMode = "beta" releaseNotes { name.set("{{data_connect_short}}") versionName.set("data-connect") diff --git a/firebase-dataconnect/gradle.properties b/firebase-dataconnect/gradle.properties index 6cf883fd07e..0e34974b3c7 100644 --- a/firebase-dataconnect/gradle.properties +++ b/firebase-dataconnect/gradle.properties @@ -1,2 +1,2 @@ -version=16.0.0-beta05 -latestReleasedVersion=16.0.0-beta04 +version=16.0.3 +latestReleasedVersion=16.0.2 diff --git a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json index 1854796df5e..81b6c26a579 100644 --- a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json +++ b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json @@ -1,5 +1,5 @@ { - "defaultVersion": "1.7.7", + "defaultVersion": "2.6.2", "versions": [ { "version": "1.3.4", @@ -414,6 +414,366 @@ "os": "linux", "size": 25268376, "sha512DigestHex": "f55feb1ce670b4728bb30be138ab427545f77f63f9e11ee458096091c075699c647d5b768c642a1ef6b3569a2db87dbbed6f2fdaf64febd1154d1a730fda4a9c" + }, + { + "version": "1.8.0", + "os": "windows", + "size": 25903616, + "sha512DigestHex": "753a5e4be35c544317bcdbaaa860f079a9c9d8a24ca3db17fed601d30b64f083a9203fbb76718d23f3ad77f1556adfb5a4226751ec48c202bd227479c57d1ae9" + }, + { + "version": "1.8.0", + "os": "macos", + "size": 25469696, + "sha512DigestHex": "23c1e405b196799a7c84b9783ca110459bba3aa86405d2fc03d83f90530642d590b02cd06588a8428e0e7bb7d1c59e6d03113bbc5c41e12cff7a7c46674fc430" + }, + { + "version": "1.8.0", + "os": "linux", + "size": 25383064, + "sha512DigestHex": "9546bb62d54b67086847d3e129397f4cfceb5b715d64f0a1cc0a053b5dfe918e8372142b7e9bacd11dede77ddd17840058efb8ed6a7073e99fd5a684fdc57bea" + }, + { + "version": "1.8.1", + "os": "windows", + "size": 25904128, + "sha512DigestHex": "26dc987e38d5d07a910da647920cc2fe990f1da0db56206def71a9833f8eeb66272d8f32ba091b0d4d6e065a3d5cd950cd835a891895c6a55d735a6f240bf4b7" + }, + { + "version": "1.8.1", + "os": "macos", + "size": 25469696, + "sha512DigestHex": "d7bcb01912b1949a003fd0a7ebbc1bb42e79e97b7fd880ba9164b62e05d1ffb634662d97fd4664343e28780e69953aadecd5fb799a8f51229a4c0fbf552936ac" + }, + { + "version": "1.8.1", + "os": "linux", + "size": 25383064, + "sha512DigestHex": "2a28ba7947f84ede9062b5f5efa145b29862be0a8724ac6b6a4210f6823024d33363bd3379a6474965fbd60376baae9103ce7e4509db9d52c2b13886bca5df92" + }, + { + "version": "1.8.2", + "os": "windows", + "size": 25936384, + "sha512DigestHex": "f2aed75baaeed388d8fcd8a3d18e629f9ed012f60de0401bc365227094688f130ce7aa02db565002fe7b06a339b1cb133a7c87da365d480fb10cdb47d55c7dfa" + }, + { + "version": "1.8.2", + "os": "macos", + "size": 25506560, + "sha512DigestHex": "d4ac9e15f5a42fed28fd2f3cb2c80bc3f4def60f76517661323c502fa7a4b085bda3d26eb62cdcb630a13999e2fb0428ee45d335e20641229a9439cc60a9e798" + }, + { + "version": "1.8.2", + "os": "linux", + "size": 25415832, + "sha512DigestHex": "fec0fb97fb3ad30bdd9d0e3b65095e2dfdcfccd15e7c6ae9fe827ec1c3b5b9b592c80c59cadb3540e387d4adcf3560922094399c5ca3d162288a33403308104d" + }, + { + "version": "1.8.3", + "os": "windows", + "size": 25965568, + "sha512DigestHex": "9b6ded9ddac61d5f137ac65944409003906d621bb3a03ba6bf037b1aeddabf23f9410de6fbc05b8ea0c9afa2a8328bb02a57ed225f8ebaa3c8d6921755ad715c" + }, + { + "version": "1.8.3", + "os": "macos", + "size": 25535232, + "sha512DigestHex": "0c88a14ae64308e68957f5e79f9e20b4b946977187132dcc24193370c81b9117487fb0ee1c5be4e8f2368945add7ed37d6d97b015c3ea8232e09664458c8e802" + }, + { + "version": "1.8.3", + "os": "linux", + "size": 25448600, + "sha512DigestHex": "6734188ed2dc41fdf9922e152848d46a4bd6a30083c918ac0de5197e1f998f8dc2b4e190c47b02c176f68b93591132c29be142b4b61a36c81aec2358a81864c6" + }, + { + "version": "1.8.4", + "os": "windows", + "size": 26020352, + "sha512DigestHex": "a93277e32a3da54e9b6f9153fa056398567a659d0e5e23422c98bea4b480db6c8d49049135575031bad5e109fb06f82cb65d9131dc8b1ecf3a89039854aacc03" + }, + { + "version": "1.8.4", + "os": "macos", + "size": 25588480, + "sha512DigestHex": "7b8d4e605b6c31b0fa82dab74ac215cbe1745f84c83cb7fc71f7d8e0e697e449d50b91f2bc02a0e20eda870169a6f4ab0d65bfda088801f5245d853fc005e98e" + }, + { + "version": "1.8.4", + "os": "linux", + "size": 25501848, + "sha512DigestHex": "be03f18228074e584d8e4b758ad75d22f71b1f6222c4a3c858f89fd081a138dd27dc03bfb43bd85ac21fb0eba6aae464d429c5f3e166a7d86b9daa0a5f3e8644" + }, + { + "version": "1.8.5", + "os": "windows", + "size": 26031616, + "sha512DigestHex": "14a1f69ee9062bfd460573722f8315781ed12e16734f3d09d635881850e33b159930fee403d5d2bd5ec3644fef3d3d869fe003d3760a9b50223de7676a95502c" + }, + { + "version": "1.8.5", + "os": "macos", + "size": 25600768, + "sha512DigestHex": "ecf07c8ab3295e70c15128d5682269efcd89517dd9d068711a028097efd7bb7995611f7e4ee8312107d9b6e0a82b5565557bac658911f2f6ade8363356007183" + }, + { + "version": "1.8.5", + "os": "linux", + "size": 25514136, + "sha512DigestHex": "628fe32575e6caac56130ae8d286156a15b220d4365bef1c99e71b7cbcd7fa76022cca41e1670816723c9ae24c9db32ee41921075293b7a6500ea58c87e08a60" + }, + { + "version": "1.9.0", + "os": "windows", + "size": 26838016, + "sha512DigestHex": "2680b28d4aec2c401974f0f8cf4110b36974acb52fd7afd8bb23d9d9b619308f66352ed4c1e7b3fc492af29ab1e490b7cee879e9f22eaf7dbf96b4d8fa978b55" + }, + { + "version": "1.9.0", + "os": "macos", + "size": 26395392, + "sha512DigestHex": "39e214d639a747f7af7cdad4aff0ed2af0bb6c3544b3c2daf95f43706039e47a3f0492e8a1c13c352e873f3f9c747af1cb4fb80470007c817bd11431905428e7" + }, + { + "version": "1.9.0", + "os": "linux", + "size": 26308760, + "sha512DigestHex": "02f3fa7c1b98876073c909b259c199ed60c6e1d89cf832d41585cc04a7314a1692cb66709c37fde45be680da6ddc14cd11619d9f80350f0eb180a0de9b8ef2da" + }, + { + "version": "1.9.1", + "os": "windows", + "size": 26846720, + "sha512DigestHex": "ef4014f58df5a9ab6e4c5d1a33a384d93affc7b9bb971a99a2672c05147d0cb64005ecda241a96a37984a9b6657ab900c3b26f2c7a5cfb32a24a2591afc9b94d" + }, + { + "version": "1.9.1", + "os": "macos", + "size": 26403584, + "sha512DigestHex": "ec90bd0c21feb5310f528e80b6415ad028a4f09a2ea99e2a1eca135d27a533ceda8f778c007f06e5ecbc5be0e32a2b13b4d8460ac5ad073e4216e7eb643f0b5d" + }, + { + "version": "1.9.1", + "os": "linux", + "size": 26316952, + "sha512DigestHex": "631cb41c1bf8ab18563180112e9f114d96a525884cf96914b69fdcfd861d32aa852b06b1c675f911148d238a4cd4c3d574ef8f73e66444bf5b8f1199da059e13" + }, + { + "version": "1.9.2", + "os": "windows", + "size": 26846208, + "sha512DigestHex": "1faedad0979fe1228b51f8a3b23f97468e2718cee08dfb65ec6b21e2a3cec99eee060cf9ee6560e80c7a5059437378a7e02f1afa77a8f4e931ef1a3294951fd2" + }, + { + "version": "1.9.2", + "os": "macos", + "size": 26403584, + "sha512DigestHex": "4be6adab666688334879a72519337b851b494d63b7059e71f4c23b443d31f442c50bd69837af3897a9a38ac7aae1aebb8d97d33111e520110dac24c2a7f29a1d" + }, + { + "version": "1.9.2", + "os": "linux", + "size": 26316952, + "sha512DigestHex": "0bd4fcb4bdb66aab502000c19df824fc8df192906e712edd0000192beeb0ba3d29f2a627fc3097735dbab2d9bcbc3715e12fb8afb26e17c2e3e40103357b49ae" + }, + { + "version": "2.0.0", + "os": "windows", + "size": 26884096, + "sha512DigestHex": "720fc3b4be8da10e684eae252f962eed8d30f370068414642ab8e7974f9a370bc1b5038f47d2acc77f266ca224c0c22afc1177ae4510dca7d249ba423844abae" + }, + { + "version": "2.0.0", + "os": "macos", + "size": 26440448, + "sha512DigestHex": "bf812823aedd709c88e7c757412fe7dedfe1e05f8526863d714637f5adcf3e894c9780b759b919a6f96ab5a5ef579d60e57969cbc3b4f0ff7bc91697311c85fd" + }, + { + "version": "2.0.0", + "os": "linux", + "size": 26353816, + "sha512DigestHex": "37a3b7f4fca4a71c5c336fd7edebd4472bb51b31ff4abac80b31f8ef55831726b439f9f7ef1152cee6006019cd22cef51572874e5e6680962826fbc4c6166530" + }, + { + "version": "2.1.0", + "os": "windows", + "size": 26884096, + "sha512DigestHex": "299525effb3d645868aadd82cbeac28a528d0ecbbbf78d0e830478d03618de9e7356fdfd5599d9cd29fa86250c3543d656f9fe10e2d35c771cb42b31e904e534" + }, + { + "version": "2.1.0", + "os": "macos", + "size": 26440448, + "sha512DigestHex": "fac00b743d08eb9f0fbb4adceac63633a5364152551916ad2c787f4e5a3f8f51a5cd50350374279e04776cbf9e8d9b89d289c6f9e06f153820f9345b0a32733b" + }, + { + "version": "2.1.0", + "os": "linux", + "size": 26357912, + "sha512DigestHex": "75661fd65f8fcb78b8ac3585816c18e821fe0b993a94c5fa7f8f928f5827da3112ecee0de246c07bffcead4291172beeccf27708f1fcf19720bae9352885768b" + }, + { + "version": "2.2.0", + "os": "windows", + "size": 26982912, + "sha512DigestHex": "6e861a3603300474536c314318826d6609aeda04476996f1695ebe0dd4b508653dabee91935fcc2054d8c40965a630618241c7fbc55f490a3c109f240e69684d" + }, + { + "version": "2.2.0", + "os": "macos", + "size": 26538752, + "sha512DigestHex": "018d73ae8c4bdc9032125bd01715a181253e411e6b0b507324a897b549efab25d9aaea11d3fe7b9e9dab38592b3967cd46df3650a5a76fb7ea80e9bea3225812" + }, + { + "version": "2.2.0", + "os": "linux", + "size": 26452120, + "sha512DigestHex": "aab441e47115489b968f90d588b08f7a5848cef79849653bde84ffea5612404ec142c3bc87c6a466036a7e3e4228eff667a56ba633d3af93be0082ef4819c25f" + }, + { + "version": "2.3.0", + "os": "windows", + "size": 27728384, + "sha512DigestHex": "c822af6c3096923c8448619e1196effcd41f3c4ef1c073743529cc661ca7360aab7e4f09802178d704464dd46d84a66e8a1302d2afe33cc593eebede5d4d08da" + }, + { + "version": "2.3.0", + "os": "macos", + "size": 27271936, + "sha512DigestHex": "0e38ba4ec5ac2ad7e4f35f0898006ac465b727ec0342353eb93272422ffa947d63cf41fe0734b427cc6d3101c52aefa1ed4e6309c0dbfb7c4df67c46dfab9bcd" + }, + { + "version": "2.3.0", + "os": "linux", + "size": 27185304, + "sha512DigestHex": "206058ffc632139a725ccbb4673edc309f18cef9ffb1c207fa4d38b8f8ada333b710e5360e931022d98a101b06ab9b0e03c8212f7f50c27687e105c97ea5401a" + }, + { + "version": "2.3.1", + "os": "windows", + "size": 27729408, + "sha512DigestHex": "86b7f8ebfe786827937bd57d51d1187deae3d910e3146e5fb0e8504cc651a1b984ed529efb6d3741e07e92d43d9605968c8b503c912d22d28d765d9ffddb78c2" + }, + { + "version": "2.3.1", + "os": "macos", + "size": 27271936, + "sha512DigestHex": "42a047df82f0cbd89ff266abd460ed2899d92e77f7e5a9a8c4746d478cb1358c7ee91d2dff60eb1873fd59c3647c8f6808ac410a6c952b654f45df50b74ba6e7" + }, + { + "version": "2.3.1", + "os": "linux", + "size": 27185304, + "sha512DigestHex": "1f7be86d576bbd7e562c612d36ad49b896618003bc94604e4809a07773a1bcde9b7383ed0dc39e6c2f540f0f49b107867c72cd1333fecc339a82e72bb821af8a" + }, + { + "version": "2.4.0", + "os": "windows", + "size": 27774464, + "sha512DigestHex": "0d3b7570daea7b4f5879ed6d8ca0e1e8e4c55b5e617dbf571aa2b479fad3a1ba207d40bdd4ceba5ab557fdb101b9b7149bdeac2c4cb0d9d7885b4afc22df16a2" + }, + { + "version": "2.4.0", + "os": "macos", + "size": 27316992, + "sha512DigestHex": "c0fd2654d874528af6dc5296d191bf6d7edca56695b2604e4fe6bff553f81a2871e348686914af22378e3fbfe10bf9c66a41d031521a617fe6c0f6728bf0ba69" + }, + { + "version": "2.4.0", + "os": "linux", + "size": 27230360, + "sha512DigestHex": "1edce919f3c2496a1c837b2b84360cca12dfc3b725bdc694a503bea17faacf2f8e32a955eab540d479180f4aab1c46ee82b7c0e752595ed1aeeabc9c01dd5c67" + }, + { + "version": "2.4.1", + "os": "windows", + "size": 27808768, + "sha512DigestHex": "bbb903abcb1648a2cbab252389a488d370cd2604222176e0fa1465117fb0b84a9f96638608c465f3fe04a10c3cd90446c5f10ddd596c22ec7157642bf2cbab3a" + }, + { + "version": "2.4.1", + "os": "macos", + "size": 27349760, + "sha512DigestHex": "0be315b5c4301386eb5fa4b227cc8fb11c5987c7408f9296895d2fdb850b56cf0e28f42af1266c627f6548eb8e8c90374bfb725bc8becd57d379161e84e059cd" + }, + { + "version": "2.4.1", + "os": "linux", + "size": 27267224, + "sha512DigestHex": "a5385cdfc5f5463208660be674ffe02f69c9ae65d1d91c060b2a9f81b3a9313978f2616fe34371fd02cc9e4cb3449eb28b32d359c9dd121c5a3ea0a1e89b746a" + }, + { + "version": "2.5.0", + "os": "windows", + "size": 27836416, + "sha512DigestHex": "e0cc3f99361cc7d561742975642fd8b071ceca7c47a6951181d76c6ec087348dbd95bcac6fbacf255d16396bc739f47ec914e18297b3f66d66233f812c7692bc" + }, + { + "version": "2.5.0", + "os": "macos", + "size": 27378432, + "sha512DigestHex": "1b1f66a22147b71f1d3ca29f895ddd4763555279d6dffb720044697cfa9e33c202471dd9d0db14e7a95f89f6084d4a35e30d7be53c3cdd9cb1b56dce763e7425" + }, + { + "version": "2.5.0", + "os": "linux", + "size": 27295896, + "sha512DigestHex": "b1f25d94af56e1e774df336cddf3bcb54dc855935d131bf3fb8dc69aa9ab814f6e6dfefb684a5637c0b6f84f57586162b32b7402d87522c19fb66e717905825a" + }, + { + "version": "2.6.0", + "os": "windows", + "size": 27875328, + "sha512DigestHex": "8f7079028b3c86ad9b2ef33b6840f7ab4c900d1468e32d6e26109f49838703b41ede4726141479dc053ec81fd82f8fbe33f94d4b865c5a5a798f36b211b0bf21" + }, + { + "version": "2.6.0", + "os": "macos", + "size": 27415296, + "sha512DigestHex": "80b5d3199f78034b65bc9ab05977d91999dccb29528c3e9658a7e36b1a22bf5ed05b5d90d08ad1793baa74994ec8685386a22f9e407b3c0a4e06c5968738663c" + }, + { + "version": "2.6.0", + "os": "linux", + "size": 27332760, + "sha512DigestHex": "c90d691c798466b3f820cc0738b96e030ab2683fe9ad1019692aec58f282e1cd6835c645d229f1a32d899713c97e2451d5e3b29e9468060711f3d08e2312afec" + }, + { + "version": "2.6.1", + "os": "windows", + "size": 27875328, + "sha512DigestHex": "06f07a5d8cfdd942393959c21e1511a3d0de6f26d67be78b4dbfdacf021938db089ad4d4945cdeb130e6af4d4a87a93a579954d36a64dac7b58f6ade99c79de3" + }, + { + "version": "2.6.1", + "os": "macos", + "size": 27415296, + "sha512DigestHex": "63461d8ef2c41a85b9ebb52fc566e44db8209d2ae750b7137d50529ff1c3861889e2a75ec380e5eaf5e4bf8094594b881d66fe572885bec2456bb33190fe653b" + }, + { + "version": "2.6.1", + "os": "linux", + "size": 27332760, + "sha512DigestHex": "668cb4261010ac6bad7136c6a92fdd19c7064a0baf7adf1d11221240a8f3f3c595e74690f048d35ac7f0b5fb9d2753f11dd12a8821dbb39a93763f2332ec44c6" + }, + { + "version": "2.6.2", + "os": "windows", + "size": 27961856, + "sha512DigestHex": "e087e9c3adb0be169d0b7bc352132075326e6af2bbea501c0c152c54f07b4dddd9813590a8d7c8b1d4eb058241127ce995f7dd0441095398abbd0c366a345e28" + }, + { + "version": "2.6.2", + "os": "macos", + "size": 27501312, + "sha512DigestHex": "02dcd439ba1b6cf1562487478a1de68bafee6e7ff3a21248f20ad336cbe6e06b6e73e250d87a76c1384d003a5f50f85b350d9e91688b5810e65d1b45d56807ea" + }, + { + "version": "2.6.2", + "os": "linux", + "size": 27414680, + "sha512DigestHex": "b57c031bda4a6de5d16f6262c9016ee1117ce84e30b439f68325fcdef58a5ee85e1f60b8d3172e498ea52cf3c0dba4d4ff5ff1bf92e8e99e331a6602d5f62e35" } ] } \ No newline at end of file diff --git a/firebase-dataconnect/scripts/generateApiTxtFile.sh b/firebase-dataconnect/scripts/generateApiTxtFile.sh new file mode 100755 index 00000000000..cb572c07140 --- /dev/null +++ b/firebase-dataconnect/scripts/generateApiTxtFile.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Copyright 2024 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. + +set -euo pipefail + +readonly PROJECT_ROOT_DIR="$(dirname "$0")/../.." + +readonly args=( + "${PROJECT_ROOT_DIR}/gradlew" + "-p" + "${PROJECT_ROOT_DIR}" + "--configure-on-demand" + "$@" + ":firebase-dataconnect:generateApiTxtFile" +) + +echo "${args[*]}" +exec "${args[@]}" diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt index ba314d0f9db..280d5209065 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt @@ -21,6 +21,7 @@ import com.google.firebase.dataconnect.core.FirebaseDataConnectInternal import com.google.firebase.dataconnect.testutil.DataConnectBackend import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase import com.google.firebase.dataconnect.testutil.InProcessDataConnectGrpcServer +import com.google.firebase.dataconnect.testutil.awaitAuthReady import com.google.firebase.dataconnect.testutil.newInstance import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect import com.google.firebase.dataconnect.testutil.schemas.PersonSchema @@ -127,6 +128,7 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { grpcServer.metadatas.map { it.get(firebaseAuthTokenHeader) }.toCollection(authTokens) } val dataConnect = dataConnectFactory.newInstance(auth.app, grpcServer) + (dataConnect as FirebaseDataConnectInternal).awaitAuthReady() val operationName = Arb.dataConnect.operationName().next(rs) val queryRef = dataConnect.query(operationName, Unit, serializer(), serializer()) @@ -155,6 +157,7 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { grpcServer.metadatas.map { it.get(firebaseAuthTokenHeader) }.toCollection(authTokens) } val dataConnect = dataConnectFactory.newInstance(auth.app, grpcServer) + (dataConnect as FirebaseDataConnectInternal).awaitAuthReady() val operationName = Arb.dataConnect.operationName().next(rs) val mutationRef = dataConnect.mutation(operationName, Unit, serializer(), serializer()) @@ -202,7 +205,7 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { } private suspend fun signIn() { - (personSchema.dataConnect as FirebaseDataConnectInternal).awaitAuthReady() + personSchema.dataConnect.awaitAuthReady() val authResult = auth.run { signInAnonymously().await() } withClue("authResult.user returned from signInAnonymously()") { authResult.user.shouldNotBeNull() diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/GrpcMetadataIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/GrpcMetadataIntegrationTest.kt index 8ff7715ddb9..394c2195332 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/GrpcMetadataIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/GrpcMetadataIntegrationTest.kt @@ -23,7 +23,6 @@ import com.google.android.gms.tasks.Tasks import com.google.firebase.appcheck.AppCheckProvider import com.google.firebase.appcheck.AppCheckProviderFactory import com.google.firebase.appcheck.FirebaseAppCheck -import com.google.firebase.dataconnect.core.FirebaseDataConnectInternal import com.google.firebase.dataconnect.generated.GeneratedConnector import com.google.firebase.dataconnect.generated.GeneratedMutation import com.google.firebase.dataconnect.generated.GeneratedQuery @@ -32,6 +31,8 @@ import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase import com.google.firebase.dataconnect.testutil.DataConnectTestAppCheckToken import com.google.firebase.dataconnect.testutil.FirebaseAuthBackend import com.google.firebase.dataconnect.testutil.InProcessDataConnectGrpcServer +import com.google.firebase.dataconnect.testutil.awaitAppCheckReady +import com.google.firebase.dataconnect.testutil.awaitAuthReady import com.google.firebase.dataconnect.testutil.getFirebaseAppIdFromStrings import com.google.firebase.dataconnect.testutil.newInstance import com.google.firebase.dataconnect.util.SuspendingLazy @@ -138,7 +139,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { fun executeQueryShouldNotSendAuthMetadataWhenNotLoggedIn() = runTest { val grpcServer = inProcessDataConnectGrpcServer.newInstance() val dataConnect = dataConnectFactory.newInstance(grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAuthReady() + dataConnect.awaitAuthReady() val queryRef = dataConnect.query("qryfyk7yfppfe", Unit, serializer(), serializer()) val metadatasJob = async { grpcServer.metadatas.first() } @@ -151,7 +152,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { fun executeMutationShouldNotSendAuthMetadataWhenNotLoggedIn() = runTest { val grpcServer = inProcessDataConnectGrpcServer.newInstance() val dataConnect = dataConnectFactory.newInstance(grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAuthReady() + dataConnect.awaitAuthReady() val mutationRef = dataConnect.mutation("mutckjpte9v9j", Unit, serializer(), serializer()) val metadatasJob = async { grpcServer.metadatas.first() } @@ -165,7 +166,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { fun executeQueryShouldSendAuthMetadataWhenLoggedIn() = runTest { val grpcServer = inProcessDataConnectGrpcServer.newInstance() val dataConnect = dataConnectFactory.newInstance(grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAuthReady() + dataConnect.awaitAuthReady() val queryRef = dataConnect.query("qryyarwrxe2fv", Unit, serializer(), serializer()) val metadatasJob = async { grpcServer.metadatas.first() } firebaseAuthSignIn(dataConnect) @@ -179,7 +180,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { fun executeMutationShouldSendAuthMetadataWhenLoggedIn() = runTest { val grpcServer = inProcessDataConnectGrpcServer.newInstance() val dataConnect = dataConnectFactory.newInstance(grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAuthReady() + dataConnect.awaitAuthReady() val mutationRef = dataConnect.mutation("mutayn7as5k7d", Unit, serializer(), serializer()) val metadatasJob = async { grpcServer.metadatas.first() } @@ -194,7 +195,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { fun executeQueryShouldNotSendAuthMetadataAfterLogout() = runTest { val grpcServer = inProcessDataConnectGrpcServer.newInstance() val dataConnect = dataConnectFactory.newInstance(grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAuthReady() + dataConnect.awaitAuthReady() val queryRef = dataConnect.query("qryyarwrxe2fv", Unit, serializer(), serializer()) val metadatasJob1 = async { grpcServer.metadatas.first() } val metadatasJob2 = async { grpcServer.metadatas.take(2).last() } @@ -212,7 +213,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { fun executeMutationShouldNotSendAuthMetadataAfterLogout() = runTest { val grpcServer = inProcessDataConnectGrpcServer.newInstance() val dataConnect = dataConnectFactory.newInstance(grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAuthReady() + dataConnect.awaitAuthReady() val mutationRef = dataConnect.mutation("mutvw945ag3vv", Unit, serializer(), serializer()) val metadatasJob1 = async { grpcServer.metadatas.first() } @@ -233,7 +234,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { // appcheck token is sent at all. val grpcServer = inProcessDataConnectGrpcServer.newInstance() val dataConnect = dataConnectFactory.newInstance(grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAppCheckReady() + dataConnect.awaitAppCheckReady() val queryRef = dataConnect.query("qrybbeekpkkck", Unit, serializer(), serializer()) val metadatasJob = async { grpcServer.metadatas.first() } @@ -248,7 +249,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { // appcheck token is sent at all. val grpcServer = inProcessDataConnectGrpcServer.newInstance() val dataConnect = dataConnectFactory.newInstance(grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAppCheckReady() + dataConnect.awaitAppCheckReady() val mutationRef = dataConnect.mutation("mutbs7hhxk39c", Unit, serializer(), serializer()) val metadatasJob = async { grpcServer.metadatas.first() } @@ -262,7 +263,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { fun executeQueryShouldSendAppCheckMetadataWhenAppCheckIsEnabled() = runTest { val grpcServer = inProcessDataConnectGrpcServer.newInstance() val dataConnect = dataConnectFactory.newInstance(grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAppCheckReady() + dataConnect.awaitAppCheckReady() val queryRef = dataConnect.query("qryyarwrxe2fv", Unit, serializer(), serializer()) val metadatasJob = async { grpcServer.metadatas.first() } val appCheck = FirebaseAppCheck.getInstance(dataConnect.app) @@ -277,7 +278,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { fun executeMutationShouldSendAppCheckMetadataWhenAppCheckIsEnabled() = runTest { val grpcServer = inProcessDataConnectGrpcServer.newInstance() val dataConnect = dataConnectFactory.newInstance(grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAppCheckReady() + dataConnect.awaitAppCheckReady() val mutationRef = dataConnect.mutation("mutz4hzqzpgb4", Unit, serializer(), serializer()) val metadatasJob = async { grpcServer.metadatas.first() } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/OperationExecutionErrorsIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/OperationExecutionErrorsIntegrationTest.kt new file mode 100644 index 00000000000..f592f58800a --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/OperationExecutionErrorsIntegrationTest.kt @@ -0,0 +1,287 @@ +/* + * Copyright 2025 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.firebase.dataconnect + +import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery +import com.google.firebase.dataconnect.testutil.shouldSatisfy +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveAtLeastSize +import io.kotest.property.Arb +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.next +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.serializer +import org.junit.Test + +class OperationExecutionErrorsIntegrationTest : DataConnectIntegrationTestBase() { + + private val personSchema: PersonSchema by lazy { PersonSchema(dataConnectFactory) } + private val dataConnect: FirebaseDataConnect by lazy { personSchema.dataConnect } + + @Test + fun executeQueryFailsWithNullDataNonEmptyErrors() = runTest { + val queryRef = + dataConnect.query( + operationName = GetPersonQuery.operationName, + variables = Arb.incompatibleVariables().next(rs), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { queryRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = "jwdbzka4k5", + expectedCause = null, + expectedRawData = null, + expectedData = null, + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Test + fun executeMutationFailsWithNullDataNonEmptyErrors() = runTest { + val mutationRef = + dataConnect.mutation( + operationName = CreatePersonMutation.operationName, + variables = Arb.incompatibleVariables().next(rs), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { mutationRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedCause = null, + expectedRawData = null, + expectedData = null, + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Test + fun executeQueryFailsWithNonNullDataEmptyErrorsButDecodingResponseDataFails() = runTest { + val id = Arb.alphanumericString().next() + val queryRef = + dataConnect.query( + operationName = GetPersonQuery.operationName, + variables = GetPersonQuery.Variables(id), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { queryRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "decoding data from the server's response failed", + expectedCause = SerializationException::class, + expectedRawData = mapOf("person" to null), + expectedData = null, + expectedErrors = emptyList(), + ) + } + + @Test + fun executeMutationFailsWithNonNullDataEmptyErrorsButDecodingResponseDataFails() = runTest { + val id = Arb.alphanumericString().next() + val name = Arb.alphanumericString().next() + val mutationRef = + dataConnect.mutation( + operationName = CreatePersonMutation.operationName, + variables = CreatePersonMutation.Variables(id, name), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { mutationRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "decoding data from the server's response failed", + expectedCause = SerializationException::class, + expectedRawData = mapOf("person_insert" to mapOf("id" to id)), + expectedData = null, + expectedErrors = emptyList(), + ) + } + + @Test + fun executeQueryFailsWithNonNullDataNonEmptyErrorsDecodingSucceeds() = runTest { + val id = Arb.alphanumericString().next() + val name = Arb.alphanumericString().next() + personSchema.createPerson(CreatePersonMutation.Variables(id, name)).execute() + val queryRef = + dataConnect.query( + operationName = "getPersonWithPartialFailure", + variables = GetPersonWithPartialFailureVariables(id), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { queryRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = "c8azjdwz2x", + expectedCause = null, + expectedRawData = mapOf("person1" to mapOf("name" to name), "person2" to null), + expectedData = GetPersonWithPartialFailureData(name), + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Test + fun executeMutationFailsWithNonNullDataNonEmptyErrorsDecodingSucceeds() = runTest { + val id = Arb.alphanumericString().next() + val name = Arb.alphanumericString().next() + val mutationRef = + dataConnect.mutation( + operationName = "createPersonWithPartialFailure", + variables = CreatePersonWithPartialFailureVariables(id = id, name = name), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { mutationRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = "ecxpjy4qfy", + expectedCause = null, + expectedRawData = mapOf("person1" to mapOf("id" to id), "person2" to null), + expectedData = CreatePersonWithPartialFailureData(id), + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Test + fun executeQueryFailsWithNonNullDataNonEmptyErrorsDecodingFails() = runTest { + val id = Arb.alphanumericString().next() + val name = Arb.alphanumericString().next() + personSchema.createPerson(CreatePersonMutation.Variables(id, name)).execute() + val queryRef = + dataConnect.query( + operationName = "getPersonWithPartialFailure", + variables = GetPersonWithPartialFailureVariables(id), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { queryRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = "c8azjdwz2x", + expectedCause = null, + expectedRawData = mapOf("person1" to mapOf("name" to name), "person2" to null), + expectedData = null, + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Test + fun executeMutationFailsWithNonNullDataNonEmptyErrorsDecodingFails() = runTest { + val id = Arb.alphanumericString().next() + val name = Arb.alphanumericString().next() + val mutationRef = + dataConnect.mutation( + operationName = "createPersonWithPartialFailure", + variables = CreatePersonWithPartialFailureVariables(id = id, name = name), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { mutationRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = "ecxpjy4qfy", + expectedCause = null, + expectedRawData = mapOf("person1" to mapOf("id" to id), "person2" to null), + expectedData = null, + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Test + fun executeMutationFailsWithNonNullDataNonEmptyErrorsDecodingFailsInTransaction() = runTest { + val id = Arb.alphanumericString().next() + val name = Arb.alphanumericString().next() + val mutationRef = + dataConnect.mutation( + operationName = "createPersonWithPartialFailureInTransaction", + variables = CreatePersonWithPartialFailureVariables(id = id, name = name), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { mutationRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = "te36b3zkvn", + expectedCause = null, + expectedRawData = mapOf("person1" to null, "person2" to null), + expectedData = null, + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Serializable private data class IncompatibleVariables(val jwdbzka4k5: String) + + @Serializable private data class IncompatibleData(val btzjhbfz7h: String) + + private fun Arb.Companion.incompatibleVariables(string: Arb = Arb.alphanumericString()) = + string.map { IncompatibleVariables(it) } + + @Serializable private data class GetPersonWithPartialFailureVariables(val id: String) + + @Serializable + private data class GetPersonWithPartialFailureData(val person1: Person, val person2: Nothing?) { + constructor(person1Name: String) : this(Person(person1Name), null) + + @Serializable private data class Person(val name: String) + } + + @Serializable + private data class CreatePersonWithPartialFailureVariables(val id: String, val name: String) + + @Serializable + private data class CreatePersonWithPartialFailureData( + val person1: Person, + val person2: Nothing? + ) { + constructor(person1Id: String) : this(Person(person1Id), null) + + @Serializable private data class Person(val id: String) + } +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt index 6b0f0c4ff47..ad0a0e73e96 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt @@ -483,9 +483,7 @@ class QuerySubscriptionIntegrationTest : DataConnectIntegrationTestBase() { val noName1Query = schema.getPerson(personId).withDataDeserializer(serializer()) - backgroundScope.launch { noName1Query.subscribe().flow.collect() } - - noName1Query.execute() + keepCacheAlive(noName1Query) schema.updatePerson(id = personId, name = "Name1").execute() @@ -571,7 +569,7 @@ class QuerySubscriptionIntegrationTest : DataConnectIntegrationTestBase() { */ private suspend fun TestScope.keepCacheAlive(query: QueryRef<*, *>) { val cachePrimed = SuspendingFlag() - backgroundScope.launch { query.subscribe().flow.onEach { cachePrimed.set() }.collect() } + backgroundScope.launch { query.subscribe().flow.collect { cachePrimed.set() } } cachePrimed.await() } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/FirebaseDataConnectInternalExts.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/FirebaseDataConnectInternalExts.kt new file mode 100644 index 00000000000..f73f1d8b50d --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/FirebaseDataConnectInternalExts.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 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.firebase.dataconnect.testutil + +import com.google.firebase.dataconnect.FirebaseDataConnect +import com.google.firebase.dataconnect.core.FirebaseDataConnectInternal + +suspend fun FirebaseDataConnect.awaitAuthReady() = + (this as FirebaseDataConnectInternal).awaitAuthReady() + +suspend fun FirebaseDataConnect.awaitAppCheckReady() = + (this as FirebaseDataConnectInternal).awaitAppCheckReady() diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index 21d9dc9d4dc..a6ed2cfd47a 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -54,6 +54,8 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { ) object CreatePersonMutation { + const val operationName = "createPerson" + @Serializable data class Data(val person_insert: PersonKey) { @Serializable data class PersonKey(val id: String) @@ -63,7 +65,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { fun createPerson(variables: CreatePersonMutation.Variables) = dataConnect.mutation( - operationName = "createPerson", + operationName = CreatePersonMutation.operationName, variables = variables, dataDeserializer = serializer(), variablesSerializer = serializer(), @@ -141,6 +143,8 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { fun deletePerson(id: String) = deletePerson(DeletePersonMutation.Variables(id = id)) object GetPersonQuery { + const val operationName = "getPerson" + @Serializable data class Data(val person: Person?) { @Serializable data class Person(val name: String, val age: Int? = null) @@ -151,7 +155,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { fun getPerson(variables: GetPersonQuery.Variables) = dataConnect.query( - operationName = "getPerson", + operationName = GetPersonQuery.operationName, variables = variables, dataDeserializer = serializer(), variablesSerializer = serializer(), diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectError.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectError.kt deleted file mode 100644 index 07e87c212a8..00000000000 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectError.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2024 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.firebase.dataconnect - -import java.util.Objects - -// See https://spec.graphql.org/draft/#sec-Errors -internal class DataConnectError( - val message: String, - val path: List, - val locations: List, -) { - - override fun hashCode(): Int = Objects.hash(message, path, locations) - - override fun equals(other: Any?): Boolean = - (other is DataConnectError) && - other.message == message && - other.path == path && - other.locations == locations - - override fun toString(): String = - StringBuilder() - .also { sb -> - path.forEachIndexed { segmentIndex, segment -> - when (segment) { - is PathSegment.Field -> { - if (segmentIndex != 0) { - sb.append('.') - } - sb.append(segment.field) - } - is PathSegment.ListIndex -> { - sb.append('[') - sb.append(segment.index) - sb.append(']') - } - } - } - - if (locations.isNotEmpty()) { - if (sb.isNotEmpty()) { - sb.append(' ') - } - sb.append("at ") - sb.append(locations.joinToString(", ")) - } - - if (path.isNotEmpty() || locations.isNotEmpty()) { - sb.append(": ") - } - - sb.append(message) - } - .toString() - - sealed interface PathSegment { - @JvmInline - value class Field(val field: String) : PathSegment { - override fun toString(): String = field - } - - @JvmInline - value class ListIndex(val index: Int) : PathSegment { - override fun toString(): String = index.toString() - } - } - - class SourceLocation(val line: Int, val column: Int) { - override fun hashCode(): Int = Objects.hash(line, column) - override fun equals(other: Any?): Boolean = - other is SourceLocation && other.line == line && other.column == column - override fun toString(): String = "$line:$column" - } -} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationException.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationException.kt new file mode 100644 index 00000000000..cb1ef1b8526 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationException.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 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.firebase.dataconnect + +/** + * The exception thrown when an error occurs in the execution of a Firebase Data Connect operation + * (that is, a query or mutation). This exception means that a response was, indeed, received from + * the backend but either the response included one or more errors or the client could not + * successfully process the result (for example, decoding the response data failed). + */ +public open class DataConnectOperationException( + message: String, + cause: Throwable? = null, + public val response: DataConnectOperationFailureResponse<*>, +) : DataConnectException(message, cause) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationFailureResponse.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationFailureResponse.kt new file mode 100644 index 00000000000..386fb8bce9d --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationFailureResponse.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2025 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.firebase.dataconnect + +// Googlers see go/dataconnect:sdk:partial-errors for design details. + +/** The data and errors provided by the backend in the response message. */ +public interface DataConnectOperationFailureResponse { + + /** + * The raw, un-decoded data provided by the backend in the response message. Will be `null` if, + * and only if, the backend explicitly sent null for the data or if the data was not present in + * the response. + * + * Otherwise, the values in the map will be one of the following: + * * `null` + * * [String] + * * [Boolean] + * * [Double] + * * [List] containing any of the types in this list of types + * * [Map] with [String] keys and values of of the types in this list of types + * + * Consider using [toJson] to get a higher-level object. + */ + public val rawData: Map? + + /** + * The list of errors provided by the backend in the response message; may be empty. + * + * See https://spec.graphql.org/draft/#sec-Errors for details. + */ + public val errors: List + + /** + * The successfully-decoded [rawData], if any. + * + * Will be `null` if [rawData] is `null`, or if decoding the [rawData] failed. + */ + public val data: Data? + + /** + * Returns a string representation of this object, useful for debugging. + * + * The string representation is _not_ guaranteed to be stable and may change without notice at any + * time. Therefore, the only recommended usage of the returned string is debugging and/or logging. + * Namely, parsing the returned string or storing the returned string in non-volatile storage + * should generally be avoided in order to be robust in case that the string representation + * changes. + * + * @return a string representation of this object, which includes the class name and the values of + * all public properties. + */ + override fun toString(): String + + /** + * Information about the error, as provided in the response payload from the backend. + * + * See https://spec.graphql.org/draft/#sec-Errors for details. + */ + public interface ErrorInfo { + /** The error's message. */ + public val message: String + + /** The path of the field in the response data to which this error relates. */ + public val path: List + + /** + * Compares this object with another object for equality. + * + * @param other The object to compare to this for equality. + * @return true if, and only if, the other object is an instance of the same implementation of + * [ErrorInfo] whose public properties compare equal using the `==` operator to the + * corresponding properties of this object. + */ + override fun equals(other: Any?): Boolean + + /** + * Calculates and returns the hash code for this object. + * + * The hash code is _not_ guaranteed to be stable across application restarts. + * + * @return the hash code for this object, that incorporates the values of this object's public + * properties. + */ + override fun hashCode(): Int + + /** + * Returns a string representation of this object, useful for debugging. + * + * The string representation is _not_ guaranteed to be stable and may change without notice at + * any time. Therefore, the only recommended usage of the returned string is debugging and/or + * logging. Namely, parsing the returned string or storing the returned string in non-volatile + * storage should generally be avoided in order to be robust in case that the string + * representation changes. + * + * @return a string representation of this object, suitable for logging the error indicated by + * this object; it will include the path formatted into a human-readable string (if the path is + * not empty), and the message. + */ + override fun toString(): String + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectPathSegment.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectPathSegment.kt new file mode 100644 index 00000000000..3bae99ef78f --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectPathSegment.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2025 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.firebase.dataconnect + +/** The "segment" of a path to a field in the response data. */ +public sealed interface DataConnectPathSegment { + + /** A named field in a path to a field in the response data. */ + @JvmInline + public value class Field(public val field: String) : DataConnectPathSegment { + + /** + * Returns a string representation of this object, useful for debugging. + * + * The string representation is _not_ guaranteed to be stable and may change without notice at + * any time. Therefore, the only recommended usage of the returned string is debugging and/or + * logging. Namely, parsing the returned string or storing the returned string in non-volatile + * storage should generally be avoided in order to be robust in case that the string + * representation changes. + * + * @return returns simply [field]. + */ + override fun toString(): String = field + } + + /** An index of a list in a path to a field in the response data. */ + @JvmInline + public value class ListIndex(public val index: Int) : DataConnectPathSegment { + /** + * Returns a string representation of this object, useful for debugging. + * + * The string representation is _not_ guaranteed to be stable and may change without notice at + * any time. Therefore, the only recommended usage of the returned string is debugging and/or + * logging. Namely, parsing the returned string or storing the returned string in non-volatile + * storage should generally be avoided in order to be robust in case that the string + * representation changes. + * + * @return returns simply the string representation of [index]. + */ + override fun toString(): String = index.toString() + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt index 638fffb913e..332cd5251e4 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt @@ -22,7 +22,7 @@ import kotlinx.serialization.encoding.Decoder internal class DataConnectUntypedData( val data: Map?, - val errors: List + val errors: List ) { override fun equals(other: Any?) = diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectCredentialsTokenManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectCredentialsTokenManager.kt index e6f4049c359..b67db33b36c 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectCredentialsTokenManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectCredentialsTokenManager.kt @@ -30,7 +30,6 @@ import com.google.firebase.inject.Provider import com.google.firebase.internal.api.FirebaseNoSignedInUserException import com.google.firebase.util.nextAlphanumericString import java.lang.ref.WeakReference -import java.util.concurrent.atomic.AtomicReference import kotlin.coroutines.coroutineContext import kotlin.random.Random import kotlinx.coroutines.CancellationException @@ -46,24 +45,22 @@ import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.yield /** Base class that shares logic for managing the Auth token and AppCheck token. */ internal sealed class DataConnectCredentialsTokenManager( private val deferredProvider: com.google.firebase.inject.Deferred, parentCoroutineScope: CoroutineScope, - blockingDispatcher: CoroutineDispatcher, + private val blockingDispatcher: CoroutineDispatcher, protected val logger: Logger, ) { val instanceId: String get() = logger.nameWithId - private val _providerAvailable = MutableStateFlow(false) - val providerAvailable: StateFlow = _providerAvailable.asStateFlow() - @Suppress("LeakingThis") private val weakThis = WeakReference(this) private val coroutineScope = @@ -78,58 +75,51 @@ internal sealed class DataConnectCredentialsTokenManager( } ) - init { - // Call `whenAvailable()` on a non-main thread because it accesses SharedPreferences, which - // performs disk i/o, violating the StrictMode policy android.os.strictmode.DiskReadViolation. - val coroutineName = CoroutineName("k6rwgqg9gh $instanceId whenAvailable") - coroutineScope.launch(coroutineName + blockingDispatcher) { - deferredProvider.whenAvailable(DeferredProviderHandlerImpl(weakThis)) - } - } - - private interface ProviderProvider { - val provider: T? - } - private sealed interface State { + /** + * State indicating that the object has just been created and [initialize] has not yet been + * called. + */ + object New : State + + /** + * State indicating that [initialize] has been invoked but the token provider is not (yet?) + * available. + */ + data class Initialized(override val forceTokenRefresh: Boolean) : + StateWithForceTokenRefresh { + constructor() : this(false) + } + /** State indicating that [close] has been invoked. */ object Closed : State - /** State indicating that there is no outstanding "get token" request. */ - class Idle( - - /** - * The [InternalAuthProvider] or [InteropAppCheckTokenProvider]; may be null if the deferred - * has not yet given us a provider. - */ - override val provider: T?, - + sealed interface StateWithForceTokenRefresh : State { /** The value to specify for `forceRefresh` on the next invocation of [getToken]. */ val forceTokenRefresh: Boolean - ) : State, ProviderProvider + } - /** State indicating that there _is_ an outstanding "get token" request. */ - class Active( + sealed interface StateWithProvider : State { + /** The token provider, [InternalAuthProvider] or [InteropAppCheckTokenProvider] */ + val provider: T + } - /** - * The [InternalAuthProvider] or [InteropAppCheckTokenProvider] that is performing the "get - * token" request. - */ + /** State indicating that there is no outstanding "get token" request. */ + data class Idle(override val provider: T, override val forceTokenRefresh: Boolean) : + StateWithProvider, StateWithForceTokenRefresh + + /** State indicating that there _is_ an outstanding "get token" request. */ + data class Active( override val provider: T, /** The job that is performing the "get token" request. */ val job: Deferred>> - ) : State, ProviderProvider + ) : StateWithProvider } - /** - * The current state of this object. The value should only be changed in a compare-and-swap loop - * in order to be thread-safe. Such a loop should call `yield()` on each iteration to allow other - * coroutines to run on the thread. - */ - private val state = - AtomicReference>(State.Idle(provider = null, forceTokenRefresh = false)) + /** The current state of this object. */ + private val state = MutableStateFlow>(State.New) /** * Adds the token listener to the given provider. @@ -151,6 +141,34 @@ internal sealed class DataConnectCredentialsTokenManager( */ protected abstract suspend fun getToken(provider: T, forceRefresh: Boolean): GetTokenResult + /** + * Initializes this object. + * + * Before calling this method, the _only_ other methods that are allowed to be called on this + * object are [awaitTokenProvider] and [close]. + * + * This method may only be called once; subsequent calls result in an exception. + */ + fun initialize() { + logger.debug { "initialize()" } + + state.update { currentState -> + when (currentState) { + is State.New -> State.Initialized() + is State.Closed -> + throw IllegalStateException("initialize() cannot be called after close()") + else -> throw IllegalStateException("initialize() has already been called") + } + } + + // Call `whenAvailable()` on a non-main thread because it accesses SharedPreferences, which + // performs disk i/o, violating the StrictMode policy android.os.strictmode.DiskReadViolation. + val coroutineName = CoroutineName("k6rwgqg9gh $instanceId whenAvailable") + coroutineScope.launch(coroutineName + blockingDispatcher) { + deferredProvider.whenAvailable(DeferredProviderHandlerImpl(weakThis)) + } + } + /** * Closes this object, releasing its resources, unregistering any registered listeners, and * cancelling any in-flight token requests. @@ -163,55 +181,92 @@ internal sealed class DataConnectCredentialsTokenManager( */ fun close() { logger.debug { "close()" } + weakThis.clear() coroutineScope.cancel() - setClosedState() - } - - // This function must ONLY be called from close(). - private fun setClosedState() { - while (true) { - val oldState = state.get() - val providerProvider: ProviderProvider = - when (oldState) { - is State.Closed -> return - is State.Idle -> oldState - is State.Active -> oldState - } - if (state.compareAndSet(oldState, State.Closed)) { - providerProvider.provider?.let { removeTokenListener(it) } - break + val oldState = state.getAndUpdate { State.Closed } + when (oldState) { + is State.New -> {} + is State.Initialized -> {} + is State.Closed -> {} + is State.StateWithProvider -> { + removeTokenListener(oldState.provider) } } } + /** + * Suspends until the token provider becomes available to this object. + * + * This method _may_ be called before [initialize], which is the method that asynchronously gets + * the token provider. + * + * If [close] has been invoked, or is invoked _before_ a token provider becomes available, then + * this method returns normally, as if a token provider _had_ become available. + */ + suspend fun awaitTokenProvider() { + logger.debug { "awaitTokenProvider() start" } + val currentState = + state + .filter { + when (it) { + State.Closed -> true + is State.New -> false + is State.Initialized -> false + is State.Idle -> true + is State.Active -> true + } + } + .first() + logger.debug { "awaitTokenProvider() done: currentState=$currentState" } + } + /** * Sets a flag to force-refresh the token upon the next call to [getToken]. * * If [close] has been called, this method does nothing. */ - suspend fun forceRefresh() { + fun forceRefresh() { logger.debug { "forceRefresh()" } - while (true) { - val oldState = state.get() - val oldStateProviderProvider = - when (oldState) { - is State.Closed -> return - is State.Idle -> oldState - is State.Active -> { - val message = "needs token refresh (wgrwbrvjxt)" - oldState.job.cancel(message, ForceRefresh(message)) - oldState + val oldState = + state.getAndUpdate { currentState -> + val newState = + when (currentState) { + is State.Closed -> State.Closed + is State.New -> currentState + is State.Initialized -> currentState.copy(forceTokenRefresh = true) + is State.Idle -> currentState.copy(forceTokenRefresh = true) + is State.Active -> State.Idle(currentState.provider, forceTokenRefresh = true) + } + + check( + newState is State.New || + newState is State.Closed || + newState is State.StateWithForceTokenRefresh + ) { + "internal error gbazc7qr66: newState should have been Closed or " + + "StateWithForceTokenRefresh, but got: $newState" + } + if (newState is State.StateWithForceTokenRefresh) { + check(newState.forceTokenRefresh) { + "internal error fnzwyrsez2: newState.forceTokenRefresh should have been true" } } - val newState = State.Idle(oldStateProviderProvider.provider, forceTokenRefresh = true) - if (state.compareAndSet(oldState, newState)) { - break + newState } - yield() + when (oldState) { + is State.Closed -> {} + is State.New -> + throw IllegalStateException("initialize() must be called before forceRefresh()") + is State.Initialized -> {} + is State.Idle -> {} + is State.Active -> { + val message = "needs token refresh (wgrwbrvjxt)" + oldState.job.cancel(message, ForceRefresh(message)) + } } } @@ -246,10 +301,12 @@ internal sealed class DataConnectCredentialsTokenManager( logger.debug { "$invocationId getToken(requestId=$requestId)" } while (true) { val attemptSequenceNumber = nextSequenceNumber() - val oldState = state.get() + val oldState = state.value val newState: State.Active = when (oldState) { + is State.New -> + throw IllegalStateException("initialize() must be called before getToken()") is State.Closed -> { logger.debug { "$invocationId getToken() throws CredentialsTokenManagerClosedException" + @@ -257,13 +314,13 @@ internal sealed class DataConnectCredentialsTokenManager( } throw CredentialsTokenManagerClosedException(this) } - is State.Idle -> { - if (oldState.provider === null) { - logger.debug { - "$invocationId getToken() returns null (token provider is not (yet?) available)" - } - return null + is State.Initialized -> { + logger.debug { + "$invocationId getToken() returns null (token provider is not (yet?) available)" } + return null + } + is State.Idle -> { newActiveState(invocationId, oldState.provider, oldState.forceTokenRefresh) } is State.Active -> { @@ -341,33 +398,38 @@ internal sealed class DataConnectCredentialsTokenManager( logger.debug { "onProviderAvailable(newProvider=$newProvider)" } addTokenListener(newProvider) - while (true) { - val oldState = state.get() - val newState = - when (oldState) { - is State.Closed -> { - logger.debug { - "onProviderAvailable(newProvider=$newProvider)" + - " unregistering token listener that was just added" - } - removeTokenListener(newProvider) - break - } - is State.Idle -> State.Idle(newProvider, oldState.forceTokenRefresh) - is State.Active -> { - val newProviderClassName = newProvider::class.qualifiedName - val message = "a new provider $newProviderClassName is available (symhxtmazy)" - oldState.job.cancel(message, NewProvider(message)) - State.Idle(newProvider, forceTokenRefresh = false) - } + val oldState = + state.getAndUpdate { currentState -> + when (currentState) { + is State.New -> currentState + is State.Closed -> State.Closed + is State.Initialized -> State.Idle(newProvider, currentState.forceTokenRefresh) + is State.Idle -> State.Idle(newProvider, currentState.forceTokenRefresh) + is State.Active -> State.Idle(newProvider, forceTokenRefresh = false) } + } - if (state.compareAndSet(oldState, newState)) { - break + when (oldState) { + is State.New -> + throw IllegalStateException( + "internal error sdpzwhmhd3: " + + "initialize() should have been called before onProviderAvailable()" + ) + is State.Closed -> { + logger.debug { + "onProviderAvailable(newProvider=$newProvider)" + + " unregistering token listener that was just added" + } + removeTokenListener(newProvider) + } + is State.Initialized -> {} + is State.Idle -> {} + is State.Active -> { + val newProviderClassName = newProvider::class.qualifiedName + val message = "a new provider $newProviderClassName is available (symhxtmazy)" + oldState.job.cancel(message, NewProvider(message)) } } - - _providerAvailable.value = true } /** diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt index b2d2270056b..d0ebe3f84c3 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt @@ -17,15 +17,16 @@ package com.google.firebase.dataconnect.core import com.google.firebase.dataconnect.* -import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.toDataConnectError +import com.google.firebase.dataconnect.DataConnectPathSegment +import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.toErrorInfoImpl import com.google.firebase.dataconnect.core.LoggerGlobals.warn import com.google.firebase.dataconnect.util.ProtoUtil.decodeFromStruct +import com.google.firebase.dataconnect.util.ProtoUtil.toCompactString import com.google.firebase.dataconnect.util.ProtoUtil.toMap import com.google.protobuf.ListValue import com.google.protobuf.Struct import com.google.protobuf.Value import google.firebase.dataconnect.proto.GraphqlError -import google.firebase.dataconnect.proto.SourceLocation import google.firebase.dataconnect.proto.executeMutationRequest import google.firebase.dataconnect.proto.executeQueryRequest import io.grpc.Status @@ -52,7 +53,7 @@ internal class DataConnectGrpcClient( data class OperationResult( val data: Struct?, - val errors: List, + val errors: List, ) suspend fun executeQuery( @@ -74,7 +75,7 @@ internal class DataConnectGrpcClient( return OperationResult( data = if (response.hasData()) response.data else null, - errors = response.errorsList.map { it.toDataConnectError() } + errors = response.errorsList.map { it.toErrorInfoImpl() } ) } @@ -97,11 +98,11 @@ internal class DataConnectGrpcClient( return OperationResult( data = if (response.hasData()) response.data else null, - errors = response.errorsList.map { it.toDataConnectError() } + errors = response.errorsList.map { it.toErrorInfoImpl() } ) } - private suspend inline fun T.retryOnGrpcUnauthenticatedError( + private inline fun T.retryOnGrpcUnauthenticatedError( requestId: String, kotlinMethodName: String, block: T.() -> R @@ -138,50 +139,72 @@ internal class DataConnectGrpcClient( internal object DataConnectGrpcClientGlobals { private fun ListValue.toPathSegment() = valuesList.map { - when (val kind = it.kindCase) { - Value.KindCase.STRING_VALUE -> DataConnectError.PathSegment.Field(it.stringValue) - Value.KindCase.NUMBER_VALUE -> - DataConnectError.PathSegment.ListIndex(it.numberValue.toInt()) - else -> DataConnectError.PathSegment.Field("invalid PathSegment kind: $kind") + when (it.kindCase) { + Value.KindCase.STRING_VALUE -> DataConnectPathSegment.Field(it.stringValue) + Value.KindCase.NUMBER_VALUE -> DataConnectPathSegment.ListIndex(it.numberValue.toInt()) + // The cases below are expected to never occur; however, implement some logic for them + // to avoid things like throwing exceptions in those cases. + Value.KindCase.NULL_VALUE -> DataConnectPathSegment.Field("null") + Value.KindCase.BOOL_VALUE -> DataConnectPathSegment.Field(it.boolValue.toString()) + Value.KindCase.LIST_VALUE -> DataConnectPathSegment.Field(it.listValue.toCompactString()) + Value.KindCase.STRUCT_VALUE -> + DataConnectPathSegment.Field(it.structValue.toCompactString()) + else -> DataConnectPathSegment.Field(it.toString()) } } - private fun List.toSourceLocations(): List = - buildList { - this@toSourceLocations.forEach { - add(DataConnectError.SourceLocation(line = it.line, column = it.column)) - } - } - - fun GraphqlError.toDataConnectError() = - DataConnectError( + fun GraphqlError.toErrorInfoImpl() = + DataConnectOperationFailureResponseImpl.ErrorInfoImpl( message = message, path = path.toPathSegment(), - this.locationsList.toSourceLocations() ) fun DataConnectGrpcClient.OperationResult.deserialize( deserializer: DeserializationStrategy, serializersModule: SerializersModule?, - ): T = + ): T { if (deserializer === DataConnectUntypedData) { - @Suppress("UNCHECKED_CAST") - DataConnectUntypedData(data?.toMap(), errors) as T - } else if (data === null) { - if (errors.isNotEmpty()) { - throw DataConnectException("operation failed: errors=$errors") - } else { - throw DataConnectException("no data included in result") - } - } else if (errors.isNotEmpty()) { - throw DataConnectException("operation failed: errors=$errors (data=$data)") - } else { - try { - decodeFromStruct(data, deserializer, serializersModule) - } catch (dataConnectException: DataConnectException) { - throw dataConnectException - } catch (throwable: Throwable) { - throw DataConnectException("decoding response data failed: $throwable", throwable) - } + @Suppress("UNCHECKED_CAST") return DataConnectUntypedData(data?.toMap(), errors) as T + } + + val decodedData: Result? = + data?.let { data -> runCatching { decodeFromStruct(data, deserializer, serializersModule) } } + + if (errors.isNotEmpty()) { + throw DataConnectOperationException( + "operation encountered errors during execution: $errors", + response = + DataConnectOperationFailureResponseImpl( + rawData = data?.toMap(), + data = decodedData?.getOrNull(), + errors = errors, + ) + ) + } + + if (decodedData == null) { + throw DataConnectOperationException( + "no data was included in the response from the server", + response = + DataConnectOperationFailureResponseImpl( + rawData = null, + data = null, + errors = emptyList(), + ) + ) } + + return decodedData.getOrElse { exception -> + throw DataConnectOperationException( + "decoding data from the server's response failed: ${exception.message}", + cause = exception, + response = + DataConnectOperationFailureResponseImpl( + rawData = data?.toMap(), + data = null, + errors = emptyList(), + ) + ) + } + } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImpl.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImpl.kt new file mode 100644 index 00000000000..85434a64b47 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImpl.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2025 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.firebase.dataconnect.core + +import com.google.firebase.dataconnect.DataConnectOperationFailureResponse +import com.google.firebase.dataconnect.DataConnectOperationFailureResponse.ErrorInfo +import com.google.firebase.dataconnect.DataConnectPathSegment +import java.util.Objects + +internal class DataConnectOperationFailureResponseImpl( + override val rawData: Map?, + override val data: Data?, + override val errors: List +) : DataConnectOperationFailureResponse { + + override fun toString(): String = + "DataConnectOperationFailureResponseImpl(rawData=$rawData, data=$data, errors=$errors)" + + internal class ErrorInfoImpl( + override val message: String, + override val path: List, + ) : ErrorInfo { + + override fun equals(other: Any?): Boolean = + other is ErrorInfoImpl && other.message == message && other.path == path + + override fun hashCode(): Int = Objects.hash("ErrorInfoImpl", message, path) + + override fun toString(): String = buildString { + path.forEachIndexed { segmentIndex, segment -> + when (segment) { + is DataConnectPathSegment.Field -> { + if (segmentIndex != 0) { + append('.') + } + append(segment.field) + } + is DataConnectPathSegment.ListIndex -> { + append('[').append(segment.index).append(']') + } + } + } + + if (path.isNotEmpty()) { + append(": ") + } + + append(message) + } + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt index d9bee50b89d..b422167950d 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt @@ -33,8 +33,6 @@ import com.google.firebase.dataconnect.querymgr.LiveQueries import com.google.firebase.dataconnect.querymgr.LiveQuery import com.google.firebase.dataconnect.querymgr.QueryManager import com.google.firebase.dataconnect.querymgr.RegisteredDataDeserializer -import com.google.firebase.dataconnect.util.NullableReference -import com.google.firebase.dataconnect.util.SuspendingLazy import com.google.firebase.util.nextAlphanumericString import com.google.protobuf.Struct import java.util.concurrent.Executor @@ -54,11 +52,9 @@ import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.modules.SerializersModule @@ -72,8 +68,8 @@ internal interface FirebaseDataConnectInternal : FirebaseDataConnect { val nonBlockingExecutor: Executor val nonBlockingDispatcher: CoroutineDispatcher - val lazyGrpcClient: SuspendingLazy - val lazyQueryManager: SuspendingLazy + val grpcClient: DataConnectGrpcClient + val queryManager: QueryManager suspend fun awaitAuthReady() suspend fun awaitAppCheckReady() @@ -120,183 +116,197 @@ internal class FirebaseDataConnectImpl( } ) - private val authProviderAvailable = MutableStateFlow(false) - private val appCheckProviderAvailable = MutableStateFlow(false) - - // Protects `closed`, `grpcClient`, `emulatorSettings`, and `queryManager`. - private val mutex = Mutex() - - // All accesses to this variable _must_ have locked `mutex`. - private var emulatorSettings: EmulatedServiceSettings? = null - - // All accesses to this variable _must_ have locked `mutex`. - private var closed = false - private val dataConnectAuth: DataConnectAuth = DataConnectAuth( - deferredAuthProvider = deferredAuthProvider, - parentCoroutineScope = coroutineScope, - blockingDispatcher = blockingDispatcher, - logger = Logger("DataConnectAuth").apply { debug { "created by $instanceId" } }, - ) + deferredAuthProvider = deferredAuthProvider, + parentCoroutineScope = coroutineScope, + blockingDispatcher = blockingDispatcher, + logger = Logger("DataConnectAuth").apply { debug { "created by $instanceId" } }, + ) + .apply { initialize() } override suspend fun awaitAuthReady() { - authProviderAvailable.first { it } - } - - init { - val name = CoroutineName("DataConnectAuth isProviderAvailable pipe for $instanceId") - coroutineScope.launch(name) { - dataConnectAuth.providerAvailable.collect { isProviderAvailable -> - logger.debug { "authProviderAvailable=$isProviderAvailable" } - authProviderAvailable.value = isProviderAvailable - } - } + dataConnectAuth.awaitTokenProvider() } private val dataConnectAppCheck: DataConnectAppCheck = DataConnectAppCheck( - deferredAppCheckTokenProvider = deferredAppCheckProvider, - parentCoroutineScope = coroutineScope, - blockingDispatcher = blockingDispatcher, - logger = Logger("DataConnectAppCheck").apply { debug { "created by $instanceId" } }, - ) + deferredAppCheckTokenProvider = deferredAppCheckProvider, + parentCoroutineScope = coroutineScope, + blockingDispatcher = blockingDispatcher, + logger = Logger("DataConnectAppCheck").apply { debug { "created by $instanceId" } }, + ) + .apply { initialize() } override suspend fun awaitAppCheckReady() { - appCheckProviderAvailable.first { it } + dataConnectAppCheck.awaitTokenProvider() } - init { - val name = CoroutineName("DataConnectAppCheck isProviderAvailable pipe for $instanceId") - coroutineScope.launch(name) { - dataConnectAppCheck.providerAvailable.collect { isProviderAvailable -> - logger.debug { "appCheckProviderAvailable=$isProviderAvailable" } - appCheckProviderAvailable.value = isProviderAvailable - } + private sealed interface State { + data class New(val emulatorSettings: EmulatedServiceSettings?) : State { + constructor() : this(null) } + data class Initialized( + val grpcRPCs: DataConnectGrpcRPCs, + val grpcClient: DataConnectGrpcClient, + val queryManager: QueryManager + ) : State + data class Closing(val grpcRPCs: DataConnectGrpcRPCs, val closeJob: Deferred) : State + object Closed : State } - private val lazyGrpcRPCs = - SuspendingLazy(mutex) { - if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") - - data class DataConnectBackendInfo( - val host: String, - val sslEnabled: Boolean, - val isEmulator: Boolean - ) - val backendInfoFromSettings = - DataConnectBackendInfo( - host = settings.host, - sslEnabled = settings.sslEnabled, - isEmulator = false - ) - val backendInfoFromEmulatorSettings = - emulatorSettings?.run { - DataConnectBackendInfo(host = "$host:$port", sslEnabled = false, isEmulator = true) - } - val backendInfo = - if (backendInfoFromEmulatorSettings == null) { - backendInfoFromSettings - } else { - if (!settings.isDefaultHost()) { - logger.warn( - "Host has been set in DataConnectSettings and useEmulator, " + - "emulator host will be used." - ) + private val state = MutableStateFlow(State.New()) + + override val grpcClient: DataConnectGrpcClient + get() = initialize().grpcClient + override val queryManager: QueryManager + get() = initialize().queryManager + + private fun initialize(): State.Initialized { + val newState = + state.updateAndGet { currentState -> + when (currentState) { + is State.New -> { + val grpcRPCs = createDataConnectGrpcRPCs(currentState.emulatorSettings) + val grpcClient = createDataConnectGrpcClient(grpcRPCs) + val queryManager = createQueryManager(grpcClient) + State.Initialized(grpcRPCs, grpcClient, queryManager) } - backendInfoFromEmulatorSettings + is State.Initialized -> currentState + is State.Closing -> currentState + is State.Closed -> currentState } + } - logger.debug { "connecting to Data Connect backend: $backendInfo" } - val grpcMetadata = - DataConnectGrpcMetadata.forSystemVersions( - firebaseApp = app, - dataConnectAuth = dataConnectAuth, - dataConnectAppCheck = dataConnectAppCheck, - connectorLocation = config.location, - parentLogger = logger, - ) - val dataConnectGrpcRPCs = - DataConnectGrpcRPCs( - context = context, - host = backendInfo.host, - sslEnabled = backendInfo.sslEnabled, - blockingCoroutineDispatcher = blockingDispatcher, - grpcMetadata = grpcMetadata, - parentLogger = logger, + return when (newState) { + is State.New -> + throw IllegalStateException( + "newState should be Initialized, but got New (error code sh2rf4wwjx)" ) + is State.Initialized -> newState + is State.Closing, + State.Closed -> throw IllegalStateException("FirebaseDataConnect instance has been closed") + } + } - if (backendInfo.isEmulator) { - logEmulatorVersion(dataConnectGrpcRPCs) - streamEmulatorErrors(dataConnectGrpcRPCs) + private fun createDataConnectGrpcRPCs( + emulatorSettings: EmulatedServiceSettings? + ): DataConnectGrpcRPCs { + data class DataConnectBackendInfo( + val host: String, + val sslEnabled: Boolean, + val isEmulator: Boolean + ) + val backendInfoFromSettings = + DataConnectBackendInfo( + host = settings.host, + sslEnabled = settings.sslEnabled, + isEmulator = false + ) + val backendInfoFromEmulatorSettings = + emulatorSettings?.run { + DataConnectBackendInfo(host = "$host:$port", sslEnabled = false, isEmulator = true) + } + val backendInfo = + if (backendInfoFromEmulatorSettings == null) { + backendInfoFromSettings + } else { + if (!settings.isDefaultHost()) { + logger.warn( + "Host has been set in DataConnectSettings and useEmulator, " + + "emulator host will be used." + ) + } + backendInfoFromEmulatorSettings } - dataConnectGrpcRPCs - } - - override val lazyGrpcClient = - SuspendingLazy(mutex) { - DataConnectGrpcClient( - projectId = projectId, - connector = config, - grpcRPCs = lazyGrpcRPCs.getLocked(), + logger.debug { "connecting to Data Connect backend: $backendInfo" } + val grpcMetadata = + DataConnectGrpcMetadata.forSystemVersions( + firebaseApp = app, dataConnectAuth = dataConnectAuth, dataConnectAppCheck = dataConnectAppCheck, - logger = Logger("DataConnectGrpcClient").apply { debug { "created by $instanceId" } }, + connectorLocation = config.location, + parentLogger = logger, + ) + val dataConnectGrpcRPCs = + DataConnectGrpcRPCs( + context = context, + host = backendInfo.host, + sslEnabled = backendInfo.sslEnabled, + blockingCoroutineDispatcher = blockingDispatcher, + grpcMetadata = grpcMetadata, + parentLogger = logger, ) - } - override val lazyQueryManager = - SuspendingLazy(mutex) { - if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") - val grpcClient = lazyGrpcClient.getLocked() - - val registeredDataDeserializerFactory = - object : LiveQuery.RegisteredDataDeserializerFactory { - override fun newInstance( - dataDeserializer: DeserializationStrategy, - dataSerializersModule: SerializersModule?, - parentLogger: Logger - ) = - RegisteredDataDeserializer( - dataDeserializer = dataDeserializer, - dataSerializersModule = dataSerializersModule, - blockingCoroutineDispatcher = blockingDispatcher, - parentLogger = parentLogger, - ) - } - val liveQueryFactory = - object : LiveQueries.LiveQueryFactory { - override fun newLiveQuery( - key: LiveQuery.Key, - operationName: String, - variables: Struct, - parentLogger: Logger - ) = - LiveQuery( - key = key, - operationName = operationName, - variables = variables, - parentCoroutineScope = coroutineScope, - nonBlockingCoroutineDispatcher = nonBlockingDispatcher, - grpcClient = grpcClient, - registeredDataDeserializerFactory = registeredDataDeserializerFactory, - parentLogger = parentLogger, - ) - } - val liveQueries = LiveQueries(liveQueryFactory, blockingDispatcher, parentLogger = logger) - QueryManager(liveQueries) + if (backendInfo.isEmulator) { + logEmulatorVersion(dataConnectGrpcRPCs) + streamEmulatorErrors(dataConnectGrpcRPCs) } + return dataConnectGrpcRPCs + } + + private fun createDataConnectGrpcClient(grpcRPCs: DataConnectGrpcRPCs): DataConnectGrpcClient = + DataConnectGrpcClient( + projectId = projectId, + connector = config, + grpcRPCs = grpcRPCs, + dataConnectAuth = dataConnectAuth, + dataConnectAppCheck = dataConnectAppCheck, + logger = Logger("DataConnectGrpcClient").apply { debug { "created by $instanceId" } }, + ) + + private fun createQueryManager(grpcClient: DataConnectGrpcClient): QueryManager { + val registeredDataDeserializerFactory = + object : LiveQuery.RegisteredDataDeserializerFactory { + override fun newInstance( + dataDeserializer: DeserializationStrategy, + dataSerializersModule: SerializersModule?, + parentLogger: Logger + ) = + RegisteredDataDeserializer( + dataDeserializer = dataDeserializer, + dataSerializersModule = dataSerializersModule, + blockingCoroutineDispatcher = blockingDispatcher, + parentLogger = parentLogger, + ) + } + val liveQueryFactory = + object : LiveQueries.LiveQueryFactory { + override fun newLiveQuery( + key: LiveQuery.Key, + operationName: String, + variables: Struct, + parentLogger: Logger + ) = + LiveQuery( + key = key, + operationName = operationName, + variables = variables, + parentCoroutineScope = coroutineScope, + nonBlockingCoroutineDispatcher = nonBlockingDispatcher, + grpcClient = grpcClient, + registeredDataDeserializerFactory = registeredDataDeserializerFactory, + parentLogger = parentLogger, + ) + } + val liveQueries = LiveQueries(liveQueryFactory, blockingDispatcher, parentLogger = logger) + return QueryManager(liveQueries) + } + override fun useEmulator(host: String, port: Int): Unit = runBlocking { - mutex.withLock { - if (lazyGrpcClient.initializedValueOrNull != null) { - throw IllegalStateException( - "Cannot call useEmulator() after instance has already been initialized." - ) + state.update { currentState -> + when (currentState) { + is State.New -> + currentState.copy(emulatorSettings = EmulatedServiceSettings(host = host, port = port)) + is State.Initialized -> + throw IllegalStateException( + "Cannot call useEmulator() after instance has already been initialized." + ) + is State.Closing -> currentState + is State.Closed -> currentState } - emulatorSettings = EmulatedServiceSettings(host = host, port = port) } } @@ -404,19 +414,17 @@ internal class FirebaseDataConnectImpl( ) } - private val closeJob = MutableStateFlow(NullableReference>(null)) - override fun close() { logger.debug { "close() called" } - @Suppress("DeferredResultUnused") runBlocking { nonBlockingClose() } + @Suppress("DeferredResultUnused") closeInternal() } override suspend fun suspendingClose() { logger.debug { "suspendingClose() called" } - nonBlockingClose().await() + closeInternal()?.await() } - private suspend fun nonBlockingClose(): Deferred { + private fun closeInternal(): Deferred? { coroutineScope.cancel() // Remove the reference to this `FirebaseDataConnect` instance from the @@ -424,41 +432,59 @@ internal class FirebaseDataConnectImpl( // called with the same arguments that a new instance of `FirebaseDataConnect` will be created. creator.remove(this) - mutex.withLock { closed = true } - // Close Auth and AppCheck synchronously to avoid race conditions with auth callbacks. // Since close() is re-entrant, this is safe even if they have already been closed. dataConnectAuth.close() dataConnectAppCheck.close() - // Start the job to asynchronously close the gRPC client. - while (true) { - val oldCloseJob = closeJob.value - - oldCloseJob.ref?.let { - if (!it.isCancelled) { - return it - } - } - + fun createCloseJob(grpcRPCs: DataConnectGrpcRPCs): Deferred { @OptIn(DelicateCoroutinesApi::class) - val newCloseJob = - GlobalScope.async(start = CoroutineStart.LAZY) { - lazyGrpcRPCs.initializedValueOrNull?.close() - } - - newCloseJob.invokeOnCompletion { exception -> - if (exception === null) { - logger.debug { "close() completed successfully" } - } else { + val closeJob = GlobalScope.async(start = CoroutineStart.LAZY) { grpcRPCs.close() } + closeJob.invokeOnCompletion { exception -> + if (exception !== null) { logger.warn(exception) { "close() failed" } + } else { + logger.debug { "close() completed successfully" } + state.update { currentState -> + check(currentState is State.Closing) { + "currentState is ${currentState}, but expected Closing (error code hsee7gfxvz)" + } + check(currentState.closeJob === closeJob) { + "currentState.closeJob is ${currentState.closeJob}, but expected $closeJob " + + "(error code n3x86pr6qn)" + } + State.Closed + } } } + return closeJob + } - if (closeJob.compareAndSet(oldCloseJob, NullableReference(newCloseJob))) { - newCloseJob.start() - return newCloseJob + val newState = + state.updateAndGet { currentState -> + when (currentState) { + is State.New -> State.Closed + is State.Initialized -> + State.Closing(currentState.grpcRPCs, createCloseJob(currentState.grpcRPCs)) + is State.Closing -> + if (currentState.closeJob.isCancelled) { + currentState.copy(closeJob = createCloseJob(currentState.grpcRPCs)) + } else { + currentState + } + is State.Closed -> State.Closed + } } + + return when (newState) { + is State.Initialized, + is State.New -> + throw IllegalStateException( + "internal error: newState is $newState, but expected Closing or Closed " + + "(error code n3x86pr6qn)" + ) + is State.Closing -> newState.closeJob.apply { start() } + is State.Closed -> null } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/MutationRefImpl.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/MutationRefImpl.kt index edd978a2ec9..8e5684c2fe6 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/MutationRefImpl.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/MutationRefImpl.kt @@ -60,8 +60,7 @@ internal class MutationRefImpl( override suspend fun execute(): MutationResultImpl { val requestId = "mut" + Random.nextAlphanumericString(length = 10) - return dataConnect.lazyGrpcClient - .get() + return dataConnect.grpcClient .executeMutation( requestId = requestId, operationName = operationName, diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QueryRefImpl.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QueryRefImpl.kt index 53e247e3d14..1b630a12cfd 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QueryRefImpl.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QueryRefImpl.kt @@ -49,7 +49,7 @@ internal class QueryRefImpl( variablesSerializersModule = variablesSerializersModule, ) { override suspend fun execute(): QueryResultImpl = - dataConnect.lazyQueryManager.get().execute(this).let { QueryResultImpl(it.ref.getOrThrow()) } + dataConnect.queryManager.execute(this).let { QueryResultImpl(it.ref.getOrThrow()) } override fun subscribe(): QuerySubscription = QuerySubscriptionImpl(this) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QuerySubscriptionImpl.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QuerySubscriptionImpl.kt index ceeb861cab8..7ec870e93ce 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QuerySubscriptionImpl.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QuerySubscriptionImpl.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch internal class QuerySubscriptionImpl(query: QueryRefImpl) : @@ -57,7 +58,7 @@ internal class QuerySubscriptionImpl(query: QueryRefImpl val querySubscriptionResult = QuerySubscriptionResultImpl(query, sequencedResult) send(querySubscriptionResult) @@ -69,7 +70,7 @@ internal class QuerySubscriptionImpl(query: QueryRefImpl(query: QueryRefImpl= prospectiveSequenceNumber) { - return - } - } - - if (_lastResult.compareAndSet(currentLastResult, NullableReference(prospectiveLastResult))) { - return + _lastResult.update { currentLastResult -> + if ( + currentLastResult.ref != null && + currentLastResult.ref.sequencedResult.sequenceNumber >= + prospectiveLastResult.sequencedResult.sequenceNumber + ) { + currentLastResult + } else { + NullableReference(prospectiveLastResult) } } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/querymgr/RegisteredDataDeserialzer.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/querymgr/RegisteredDataDeserialzer.kt index 3f94a7f95a0..1fa6d94eae4 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/querymgr/RegisteredDataDeserialzer.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/querymgr/RegisteredDataDeserialzer.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.onSubscription +import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.modules.SerializersModule @@ -84,17 +85,14 @@ internal class RegisteredDataDeserializer( lazyDeserialize(requestId, sequencedResult) ) - // Use a compare-and-swap ("CAS") loop to ensure that an old update never clobbers a newer one. - while (true) { - val currentUpdate = latestUpdate.value + latestUpdate.update { currentUpdate -> if ( currentUpdate.ref !== null && currentUpdate.ref.sequenceNumber > sequencedResult.sequenceNumber ) { - break // don't clobber a newer update with an older one - } - if (latestUpdate.compareAndSet(currentUpdate, NullableReference(newUpdate))) { - break + currentUpdate // don't clobber a newer update with an older one + } else { + NullableReference(newUpdate) } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt index 0ba6a34b34a..94a3a63a68d 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt @@ -16,7 +16,7 @@ package com.google.firebase.dataconnect.util -import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.toDataConnectError +import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.toErrorInfoImpl import com.google.firebase.dataconnect.util.ProtoUtil.nullProtoValue import com.google.firebase.dataconnect.util.ProtoUtil.toValueProto import com.google.protobuf.ListValue @@ -136,6 +136,10 @@ internal object ProtoUtil { fun Struct.toCompactString(keySortSelector: ((String) -> String)? = null): String = Value.newBuilder().setStructValue(this).build().toCompactString(keySortSelector) + /** Generates and returns a string similar to [Struct.toString] but more compact. */ + fun ListValue.toCompactString(keySortSelector: ((String) -> String)? = null): String = + Value.newBuilder().setListValue(this).build().toCompactString(keySortSelector) + /** Generates and returns a string similar to [Value.toString] but more compact. */ fun Value.toCompactString(keySortSelector: ((String) -> String)? = null): String { val charArrayWriter = CharArrayWriter() @@ -204,7 +208,7 @@ internal object ProtoUtil { fun ExecuteQueryResponse.toStructProto(): Struct = buildStructProto { if (hasData()) put("data", data) - putList("errors") { errorsList.forEach { add(it.toDataConnectError().toString()) } } + putList("errors") { errorsList.forEach { add(it.toErrorInfoImpl().toString()) } } } fun ExecuteMutationRequest.toCompactString(): String = toStructProto().toCompactString() @@ -219,7 +223,7 @@ internal object ProtoUtil { fun ExecuteMutationResponse.toStructProto(): Struct = buildStructProto { if (hasData()) put("data", data) - putList("errors") { errorsList.forEach { add(it.toDataConnectError().toString()) } } + putList("errors") { errorsList.forEach { add(it.toErrorInfoImpl().toString()) } } } fun EmulatorInfo.toStructProto(): Struct = buildStructProto { diff --git a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto index 918227ef686..bb4bc986769 100644 --- a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto +++ b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto @@ -18,7 +18,7 @@ syntax = "proto3"; -package google.firebase.dataconnect.v1beta; +package google.firebase.dataconnect.v1; import "google/firebase/dataconnect/proto/graphql_error.proto"; import "google/protobuf/struct.proto"; diff --git a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto index f2ca45e9f66..be19dcbfa35 100644 --- a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto +++ b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto @@ -18,7 +18,7 @@ syntax = "proto3"; -package google.firebase.dataconnect.v1beta; +package google.firebase.dataconnect.v1; import "google/protobuf/struct.proto"; diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorUnitTest.kt deleted file mode 100644 index 204b6f1fc48..00000000000 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorUnitTest.kt +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Copyright 2024 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. - */ - -@file:Suppress("ReplaceCallWithBinaryOperator") -@file:OptIn(ExperimentalKotest::class) - -package com.google.firebase.dataconnect - -import com.google.firebase.dataconnect.DataConnectError.PathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnectError -import com.google.firebase.dataconnect.testutil.property.arbitrary.fieldPathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.listIndexPathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.pathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.sourceLocation -import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText -import io.kotest.assertions.assertSoftly -import io.kotest.common.ExperimentalKotest -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.types.shouldBeSameInstanceAs -import io.kotest.property.Arb -import io.kotest.property.PropTestConfig -import io.kotest.property.arbitrary.Codepoint -import io.kotest.property.arbitrary.az -import io.kotest.property.arbitrary.choice -import io.kotest.property.arbitrary.constant -import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.list -import io.kotest.property.arbitrary.next -import io.kotest.property.arbitrary.string -import io.kotest.property.assume -import io.kotest.property.checkAll -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class DataConnectErrorUnitTest { - - @Test - fun `properties should be the same objects given to the constructor`() = runTest { - val messages = Arb.dataConnect.string() - val paths = Arb.list(Arb.dataConnect.pathSegment(), 0..5) - val sourceLocations = Arb.list(Arb.dataConnect.sourceLocation(), 0..5) - checkAll(propTestConfig, messages, paths, sourceLocations) { message, path, locations -> - val dataConnectError = DataConnectError(message = message, path = path, locations = locations) - assertSoftly { - dataConnectError.message shouldBeSameInstanceAs message - dataConnectError.path shouldBeSameInstanceAs path - dataConnectError.locations shouldBeSameInstanceAs locations - } - } - } - - @Test - fun `toString() should incorporate the message`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError -> - dataConnectError.toString() shouldContainWithNonAbuttingText dataConnectError.message - } - } - - @Test - fun `toString() should incorporate the fields from the path separated by dots`() = runTest { - val paths = Arb.list(Arb.dataConnect.fieldPathSegment(), 0..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(path = paths)) { dataConnectError -> - val expectedSubstring = dataConnectError.path.joinToString(".") - dataConnectError.toString() shouldContainWithNonAbuttingText expectedSubstring - } - } - - @Test - fun `toString() should incorporate the list indexes from the path surround by square brackets`() = - runTest { - val paths = Arb.list(Arb.dataConnect.listIndexPathSegment(), 1..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(path = paths)) { dataConnectError -> - val expectedSubstring = dataConnectError.path.joinToString(separator = "") { "[$it]" } - dataConnectError.toString() shouldContainWithNonAbuttingText expectedSubstring - } - } - - @Test - fun `toString() should incorporate the fields and list indexes from the path`() { - // Use an example instead of Arb here because using Arb would essentially be re-writing the - // logic that is implemented in DataConnectError.toString(). - val path = - listOf( - PathSegment.Field("foo"), - PathSegment.ListIndex(99), - PathSegment.Field("bar"), - PathSegment.ListIndex(22), - PathSegment.ListIndex(33) - ) - val dataConnectError = Arb.dataConnect.dataConnectError(path = Arb.constant(path)).next() - - dataConnectError.toString() shouldContainWithNonAbuttingText "foo[99].bar[22][33]" - } - - @Test - fun `toString() should incorporate the locations`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError -> - assertSoftly { - dataConnectError.locations.forEach { - dataConnectError.toString() shouldContainWithNonAbuttingText "${it.line}:${it.column}" - } - } - } - } - - @Test - fun `equals() should return true for the exact same instance`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError -> - dataConnectError.equals(dataConnectError) shouldBe true - } - } - - @Test - fun `equals() should return true for an equal instance`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError1 -> - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = List(dataConnectError1.path.size) { dataConnectError1.path[it] }, - locations = List(dataConnectError1.locations.size) { dataConnectError1.locations[it] }, - ) - dataConnectError1.equals(dataConnectError2) shouldBe true - dataConnectError2.equals(dataConnectError1) shouldBe true - } - } - - @Test - fun `equals() should return false for null`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError -> - dataConnectError.equals(null) shouldBe false - } - } - - @Test - fun `equals() should return false for a different type`() = runTest { - val otherTypes = Arb.choice(Arb.string(), Arb.int(), Arb.dataConnect.sourceLocation()) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), otherTypes) { - dataConnectError, - other -> - dataConnectError.equals(other) shouldBe false - } - } - - @Test - fun `equals() should return false when only message differs`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), Arb.string()) { - dataConnectError1, - newMessage -> - assume(dataConnectError1.message != newMessage) - val dataConnectError2 = - DataConnectError( - message = newMessage, - path = dataConnectError1.path, - locations = dataConnectError1.locations, - ) - dataConnectError1.equals(dataConnectError2) shouldBe false - } - } - - @Test - fun `equals() should return false when message differs only in character case`() = runTest { - val message = Arb.string(1..100, Codepoint.az()) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(message = message)) { dataConnectError - -> - val dataConnectError1 = - DataConnectError( - message = dataConnectError.message.uppercase(), - path = dataConnectError.path, - locations = dataConnectError.locations, - ) - val dataConnectError2 = - DataConnectError( - message = dataConnectError.message.lowercase(), - path = dataConnectError.path, - locations = dataConnectError.locations, - ) - dataConnectError1.equals(dataConnectError2) shouldBe false - } - } - - @Test - fun `equals() should return false when path differs`() = runTest { - val paths = Arb.list(Arb.dataConnect.pathSegment(), 0..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), paths) { - dataConnectError1, - otherPath -> - assume(dataConnectError1.path != otherPath) - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = otherPath, - locations = dataConnectError1.locations, - ) - dataConnectError1.equals(dataConnectError2) shouldBe false - } - } - - @Test - fun `equals() should return false when locations differ`() = runTest { - val location = Arb.list(Arb.dataConnect.sourceLocation(), 0..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), location) { - dataConnectError1, - otherLocations -> - assume(dataConnectError1.locations != otherLocations) - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = dataConnectError1.path, - locations = otherLocations, - ) - dataConnectError1.equals(dataConnectError2) shouldBe false - } - } - - @Test - fun `hashCode() should return the same value each time it is invoked on a given object`() = - runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError -> - val hashCode1 = dataConnectError.hashCode() - dataConnectError.hashCode() shouldBe hashCode1 - dataConnectError.hashCode() shouldBe hashCode1 - } - } - - @Test - fun `hashCode() should return the same value on equal objects`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError1 -> - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = dataConnectError1.path, - locations = dataConnectError1.locations, - ) - dataConnectError1.hashCode() shouldBe dataConnectError2.hashCode() - } - } - - @Test - fun `hashCode() should return a different value if message is different`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), Arb.string()) { - dataConnectError1, - newMessage -> - assume(dataConnectError1.message.hashCode() != newMessage.hashCode()) - val dataConnectError2 = - DataConnectError( - message = newMessage, - path = dataConnectError1.path, - locations = dataConnectError1.locations, - ) - dataConnectError1.hashCode() shouldNotBe dataConnectError2.hashCode() - } - } - - @Test - fun `hashCode() should return a different value if path is different`() = runTest { - val paths = Arb.list(Arb.dataConnect.pathSegment(), 0..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), paths) { dataConnectError1, newPath - -> - assume(dataConnectError1.path.hashCode() != newPath.hashCode()) - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = newPath, - locations = dataConnectError1.locations, - ) - dataConnectError1.hashCode() shouldNotBe dataConnectError2.hashCode() - } - } - - @Test - fun `hashCode() should return a different value if locations is different`() = runTest { - val locations = Arb.list(Arb.dataConnect.sourceLocation(), 0..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), locations) { - dataConnectError1, - newLocations -> - assume(dataConnectError1.locations.hashCode() != newLocations.hashCode()) - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = dataConnectError1.path, - locations = newLocations, - ) - dataConnectError1.hashCode() shouldNotBe dataConnectError2.hashCode() - } - } - - private companion object { - val propTestConfig = PropTestConfig(iterations = 20) - } -} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectPathSegmentUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectPathSegmentUnitTest.kt new file mode 100644 index 00000000000..d4029102a80 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectPathSegmentUnitTest.kt @@ -0,0 +1,226 @@ +/* + * Copyright 2025 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. + */ +@file:OptIn(ExperimentalKotest::class) +@file:Suppress("ReplaceCallWithBinaryOperator") + +package com.google.firebase.dataconnect + +import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb.fieldPathSegment as fieldPathSegmentArb +import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb.listIndexPathSegment as listIndexPathSegmentArb +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.choice +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.string +import io.kotest.property.assume +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private val propTestConfig = + PropTestConfig(iterations = 20, edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.25)) + +/** Unit tests for [DataConnectPathSegment.Field] */ +class DataConnectPathSegmentFieldUnitTest { + + @Test + fun `constructor should set field property`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { field -> + val pathSegment = DataConnectPathSegment.Field(field) + pathSegment.field shouldBeSameInstanceAs field + } + } + + @Test + fun `toString() should return a string equal to the field property`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment: DataConnectPathSegment.Field -> + pathSegment.toString() shouldBeSameInstanceAs pathSegment.field + } + } + + @Test + fun `equals() should return true for the exact same instance`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment: DataConnectPathSegment.Field -> + pathSegment.equals(pathSegment) shouldBe true + } + } + + @Test + fun `equals() should return true for an equal instance`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment1: DataConnectPathSegment.Field -> + val pathSegment2 = DataConnectPathSegment.Field(pathSegment1.field) + pathSegment1.equals(pathSegment2) shouldBe true + pathSegment2.equals(pathSegment1) shouldBe true + } + } + + @Test + fun `equals() should return false for null`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment: DataConnectPathSegment.Field -> + pathSegment.equals(null) shouldBe false + } + } + + @Test + fun `equals() should return false for a different type`() = runTest { + val otherTypes = Arb.choice(Arb.string(), Arb.int(), listIndexPathSegmentArb()) + checkAll(propTestConfig, fieldPathSegmentArb(), otherTypes) { + pathSegment: DataConnectPathSegment.Field, + other -> + pathSegment.equals(other) shouldBe false + } + } + + @Test + fun `equals() should return false when field differs`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb(), fieldPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.Field, + pathSegment2: DataConnectPathSegment.Field -> + assume(pathSegment1.field != pathSegment2.field) + pathSegment1.equals(pathSegment2) shouldBe false + } + } + + @Test + fun `hashCode() should return the same value each time it is invoked on a given object`() = + runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment: DataConnectPathSegment.Field -> + val hashCode1 = pathSegment.hashCode() + pathSegment.hashCode() shouldBe hashCode1 + pathSegment.hashCode() shouldBe hashCode1 + } + } + + @Test + fun `hashCode() should return the same value on equal objects`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment1: DataConnectPathSegment.Field -> + val pathSegment2 = DataConnectPathSegment.Field(pathSegment1.field) + pathSegment1.hashCode() shouldBe pathSegment2.hashCode() + } + } + + @Test + fun `hashCode() should return a different value if field is different`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb(), fieldPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.Field, + pathSegment2: DataConnectPathSegment.Field -> + assume(pathSegment1.field.hashCode() != pathSegment2.field.hashCode()) + pathSegment1.hashCode() shouldNotBe pathSegment2.hashCode() + } + } +} + +/** Unit tests for [DataConnectPathSegment.ListIndex] */ +class DataConnectPathSegmentListIndexUnitTest { + + @Test + fun `constructor should set index property`() = runTest { + checkAll(propTestConfig, Arb.int()) { listIndex -> + val pathSegment = DataConnectPathSegment.ListIndex(listIndex) + pathSegment.index shouldBe listIndex + } + } + + @Test + fun `toString() should return a string equal to the index property`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment: DataConnectPathSegment.ListIndex -> + pathSegment.toString() shouldBe "${pathSegment.index}" + } + } + + @Test + fun `equals() should return true for the exact same instance`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment: DataConnectPathSegment.ListIndex -> + pathSegment.equals(pathSegment) shouldBe true + } + } + + @Test + fun `equals() should return true for an equal instance`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.ListIndex -> + val pathSegment2 = DataConnectPathSegment.ListIndex(pathSegment1.index) + pathSegment1.equals(pathSegment2) shouldBe true + pathSegment2.equals(pathSegment1) shouldBe true + } + } + + @Test + fun `equals() should return false for null`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment: DataConnectPathSegment.ListIndex -> + pathSegment.equals(null) shouldBe false + } + } + + @Test + fun `equals() should return false for a different type`() = runTest { + val otherTypes = Arb.choice(Arb.string(), Arb.int(), fieldPathSegmentArb()) + checkAll(propTestConfig, listIndexPathSegmentArb(), otherTypes) { + pathSegment: DataConnectPathSegment.ListIndex, + other -> + pathSegment.equals(other) shouldBe false + } + } + + @Test + fun `equals() should return false when field differs`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb(), listIndexPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.ListIndex, + pathSegment2: DataConnectPathSegment.ListIndex -> + assume(pathSegment1.index != pathSegment2.index) + pathSegment1.equals(pathSegment2) shouldBe false + } + } + + @Test + fun `hashCode() should return the same value each time it is invoked on a given object`() = + runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment: DataConnectPathSegment.ListIndex -> + val hashCode1 = pathSegment.hashCode() + pathSegment.hashCode() shouldBe hashCode1 + pathSegment.hashCode() shouldBe hashCode1 + } + } + + @Test + fun `hashCode() should return the same value on equal objects`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.ListIndex -> + val pathSegment2 = DataConnectPathSegment.ListIndex(pathSegment1.index) + pathSegment1.hashCode() shouldBe pathSegment2.hashCode() + } + } + + @Test + fun `hashCode() should return a different value if index is different`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb(), listIndexPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.ListIndex, + pathSegment2: DataConnectPathSegment.ListIndex -> + assume(pathSegment1.index.hashCode() != pathSegment2.index.hashCode()) + pathSegment1.hashCode() shouldNotBe pathSegment2.hashCode() + } + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectSettingsUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectSettingsUnitTest.kt index 98c5d441433..48c5a12f878 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectSettingsUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectSettingsUnitTest.kt @@ -20,7 +20,6 @@ package com.google.firebase.dataconnect import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.sourceLocation import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText import io.kotest.assertions.assertSoftly import io.kotest.common.ExperimentalKotest @@ -99,7 +98,7 @@ class DataConnectSettingsUnitTest { @Test fun `equals() should return false for a different type`() = runTest { - val otherTypes = Arb.choice(Arb.string(), Arb.int(), Arb.dataConnect.sourceLocation()) + val otherTypes = Arb.choice(Arb.string(), Arb.int(), Arb.dataConnect.errorPath()) checkAll(propTestConfig, Arb.dataConnect.dataConnectSettings(), otherTypes) { settings, other -> settings.equals(other) shouldBe false } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldUnitTest.kt deleted file mode 100644 index 75cf78107b3..00000000000 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldUnitTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2024 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. - */ - -@file:Suppress("ReplaceCallWithBinaryOperator") -@file:OptIn(ExperimentalKotest::class) - -package com.google.firebase.dataconnect - -import com.google.firebase.dataconnect.DataConnectError.PathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.fieldPathSegment -import io.kotest.common.ExperimentalKotest -import io.kotest.matchers.shouldBe -import io.kotest.property.Arb -import io.kotest.property.PropTestConfig -import io.kotest.property.arbitrary.choice -import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.string -import io.kotest.property.assume -import io.kotest.property.checkAll -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class PathSegmentFieldUnitTest { - - @Test - fun `field should equal the value given to the constructor`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.string()) { field -> - val segment = PathSegment.Field(field) - segment.field shouldBe field - } - } - - @Test - fun `toString() should equal the field`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.string()) { field -> - val segment = PathSegment.Field(field) - segment.toString() shouldBe field - } - } - - @Test - fun `equals() should return true for the same instance`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.fieldPathSegment()) { segment -> - segment.equals(segment) shouldBe true - } - } - - @Test - fun `equals() should return true for an equal field`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.string()) { field -> - val segment1 = PathSegment.Field(field) - val segment2 = PathSegment.Field(field) - segment1.equals(segment2) shouldBe true - } - } - - @Test - fun `equals() should return false for null`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.fieldPathSegment()) { segment -> - segment.equals(null) shouldBe false - } - } - - @Test - fun `equals() should return false for a different type`() = runTest { - val others = Arb.choice(Arb.string(), Arb.int(), Arb.dataConnect.dataConnectSettings()) - checkAll(propTestConfig, Arb.dataConnect.fieldPathSegment(), others) { segment, other -> - segment.equals(other) shouldBe false - } - } - - @Test - fun `equals() should return false for a different field`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.string(), Arb.dataConnect.string()) { field1, field2 -> - assume(field1 != field2) - val segment1 = PathSegment.Field(field1) - val segment2 = PathSegment.Field(field2) - segment1.equals(segment2) shouldBe false - } - } - - @Test - fun `hashCode() should return the same value as the field's hashCode() method`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.string()) { field -> - val segment = PathSegment.Field(field) - segment.hashCode() shouldBe field.hashCode() - } - } - - private companion object { - val propTestConfig = PropTestConfig(iterations = 20) - } -} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexUnitTest.kt deleted file mode 100644 index e8a3046d212..00000000000 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexUnitTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2024 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. - */ - -@file:Suppress("ReplaceCallWithBinaryOperator") -@file:OptIn(ExperimentalKotest::class) - -package com.google.firebase.dataconnect - -import com.google.firebase.dataconnect.DataConnectError.PathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.listIndexPathSegment -import io.kotest.common.ExperimentalKotest -import io.kotest.matchers.shouldBe -import io.kotest.property.Arb -import io.kotest.property.PropTestConfig -import io.kotest.property.arbitrary.choice -import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.string -import io.kotest.property.assume -import io.kotest.property.checkAll -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class PathSegmentListIndexUnitTest { - - @Test - fun `index should equal the value given to the constructor`() = runTest { - checkAll(propTestConfig, Arb.int()) { index -> - val segment = PathSegment.ListIndex(index) - segment.index shouldBe index - } - } - - @Test - fun `toString() should equal the index`() = runTest { - checkAll(propTestConfig, Arb.int()) { index -> - val segment = PathSegment.ListIndex(index) - segment.toString() shouldBe "$index" - } - } - - @Test - fun `equals() should return true for the same instance`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.listIndexPathSegment()) { segment -> - segment.equals(segment) shouldBe true - } - } - - @Test - fun `equals() should return true for an equal field`() = runTest { - checkAll(propTestConfig, Arb.int()) { index -> - val segment1 = PathSegment.ListIndex(index) - val segment2 = PathSegment.ListIndex(index) - segment1.equals(segment2) shouldBe true - } - } - - @Test - fun `equals() should return false for null`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.listIndexPathSegment()) { segment -> - segment.equals(null) shouldBe false - } - } - - @Test - fun `equals() should return false for a different type`() = runTest { - val others = Arb.choice(Arb.string(), Arb.int(), Arb.dataConnect.dataConnectSettings()) - checkAll(propTestConfig, Arb.dataConnect.listIndexPathSegment(), others) { segment, other -> - segment.equals(other) shouldBe false - } - } - - @Test - fun `equals() should return false for a different index`() = runTest { - checkAll(propTestConfig, Arb.int(), Arb.int()) { index1, index2 -> - assume(index1 != index2) - val segment1 = PathSegment.ListIndex(index1) - val segment2 = PathSegment.ListIndex(index2) - segment1.equals(segment2) shouldBe false - } - } - - @Test - fun `hashCode() should return the same value as the index's hashCode() method`() = runTest { - checkAll(propTestConfig, Arb.int()) { index -> - val segment = PathSegment.ListIndex(index) - segment.hashCode() shouldBe index.hashCode() - } - } - - private companion object { - val propTestConfig = PropTestConfig(iterations = 20) - } -} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectAuthUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectAuthUnitTest.kt index a30121a707f..b35b3c0a402 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectAuthUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectAuthUnitTest.kt @@ -32,6 +32,7 @@ import com.google.firebase.dataconnect.testutil.UnavailableDeferred import com.google.firebase.dataconnect.testutil.newBackgroundScopeThatAdvancesLikeForeground import com.google.firebase.dataconnect.testutil.newMockLogger import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingTextIgnoringCase import com.google.firebase.dataconnect.testutil.shouldHaveLoggedAtLeastOneMessageContaining import com.google.firebase.dataconnect.testutil.shouldHaveLoggedExactlyOneMessageContaining @@ -39,6 +40,7 @@ import com.google.firebase.dataconnect.testutil.shouldNotHaveLoggedAnyMessagesCo import com.google.firebase.inject.Deferred.DeferredHandler import com.google.firebase.internal.api.FirebaseNoSignedInUserException import io.kotest.assertions.asClue +import io.kotest.assertions.assertSoftly import io.kotest.assertions.nondeterministic.continually import io.kotest.assertions.nondeterministic.eventually import io.kotest.assertions.nondeterministic.eventuallyConfig @@ -93,11 +95,57 @@ class DataConnectAuthUnitTest { private val mockLogger = newMockLogger("ecvqkga56c") @Test - fun `close() should succeed if called on a brand new instance()`() = runTest { + fun `initialize() should succeed if called on a brand new instance`() = runTest { + val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() + } + + @Test + fun `initialize() should log a message`() = runTest { + val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() + mockLogger.shouldHaveLoggedExactlyOneMessageContaining("initialize()") + } + + @Test + fun `initialize() should throw if called twice`() = runTest { + val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() + + val exception = shouldThrow { dataConnectAuth.initialize() } + + assertSoftly { + exception.message shouldContainWithNonAbuttingText "initialize()" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "already been called" + } + } + + @Test + fun `initialize() should throw if called after close()`() = runTest { + val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.close() + + val exception = shouldThrow { dataConnectAuth.initialize() } + + assertSoftly { + exception.message shouldContainWithNonAbuttingText "initialize()" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "called after close()" + } + } + + @Test + fun `close() should succeed if called on a brand new instance`() = runTest { val dataConnectAuth = newDataConnectAuth() dataConnectAuth.close() } + @Test + fun `close() should succeed if called immediately after initialize()`() = runTest { + val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() + dataConnectAuth.close() + } + @Test fun `close() should log a message`() = runTest { val dataConnectAuth = newDataConnectAuth() @@ -110,6 +158,7 @@ class DataConnectAuthUnitTest { @Test fun `close() should cancel in-flight requests to get a token`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() coEvery { mockInternalAuthProvider.getAccessToken(any()) } coAnswers @@ -132,6 +181,7 @@ class DataConnectAuthUnitTest { @Test fun `close() should remove the IdTokenListener`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() val idTokenListenerSlot = slot() @@ -146,6 +196,7 @@ class DataConnectAuthUnitTest { @Test fun `close() should be callable multiple times, from multiple threads`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() val latch = SuspendingCountDownLatch(100) @@ -172,9 +223,34 @@ class DataConnectAuthUnitTest { dataConnectAuth.forceRefresh() } + @Test + fun `forceRefresh() should throw if invoked before initialize() close()`() = runTest { + val dataConnectAuth = newDataConnectAuth() + + val exception = shouldThrow { dataConnectAuth.forceRefresh() } + + assertSoftly { + exception.message shouldContainWithNonAbuttingText "forceRefresh()" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "initialize() must be called" + } + } + + @Test + fun `getToken() should throw if invoked before initialize() close()`() = runTest { + val dataConnectAuth = newDataConnectAuth() + + val exception = shouldThrow { dataConnectAuth.getToken(requestId) } + + assertSoftly { + exception.message shouldContainWithNonAbuttingText "getToken()" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "initialize() must be called" + } + } + @Test fun `getToken() should return null if InternalAuthProvider is not available`() = runTest { val dataConnectAuth = newDataConnectAuth(deferredInternalAuthProvider = UnavailableDeferred()) + dataConnectAuth.initialize() advanceUntilIdle() val result = dataConnectAuth.getToken(requestId) @@ -204,6 +280,7 @@ class DataConnectAuthUnitTest { @Test fun `getToken() should return null if no user is signed in`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() coEvery { mockInternalAuthProvider.getAccessToken(any()) } returns Tasks.forException(FirebaseNoSignedInUserException("j8rkghbcnz")) @@ -219,6 +296,7 @@ class DataConnectAuthUnitTest { @Test fun `getToken() should return the token returned from FirebaseAuth`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() coEvery { mockInternalAuthProvider.getAccessToken(any()) } returns taskForToken(accessToken) @@ -239,6 +317,7 @@ class DataConnectAuthUnitTest { val exception = TestException("xqtbckcn6w") val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() coEvery { mockInternalAuthProvider.getAccessToken(any()) } returns Tasks.forException(exception) @@ -260,6 +339,7 @@ class DataConnectAuthUnitTest { val exception = TestException("s4c4xr9z4p") val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() coEvery { mockInternalAuthProvider.getAccessToken(any()) } answers { throw exception } @@ -276,6 +356,7 @@ class DataConnectAuthUnitTest { @Test fun `getToken() should force refresh the access token after calling forceRefresh()`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() coEvery { mockInternalAuthProvider.getAccessToken(any()) } returns taskForToken(accessToken) @@ -297,6 +378,7 @@ class DataConnectAuthUnitTest { fun `getToken() should NOT force refresh the access token without calling forceRefresh()`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() coEvery { mockInternalAuthProvider.getAccessToken(any()) } returns taskForToken(accessToken) @@ -311,6 +393,7 @@ class DataConnectAuthUnitTest { fun `getToken() should NOT force refresh the access token after it is force refreshed`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() coEvery { mockInternalAuthProvider.getAccessToken(any()) } returns taskForToken(accessToken) @@ -328,6 +411,7 @@ class DataConnectAuthUnitTest { @Test fun `getToken() should ask for a token from FirebaseAuth on every invocation`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() val tokens = CopyOnWriteArrayList() coEvery { mockInternalAuthProvider.getAccessToken(any()) } answers @@ -343,6 +427,7 @@ class DataConnectAuthUnitTest { @Test fun `getToken() should conflate concurrent requests`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() val tokens = CopyOnWriteArrayList() coEvery { mockInternalAuthProvider.getAccessToken(any()) } answers @@ -372,6 +457,7 @@ class DataConnectAuthUnitTest { @Test fun `getToken() should re-fetch token if invalidated concurrently`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() val invocationCount = AtomicInteger(0) val tokens = CopyOnWriteArrayList().apply { add(accessToken) } @@ -406,6 +492,7 @@ class DataConnectAuthUnitTest { @Test fun `getToken() should ignore results with lower sequence number`() = runTest { val dataConnectAuth = newDataConnectAuth() + dataConnectAuth.initialize() advanceUntilIdle() val invocationCount = AtomicInteger(0) val tokens = CopyOnWriteArrayList() @@ -447,6 +534,7 @@ class DataConnectAuthUnitTest { } val dataConnectAuth = newDataConnectAuth(deferredInternalAuthProvider = deferredInternalAuthProvider) + dataConnectAuth.initialize() advanceUntilIdle() val result = dataConnectAuth.getToken(requestId) @@ -467,6 +555,7 @@ class DataConnectAuthUnitTest { val deferredInternalAuthProvider: DeferredInternalAuthProvider = mockk(relaxed = true) val dataConnectAuth = newDataConnectAuth(deferredInternalAuthProvider = deferredInternalAuthProvider) + dataConnectAuth.initialize() advanceUntilIdle() dataConnectAuth.close() val deferredInternalAuthProviderHandlerSlot = slot>() @@ -488,6 +577,7 @@ class DataConnectAuthUnitTest { val deferredInternalAuthProvider = DelayedDeferred(mockInternalAuthProvider) val dataConnectAuth = newDataConnectAuth(deferredInternalAuthProvider = deferredInternalAuthProvider) + dataConnectAuth.initialize() advanceUntilIdle() every { mockInternalAuthProvider.addIdTokenListener(any()) } answers { diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt index 61273ec0d24..5cce39e1c3c 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt @@ -13,27 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalKotest::class) + package com.google.firebase.dataconnect.core -import com.google.firebase.dataconnect.DataConnectError -import com.google.firebase.dataconnect.DataConnectException +import com.google.firebase.dataconnect.DataConnectOperationException +import com.google.firebase.dataconnect.DataConnectOperationFailureResponse.ErrorInfo +import com.google.firebase.dataconnect.DataConnectPathSegment import com.google.firebase.dataconnect.DataConnectUntypedData import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.core.DataConnectGrpcClient.OperationResult import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.deserialize +import com.google.firebase.dataconnect.core.DataConnectOperationFailureResponseImpl.ErrorInfoImpl import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule +import com.google.firebase.dataconnect.testutil.RandomSeedTestRule import com.google.firebase.dataconnect.testutil.newMockLogger import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnectError import com.google.firebase.dataconnect.testutil.property.arbitrary.iterator -import com.google.firebase.dataconnect.testutil.property.arbitrary.operationResult +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationErrors import com.google.firebase.dataconnect.testutil.property.arbitrary.proto import com.google.firebase.dataconnect.testutil.property.arbitrary.struct import com.google.firebase.dataconnect.testutil.shouldHaveLoggedExactlyOneMessageContaining +import com.google.firebase.dataconnect.testutil.shouldSatisfy import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto import com.google.firebase.dataconnect.util.ProtoUtil.encodeToStruct import com.google.firebase.dataconnect.util.ProtoUtil.toMap import com.google.protobuf.ListValue +import com.google.protobuf.Struct import com.google.protobuf.Value import google.firebase.dataconnect.proto.ExecuteMutationRequest import google.firebase.dataconnect.proto.ExecuteMutationResponse @@ -43,50 +49,57 @@ import google.firebase.dataconnect.proto.GraphqlError import google.firebase.dataconnect.proto.SourceLocation import io.grpc.Status import io.grpc.StatusException -import io.kotest.assertions.asClue +import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.maps.shouldContainExactly import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain import io.kotest.matchers.types.shouldBeSameInstanceAs import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig import io.kotest.property.RandomSource import io.kotest.property.arbitrary.Codepoint import io.kotest.property.arbitrary.alphanumeric import io.kotest.property.arbitrary.egyptianHieroglyphs import io.kotest.property.arbitrary.enum -import io.kotest.property.arbitrary.filter import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.merge import io.kotest.property.arbitrary.next import io.kotest.property.arbitrary.string -import io.kotest.property.arbs.firstName -import io.kotest.property.arbs.travel.airline +import io.kotest.property.assume import io.kotest.property.checkAll import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.mockk import io.mockk.slot import io.mockk.spyk import io.mockk.verify import java.util.concurrent.atomic.AtomicBoolean +import kotlin.reflect.KClass import kotlinx.coroutines.test.runTest import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.serializer import org.junit.Rule import org.junit.Test +private val propTestConfig = + PropTestConfig(iterations = 20, edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.25)) + class DataConnectGrpcClientUnitTest { @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @get:Rule val randomSeedTestRule = RandomSeedTestRule() - private val rs = RandomSource.default() + private val rs: RandomSource by randomSeedTestRule.rs private val projectId = Arb.dataConnect.projectId().next(rs) private val connectorConfig = Arb.dataConnect.connectorConfig().next(rs) private val requestId = Arb.dataConnect.requestId().next(rs) @@ -192,7 +205,7 @@ class DataConnectGrpcClientUnitTest { dataConnectGrpcClient.executeQuery(requestId, operationName, variables, callerSdkType) operationResult shouldBe - OperationResult(data = responseData, errors = responseErrors.map { it.dataConnectError }) + OperationResult(data = responseData, errors = responseErrors.map { it.errorInfo }) } @Test @@ -209,7 +222,7 @@ class DataConnectGrpcClientUnitTest { dataConnectGrpcClient.executeMutation(requestId, operationName, variables, callerSdkType) operationResult shouldBe - OperationResult(data = responseData, errors = responseErrors.map { it.dataConnectError }) + OperationResult(data = responseData, errors = responseErrors.map { it.errorInfo }) } @Test @@ -492,7 +505,7 @@ class DataConnectGrpcClientUnitTest { private data class GraphqlErrorInfo( val graphqlError: GraphqlError, - val dataConnectError: DataConnectError, + val errorInfo: ErrorInfoImpl, ) { companion object { private val randomPathComponents = @@ -510,28 +523,24 @@ class DataConnectGrpcClientUnitTest { fun random(rs: RandomSource): GraphqlErrorInfo { - val dataConnectErrorPath = mutableListOf() + val dataConnectErrorPath = mutableListOf() val graphqlErrorPath = ListValue.newBuilder() repeat(6) { if (rs.random.nextFloat() < 0.33f) { val pathComponent = randomInts.next(rs) - dataConnectErrorPath.add(DataConnectError.PathSegment.ListIndex(pathComponent)) + dataConnectErrorPath.add(DataConnectPathSegment.ListIndex(pathComponent)) graphqlErrorPath.addValues(Value.newBuilder().setNumberValue(pathComponent.toDouble())) } else { val pathComponent = randomPathComponents.next(rs) - dataConnectErrorPath.add(DataConnectError.PathSegment.Field(pathComponent)) + dataConnectErrorPath.add(DataConnectPathSegment.Field(pathComponent)) graphqlErrorPath.addValues(Value.newBuilder().setStringValue(pathComponent)) } } - val dataConnectErrorLocations = mutableListOf() val graphqlErrorLocations = mutableListOf() repeat(3) { val line = randomInts.next(rs) val column = randomInts.next(rs) - dataConnectErrorLocations.add( - DataConnectError.SourceLocation(line = line, column = column) - ) graphqlErrorLocations.add( SourceLocation.newBuilder().setLine(line).setColumn(column).build() ) @@ -547,14 +556,13 @@ class DataConnectGrpcClientUnitTest { } .build() - val dataConnectError = - DataConnectError( + val errorInfo = + ErrorInfoImpl( message = message, path = dataConnectErrorPath.toList(), - locations = dataConnectErrorLocations.toList() ) - return GraphqlErrorInfo(graphqlError, dataConnectError) + return GraphqlErrorInfo(graphqlError, errorInfo) } } } @@ -563,69 +571,147 @@ class DataConnectGrpcClientUnitTest { @Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_AGAINST_NOT_NOTHING_EXPECTED_TYPE") class DataConnectGrpcClientOperationResultUnitTest { - private val rs = RandomSource.default() - @Test fun `deserialize() should ignore the module given with DataConnectUntypedData`() { - val errors = listOf(Arb.dataConnect.dataConnectError().next()) - val operationResult = OperationResult(buildStructProto { put("foo", 42.0) }, errors) + val data = buildStructProto { put("foo", 42.0) } + val errors = Arb.dataConnect.operationErrors().next() + val operationResult = OperationResult(data, errors) val result = operationResult.deserialize(DataConnectUntypedData, mockk()) - result shouldBe DataConnectUntypedData(mapOf("foo" to 42.0), errors) + result.shouldHaveDataAndErrors(data, errors) } @Test - fun `deserialize() should treat DataConnectUntypedData specially`() = runTest { - checkAll(iterations = 20, Arb.dataConnect.operationResult()) { operationResult -> + fun `deserialize() with null data should treat DataConnectUntypedData specially`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrors()) { errors -> + val operationResult = OperationResult(null, errors) val result = operationResult.deserialize(DataConnectUntypedData, serializersModule = null) + result.shouldHaveDataAndErrors(null, errors) + } + } - result.asClue { - if (operationResult.data === null) { - it.data.shouldBeNull() - } else { - it.data shouldBe operationResult.data.toMap() - } - it.errors shouldContainExactly operationResult.errors - } + @Test + fun `deserialize() with non-null data should treat DataConnectUntypedData specially`() = runTest { + checkAll(propTestConfig, Arb.proto.struct(), Arb.dataConnect.operationErrors()) { data, errors + -> + val operationResult = OperationResult(data, errors) + val result = operationResult.deserialize(DataConnectUntypedData, serializersModule = null) + result.shouldHaveDataAndErrors(data, errors) } } @Test - fun `deserialize() should throw if one or more errors and data is null`() = runTest { - val arb = - Arb.dataConnect - .operationResult() - .filter { it.errors.isNotEmpty() } - .map { it.copy(data = null) } - checkAll(iterations = 5, arb) { operationResult -> - val exception = - shouldThrow { - operationResult.deserialize(mockk(), serializersModule = null) - } - exception.message shouldContain "${operationResult.errors}" + fun `deserialize() successfully deserializes`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { fooValue -> + val dataStruct = buildStructProto { put("foo", fooValue) } + val operationResult = OperationResult(dataStruct, emptyList()) + + val deserializedData = operationResult.deserialize(serializer(), null) + + deserializedData shouldBe TestData(fooValue) } } @Test - fun `deserialize() should throw if one or more errors and data is _not_ null`() = runTest { - val arb = - Arb.dataConnect.operationResult().filter { it.data !== null && it.errors.isNotEmpty() } - checkAll(iterations = 5, arb) { operationResult -> - val exception = - shouldThrow { + fun `deserialize() should throw if one or more errors and data is null`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrors(range = 1..10)) { errors -> + val operationResult = OperationResult(null, errors) + val exception: DataConnectOperationException = + shouldThrow { operationResult.deserialize(mockk(), serializersModule = null) } - exception.message shouldContain "${operationResult.errors}" + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = errors.toString(), + expectedCause = null, + expectedRawData = null, + expectedData = null, + expectedErrors = errors, + ) } } + @Test + fun `deserialize() should throw if one or more errors, data is NOT null, and decoding fails`() = + runTest { + checkAll( + propTestConfig, + Arb.proto.struct(), + Arb.dataConnect.operationErrors(range = 1..10) + ) { dataStruct, errors -> + val operationResult = OperationResult(dataStruct, errors) + val exception: DataConnectOperationException = + shouldThrow { + operationResult.deserialize(mockk(), serializersModule = null) + } + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = errors.toString(), + expectedCause = null, + expectedRawData = dataStruct, + expectedData = null, + expectedErrors = errors, + ) + } + } + + @Test + fun `deserialize() should throw if one or more errors, data is NOT null, and decoding succeeds`() = + runTest { + checkAll( + propTestConfig, + Arb.dataConnect.string(), + Arb.dataConnect.operationErrors(range = 1..10) + ) { fooValue, errors -> + val dataStruct = buildStructProto { put("foo", fooValue) } + val operationResult = OperationResult(dataStruct, errors) + val exception: DataConnectOperationException = + shouldThrow { + operationResult.deserialize(serializer(), serializersModule = null) + } + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = errors.toString(), + expectedCause = null, + expectedRawData = dataStruct, + expectedData = TestData(fooValue), + expectedErrors = errors, + ) + } + } + @Test fun `deserialize() should throw if data is null and errors is empty`() { - val operationResult = OperationResult(data = null, errors = emptyList()) - val exception = - shouldThrow { - operationResult.deserialize(mockk(), serializersModule = null) + val operationResult = OperationResult(null, emptyList()) + val exception: DataConnectOperationException = + shouldThrow { + operationResult.deserialize(serializer(), serializersModule = null) } - exception.message shouldContain "no data" + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "no data was included", + expectedCause = null, + expectedRawData = null, + expectedData = null, + expectedErrors = emptyList(), + ) + } + + @Test + fun `deserialize() should throw if decoding fails and error list is empty`() = runTest { + checkAll(propTestConfig, Arb.proto.struct()) { dataStruct -> + assume(!dataStruct.containsFields("foo")) + val operationResult = OperationResult(dataStruct, emptyList()) + val exception: DataConnectOperationException = + shouldThrow { + operationResult.deserialize(serializer(), serializersModule = null) + } + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "decoding data from the server's response failed", + expectedCause = SerializationException::class, + expectedRawData = dataStruct, + expectedData = null, + expectedErrors = emptyList(), + ) + } } @Test @@ -642,51 +728,50 @@ class DataConnectGrpcClientOperationResultUnitTest { slot.captured.serializersModule shouldBeSameInstanceAs serializersModule } - @Test - fun `deserialize() successfully deserializes`() = runTest { - val testData = TestData(Arb.firstName().next().name) - val operationResult = OperationResult(encodeToStruct(testData), errors = emptyList()) - - val deserializedData = operationResult.deserialize(serializer(), null) - - deserializedData shouldBe testData - } - - @Test - fun `deserialize() throws if decoding fails`() = runTest { - val data = Arb.proto.struct().next(rs) - val operationResult = OperationResult(data, errors = emptyList()) - shouldThrow { operationResult.deserialize(serializer(), null) } - } - - @Test - fun `deserialize() re-throws DataConnectException`() = runTest { - val data = encodeToStruct(TestData("fe45zhyd3m")) - val operationResult = OperationResult(data = data, errors = emptyList()) - val deserializer: DeserializationStrategy = spyk(serializer()) - val exception = DataConnectException(message = Arb.airline().next().name) - every { deserializer.deserialize(any()) } throws (exception) - - val thrownException = - shouldThrow { operationResult.deserialize(deserializer, null) } - - thrownException shouldBeSameInstanceAs exception - } - - @Test - fun `deserialize() wraps non-DataConnectException in DataConnectException`() = runTest { - val data = encodeToStruct(TestData("rbmkny6b4r")) - val operationResult = OperationResult(data = data, errors = emptyList()) - val deserializer: DeserializationStrategy = spyk(serializer()) - class MyException : Exception("y3cx44q43q") - val exception = MyException() - every { deserializer.deserialize(any()) } throws (exception) + @Serializable data class TestData(val foo: String) - val thrownException = - shouldThrow { operationResult.deserialize(deserializer, null) } + private companion object { + + fun DataConnectOperationException.shouldSatisfy( + expectedMessageSubstringCaseInsensitive: String, + expectedMessageSubstringCaseSensitive: String? = null, + expectedCause: KClass<*>?, + expectedRawData: Struct?, + expectedData: T?, + expectedErrors: List, + ) = + shouldSatisfy( + expectedMessageSubstringCaseInsensitive = expectedMessageSubstringCaseInsensitive, + expectedMessageSubstringCaseSensitive = expectedMessageSubstringCaseSensitive, + expectedCause = expectedCause, + expectedRawData = expectedRawData?.toMap(), + expectedData = expectedData, + expectedErrors = expectedErrors, + ) + + fun DataConnectUntypedData.shouldHaveDataAndErrors( + expectedData: Map, + expectedErrors: List, + ) { + assertSoftly { + withClue("data") { data.shouldNotBeNull().shouldContainExactly(expectedData) } + withClue("errors") { errors shouldContainExactly expectedErrors } + } + } - thrownException.cause shouldBeSameInstanceAs exception + fun DataConnectUntypedData.shouldHaveDataAndErrors( + expectedData: Struct, + expectedErrors: List, + ) = shouldHaveDataAndErrors(expectedData.toMap(), expectedErrors) + + fun DataConnectUntypedData.shouldHaveDataAndErrors( + @Suppress("UNUSED_PARAMETER") expectedData: Nothing?, + expectedErrors: List, + ) { + assertSoftly { + withClue("data") { data.shouldBeNull() } + withClue("errors") { errors shouldContainExactly expectedErrors } + } + } } - - @Serializable data class TestData(val foo: String) } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImplUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImplUnitTest.kt new file mode 100644 index 00000000000..994d1b405fa --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImplUnitTest.kt @@ -0,0 +1,350 @@ +/* + * Copyright 2025 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. + */ + +@file:OptIn(ExperimentalKotest::class) +@file:Suppress("ReplaceCallWithBinaryOperator") + +package com.google.firebase.dataconnect.core + +import com.google.firebase.dataconnect.DataConnectPathSegment +import com.google.firebase.dataconnect.core.DataConnectOperationFailureResponseImpl.ErrorInfoImpl +import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb.errorPath as errorPathArb +import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb.fieldPathSegment as fieldPathSegmentArb +import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb.listIndexPathSegment as listIndexPathSegmentArb +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationData +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationErrorInfo +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationErrors +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationFailureResponseImpl +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationRawData +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldEndWith +import io.kotest.matchers.string.shouldStartWith +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.bind +import io.kotest.property.arbitrary.choice +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.string +import io.kotest.property.assume +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private val propTestConfig = + PropTestConfig(iterations = 20, edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.25)) + +/** Unit tests for [DataConnectOperationFailureResponseImpl] */ +class DataConnectOperationFailureResponseImplUnitTest { + + @Test + fun `constructor should set properties to the given values`() = runTest { + checkAll( + propTestConfig, + Arb.dataConnect.operationRawData(), + Arb.dataConnect.operationData(), + Arb.dataConnect.operationErrors() + ) { rawData, data, errors -> + val response = DataConnectOperationFailureResponseImpl(rawData, data, errors) + assertSoftly { + withClue("rawData") { response.rawData shouldBeSameInstanceAs rawData } + withClue("data") { response.data shouldBeSameInstanceAs data } + withClue("errors") { response.errors shouldBeSameInstanceAs errors } + } + } + } + + @Test + fun `toString() should incorporate property values`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationFailureResponseImpl()) { + response: DataConnectOperationFailureResponseImpl<*> -> + val toStringResult = response.toString() + assertSoftly { + toStringResult shouldStartWith "DataConnectOperationFailureResponseImpl(" + toStringResult shouldEndWith ")" + toStringResult shouldContainWithNonAbuttingText "rawData=${response.rawData}" + toStringResult shouldContainWithNonAbuttingText "data=${response.data}" + toStringResult shouldContainWithNonAbuttingText "errors=${response.errors}" + } + } + } +} + +/** Unit tests for [DataConnectOperationFailureResponseImpl.ErrorInfoImpl] */ +class DataConnectOperationFailureResponseImplErrorInfoImplUnitTest { + + @Test + fun `constructor should set properties to the given values`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), errorPathArb()) { message, path -> + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.message shouldBeSameInstanceAs message + errorInfo.path shouldBeSameInstanceAs path + } + } + + @Test + fun `toString() should return an empty string if both message and path are empty`() { + val errorInfo = ErrorInfoImpl("", emptyList()) + errorInfo.toString() shouldBe "" + } + + @Test + fun `toString() should return the message if message is non-empty and path is empty`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { message -> + val errorInfo = ErrorInfoImpl(message, emptyList()) + errorInfo.toString() shouldBe message + } + } + + @Test + fun `toString() should not do anything different with an empty message`() = runTest { + checkAll(propTestConfig, errorPathArb()) { path -> + assume(path.isNotEmpty()) + val errorInfo = ErrorInfoImpl("", path) + val errorInfoToStringResult = errorInfo.toString() + errorInfoToStringResult shouldEndWith ": " + path.forEachIndexed { index, pathSegment -> + withClue("path[$index]") { + errorInfoToStringResult shouldContainWithNonAbuttingText pathSegment.toString() + } + } + } + } + + @Test + fun `toString() should print field path segments separated by dots`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), Arb.list(fieldPathSegmentArb(), 1..10)) { + message, + path -> + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe path.joinToString(".") + ": $message" + } + } + + @Test + fun `toString() should print list index path segments separated by dots`() = runTest { + checkAll( + propTestConfig, + Arb.dataConnect.string(), + Arb.list(listIndexPathSegmentArb(), 1..10) + ) { message, path -> + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe path.joinToString("") { "[${it.index}]" } + ": $message" + } + } + + @Test + fun `toString() for path is field, listIndex`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.listIndex1) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe "${segments.field1.field}[${segments.listIndex1}]: $message" + } + } + + @Test + fun `toString() for path is listIndex, field`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.listIndex1, segments.field1) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe "[${segments.listIndex1}].${segments.field1.field}: $message" + } + } + + @Test + fun `toString() for path is field, listIndex, field`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.listIndex1, segments.field2) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe + "${segments.field1.field}[${segments.listIndex1}].${segments.field2.field}: $message" + } + } + + @Test + fun `toString() for path is field, listIndex, listIndex`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.listIndex1, segments.listIndex2) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe + "${segments.field1.field}[${segments.listIndex1}][${segments.listIndex2}]: $message" + } + } + + @Test + fun `toString() for path is field, field, listIndex`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.field2, segments.listIndex1) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe + "${segments.field1.field}.${segments.field2.field}[${segments.listIndex1}]: $message" + } + } + + @Test + fun `toString() for path is field, listIndex, field, listIndex`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.listIndex1, segments.field2, segments.listIndex2) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe + "${segments.field1.field}[${segments.listIndex1}].${segments.field2.field}[${segments.listIndex2}]: $message" + } + } + + @Test + fun `toString() for path is field, listIndex, listIndex, field`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.listIndex1, segments.listIndex2, segments.field2) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe + "${segments.field1.field}[${segments.listIndex1}][${segments.listIndex2}].${segments.field2.field}: $message" + } + } + + @Test + fun `equals() should return true for the exact same instance`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo()) { errorInfo: ErrorInfoImpl -> + errorInfo.equals(errorInfo) shouldBe true + } + } + + @Test + fun `equals() should return true for an equal instance`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo()) { errorInfo1: ErrorInfoImpl -> + val errorInfo2 = ErrorInfoImpl(errorInfo1.message, errorInfo1.path) + errorInfo1.equals(errorInfo2) shouldBe true + errorInfo2.equals(errorInfo1) shouldBe true + } + } + + @Test + fun `equals() should return false for null`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo()) { errorInfo: ErrorInfoImpl -> + errorInfo.equals(null) shouldBe false + } + } + + @Test + fun `equals() should return false for a different type`() = runTest { + val otherTypes = Arb.choice(Arb.string(), Arb.int(), listIndexPathSegmentArb()) + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo(), otherTypes) { + errorInfo: ErrorInfoImpl, + other -> + errorInfo.equals(other) shouldBe false + } + } + + @Test + fun `equals() should return false when message differs`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo(), Arb.dataConnect.string()) { + errorInfo1: ErrorInfoImpl, + otherMessage: String -> + assume(errorInfo1.message != otherMessage) + val errorInfo2 = ErrorInfoImpl(otherMessage, errorInfo1.path) + errorInfo1.equals(errorInfo2) shouldBe false + } + } + + @Test + fun `equals() should return false when path differs`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo(), errorPathArb()) { + errorInfo1: ErrorInfoImpl, + otherPath: List -> + assume(errorInfo1.path != otherPath) + val errorInfo2 = ErrorInfoImpl(errorInfo1.message, otherPath) + errorInfo1.equals(errorInfo2) shouldBe false + } + } + + @Test + fun `hashCode() should return the same value each time it is invoked on a given object`() = + runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo()) { errorInfo: ErrorInfoImpl -> + val hashCode1 = errorInfo.hashCode() + errorInfo.hashCode() shouldBe hashCode1 + errorInfo.hashCode() shouldBe hashCode1 + } + } + + @Test + fun `hashCode() should return the same value on equal objects`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo()) { errorInfo1: ErrorInfoImpl -> + val errorInfo2 = ErrorInfoImpl(errorInfo1.message, errorInfo1.path) + errorInfo1.hashCode() shouldBe errorInfo2.hashCode() + } + } + + @Test + fun `hashCode() should return a different value if message is different`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo(), Arb.dataConnect.string()) { + errorInfo1: ErrorInfoImpl, + otherMessage: String -> + assume(errorInfo1.message.hashCode() != otherMessage.hashCode()) + val errorInfo2 = ErrorInfoImpl(otherMessage, errorInfo1.path) + errorInfo1.equals(errorInfo2) shouldBe false + } + } + + @Test + fun `hashCode() should return a different value if path is different`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo(), errorPathArb()) { + errorInfo1: ErrorInfoImpl, + otherPath: List -> + assume(errorInfo1.path.hashCode() != otherPath.hashCode()) + val errorInfo2 = ErrorInfoImpl(errorInfo1.message, otherPath) + errorInfo1.equals(errorInfo2) shouldBe false + } + } +} + +private object MyArb { + + fun samplePathSegments( + field: Arb = fieldPathSegmentArb(), + listIndex: Arb = listIndexPathSegmentArb(), + ): Arb = + Arb.bind(field, field, listIndex, listIndex) { field1, field2, listIndex1, listIndex2 -> + SamplePathSegments(field1, field2, listIndex1, listIndex2) + } + + data class SamplePathSegments( + val field1: DataConnectPathSegment.Field, + val field2: DataConnectPathSegment.Field, + val listIndex1: DataConnectPathSegment.ListIndex, + val listIndex2: DataConnectPathSegment.ListIndex, + ) +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/MutationRefImplUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/MutationRefImplUnitTest.kt index f61a824630e..ff43c85ad01 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/MutationRefImplUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/MutationRefImplUnitTest.kt @@ -26,9 +26,9 @@ import com.google.firebase.dataconnect.core.DataConnectGrpcClient.OperationResul import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb import com.google.firebase.dataconnect.testutil.property.arbitrary.OperationRefConstructorArguments import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnectError import com.google.firebase.dataconnect.testutil.property.arbitrary.mock import com.google.firebase.dataconnect.testutil.property.arbitrary.mutationRefImpl +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationErrors import com.google.firebase.dataconnect.testutil.property.arbitrary.operationRefConstructorArguments import com.google.firebase.dataconnect.testutil.property.arbitrary.operationRefImpl import com.google.firebase.dataconnect.testutil.property.arbitrary.queryRefImpl @@ -37,7 +37,6 @@ import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto import com.google.firebase.dataconnect.util.ProtoUtil.encodeToStruct import com.google.firebase.dataconnect.util.ProtoUtil.toStructProto -import com.google.firebase.dataconnect.util.SuspendingLazy import com.google.protobuf.Struct import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow @@ -181,7 +180,7 @@ class MutationRefImplUnitTest { @Test fun `execute() handles DataConnectUntypedVariables and DataConnectUntypedData`() = runTest { val variables = DataConnectUntypedVariables("foo" to 42.0) - val errors = listOf(Arb.dataConnect.dataConnectError().next()) + val errors = Arb.dataConnect.operationErrors().next() val data = DataConnectUntypedData(mapOf("bar" to 24.0), errors) val variablesSlot: CapturingSlot = slot() val operationResult = OperationResult(buildStructProto { put("bar", 24.0) }, errors) @@ -673,18 +672,16 @@ class MutationRefImplUnitTest { ): FirebaseDataConnectInternal = mockk(relaxed = true) { every { blockingDispatcher } returns UnconfinedTestDispatcher(testScheduler) - every { lazyGrpcClient } returns - SuspendingLazy { - mockk { - coEvery { - executeMutation( - capture(requestIdSlot), - capture(operationNameSlot), - capture(variablesSlot), - capture(callerSdkTypeSlot), - ) - } returns result.getOrThrow() - } + every { grpcClient } returns + mockk { + coEvery { + executeMutation( + capture(requestIdSlot), + capture(operationNameSlot), + capture(variablesSlot), + capture(callerSdkTypeSlot), + ) + } returns result.getOrThrow() } } } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/QueryRefImplUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/QueryRefImplUnitTest.kt index fc6baffe601..904779cb4ce 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/QueryRefImplUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/QueryRefImplUnitTest.kt @@ -29,7 +29,6 @@ import com.google.firebase.dataconnect.testutil.property.arbitrary.queryRefImpl import com.google.firebase.dataconnect.testutil.property.arbitrary.shouldHavePropertiesEqualTo import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText import com.google.firebase.dataconnect.util.SequencedReference -import com.google.firebase.dataconnect.util.SuspendingLazy import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.withClue @@ -577,11 +576,9 @@ class QueryRefImplUnitTest { querySlot: CapturingSlot> ): FirebaseDataConnectInternal = mockk(relaxed = true) { - every { lazyQueryManager } returns - SuspendingLazy { - mockk { - coEvery { execute(capture(querySlot)) } returns SequencedReference(123, result) - } + every { queryManager } returns + mockk { + coEvery { execute(capture(querySlot)) } returns SequencedReference(123, result) } } } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt index 6bc0c45d3bc..2598d176e47 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt @@ -205,7 +205,11 @@ class LocalDateSerializerUnitTest { } fun Arb.Companion.unparseableDash(): Arb { - val invalidString = string(1..5, codepoints.filterNot { it.value == '-'.code }) + val invalidString = + string( + 1..5, + codepoints.filterNot { it.value == '-'.code || it.value in '0'.code..'9'.code } + ) return arbitrary { rs -> val flags = Array(3) { rs.random.nextBoolean() } if (!flags[0]) { diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt index 44c2a5a4720..89b2c89bb0f 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt @@ -18,20 +18,22 @@ package com.google.firebase.dataconnect.testutil.property.arbitrary -import com.google.firebase.dataconnect.DataConnectError -import com.google.firebase.dataconnect.DataConnectError.PathSegment +import com.google.firebase.dataconnect.DataConnectPathSegment import com.google.firebase.dataconnect.FirebaseDataConnect.CallerSdkType import com.google.firebase.dataconnect.OperationRef import com.google.firebase.dataconnect.core.DataConnectAppCheck import com.google.firebase.dataconnect.core.DataConnectAuth import com.google.firebase.dataconnect.core.DataConnectGrpcClient import com.google.firebase.dataconnect.core.DataConnectGrpcMetadata +import com.google.firebase.dataconnect.core.DataConnectOperationFailureResponseImpl +import com.google.firebase.dataconnect.core.DataConnectOperationFailureResponseImpl.ErrorInfoImpl import com.google.firebase.dataconnect.core.FirebaseDataConnectImpl import com.google.firebase.dataconnect.core.FirebaseDataConnectInternal import com.google.firebase.dataconnect.core.MutationRefImpl import com.google.firebase.dataconnect.core.OperationRefImpl import com.google.firebase.dataconnect.core.QueryRefImpl import com.google.firebase.dataconnect.testutil.StubOperationRefImpl +import com.google.firebase.dataconnect.util.ProtoUtil.toMap import com.google.protobuf.Struct import io.kotest.assertions.assertSoftly import io.kotest.assertions.withClue @@ -40,11 +42,12 @@ import io.kotest.property.Arb import io.kotest.property.arbitrary.Codepoint import io.kotest.property.arbitrary.alphanumeric import io.kotest.property.arbitrary.arbitrary -import io.kotest.property.arbitrary.choice +import io.kotest.property.arbitrary.bind import io.kotest.property.arbitrary.constant import io.kotest.property.arbitrary.enum import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.orNull import io.kotest.property.arbitrary.string import io.mockk.mockk @@ -75,36 +78,39 @@ internal fun DataConnectArb.dataConnectGrpcMetadata( ) } -internal fun DataConnectArb.fieldPathSegment( - string: Arb = string() -): Arb = arbitrary { PathSegment.Field(string.bind()) } +internal fun DataConnectArb.operationErrorInfo( + message: Arb = string(), + path: Arb> = errorPath(), +): Arb = + Arb.bind(message, path) { message0, path0 -> ErrorInfoImpl(message0, path0) } -internal fun DataConnectArb.listIndexPathSegment( - int: Arb = Arb.int() -): Arb = arbitrary { PathSegment.ListIndex(int.bind()) } +internal fun DataConnectArb.operationRawData(): Arb?> = + Arb.proto.struct().map { it.toMap() }.orNull(nullProbability = 0.33) -internal fun DataConnectArb.pathSegment(): Arb = - Arb.choice(fieldPathSegment(), listIndexPathSegment()) +internal data class SampleOperationData(val value: String) -internal fun DataConnectArb.sourceLocation( - line: Arb = Arb.int(), - column: Arb = Arb.int() -): Arb = arbitrary { - DataConnectError.SourceLocation(line = line.bind(), column = column.bind()) -} +internal fun DataConnectArb.operationData(): Arb = + string().map { SampleOperationData(it) }.orNull(nullProbability = 0.33) -internal fun DataConnectArb.dataConnectError( - message: Arb = string(), - path: Arb> = Arb.list(pathSegment(), 0..5), - locations: Arb> = Arb.list(sourceLocation(), 0..5) -): Arb = arbitrary { - DataConnectError(message = message.bind(), path = path.bind(), locations = locations.bind()) -} +internal fun DataConnectArb.operationErrors( + errorInfoImpl: Arb = operationErrorInfo(), + range: IntRange = 0..10, +): Arb> = Arb.list(errorInfoImpl, range) + +internal fun DataConnectArb.operationFailureResponseImpl( + rawData: Arb?> = operationRawData(), + data: Arb = operationData(), + errors: Arb> = operationErrors(), +): Arb> = + Arb.bind(rawData, data, errors) { rawData0, data0, errors0 -> + DataConnectOperationFailureResponseImpl(rawData0, data0, errors0) + } internal fun DataConnectArb.operationResult( data: Arb = Arb.proto.struct().orNull(nullProbability = 0.2), - errors: Arb> = Arb.list(dataConnectError(), 0..3), -) = arbitrary { DataConnectGrpcClient.OperationResult(data.bind(), errors.bind()) } + errors: Arb> = operationErrors(), +) = + Arb.bind(data, errors) { data0, errors0 -> DataConnectGrpcClient.OperationResult(data0, errors0) } internal fun DataConnectArb.queryRefImpl( variables: Arb, diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectOperationExceptionTestUtils.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectOperationExceptionTestUtils.kt new file mode 100644 index 00000000000..ac1ec9de005 --- /dev/null +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectOperationExceptionTestUtils.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2025 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.firebase.dataconnect.testutil + +import com.google.firebase.dataconnect.DataConnectOperationException +import com.google.firebase.dataconnect.DataConnectOperationFailureResponse.ErrorInfo +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.withClue +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import kotlin.reflect.KClass + +fun DataConnectOperationException.shouldSatisfy( + expectedMessageSubstringCaseInsensitive: String, + expectedMessageSubstringCaseSensitive: String? = null, + expectedCause: KClass<*>?, + expectedRawData: Map?, + expectedData: T?, + expectedErrors: List, +): Unit = + shouldSatisfy( + expectedMessageSubstringCaseInsensitive = expectedMessageSubstringCaseInsensitive, + expectedMessageSubstringCaseSensitive = expectedMessageSubstringCaseSensitive, + expectedCause = expectedCause, + expectedRawData = expectedRawData, + expectedData = expectedData, + errorsValidator = { it.shouldContainExactly(expectedErrors) }, + ) + +fun DataConnectOperationException.shouldSatisfy( + expectedMessageSubstringCaseInsensitive: String, + expectedMessageSubstringCaseSensitive: String? = null, + expectedCause: KClass<*>?, + expectedRawData: Map?, + expectedData: T?, + errorsValidator: (List) -> Unit, +): Unit { + assertSoftly { + withClue("exception.message") { + message shouldContainWithNonAbuttingTextIgnoringCase expectedMessageSubstringCaseInsensitive + if (expectedMessageSubstringCaseSensitive != null) { + message shouldContainWithNonAbuttingText expectedMessageSubstringCaseSensitive + } + } + withClue("exception.cause") { + if (expectedCause == null) { + cause.shouldBeNull() + } else { + val cause = cause.shouldNotBeNull() + if (!expectedCause.isInstance(cause)) { + io.kotest.assertions.fail( + "cause was an instance of ${cause::class.qualifiedName}, " + + "but expected it to be an instance of ${expectedCause.qualifiedName}" + ) + } + } + } + withClue("exception.response.rawData") { response.rawData shouldBe expectedRawData } + withClue("exception.response.data") { response.data shouldBe expectedData } + withClue("exception.response.errors") { errorsValidator(response.errors) } + } +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/SuspendingCountDownLatch.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/SuspendingCountDownLatch.kt index 7098a390886..b7828e0c36a 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/SuspendingCountDownLatch.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/SuspendingCountDownLatch.kt @@ -19,6 +19,7 @@ package com.google.firebase.dataconnect.testutil import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update /** * An implementation of [java.util.concurrent.CountDownLatch] that suspends instead of blocking. @@ -60,14 +61,10 @@ class SuspendingCountDownLatch(count: Int) { * @throws IllegalStateException if called when the count has already reached zero. */ fun countDown(): SuspendingCountDownLatch { - while (true) { - val oldValue = _count.value - check(oldValue > 0) { "countDown() called too many times (oldValue=$oldValue)" } - - val newValue = oldValue - 1 - if (_count.compareAndSet(oldValue, newValue)) { - return this - } + _count.update { currentValue -> + check(currentValue > 0) { "countDown() called too many times (currentValue=$currentValue)" } + currentValue - 1 } + return this } } diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt index 17d63fc7ebf..4a3f89a7ba8 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt @@ -19,6 +19,7 @@ package com.google.firebase.dataconnect.testutil.property.arbitrary import com.google.firebase.dataconnect.ConnectorConfig +import com.google.firebase.dataconnect.DataConnectPathSegment import com.google.firebase.dataconnect.DataConnectSettings import io.kotest.property.Arb import io.kotest.property.arbitrary.Codepoint @@ -27,11 +28,15 @@ import io.kotest.property.arbitrary.arabic import io.kotest.property.arbitrary.arbitrary import io.kotest.property.arbitrary.ascii import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.choose import io.kotest.property.arbitrary.cyrillic import io.kotest.property.arbitrary.double import io.kotest.property.arbitrary.egyptianHieroglyphs import io.kotest.property.arbitrary.filterNot import io.kotest.property.arbitrary.hex +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.merge import io.kotest.property.arbitrary.orNull import io.kotest.property.arbitrary.string @@ -132,6 +137,24 @@ object DataConnectArb { fun serializersModule(): Arb = arbitrary { mockk() }.orNull(nullProbability = 0.333) + + fun fieldPathSegment(string: Arb = string()): Arb = + string.map { DataConnectPathSegment.Field(it) } + + fun listIndexPathSegment(int: Arb = Arb.int()): Arb = + int.map { DataConnectPathSegment.ListIndex(it) } + + fun pathSegment( + field: Arb = fieldPathSegment(), + fieldWeight: Int = 1, + listIndex: Arb = listIndexPathSegment(), + listIndexWeight: Int = 1, + ): Arb = Arb.choose(fieldWeight to field, listIndexWeight to listIndex) + + fun errorPath( + pathSegment: Arb = pathSegment(), + range: IntRange = 0..10, + ): Arb> = Arb.list(pathSegment, range) } val Arb.Companion.dataConnect: DataConnectArb diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 66fce5b35ce..29416bcf9a4 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,6 +1,26 @@ # Unreleased +# 25.1.4 +* [fixed] Fixed the `null` value handling in `whereNotEqualTo` and `whereNotIn` filters. +* [fixed] Catch exception when stream is already cancelled during close. [#6894](//github.com/firebase/firebase-android-sdk/pull/6894) + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-firestore` library. The Kotlin extensions library has no additional +updates. + +# 25.1.3 +* [fixed] Use lazy encoding in UTF-8 encoded byte comparison for strings to solve performance issues. [#6706](//github.com/firebase/firebase-android-sdk/pull/6706) +* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-firestore` library. The Kotlin extensions library has no additional +updates. + # 25.1.2 * [fixed] Fixed a server and sdk mismatch in unicode string sorting. [#6615](//github.com/firebase/firebase-android-sdk/pull/6615) diff --git a/firebase-firestore/gradle.properties b/firebase-firestore/gradle.properties index baa5399b1dc..e95453bed4d 100644 --- a/firebase-firestore/gradle.properties +++ b/firebase-firestore/gradle.properties @@ -1,2 +1,2 @@ -version=25.1.3 -latestReleasedVersion=25.1.2 +version=25.1.5 +latestReleasedVersion=25.1.4 diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java index aa9be3bcf01..3c5ad1340ae 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java @@ -91,24 +91,27 @@ public void testOrQueriesWithCompositeIndexes() { Query query = collection.where(or(greaterThan("a", 2), equalTo("b", 1))); // with one inequality: a>2 || b==1. - testHelper.assertOnlineAndOfflineResultsMatch(testHelper.query(query), "doc5", "doc2", "doc3"); + testHelper.assertOnlineAndOfflineResultsMatch( + collection, testHelper.query(query), "doc5", "doc2", "doc3"); // Test with limits (implicit order by ASC): (a==1) || (b > 0) LIMIT 2 query = collection.where(or(equalTo("a", 1), greaterThan("b", 0))).limit(2); - testHelper.assertOnlineAndOfflineResultsMatch(testHelper.query(query), "doc1", "doc2"); + testHelper.assertOnlineAndOfflineResultsMatch( + collection, testHelper.query(query), "doc1", "doc2"); // Test with limits (explicit order by): (a==1) || (b > 0) LIMIT_TO_LAST 2 // Note: The public query API does not allow implicit ordering when limitToLast is used. query = collection.where(or(equalTo("a", 1), greaterThan("b", 0))).limitToLast(2).orderBy("b"); - testHelper.assertOnlineAndOfflineResultsMatch(testHelper.query(query), "doc3", "doc4"); + testHelper.assertOnlineAndOfflineResultsMatch( + collection, testHelper.query(query), "doc3", "doc4"); // Test with limits (explicit order by ASC): (a==2) || (b == 1) ORDER BY a LIMIT 1 query = collection.where(or(equalTo("a", 2), equalTo("b", 1))).limit(1).orderBy("a"); - testHelper.assertOnlineAndOfflineResultsMatch(testHelper.query(query), "doc5"); + testHelper.assertOnlineAndOfflineResultsMatch(collection, testHelper.query(query), "doc5"); // Test with limits (explicit order by DESC): (a==2) || (b == 1) ORDER BY a LIMIT_TO_LAST 1 query = collection.where(or(equalTo("a", 2), equalTo("b", 1))).limitToLast(1).orderBy("a"); - testHelper.assertOnlineAndOfflineResultsMatch(testHelper.query(query), "doc2"); + testHelper.assertOnlineAndOfflineResultsMatch(collection, testHelper.query(query), "doc2"); } @Test @@ -771,17 +774,17 @@ public void testMultipleInequalityFromCacheAndFromServer() { // implicit AND: a != 1 && b < 2 Query query1 = testHelper.query(collection).whereNotEqualTo("a", 1).whereLessThan("b", 2); - testHelper.assertOnlineAndOfflineResultsMatch(query1, "doc2"); + testHelper.assertOnlineAndOfflineResultsMatch(collection, query1, "doc2"); // explicit AND: a != 1 && b < 2 Query query2 = testHelper.query(collection).where(and(notEqualTo("a", 1), lessThan("b", 2))); - testHelper.assertOnlineAndOfflineResultsMatch(query2, "doc2"); + testHelper.assertOnlineAndOfflineResultsMatch(collection, query2, "doc2"); // explicit AND: a < 3 && b not-in [2, 3] // Implicitly ordered by: a asc, b asc, __name__ asc Query query3 = testHelper.query(collection).where(and(lessThan("a", 3), notInArray("b", asList(2, 3)))); - testHelper.assertOnlineAndOfflineResultsMatch(query3, "doc1", "doc5", "doc2"); + testHelper.assertOnlineAndOfflineResultsMatch(collection, query3, "doc1", "doc5", "doc2"); // a <3 && b != 0, ordered by: b desc, a desc, __name__ desc Query query4 = @@ -791,11 +794,11 @@ public void testMultipleInequalityFromCacheAndFromServer() { .whereNotEqualTo("b", 0) .orderBy("b", Direction.DESCENDING) .limit(2); - testHelper.assertOnlineAndOfflineResultsMatch(query4, "doc4", "doc2"); + testHelper.assertOnlineAndOfflineResultsMatch(collection, query4, "doc4", "doc2"); // explicit OR: a>2 || b<1. Query query5 = testHelper.query(collection).where(or(greaterThan("a", 2), lessThan("b", 1))); - testHelper.assertOnlineAndOfflineResultsMatch(query5, "doc1", "doc3"); + testHelper.assertOnlineAndOfflineResultsMatch(collection, query5, "doc1", "doc3"); } @Test diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java index 796632e192e..6afbd54b60f 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java @@ -1651,24 +1651,40 @@ public void sdkOrdersQueryByDocumentIdTheSameWayOnlineAndOffline() { "a"); // Run query with snapshot listener - checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0])); + checkOnlineAndOfflineResultsMatch(colRef, orderedQuery, expectedDocIds.toArray(new String[0])); } @Test public void snapshotListenerSortsUnicodeStringsAsServer() { Map> testDocs = map( - "a", map("value", "Łukasiewicz"), - "b", map("value", "Sierpiński"), - "c", map("value", "岩澤"), - "d", map("value", "🄟"), - "e", map("value", "P"), - "f", map("value", "︒"), - "g", map("value", "🐵")); + "a", + map("value", "Łukasiewicz"), + "b", + map("value", "Sierpiński"), + "c", + map("value", "岩澤"), + "d", + map("value", "🄟"), + "e", + map("value", "P"), + "f", + map("value", "︒"), + "g", + map("value", "🐵"), + "h", + map("value", "你好"), + "i", + map("value", "你顥"), + "j", + map("value", "😁"), + "k", + map("value", "😀")); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1692,24 +1708,40 @@ public void snapshotListenerSortsUnicodeStringsAsServer() { assertTrue(getSnapshotDocIds.equals(expectedDocIds)); assertTrue(watchSnapshotDocIds.equals(expectedDocIds)); - checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0])); + checkOnlineAndOfflineResultsMatch(colRef, orderedQuery, expectedDocIds.toArray(new String[0])); } @Test public void snapshotListenerSortsUnicodeStringsInArrayAsServer() { Map> testDocs = map( - "a", map("value", Arrays.asList("Łukasiewicz")), - "b", map("value", Arrays.asList("Sierpiński")), - "c", map("value", Arrays.asList("岩澤")), - "d", map("value", Arrays.asList("🄟")), - "e", map("value", Arrays.asList("P")), - "f", map("value", Arrays.asList("︒")), - "g", map("value", Arrays.asList("🐵"))); + "a", + map("value", Arrays.asList("Łukasiewicz")), + "b", + map("value", Arrays.asList("Sierpiński")), + "c", + map("value", Arrays.asList("岩澤")), + "d", + map("value", Arrays.asList("🄟")), + "e", + map("value", Arrays.asList("P")), + "f", + map("value", Arrays.asList("︒")), + "g", + map("value", Arrays.asList("🐵")), + "h", + map("value", Arrays.asList("你好")), + "i", + map("value", Arrays.asList("你顥")), + "j", + map("value", Arrays.asList("😁")), + "k", + map("value", Arrays.asList("😀"))); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1733,24 +1765,40 @@ public void snapshotListenerSortsUnicodeStringsInArrayAsServer() { assertTrue(getSnapshotDocIds.equals(expectedDocIds)); assertTrue(watchSnapshotDocIds.equals(expectedDocIds)); - checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0])); + checkOnlineAndOfflineResultsMatch(colRef, orderedQuery, expectedDocIds.toArray(new String[0])); } @Test public void snapshotListenerSortsUnicodeStringsInMapAsServer() { Map> testDocs = map( - "a", map("value", map("foo", "Łukasiewicz")), - "b", map("value", map("foo", "Sierpiński")), - "c", map("value", map("foo", "岩澤")), - "d", map("value", map("foo", "🄟")), - "e", map("value", map("foo", "P")), - "f", map("value", map("foo", "︒")), - "g", map("value", map("foo", "🐵"))); + "a", + map("value", map("foo", "Łukasiewicz")), + "b", + map("value", map("foo", "Sierpiński")), + "c", + map("value", map("foo", "岩澤")), + "d", + map("value", map("foo", "🄟")), + "e", + map("value", map("foo", "P")), + "f", + map("value", map("foo", "︒")), + "g", + map("value", map("foo", "🐵")), + "h", + map("value", map("foo", "你好")), + "i", + map("value", map("foo", "你顥")), + "j", + map("value", map("foo", "😁")), + "k", + map("value", map("foo", "😀"))); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1774,24 +1822,40 @@ public void snapshotListenerSortsUnicodeStringsInMapAsServer() { assertTrue(getSnapshotDocIds.equals(expectedDocIds)); assertTrue(watchSnapshotDocIds.equals(expectedDocIds)); - checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0])); + checkOnlineAndOfflineResultsMatch(colRef, orderedQuery, expectedDocIds.toArray(new String[0])); } @Test public void snapshotListenerSortsUnicodeStringsInMapKeyAsServer() { Map> testDocs = map( - "a", map("value", map("Łukasiewicz", "foo")), - "b", map("value", map("Sierpiński", "foo")), - "c", map("value", map("岩澤", "foo")), - "d", map("value", map("🄟", "foo")), - "e", map("value", map("P", "foo")), - "f", map("value", map("︒", "foo")), - "g", map("value", map("🐵", "foo"))); + "a", + map("value", map("Łukasiewicz", "foo")), + "b", + map("value", map("Sierpiński", "foo")), + "c", + map("value", map("岩澤", "foo")), + "d", + map("value", map("🄟", "foo")), + "e", + map("value", map("P", "foo")), + "f", + map("value", map("︒", "foo")), + "g", + map("value", map("🐵", "foo")), + "h", + map("value", map("你好", "foo")), + "i", + map("value", map("你顥", "foo")), + "j", + map("value", map("😁", "foo")), + "k", + map("value", map("😀", "foo"))); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1815,25 +1879,90 @@ public void snapshotListenerSortsUnicodeStringsInMapKeyAsServer() { assertTrue(getSnapshotDocIds.equals(expectedDocIds)); assertTrue(watchSnapshotDocIds.equals(expectedDocIds)); - checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0])); + checkOnlineAndOfflineResultsMatch(colRef, orderedQuery, expectedDocIds.toArray(new String[0])); } @Test public void snapshotListenerSortsUnicodeStringsInDocumentKeyAsServer() { Map> testDocs = map( - "Łukasiewicz", map("value", "foo"), - "Sierpiński", map("value", "foo"), - "岩澤", map("value", "foo"), - "🄟", map("value", "foo"), - "P", map("value", "foo"), - "︒", map("value", "foo"), - "🐵", map("value", "foo")); + "Łukasiewicz", + map("value", "foo"), + "Sierpiński", + map("value", "foo"), + "岩澤", + map("value", "foo"), + "🄟", + map("value", "foo"), + "P", + map("value", "foo"), + "︒", + map("value", "foo"), + "🐵", + map("value", "foo"), + "你好", + map("value", "foo"), + "你顥", + map("value", "foo"), + "😁", + map("value", "foo"), + "😀", + map("value", "foo")); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy(FieldPath.documentId()); List expectedDocIds = - Arrays.asList("Sierpiński", "Łukasiewicz", "岩澤", "︒", "P", "🄟", "🐵"); + Arrays.asList( + "Sierpiński", "Łukasiewicz", "你好", "你顥", "岩澤", "︒", "P", "🄟", "🐵", "😀", "😁"); + + QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); + List getSnapshotDocIds = + getSnapshot.getDocuments().stream().map(ds -> ds.getId()).collect(Collectors.toList()); + + EventAccumulator eventAccumulator = new EventAccumulator(); + ListenerRegistration registration = + orderedQuery.addSnapshotListener(eventAccumulator.listener()); + + List watchSnapshotDocIds = new ArrayList<>(); + try { + QuerySnapshot watchSnapshot = eventAccumulator.await(); + watchSnapshotDocIds = + watchSnapshot.getDocuments().stream() + .map(documentSnapshot -> documentSnapshot.getId()) + .collect(Collectors.toList()); + } finally { + registration.remove(); + } + + assertTrue(getSnapshotDocIds.equals(expectedDocIds)); + assertTrue(watchSnapshotDocIds.equals(expectedDocIds)); + + checkOnlineAndOfflineResultsMatch(colRef, orderedQuery, expectedDocIds.toArray(new String[0])); + } + + @Test + public void snapshotListenerSortsInvalidUnicodeStringsAsServer() { + // Note: Protocol Buffer converts any invalid surrogates to "?". + Map> testDocs = + map( + "a", + map("value", "Z"), + "b", + map("value", "你好"), + "c", + map("value", "😀"), + "d", + map("value", "ab\uD800"), // Lone high surrogate + "e", + map("value", "ab\uDC00"), // Lone low surrogate + "f", + map("value", "ab\uD800\uD800"), // Unpaired high surrogate + "g", + map("value", "ab\uDC00\uDC00")); // Unpaired low surrogate + + CollectionReference colRef = testCollectionWithDocs(testDocs); + Query orderedQuery = colRef.orderBy("value"); + List expectedDocIds = Arrays.asList("a", "d", "e", "f", "g", "b", "c"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1857,6 +1986,6 @@ public void snapshotListenerSortsUnicodeStringsInDocumentKeyAsServer() { assertTrue(getSnapshotDocIds.equals(expectedDocIds)); assertTrue(watchSnapshotDocIds.equals(expectedDocIds)); - checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0])); + checkOnlineAndOfflineResultsMatch(colRef, orderedQuery, expectedDocIds.toArray(new String[0])); } } diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java index 29ca658515e..f6209dd49a4 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java @@ -1470,10 +1470,16 @@ public void testOrQueries() { // Two equalities: a==1 || b==1. checkOnlineAndOfflineResultsMatch( - collection.where(or(equalTo("a", 1), equalTo("b", 1))), "doc1", "doc2", "doc4", "doc5"); + collection, + collection.where(or(equalTo("a", 1), equalTo("b", 1))), + "doc1", + "doc2", + "doc4", + "doc5"); // (a==1 && b==0) || (a==3 && b==2) checkOnlineAndOfflineResultsMatch( + collection, collection.where( or(and(equalTo("a", 1), equalTo("b", 0)), and(equalTo("a", 3), equalTo("b", 2)))), "doc1", @@ -1481,19 +1487,21 @@ public void testOrQueries() { // a==1 && (b==0 || b==3). checkOnlineAndOfflineResultsMatch( + collection, collection.where(and(equalTo("a", 1), or(equalTo("b", 0), equalTo("b", 3)))), "doc1", "doc4"); // (a==2 || b==2) && (a==3 || b==3) checkOnlineAndOfflineResultsMatch( + collection, collection.where( and(or(equalTo("a", 2), equalTo("b", 2)), or(equalTo("a", 3), equalTo("b", 3)))), "doc3"); // Test with limits without orderBy (the __name__ ordering is the tie breaker). checkOnlineAndOfflineResultsMatch( - collection.where(or(equalTo("a", 2), equalTo("b", 1))).limit(1), "doc2"); + collection, collection.where(or(equalTo("a", 2), equalTo("b", 1))).limit(1), "doc2"); } @Test @@ -1510,7 +1518,11 @@ public void testOrQueriesWithIn() { // a==2 || b in [2,3] checkOnlineAndOfflineResultsMatch( - collection.where(or(equalTo("a", 2), inArray("b", asList(2, 3)))), "doc3", "doc4", "doc6"); + collection, + collection.where(or(equalTo("a", 2), inArray("b", asList(2, 3)))), + "doc3", + "doc4", + "doc6"); } @Test @@ -1527,10 +1539,15 @@ public void testOrQueriesWithArrayMembership() { // a==2 || b array-contains 7 checkOnlineAndOfflineResultsMatch( - collection.where(or(equalTo("a", 2), arrayContains("b", 7))), "doc3", "doc4", "doc6"); + collection, + collection.where(or(equalTo("a", 2), arrayContains("b", 7))), + "doc3", + "doc4", + "doc6"); // a==2 || b array-contains-any [0, 3] checkOnlineAndOfflineResultsMatch( + collection, collection.where(or(equalTo("a", 2), arrayContainsAny("b", asList(0, 3)))), "doc1", "doc4", @@ -1551,12 +1568,12 @@ public void testMultipleInOps() { // Two IN operations on different fields with disjunction. Query query1 = collection.where(or(inArray("a", asList(2, 3)), inArray("b", asList(0, 2)))); - checkOnlineAndOfflineResultsMatch(query1, "doc1", "doc3", "doc6"); + checkOnlineAndOfflineResultsMatch(collection, query1, "doc1", "doc3", "doc6"); // Two IN operations on the same field with disjunction. // a IN [0,3] || a IN [0,2] should union them (similar to: a IN [0,2,3]). Query query2 = collection.where(or(inArray("a", asList(0, 3)), inArray("a", asList(0, 2)))); - checkOnlineAndOfflineResultsMatch(query2, "doc3", "doc6"); + checkOnlineAndOfflineResultsMatch(collection, query2, "doc3", "doc6"); } @Test @@ -1573,14 +1590,14 @@ public void testUsingInWithArrayContainsAny() { Query query1 = collection.where(or(inArray("a", asList(2, 3)), arrayContainsAny("b", asList(0, 7)))); - checkOnlineAndOfflineResultsMatch(query1, "doc1", "doc3", "doc4", "doc6"); + checkOnlineAndOfflineResultsMatch(collection, query1, "doc1", "doc3", "doc4", "doc6"); Query query2 = collection.where( or( and(inArray("a", asList(2, 3)), equalTo("c", 10)), arrayContainsAny("b", asList(0, 7)))); - checkOnlineAndOfflineResultsMatch(query2, "doc1", "doc3", "doc4"); + checkOnlineAndOfflineResultsMatch(collection, query2, "doc1", "doc3", "doc4"); } @Test @@ -1596,20 +1613,20 @@ public void testUsingInWithArrayContains() { CollectionReference collection = testCollectionWithDocs(testDocs); Query query1 = collection.where(or(inArray("a", asList(2, 3)), arrayContains("b", 3))); - checkOnlineAndOfflineResultsMatch(query1, "doc3", "doc4", "doc6"); + checkOnlineAndOfflineResultsMatch(collection, query1, "doc3", "doc4", "doc6"); Query query2 = collection.where(and(inArray("a", asList(2, 3)), arrayContains("b", 7))); - checkOnlineAndOfflineResultsMatch(query2, "doc3"); + checkOnlineAndOfflineResultsMatch(collection, query2, "doc3"); Query query3 = collection.where( or(inArray("a", asList(2, 3)), and(arrayContains("b", 3), equalTo("a", 1)))); - checkOnlineAndOfflineResultsMatch(query3, "doc3", "doc4", "doc6"); + checkOnlineAndOfflineResultsMatch(collection, query3, "doc3", "doc4", "doc6"); Query query4 = collection.where( and(inArray("a", asList(2, 3)), or(arrayContains("b", 7), equalTo("a", 1)))); - checkOnlineAndOfflineResultsMatch(query4, "doc3"); + checkOnlineAndOfflineResultsMatch(collection, query4, "doc3"); } @Test @@ -1625,9 +1642,58 @@ public void testOrderByEquality() { CollectionReference collection = testCollectionWithDocs(testDocs); Query query1 = collection.where(equalTo("a", 1)).orderBy("a"); - checkOnlineAndOfflineResultsMatch(query1, "doc1", "doc4", "doc5"); + checkOnlineAndOfflineResultsMatch(collection, query1, "doc1", "doc4", "doc5"); Query query2 = collection.where(inArray("a", asList(2, 3))).orderBy("a"); - checkOnlineAndOfflineResultsMatch(query2, "doc6", "doc3"); + checkOnlineAndOfflineResultsMatch(collection, query2, "doc6", "doc3"); + } + + @Test + public void testSDKUsesNotEqualFiltersSameAsServer() { + Map> testDocs = + map( + "a", map("zip", Double.NaN), + "b", map("zip", 91102L), + "c", map("zip", 98101L), + "d", map("zip", "98101"), + "e", map("zip", asList(98101L)), + "f", map("zip", asList(98101L, 98102L)), + "g", map("zip", asList("98101", map("zip", 98101L))), + "h", map("zip", map("code", 500L)), + "i", map("zip", null), + "j", map("code", 500L)); + CollectionReference collection = testCollectionWithDocs(testDocs); + + Query query = collection.whereNotEqualTo("zip", 98101L); + checkOnlineAndOfflineResultsMatch(collection, query, "a", "b", "d", "e", "f", "g", "h"); + + query = collection.whereNotEqualTo("zip", Double.NaN); + checkOnlineAndOfflineResultsMatch(collection, query, "b", "c", "d", "e", "f", "g", "h"); + + query = collection.whereNotEqualTo("zip", null); + checkOnlineAndOfflineResultsMatch(collection, query, "a", "b", "c", "d", "e", "f", "g", "h"); + } + + @Test + public void testSDKUsesNotInFiltersSameAsServer() { + Map> testDocs = + map( + "a", map("zip", Double.NaN), + "b", map("zip", 91102L), + "c", map("zip", 98101L), + "d", map("zip", "98101"), + "e", map("zip", asList(98101L)), + "f", map("zip", asList(98101L, 98102L)), + "g", map("zip", asList("98101", map("zip", 98101L))), + "h", map("zip", map("code", 500L)), + "i", map("zip", null), + "j", map("code", 500L)); + CollectionReference collection = testCollectionWithDocs(testDocs); + + Query query = collection.whereNotIn("zip", asList(98101L, 98103L, asList(98101L, 98102L))); + checkOnlineAndOfflineResultsMatch(collection, query, "a", "b", "d", "e", "g", "h"); + + query = collection.whereNotIn("zip", nullList()); + checkOnlineAndOfflineResultsMatch(collection, query); } } diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/VectorTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/VectorTest.java index 8f51a3800d8..bab92383216 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/VectorTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/VectorTest.java @@ -323,7 +323,8 @@ public void vectorFieldOrderOnlineAndOffline() throws Exception { Query orderedQuery = randomColl.orderBy("embedding"); // Run query with snapshot listener - checkOnlineAndOfflineResultsMatch(orderedQuery, docIds.stream().toArray(String[]::new)); + checkOnlineAndOfflineResultsMatch( + randomColl, orderedQuery, docIds.stream().toArray(String[]::new)); } /** Verifies that the SDK filters vector fields the same way for online and offline queries*/ @@ -363,13 +364,15 @@ public void vectorFieldFilterOnlineAndOffline() throws Exception { .orderBy("embedding") .whereLessThan("embedding", FieldValue.vector(new double[] {1, 2, 100, 4, 4})); checkOnlineAndOfflineResultsMatch( - orderedQueryLessThan, docIds.subList(2, 11).stream().toArray(String[]::new)); + randomColl, orderedQueryLessThan, docIds.subList(2, 11).stream().toArray(String[]::new)); Query orderedQueryGreaterThan = randomColl .orderBy("embedding") .whereGreaterThan("embedding", FieldValue.vector(new double[] {1, 2, 100, 4, 4})); checkOnlineAndOfflineResultsMatch( - orderedQueryGreaterThan, docIds.subList(12, 13).stream().toArray(String[]::new)); + randomColl, + orderedQueryGreaterThan, + docIds.subList(12, 13).stream().toArray(String[]::new)); } } diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/CompositeIndexTestHelper.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/CompositeIndexTestHelper.java index 0751473ae4a..23b949c96d4 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/CompositeIndexTestHelper.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/CompositeIndexTestHelper.java @@ -122,8 +122,14 @@ private Map> prepareTestDocuments( // actual document IDs created by the test helper. @NonNull public void assertOnlineAndOfflineResultsMatch( - @NonNull Query query, @NonNull String... expectedDocs) { - checkOnlineAndOfflineResultsMatch(query, toHashedIds(expectedDocs)); + @NonNull CollectionReference collection, + @NonNull Query query, + @NonNull String... expectedDocs) { + // `checkOnlineAndOfflineResultsMatch` first makes sure all documents needed for + // `query` are in the cache. It does so making a `get` on the first argument. + // Since *all* composite index tests use the same collection, this is very inefficient to do. + // Therefore, we should only do so for tests where `TEST_ID_FIELD` matches the current test. + checkOnlineAndOfflineResultsMatch(this.query(collection), query, toHashedIds(expectedDocs)); } // Asserts that the IDs in the query snapshot matches the expected Ids. The expected document diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index a7417d96563..dd676b5f0ab 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -524,17 +524,41 @@ public static List nullList() { * documents as running the query while offline. If `expectedDocs` is provided, it also checks * that both online and offline query result is equal to the expected documents. * + * This function first performs a "get" for the entire COLLECTION from the server. + * It then performs the QUERY from CACHE which, results in `executeFullCollectionScan()` + * It then performs the QUERY from SERVER. + * It then performs the QUERY from CACHE again, which results in `performQueryUsingRemoteKeys()`. + * It then ensure that all the above QUERY results are the same. + * + * @param collection The collection on which the query is performed. * @param query The query to check * @param expectedDocs Ordered list of document keys that are expected to match the query */ - public static void checkOnlineAndOfflineResultsMatch(Query query, String... expectedDocs) { + public static void checkOnlineAndOfflineResultsMatch( + Query collection, Query query, String... expectedDocs) { + // Note: Order matters. The following has to be done in the specific order: + + // 1- Pre-populate the cache with the entire collection. + waitFor(collection.get(Source.SERVER)); + + // 2- This performs the query against the cache using full collection scan. + QuerySnapshot docsFromCacheFullCollectionScan = waitFor(query.get(Source.CACHE)); + + // 3- This goes to the server (backend/emulator). QuerySnapshot docsFromServer = waitFor(query.get(Source.SERVER)); - QuerySnapshot docsFromCache = waitFor(query.get(Source.CACHE)); - assertEquals(querySnapshotToIds(docsFromServer), querySnapshotToIds(docsFromCache)); - List expected = asList(expectedDocs); - if (!expected.isEmpty()) { - assertEquals(expected, querySnapshotToIds(docsFromCache)); + // 4- This performs the query against the cache using remote keys. + QuerySnapshot docsFromCacheUsingRemoteKeys = waitFor(query.get(Source.CACHE)); + + assertEquals( + querySnapshotToIds(docsFromServer), querySnapshotToIds(docsFromCacheFullCollectionScan)); + assertEquals( + querySnapshotToIds(docsFromServer), querySnapshotToIds(docsFromCacheUsingRemoteKeys)); + + // Expected document IDs. + List expectedDocIds = asList(expectedDocs); + if (!expectedDocIds.isEmpty()) { + assertEquals(expectedDocIds, querySnapshotToIds(docsFromServer)); } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java index 6ebf26d0718..04a6f252a80 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java @@ -115,7 +115,9 @@ public boolean matches(Document doc) { Value other = doc.getField(field); // Types do not have to match in NOT_EQUAL filters. if (operator == Operator.NOT_EQUAL) { - return other != null && this.matchesComparison(Values.compare(other, value)); + return other != null + && !other.hasNullValue() + && this.matchesComparison(Values.compare(other, value)); } // Only compare types with matching backend order (such as double and int). return other != null diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/NotInFilter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/NotInFilter.java index 1f827f688e8..2f430d7f71c 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/NotInFilter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/NotInFilter.java @@ -34,6 +34,8 @@ public boolean matches(Document doc) { return false; } Value other = doc.getField(getField()); - return other != null && !Values.contains(getValue().getArrayValue(), other); + return other != null + && !other.hasNullValue() + && !Values.contains(getValue().getArrayValue(), other); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java index c1cd01dfbe9..963cb4ca532 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java @@ -352,7 +352,21 @@ private void close(State finalState, Status status) { getClass().getSimpleName(), "(%x) Closing stream client-side", System.identityHashCode(this)); - call.halfClose(); + try { + call.halfClose(); + } catch (IllegalStateException e) { + // Secondary failure encountered. The underlying RPC has entered an error state. We will + // log and continue since the RPC is being discarded anyway. + // + // Example, "IllegalStateException: call was cancelled" was observed in + // https://github.com/firebase/firebase-android-sdk/issues/6883 + // Likely caused by other part of system already cancelling stream. + Logger.debug( + getClass().getSimpleName(), + "(%x) Closing stream client-side result in exception: [%s]", + System.identityHashCode(this), + e); + } } call = null; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java index 543da11e7d3..2cc39337002 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java @@ -87,9 +87,44 @@ public static int compareIntegers(int i1, int i2) { /** Compare strings in UTF-8 encoded byte order */ public static int compareUtf8Strings(String left, String right) { - ByteString leftBytes = ByteString.copyFromUtf8(left); - ByteString rightBytes = ByteString.copyFromUtf8(right); - return compareByteStrings(leftBytes, rightBytes); + int i = 0; + while (i < left.length() && i < right.length()) { + int leftCodePoint = left.codePointAt(i); + int rightCodePoint = right.codePointAt(i); + + if (leftCodePoint != rightCodePoint) { + if (leftCodePoint < 128 && rightCodePoint < 128) { + // ASCII comparison + return Integer.compare(leftCodePoint, rightCodePoint); + } else { + // substring and do UTF-8 encoded byte comparison + ByteString leftBytes = ByteString.copyFromUtf8(getUtf8SafeBytes(left, i)); + ByteString rightBytes = ByteString.copyFromUtf8(getUtf8SafeBytes(right, i)); + int comp = compareByteStrings(leftBytes, rightBytes); + if (comp != 0) { + return comp; + } else { + // EXTREMELY RARE CASE: Code points differ, but their UTF-8 byte representations are + // identical. This can happen with malformed input (invalid surrogate pairs), where + // Java's encoding leads to unexpected byte sequences. Meanwhile, any invalid surrogate + // inputs get converted to "?" by protocol buffer while round tripping, so we almost + // never receive invalid strings from backend. + // Fallback to code point comparison for graceful handling. + return Integer.compare(leftCodePoint, rightCodePoint); + } + } + } + // Increment by 2 for surrogate pairs, 1 otherwise. + i += Character.charCount(leftCodePoint); + } + + // Compare lengths if all characters are equal + return Integer.compare(left.length(), right.length()); + } + + private static String getUtf8SafeBytes(String str, int index) { + int firstCodePoint = str.codePointAt(index); + return str.substring(index, index + Character.charCount(firstCodePoint)); } /** diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryTest.java index de3de67463c..cdc932bfa01 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryTest.java @@ -237,7 +237,7 @@ public void testNotInFilters() { // Null match. document = doc("collection/1", 0, map("zip", null)); - assertTrue(query.matches(document)); + assertFalse(query.matches(document)); // NaN match. document = doc("collection/1", 0, map("zip", Double.NaN)); @@ -333,7 +333,7 @@ public void testNaNFilter() { assertTrue(query.matches(doc3)); assertTrue(query.matches(doc4)); assertTrue(query.matches(doc5)); - assertTrue(query.matches(doc6)); + assertFalse(query.matches(doc6)); } @Test diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java index 6ff424ef994..ccd88854ba7 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.firebase.firestore.util.Util.firstNEntries; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import com.google.firebase.firestore.testutil.TestUtil; import com.google.protobuf.ByteString; @@ -26,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Random; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -87,4 +89,184 @@ private void validateDiffCollection(List before, List after) { Util.diffCollections(before, after, String::compareTo, result::add, result::remove); assertThat(result).containsExactlyElementsIn(after); } + + @Test + public void compareUtf8StringsShouldReturnCorrectValue() { + ArrayList errors = new ArrayList<>(); + int seed = new Random().nextInt(Integer.MAX_VALUE); + int passCount = 0; + StringGenerator stringGenerator = new StringGenerator(29750468); + StringPairGenerator stringPairGenerator = new StringPairGenerator(stringGenerator); + for (int i = 0; i < 1_000_000 && errors.size() < 10; i++) { + StringPairGenerator.StringPair stringPair = stringPairGenerator.next(); + final String s1 = stringPair.s1; + final String s2 = stringPair.s2; + + int actual = Util.compareUtf8Strings(s1, s2); + + ByteString b1 = ByteString.copyFromUtf8(s1); + ByteString b2 = ByteString.copyFromUtf8(s2); + int expected = Util.compareByteStrings(b1, b2); + + if (actual == expected) { + passCount++; + } else { + errors.add( + "compareUtf8Strings(s1=\"" + + s1 + + "\", s2=\"" + + s2 + + "\") returned " + + actual + + ", but expected " + + expected + + " (i=" + + i + + ", s1.length=" + + s1.length() + + ", s2.length=" + + s2.length() + + ")"); + } + } + + if (!errors.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(errors.size()).append(" test cases failed, "); + sb.append(passCount).append(" test cases passed, "); + sb.append("seed=").append(seed).append(";"); + for (int i = 0; i < errors.size(); i++) { + sb.append("\nerrors[").append(i).append("]: ").append(errors.get(i)); + } + fail(sb.toString()); + } + } + + private static class StringPairGenerator { + + private final StringGenerator stringGenerator; + + public StringPairGenerator(StringGenerator stringGenerator) { + this.stringGenerator = stringGenerator; + } + + public StringPair next() { + String prefix = stringGenerator.next(); + String s1 = prefix + stringGenerator.next(); + String s2 = prefix + stringGenerator.next(); + return new StringPair(s1, s2); + } + + public static class StringPair { + public final String s1, s2; + + public StringPair(String s1, String s2) { + this.s1 = s1; + this.s2 = s2; + } + } + } + + private static class StringGenerator { + + private static final float DEFAULT_SURROGATE_PAIR_PROBABILITY = 0.33f; + private static final int DEFAULT_MAX_LENGTH = 20; + + private static final int MIN_HIGH_SURROGATE = 0xD800; + private static final int MAX_HIGH_SURROGATE = 0xDBFF; + private static final int MIN_LOW_SURROGATE = 0xDC00; + private static final int MAX_LOW_SURROGATE = 0xDFFF; + + private final Random rnd; + private final float surrogatePairProbability; + private final int maxLength; + + public StringGenerator(int seed) { + this(new Random(seed), DEFAULT_SURROGATE_PAIR_PROBABILITY, DEFAULT_MAX_LENGTH); + } + + public StringGenerator(Random rnd, float surrogatePairProbability, int maxLength) { + this.rnd = rnd; + this.surrogatePairProbability = validateProbability(surrogatePairProbability); + this.maxLength = validateLength(maxLength); + } + + private static float validateProbability(float probability) { + if (!Float.isFinite(probability)) { + throw new IllegalArgumentException( + "invalid surrogate pair probability: " + + probability + + " (must be between 0.0 and 1.0, inclusive)"); + } else if (probability < 0.0f) { + throw new IllegalArgumentException( + "invalid surrogate pair probability: " + + probability + + " (must be greater than or equal to zero)"); + } else if (probability > 1.0f) { + throw new IllegalArgumentException( + "invalid surrogate pair probability: " + + probability + + " (must be less than or equal to 1)"); + } + return probability; + } + + private static int validateLength(int length) { + if (length < 0) { + throw new IllegalArgumentException( + "invalid maximum string length: " + + length + + " (must be greater than or equal to zero)"); + } + return length; + } + + public String next() { + final int length = rnd.nextInt(maxLength + 1); + final StringBuilder sb = new StringBuilder(); + while (sb.length() < length) { + int codePoint = nextCodePoint(); + sb.appendCodePoint(codePoint); + } + return sb.toString(); + } + + private boolean isNextSurrogatePair() { + return nextBoolean(rnd, surrogatePairProbability); + } + + private static boolean nextBoolean(Random rnd, float probability) { + if (probability == 0.0f) { + return false; + } else if (probability == 1.0f) { + return true; + } else { + return rnd.nextFloat() < probability; + } + } + + private int nextCodePoint() { + if (isNextSurrogatePair()) { + return nextSurrogateCodePoint(); + } else { + return nextNonSurrogateCodePoint(); + } + } + + private int nextSurrogateCodePoint() { + int highSurrogate = + rnd.nextInt(MAX_HIGH_SURROGATE - MIN_HIGH_SURROGATE + 1) + MIN_HIGH_SURROGATE; + int lowSurrogate = rnd.nextInt(MAX_LOW_SURROGATE - MIN_LOW_SURROGATE + 1) + MIN_LOW_SURROGATE; + return Character.toCodePoint((char) highSurrogate, (char) lowSurrogate); + } + + private int nextNonSurrogateCodePoint() { + int codePoint; + do { + codePoint = rnd.nextInt(0x10000); // BMP range + } while (codePoint >= MIN_HIGH_SURROGATE + && codePoint <= MAX_LOW_SURROGATE); // Exclude surrogate range + return codePoint; + } + } } diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md index e9fe66c897d..72ae1363ca1 100644 --- a/firebase-functions/CHANGELOG.md +++ b/firebase-functions/CHANGELOG.md @@ -1,8 +1,35 @@ # Unreleased + + +# 21.2.1 +* [fixed] Fixed issue that caused the SDK to crash when trying to stream a function that does not exist. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-functions` library. The Kotlin extensions library has no additional +updates. + +# 21.2.0 +* [feature] Streaming callable functions are now supported. +* [fixed] Fixed an issue that prevented the App Check token from being handled correctly in case of error. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-functions` library. The Kotlin extensions library has no additional +updates. + +# 21.1.1 * [fixed] Resolve Kotlin migration visibility issues ([#6522](//github.com/firebase/firebase-android-sdk/pull/6522)) +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-functions` library. The Kotlin extensions library has no additional +updates. + # 21.1.0 * [changed] Migrated to Kotlin @@ -217,3 +244,4 @@ updates. optional region to override the default "us-central1". * [feature] New `useFunctionsEmulator` method allows testing against a local instance of the [Cloud Functions Emulator](https://firebase.google.com/docs/functions/local-emulator). + diff --git a/firebase-functions/api.txt b/firebase-functions/api.txt index a9a05c703a8..1a12a250b35 100644 --- a/firebase-functions/api.txt +++ b/firebase-functions/api.txt @@ -84,6 +84,8 @@ package com.google.firebase.functions { method public com.google.android.gms.tasks.Task call(Object? data); method public long getTimeout(); method public void setTimeout(long timeout, java.util.concurrent.TimeUnit units); + method public org.reactivestreams.Publisher stream(); + method public org.reactivestreams.Publisher stream(Object? data = null); method public com.google.firebase.functions.HttpsCallableReference withTimeout(long timeout, java.util.concurrent.TimeUnit units); property public final long timeout; } @@ -93,6 +95,21 @@ package com.google.firebase.functions { field public final Object? data; } + public abstract class StreamResponse { + } + + public static final class StreamResponse.Message extends com.google.firebase.functions.StreamResponse { + ctor public StreamResponse.Message(com.google.firebase.functions.HttpsCallableResult message); + method public com.google.firebase.functions.HttpsCallableResult getMessage(); + property public final com.google.firebase.functions.HttpsCallableResult message; + } + + public static final class StreamResponse.Result extends com.google.firebase.functions.StreamResponse { + ctor public StreamResponse.Result(com.google.firebase.functions.HttpsCallableResult result); + method public com.google.firebase.functions.HttpsCallableResult getResult(); + property public final com.google.firebase.functions.HttpsCallableResult result; + } + } package com.google.firebase.functions.ktx { diff --git a/firebase-functions/firebase-functions.gradle.kts b/firebase-functions/firebase-functions.gradle.kts index 7ec958bdd79..b1c220a6594 100644 --- a/firebase-functions/firebase-functions.gradle.kts +++ b/firebase-functions/firebase-functions.gradle.kts @@ -112,6 +112,8 @@ dependencies { implementation(libs.okhttp) implementation(libs.playservices.base) implementation(libs.playservices.basement) + api(libs.reactive.streams) + api(libs.playservices.tasks) kapt(libs.autovalue) @@ -131,6 +133,7 @@ dependencies { androidTestImplementation(libs.truth) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.kotlinx.coroutines.reactive) androidTestImplementation(libs.mockito.core) androidTestImplementation(libs.mockito.dexmaker) kapt("com.google.dagger:dagger-android-processor:2.43.2") diff --git a/firebase-functions/gradle.properties b/firebase-functions/gradle.properties index ff0fa6afed0..2c555435ffd 100644 --- a/firebase-functions/gradle.properties +++ b/firebase-functions/gradle.properties @@ -1,3 +1,3 @@ -version=21.1.1 -latestReleasedVersion=21.1.0 +version=21.2.2 +latestReleasedVersion=21.2.1 android.enableUnitTestBinaryResources=true diff --git a/firebase-functions/src/androidTest/backend/functions/index.js b/firebase-functions/src/androidTest/backend/functions/index.js index fed5a371b89..db1b9ab13e6 100644 --- a/firebase-functions/src/androidTest/backend/functions/index.js +++ b/firebase-functions/src/androidTest/backend/functions/index.js @@ -14,6 +14,16 @@ const assert = require('assert'); const functions = require('firebase-functions'); +const functionsV2 = require('firebase-functions/v2'); + +/** + * Pauses the execution for a specified amount of time. + * @param {number} ms - The number of milliseconds to sleep. + * @return {Promise} A promise that resolves after the specified time. + */ +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} exports.dataTest = functions.https.onRequest((request, response) => { assert.deepEqual(request.body, { @@ -122,3 +132,105 @@ exports.timeoutTest = functions.https.onRequest((request, response) => { // Wait for longer than 500ms. setTimeout(() => response.send({data: true}), 500); }); + +const streamData = ['hello', 'world', 'this', 'is', 'cool']; + +/** + * Generates chunks of text asynchronously, yielding one chunk at a time. + * @async + * @generator + * @yields {string} A chunk of text from the data array. + */ +async function* generateText() { + for (const chunk of streamData) { + yield chunk; + await sleep(100); + } +} + +exports.genStream = functionsV2.https.onCall(async (request, response) => { + if (request.acceptsStreaming) { + for await (const chunk of generateText()) { + response.sendChunk(chunk); + } + } + else { + console.log("CLIENT DOES NOT SUPPORT STEAMING"); + } + return streamData.join(' '); +}); + +exports.genStreamError = functionsV2.https.onCall( + async (request, response) => { + // Note: The functions backend does not pass the error message to the + // client at this time. + throw Error("BOOM") + }); + +const weatherForecasts = { + Toronto: { conditions: 'snowy', temperature: 25 }, + London: { conditions: 'rainy', temperature: 50 }, + Dubai: { conditions: 'sunny', temperature: 75 } +}; + +/** + * Generates weather forecasts asynchronously for the given locations. + * @async + * @generator + * @param {Array<{name: string}>} locations - An array of location objects. + */ +async function* generateForecast(locations) { + for (const location of locations) { + yield { 'location': location, ...weatherForecasts[location.name] }; + await sleep(100); + } +}; + +exports.genStreamWeather = functionsV2.https.onCall( + async (request, response) => { + const locations = request.data && request.data.data? + request.data.data: []; + const forecasts = []; + if (request.acceptsStreaming) { + for await (const chunk of generateForecast(locations)) { + forecasts.push(chunk); + response.sendChunk(chunk); + } + } + return {forecasts}; + }); + +exports.genStreamEmpty = functionsV2.https.onCall( + async (request, response) => { + if (request.acceptsStreaming) { + // Send no chunks + } + // Implicitly return null. + } +); + +exports.genStreamResultOnly = functionsV2.https.onCall( + async (request, response) => { + if (request.acceptsStreaming) { + // Do not send any chunks. + } + return "Only a result"; + } +); + +exports.genStreamLargeData = functionsV2.https.onCall( + async (request, response) => { + if (request.acceptsStreaming) { + const largeString = 'A'.repeat(10000); + const chunkSize = 1024; + for (let i = 0; i < largeString.length; i += chunkSize) { + const chunk = largeString.substring(i, i + chunkSize); + response.sendChunk(chunk); + await sleep(100); + } + } else { + console.log("CLIENT DOES NOT SUPPORT STEAMING") + } + return "Stream Completed"; + } +); diff --git a/firebase-functions/src/androidTest/backend/functions/package.json b/firebase-functions/src/androidTest/backend/functions/package.json index 6c5f9933d8b..53a2aac9d4a 100644 --- a/firebase-functions/src/androidTest/backend/functions/package.json +++ b/firebase-functions/src/androidTest/backend/functions/package.json @@ -2,11 +2,11 @@ "name": "functions", "description": "Cloud Functions for Firebase", "dependencies": { - "firebase-admin": "11.8.0", - "firebase-functions": "4.4.0" + "firebase-admin": "13.2.0", + "firebase-functions": "6.3.2" }, "private": true, "engines": { - "node": "18" + "node": "22" } } diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java b/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java index 1126ae55fbb..384230867d9 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java @@ -117,7 +117,7 @@ public void getContext_whenOnlyAuthIsAvailableAndNotSignedIn_shouldContainOnlyIi } @Test - public void getContext_whenOnlyAppCheckIsAvailableAndHasError_shouldContainOnlyIid() + public void getContext_whenOnlyAppCheckIsAvailableAndHasError() throws ExecutionException, InterruptedException { FirebaseContextProvider contextProvider = new FirebaseContextProvider( @@ -129,11 +129,12 @@ public void getContext_whenOnlyAppCheckIsAvailableAndHasError_shouldContainOnlyI HttpsCallableContext context = Tasks.await(contextProvider.getContext(false)); assertThat(context.getAuthToken()).isNull(); assertThat(context.getInstanceIdToken()).isEqualTo(IID_TOKEN); - assertThat(context.getAppCheckToken()).isNull(); + // AppCheck token needs to be send in all circumstances. + assertThat(context.getAppCheckToken()).isEqualTo(APP_CHECK_TOKEN); } @Test - public void getContext_facLimitedUse_whenOnlyAppCheckIsAvailableAndHasError_shouldContainOnlyIid() + public void getContext_facLimitedUse_whenOnlyAppCheckIsAvailableAndHasError() throws ExecutionException, InterruptedException { FirebaseContextProvider contextProvider = new FirebaseContextProvider( @@ -145,7 +146,8 @@ public void getContext_facLimitedUse_whenOnlyAppCheckIsAvailableAndHasError_shou HttpsCallableContext context = Tasks.await(contextProvider.getContext(true)); assertThat(context.getAuthToken()).isNull(); assertThat(context.getInstanceIdToken()).isEqualTo(IID_TOKEN); - assertThat(context.getAppCheckToken()).isNull(); + // AppCheck token needs to be sent in all circumstances. + assertThat(context.getAppCheckToken()).isEqualTo(APP_CHECK_LIMITED_USE_TOKEN); } @Test diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt new file mode 100644 index 00000000000..8e0d26bff3e --- /dev/null +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt @@ -0,0 +1,239 @@ +/* + * Copyright 2025 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.firebase.functions + +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.firebase.Firebase +import com.google.firebase.initialize +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.delay +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription + +@RunWith(AndroidJUnit4::class) +class StreamTests { + + private lateinit var functions: FirebaseFunctions + + @Before + fun setup() { + Firebase.initialize(ApplicationProvider.getApplicationContext()) + functions = Firebase.functions + } + + internal class StreamSubscriber : Subscriber { + internal val messages = mutableListOf() + internal var result: StreamResponse.Result? = null + internal var throwable: Throwable? = null + internal var isComplete = false + internal lateinit var subscription: Subscription + + override fun onSubscribe(subscription: Subscription) { + this.subscription = subscription + subscription.request(Long.MAX_VALUE) + } + + override fun onNext(streamResponse: StreamResponse) { + if (streamResponse is StreamResponse.Message) { + messages.add(streamResponse) + } else { + result = streamResponse as StreamResponse.Result + } + } + + override fun onError(t: Throwable?) { + throwable = t + } + + override fun onComplete() { + isComplete = true + } + } + + @Test + fun genStream_withPublisher_receivesMessagesAndFinalResult() = runBlocking { + val input = mapOf("data" to "Why is the sky blue") + val function = functions.getHttpsCallable("genStream") + val subscriber = StreamSubscriber() + + function.stream(input).subscribe(subscriber) + + while (!subscriber.isComplete) { + delay(100) + } + assertThat(subscriber.messages.map { it.message.data.toString() }) + .containsExactly("hello", "world", "this", "is", "cool") + assertThat(subscriber.result).isNotNull() + assertThat(subscriber.result!!.result.data.toString()).isEqualTo("hello world this is cool") + assertThat(subscriber.throwable).isNull() + assertThat(subscriber.isComplete).isTrue() + } + + @Test + fun genStream_withFlow_receivesMessagesAndFinalResult() = runBlocking { + val input = mapOf("data" to "Why is the sky blue") + val function = functions.getHttpsCallable("genStream") + var isComplete = false + var throwable: Throwable? = null + val messages = mutableListOf() + var result: StreamResponse.Result? = null + + val flow = function.stream(input).asFlow() + try { + withTimeout(10_000) { + flow.collect { response -> + if (response is StreamResponse.Message) { + messages.add(response) + } else { + result = response as StreamResponse.Result + } + } + } + isComplete = true + } catch (e: Throwable) { + throwable = e + } + + assertThat(throwable).isNull() + assertThat(messages.map { it.message.data.toString() }) + .containsExactly("hello", "world", "this", "is", "cool") + assertThat(result).isNotNull() + assertThat(result!!.result.data.toString()).isEqualTo("hello world this is cool") + assertThat(isComplete).isTrue() + } + + @Test + fun genStreamError_receivesError() = runBlocking { + val input = mapOf("data" to "test error") + val function = + functions.getHttpsCallable("genStreamError").withTimeout(10_000, TimeUnit.MILLISECONDS) + val subscriber = StreamSubscriber() + + function.stream(input).subscribe(subscriber) + + withTimeout(10_000) { + while (subscriber.throwable == null) { + delay(1_000) + } + } + + assertThat(subscriber.throwable).isNotNull() + assertThat(subscriber.throwable).isInstanceOf(FirebaseFunctionsException::class.java) + } + + @Test + fun nonExistentFunction_receivesError() = runBlocking { + val function = + functions.getHttpsCallable("nonexistentFunction").withTimeout(10_000, TimeUnit.MILLISECONDS) + val subscriber = StreamSubscriber() + + function.stream().subscribe(subscriber) + + withTimeout(10_000) { + while (subscriber.throwable == null) { + delay(1_000) + } + } + + assertThat(subscriber.throwable).isNotNull() + assertThat(subscriber.throwable).isInstanceOf(FirebaseFunctionsException::class.java) + assertThat((subscriber.throwable as FirebaseFunctionsException).code) + .isEqualTo(FirebaseFunctionsException.Code.NOT_FOUND) + } + + @Test + fun genStreamWeather_receivesWeatherForecasts() = runBlocking { + val inputData = listOf(mapOf("name" to "Toronto"), mapOf("name" to "London")) + val input = mapOf("data" to inputData) + + val function = functions.getHttpsCallable("genStreamWeather") + val subscriber = StreamSubscriber() + + function.stream(input).subscribe(subscriber) + + while (!subscriber.isComplete) { + delay(100) + } + + assertThat(subscriber.messages.map { it.message.data.toString() }) + .containsExactly( + "{temperature=25, location={name=Toronto}, conditions=snowy}", + "{temperature=50, location={name=London}, conditions=rainy}" + ) + assertThat(subscriber.result).isNotNull() + assertThat(subscriber.result!!.result.data.toString()).contains("forecasts") + assertThat(subscriber.throwable).isNull() + assertThat(subscriber.isComplete).isTrue() + } + + @Test + fun genStreamEmpty_receivesNoMessages() = runBlocking { + val function = functions.getHttpsCallable("genStreamEmpty") + val subscriber = StreamSubscriber() + + function.stream(mapOf("data" to "test")).subscribe(subscriber) + + withTimeout(10_000) { delay(1000) } + assertThat(subscriber.throwable).isNull() + assertThat(subscriber.messages).isEmpty() + assertThat(subscriber.result).isNull() + } + + @Test + fun genStreamResultOnly_receivesOnlyResult() = runBlocking { + val function = functions.getHttpsCallable("genStreamResultOnly") + val subscriber = StreamSubscriber() + + function.stream(mapOf("data" to "test")).subscribe(subscriber) + + while (!subscriber.isComplete) { + delay(100) + } + assertThat(subscriber.messages).isEmpty() + assertThat(subscriber.result).isNotNull() + assertThat(subscriber.result!!.result.data.toString()).isEqualTo("Only a result") + } + + @Test + fun genStreamLargeData_receivesMultipleChunks() = runBlocking { + val function = functions.getHttpsCallable("genStreamLargeData") + val subscriber = StreamSubscriber() + + function.stream(mapOf("data" to "test large data")).subscribe(subscriber) + + while (!subscriber.isComplete) { + delay(100) + } + assertThat(subscriber.messages).isNotEmpty() + assertThat(subscriber.messages.size).isEqualTo(10) + val receivedString = + subscriber.messages.joinToString(separator = "") { it.message.data.toString() } + val expectedString = "A".repeat(10000) + assertThat(receivedString.length).isEqualTo(10000) + assertThat(receivedString).isEqualTo(expectedString) + assertThat(subscriber.result).isNotNull() + assertThat(subscriber.result!!.result.data.toString()).isEqualTo("Stream Completed") + } +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt index 7ab1f74bf5d..96f18eb2c05 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt @@ -88,11 +88,9 @@ constructor( if (getLimitedUseAppCheckToken) appCheck.limitedUseToken else appCheck.getToken(false) return tokenTask.onSuccessTask(executor) { result: AppCheckTokenResult -> if (result.error != null) { - // If there was an error getting the App Check token, do NOT send the placeholder - // token. Only valid App Check tokens should be sent to the functions backend. Log.w(TAG, "Error getting App Check token. Error: " + result.error) - return@onSuccessTask Tasks.forResult(null) } + // Send valid token (success) or placeholder (failure). Tasks.forResult(result.token) } } diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt index 824670c4346..8839763c4a3 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt @@ -45,6 +45,7 @@ import okhttp3.RequestBody import okhttp3.Response import org.json.JSONException import org.json.JSONObject +import org.reactivestreams.Publisher /** FirebaseFunctions lets you call Cloud Functions for Firebase. */ public class FirebaseFunctions @@ -311,6 +312,21 @@ internal constructor( return tcs.task } + internal fun stream( + name: String, + data: Any?, + options: HttpsCallOptions + ): Publisher = stream(getURL(name), data, options) + + internal fun stream(url: URL, data: Any?, options: HttpsCallOptions): Publisher { + val task = + providerInstalled.task.continueWithTask(executor) { + contextProvider.getContext(options.limitedUseAppCheckTokens) + } + + return PublisherStream(url, data, options, client, this.serializer, task, executor) + } + public companion object { /** A task that will be resolved once ProviderInstaller has installed what it needs to. */ private val providerInstalled = TaskCompletionSource() diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt index 88db9db4ee4..215722584ba 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt @@ -17,6 +17,7 @@ import androidx.annotation.VisibleForTesting import com.google.android.gms.tasks.Task import java.net.URL import java.util.concurrent.TimeUnit +import org.reactivestreams.Publisher /** A reference to a particular Callable HTTPS trigger in Cloud Functions. */ public class HttpsCallableReference { @@ -61,10 +62,8 @@ public class HttpsCallableReference { * * * Any primitive type, including null, int, long, float, and boolean. * * [String] - * * [List&lt;?&gt;][java.util.List], where the contained objects are also one of these - * types. - * * [Map&lt;String, ?&gt;>][java.util.Map], where the values are also one of these - * types. + * * [List][java.util.List], where the contained objects are also one of these types. + * * [Map][java.util.Map], where the values are also one of these types. * * [org.json.JSONArray] * * [org.json.JSONObject] * * [org.json.JSONObject.NULL] @@ -125,6 +124,55 @@ public class HttpsCallableReference { } } + /** + * Streams data to the specified HTTPS endpoint. + * + * The data passed into the trigger can be any of the following types: + * + * * Any primitive type, including null, int, long, float, and boolean. + * * [String] + * * [List][java.util.List], where the contained objects are also one of these types. + * * [Map][java.util.Map], where the values are also one of these types. + * * [org.json.JSONArray] + * * [org.json.JSONObject] + * * [org.json.JSONObject.NULL] + * + * If the returned streamResponse fails, the exception will be one of the following types: + * + * * [java.io.IOException] + * - if the HTTPS request failed to connect. + * * [FirebaseFunctionsException] + * - if the request connected, but the function returned an error. + * + * The request to the Cloud Functions backend made by this method automatically includes a + * Firebase Instance ID token to identify the app instance. If a user is logged in with Firebase + * Auth, an auth token for the user will also be automatically included. + * + * Firebase Instance ID sends data to the Firebase backend periodically to collect information + * regarding the app instance. To stop this, see + * [com.google.firebase.iid.FirebaseInstanceId.deleteInstanceId]. It will resume with a new + * Instance ID the next time you call this method. + * + * @param data Parameters to pass to the endpoint. Defaults to `null` if not provided. + * @return [Publisher] that will emit intermediate data, and the final result, as it is generated + * by the function. + * @see org.json.JSONArray + * + * @see org.json.JSONObject + * + * @see java.io.IOException + * + * @see FirebaseFunctionsException + */ + @JvmOverloads + public fun stream(data: Any? = null): Publisher { + return if (name != null) { + functionsClient.stream(name, data, options) + } else { + functionsClient.stream(requireNotNull(url), data, options) + } + } + /** * Changes the timeout for calls from this instance of Functions. The default is 60 seconds. * diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt b/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt new file mode 100644 index 00000000000..d853dfbbb7b --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt @@ -0,0 +1,345 @@ +/* + * Copyright 2025 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.firebase.functions + +import com.google.android.gms.tasks.Task +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.io.InterruptedIOException +import java.net.URL +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicLong +import okhttp3.Call +import okhttp3.Callback +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import org.json.JSONObject +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription + +internal class PublisherStream( + private val url: URL, + private val data: Any?, + private val options: HttpsCallOptions, + private val client: OkHttpClient, + private val serializer: Serializer, + private val contextTask: Task, + private val executor: Executor +) : Publisher { + + private val subscribers = ConcurrentLinkedQueue, AtomicLong>>() + private var activeCall: Call? = null + @Volatile private var isStreamingStarted = false + @Volatile private var isCompleted = false + private val messageQueue = ConcurrentLinkedQueue() + + override fun subscribe(subscriber: Subscriber) { + synchronized(this) { + if (isCompleted) { + subscriber.onError( + FirebaseFunctionsException( + "Cannot subscribe: Streaming has already completed.", + FirebaseFunctionsException.Code.CANCELLED, + null + ) + ) + return + } + subscribers.add(subscriber to AtomicLong(0)) + } + + subscriber.onSubscribe( + object : Subscription { + override fun request(n: Long) { + if (n <= 0) { + subscriber.onError(IllegalArgumentException("Requested messages must be positive.")) + return + } + + synchronized(this@PublisherStream) { + if (isCompleted) return + + val subscriberEntry = subscribers.find { it.first == subscriber } + subscriberEntry?.second?.addAndGet(n) + dispatchMessages() + if (!isStreamingStarted) { + isStreamingStarted = true + startStreaming() + } + } + } + + override fun cancel() { + synchronized(this@PublisherStream) { + notifyError( + FirebaseFunctionsException( + "Stream was canceled", + FirebaseFunctionsException.Code.CANCELLED, + null + ) + ) + val iterator = subscribers.iterator() + while (iterator.hasNext()) { + val pair = iterator.next() + if (pair.first == subscriber) { + iterator.remove() + } + } + if (subscribers.isEmpty()) { + cancelStream() + } + } + } + } + ) + } + + private fun startStreaming() { + contextTask.addOnCompleteListener(executor) { contextTask -> + if (!contextTask.isSuccessful) { + notifyError( + FirebaseFunctionsException( + "Error retrieving context", + FirebaseFunctionsException.Code.INTERNAL, + null, + contextTask.exception + ) + ) + return@addOnCompleteListener + } + + val context = contextTask.result + val configuredClient = options.apply(client) + val requestBody = + RequestBody.create( + MediaType.parse("application/json"), + JSONObject(mapOf("data" to serializer.encode(data))).toString() + ) + val request = + Request.Builder() + .url(url) + .post(requestBody) + .apply { + header("Accept", "text/event-stream") + header("Content-Type", "application/json") + context?.apply { + authToken?.let { header("Authorization", "Bearer $it") } + instanceIdToken?.let { header("Firebase-Instance-ID-Token", it) } + appCheckToken?.let { header("X-Firebase-AppCheck", it) } + } + } + .build() + val call = configuredClient.newCall(request) + activeCall = call + + call.enqueue( + object : Callback { + override fun onFailure(call: Call, e: IOException) { + val code: FirebaseFunctionsException.Code = + if (e is InterruptedIOException) { + FirebaseFunctionsException.Code.DEADLINE_EXCEEDED + } else { + FirebaseFunctionsException.Code.INTERNAL + } + notifyError(FirebaseFunctionsException(code.name, code, null, e)) + } + + override fun onResponse(call: Call, response: Response) { + validateResponse(response) + val bodyStream = response.body()?.byteStream() + if (bodyStream != null) { + processSSEStream(bodyStream) + } else { + notifyError( + FirebaseFunctionsException( + "Response body is null", + FirebaseFunctionsException.Code.INTERNAL, + null + ) + ) + } + } + } + ) + } + } + + private fun cancelStream() { + activeCall?.cancel() + notifyError( + FirebaseFunctionsException( + "Stream was canceled", + FirebaseFunctionsException.Code.CANCELLED, + null + ) + ) + } + + private fun processSSEStream(inputStream: InputStream) { + BufferedReader(InputStreamReader(inputStream)).use { reader -> + try { + val eventBuffer = StringBuilder() + reader.lineSequence().forEach { line -> + if (line.isBlank()) { + processEvent(eventBuffer.toString()) + eventBuffer.clear() + } else { + val dataChunk = + when { + line.startsWith("data:") -> line.removePrefix("data:") + line.startsWith("result:") -> line.removePrefix("result:") + else -> return@forEach + } + eventBuffer.append(dataChunk.trim()).append("\n") + } + } + if (eventBuffer.isNotEmpty()) { + processEvent(eventBuffer.toString()) + } + } catch (e: Exception) { + notifyError( + FirebaseFunctionsException( + e.message ?: "Error reading stream", + FirebaseFunctionsException.Code.INTERNAL, + e + ) + ) + } + } + } + + private fun processEvent(dataChunk: String) { + try { + val json = JSONObject(dataChunk) + when { + json.has("message") -> { + serializer.decode(json.opt("message"))?.let { + messageQueue.add(StreamResponse.Message(message = HttpsCallableResult(it))) + } + dispatchMessages() + } + json.has("error") -> { + serializer.decode(json.opt("error"))?.let { + notifyError( + FirebaseFunctionsException( + it.toString(), + FirebaseFunctionsException.Code.INTERNAL, + it + ) + ) + } + } + json.has("result") -> { + serializer.decode(json.opt("result"))?.let { + messageQueue.add(StreamResponse.Result(result = HttpsCallableResult(it))) + dispatchMessages() + notifyComplete() + } + } + } + } catch (e: Throwable) { + notifyError( + FirebaseFunctionsException( + "Invalid JSON: $dataChunk", + FirebaseFunctionsException.Code.INTERNAL, + e + ) + ) + } + } + + private fun dispatchMessages() { + synchronized(this) { + val iterator = subscribers.iterator() + while (iterator.hasNext()) { + val (subscriber, requestedCount) = iterator.next() + while (requestedCount.get() > 0 && messageQueue.isNotEmpty()) { + subscriber.onNext(messageQueue.poll()) + requestedCount.decrementAndGet() + } + } + } + } + + private fun notifyError(e: Throwable) { + if (!isCompleted) { + isCompleted = true + subscribers.forEach { (subscriber, _) -> + try { + subscriber.onError(e) + } catch (ignored: Exception) {} + } + subscribers.clear() + messageQueue.clear() + } + } + + private fun notifyComplete() { + if (!isCompleted) { + isCompleted = true + subscribers.forEach { (subscriber, _) -> subscriber.onComplete() } + subscribers.clear() + messageQueue.clear() + } + } + + private fun validateResponse(response: Response) { + if (response.isSuccessful) return + + val errorMessage: String + if ( + response.code() == 404 && + MediaType.parse(response.header("Content-Type") ?: "")?.subtype() == "html" + ) { + errorMessage = """URL not found. Raw response: ${response.body()?.string()}""".trimMargin() + notifyError( + FirebaseFunctionsException( + errorMessage, + FirebaseFunctionsException.Code.fromHttpStatus(response.code()), + null + ) + ) + return + } + + val text = response.body()?.string() ?: "" + val error: Any? + try { + val json = JSONObject(text) + error = serializer.decode(json.opt("error")) + } catch (e: Throwable) { + notifyError( + FirebaseFunctionsException( + "${e.message} Unexpected Response:\n$text ", + FirebaseFunctionsException.Code.INTERNAL, + e + ) + ) + return + } + notifyError( + FirebaseFunctionsException(error.toString(), FirebaseFunctionsException.Code.INTERNAL, error) + ) + } +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt b/firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt new file mode 100644 index 00000000000..123f804614d --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2025 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.firebase.functions + +/** + * Represents a response from a Server-Sent Event (SSE) stream. + * + * The SSE stream consists of two types of responses: + * - [Message]: Represents an intermediate event pushed from the server. + * - [Result]: Represents the final response that signifies the stream has ended. + */ +public abstract class StreamResponse private constructor() { + + /** + * An event message received during the stream. + * + * Messages are intermediate data chunks sent by the server while processing a request. + * + * Example SSE format: + * ```json + * data: { "message": { "chunk": "foo" } } + * ``` + * + * @property message the intermediate data received from the server. + */ + public class Message(public val message: HttpsCallableResult) : StreamResponse() + + /** + * The final result of the computation, marking the end of the stream. + * + * Unlike [Message], which represents intermediate data chunks, [Result] contains the complete + * computation output. If clients only care about the final result, they can process this type + * alone and ignore intermediate messages. + * + * Example SSE format: + * ```json + * data: { "result": { "text": "foo bar" } } + * ``` + * + * @property result the final computed result received from the server. + */ + public class Result(public val result: HttpsCallableResult) : StreamResponse() +} diff --git a/firebase-inappmessaging-display/CHANGELOG.md b/firebase-inappmessaging-display/CHANGELOG.md index 15bd2abe75a..706aad30b1d 100644 --- a/firebase-inappmessaging-display/CHANGELOG.md +++ b/firebase-inappmessaging-display/CHANGELOG.md @@ -1,6 +1,15 @@ # Unreleased +# 21.0.2 +* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-inappmessaging-display` library. The Kotlin extensions library has no additional +updates. + # 21.0.1 * [changed] Updated protobuf dependency to `3.25.5` to fix [CVE-2024-7254](https://nvd.nist.gov/vuln/detail/CVE-2024-7254). diff --git a/firebase-inappmessaging-display/gradle.properties b/firebase-inappmessaging-display/gradle.properties index 4ad037837d4..f8301e25bcc 100644 --- a/firebase-inappmessaging-display/gradle.properties +++ b/firebase-inappmessaging-display/gradle.properties @@ -1,2 +1,2 @@ -version=21.0.2 -latestReleasedVersion=21.0.1 +version=21.0.3 +latestReleasedVersion=21.0.2 diff --git a/firebase-inappmessaging/CHANGELOG.md b/firebase-inappmessaging/CHANGELOG.md index 90b3e93ccae..c6f88c8e69d 100644 --- a/firebase-inappmessaging/CHANGELOG.md +++ b/firebase-inappmessaging/CHANGELOG.md @@ -1,6 +1,15 @@ # Unreleased +# 21.0.2 +* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-inappmessaging` library. The Kotlin extensions library has no additional +updates. + # 21.0.1 * [changed] Updated protobuf dependency to `3.25.5` to fix [CVE-2024-7254](https://nvd.nist.gov/vuln/detail/CVE-2024-7254). diff --git a/firebase-inappmessaging/gradle.properties b/firebase-inappmessaging/gradle.properties index 4ad037837d4..f8301e25bcc 100644 --- a/firebase-inappmessaging/gradle.properties +++ b/firebase-inappmessaging/gradle.properties @@ -1,2 +1,2 @@ -version=21.0.2 -latestReleasedVersion=21.0.1 +version=21.0.3 +latestReleasedVersion=21.0.2 diff --git a/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/internal/ForegroundNotifierTest.java b/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/internal/ForegroundNotifierTest.java index 2cedc0b7953..af01be33fb6 100644 --- a/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/internal/ForegroundNotifierTest.java +++ b/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/internal/ForegroundNotifierTest.java @@ -16,13 +16,14 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.firebase.inappmessaging.internal.InAppMessageStreamManager.ON_FOREGROUND; +import static org.robolectric.Shadows.shadowOf; +import android.os.Looper; import io.reactivex.flowables.ConnectableFlowable; import io.reactivex.subscribers.TestSubscriber; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -61,7 +62,7 @@ public void notifier_onActivityResumedAfterRunnableExecution_notifiesListener() foregroundNotifier.onActivityResumed(null); // 1 assertThat(subscriber.getEvents().get(0)).hasSize(1); foregroundNotifier.onActivityPaused(null); - Robolectric.flushForegroundThreadScheduler(); + shadowOf(Looper.getMainLooper()).runToEndOfTasks(); foregroundNotifier.onActivityResumed(null); // 2 assertThat(subscriber.getEvents().get(0)).hasSize(2); } diff --git a/firebase-messaging-directboot/CHANGELOG.md b/firebase-messaging-directboot/CHANGELOG.md index ea728d95e54..1d9568c4f09 100644 --- a/firebase-messaging-directboot/CHANGELOG.md +++ b/firebase-messaging-directboot/CHANGELOG.md @@ -1,6 +1,9 @@ # Unreleased +# 24.1.1 +* [unchanged] Updated to keep messaging SDK versions aligned. + # 24.1.0 * [unchanged] Updated to keep messaging SDK versions aligned. diff --git a/firebase-messaging-directboot/gradle.properties b/firebase-messaging-directboot/gradle.properties index 11e55c591b5..23127f4cada 100644 --- a/firebase-messaging-directboot/gradle.properties +++ b/firebase-messaging-directboot/gradle.properties @@ -1,3 +1,3 @@ -version=24.1.1 -latestReleasedVersion=24.1.0 +version=24.1.2 +latestReleasedVersion=24.1.1 android.enableUnitTestBinaryResources=true diff --git a/firebase-messaging/CHANGELOG.md b/firebase-messaging/CHANGELOG.md index 4a7e28a5766..120620148dc 100644 --- a/firebase-messaging/CHANGELOG.md +++ b/firebase-messaging/CHANGELOG.md @@ -1,5 +1,16 @@ # Unreleased +* [changed] Added a NamedThreadFactory to WithinAppServiceConnection's service + connection Executor. +# 24.1.1 +* [changed] Bug fix in SyncTask to always unregister the receiver on the same + context on which it was registered. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-messaging` library. The Kotlin extensions library has no additional +updates. # 24.1.0 * [deprecated] Deprecated additional FCM upstream messaging methods and updated diff --git a/firebase-messaging/gradle.properties b/firebase-messaging/gradle.properties index 11e55c591b5..23127f4cada 100644 --- a/firebase-messaging/gradle.properties +++ b/firebase-messaging/gradle.properties @@ -1,3 +1,3 @@ -version=24.1.1 -latestReleasedVersion=24.1.0 +version=24.1.2 +latestReleasedVersion=24.1.1 android.enableUnitTestBinaryResources=true diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java index c0c4074c11e..cd821f0e1f3 100644 --- a/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java +++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java @@ -161,6 +161,7 @@ boolean isDeviceConnected() { static class ConnectivityChangeReceiver extends BroadcastReceiver { @Nullable private SyncTask task; // task is set to null after it has been fired. + @Nullable private Context receiverContext; public ConnectivityChangeReceiver(SyncTask task) { this.task = task; @@ -171,7 +172,10 @@ public void registerReceiver() { Log.d(TAG, "Connectivity change received registered"); } IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); - task.getContext().registerReceiver(this, intentFilter); + if (task != null) { + receiverContext = task.getContext(); + receiverContext.registerReceiver(this, intentFilter); + } } @Override @@ -191,7 +195,9 @@ public void onReceive(Context context, Intent intent) { Log.d(TAG, "Connectivity changed. Starting background sync."); } task.firebaseMessaging.enqueueTaskWithDelaySeconds(task, 0); - task.getContext().unregisterReceiver(this); + if (receiverContext != null) { + receiverContext.unregisterReceiver(this); + } task = null; } } diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/WithinAppServiceConnection.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/WithinAppServiceConnection.java index 63d13467edd..08823c88c90 100644 --- a/firebase-messaging/src/main/java/com/google/firebase/messaging/WithinAppServiceConnection.java +++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/WithinAppServiceConnection.java @@ -27,6 +27,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.stats.ConnectionTracker; +import com.google.android.gms.common.util.concurrent.NamedThreadFactory; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -109,7 +110,9 @@ void finish() { @SuppressLint("ThreadPoolCreation") private static ScheduledThreadPoolExecutor createScheduledThreadPoolExecutor() { - ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(1); + ScheduledThreadPoolExecutor threadPoolExecutor = + new ScheduledThreadPoolExecutor( + 1, new NamedThreadFactory("Firebase-FirebaseInstanceIdServiceConnection")); threadPoolExecutor.setKeepAliveTime(EnhancedIntentService.MESSAGE_TIMEOUT_S * 2, SECONDS); threadPoolExecutor.allowCoreThreadTimeOut(true); return threadPoolExecutor; diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md index 2112244a524..dabc2485b29 100644 --- a/firebase-perf/CHANGELOG.md +++ b/firebase-perf/CHANGELOG.md @@ -1,6 +1,16 @@ # Unreleased +# 21.0.5 +* [changed] Updated `protolite-well-known-types` dependency to v18.0.1 [#6716] +* [fixed] Fixed a bug that allowed invalid payload bytes value in network request metrics [#6721] + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-performance` library. The Kotlin extensions library has no additional +updates. + # 21.0.4 * [fixed] Fixed a performance issue with shared preferences calling `.apply()` every time a value is read from remote config (#6407) diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle index b6028e75b61..c0fd6df6056 100644 --- a/firebase-perf/firebase-perf.gradle +++ b/firebase-perf/firebase-perf.gradle @@ -111,7 +111,7 @@ dependencies { implementation libs.dagger.dagger api 'com.google.firebase:firebase-annotations:16.2.0' api 'com.google.firebase:firebase-installations-interop:17.1.0' - api 'com.google.firebase:protolite-well-known-types:18.0.0' + api project(":protolite-well-known-types") implementation libs.okhttp api("com.google.firebase:firebase-common:21.0.0") api("com.google.firebase:firebase-common-ktx:21.0.0") diff --git a/firebase-perf/gradle.properties b/firebase-perf/gradle.properties index 4b2de75bc47..beb8d9bb532 100644 --- a/firebase-perf/gradle.properties +++ b/firebase-perf/gradle.properties @@ -15,7 +15,7 @@ # # -version=21.0.5 -latestReleasedVersion=21.0.4 +version=21.0.6 +latestReleasedVersion=21.0.5 android.enableUnitTestBinaryResources=true diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/validator/PerfMetricValidator.java b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/validator/PerfMetricValidator.java index 8791f0e9742..20de8ae642a 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/validator/PerfMetricValidator.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/validator/PerfMetricValidator.java @@ -23,10 +23,16 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** An abstract class providing an interface to validate PerfMetric */ public abstract class PerfMetricValidator { + // Regex to validate Attribute key + private static final Pattern ATTRIBUTE_KEY_PATTERN = + Pattern.compile("^(?!(firebase_|google_|ga_))[A-Za-z][A-Za-z_0-9]*"); + /** * Creates a list of PerfMetricValidator classes based on the contents of PerfMetric * @@ -164,7 +170,8 @@ public static void validateAttribute(@NonNull String key, @NonNull String value) Constants.MAX_ATTRIBUTE_VALUE_LENGTH)); } - if (!key.matches("^(?!(firebase_|google_|ga_))[A-Za-z][A-Za-z_0-9]*")) { + Matcher attributeKeyMatcher = ATTRIBUTE_KEY_PATTERN.matcher(key); + if (!attributeKeyMatcher.matches()) { throw new IllegalArgumentException( "Attribute key must start with letter, must only contain alphanumeric characters and" + " underscore and must not start with \"firebase_\", \"google_\" and \"ga_"); diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java b/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java index fc660c70426..5ffff6c0d2f 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java @@ -30,13 +30,7 @@ public final class InstrHttpInputStream extends InputStream { private long timeToResponseInitiated; private long timeToResponseLastRead = -1; - /** - * Instrumented inputStream object - * - * @param inputStream - * @param builder - * @param timer - */ + /** Instrumented inputStream object */ public InstrHttpInputStream( final InputStream inputStream, final NetworkRequestMetricBuilder builder, Timer timer) { this.timer = timer; @@ -99,12 +93,13 @@ public int read() throws IOException { if (timeToResponseInitiated == -1) { timeToResponseInitiated = tempTime; } - if (bytesRead == -1 && timeToResponseLastRead == -1) { + boolean endOfStream = bytesRead == -1; + if (endOfStream && timeToResponseLastRead == -1) { timeToResponseLastRead = tempTime; networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead); networkMetricBuilder.build(); } else { - this.bytesRead++; + incrementBytesRead(1); networkMetricBuilder.setResponsePayloadBytes(this.bytesRead); } return bytesRead; @@ -124,12 +119,13 @@ public int read(final byte[] buffer, final int byteOffset, final int byteCount) if (timeToResponseInitiated == -1) { timeToResponseInitiated = tempTime; } - if (bytesRead == -1 && timeToResponseLastRead == -1) { + boolean endOfStream = bytesRead == -1; + if (endOfStream && timeToResponseLastRead == -1) { timeToResponseLastRead = tempTime; networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead); networkMetricBuilder.build(); } else { - this.bytesRead += bytesRead; + incrementBytesRead(bytesRead); networkMetricBuilder.setResponsePayloadBytes(this.bytesRead); } return bytesRead; @@ -148,12 +144,13 @@ public int read(final byte[] buffer) throws IOException { if (timeToResponseInitiated == -1) { timeToResponseInitiated = tempTime; } - if (bytesRead == -1 && timeToResponseLastRead == -1) { + boolean endOfStream = bytesRead == -1; + if (endOfStream && timeToResponseLastRead == -1) { timeToResponseLastRead = tempTime; networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead); networkMetricBuilder.build(); } else { - this.bytesRead += bytesRead; + incrementBytesRead(bytesRead); networkMetricBuilder.setResponsePayloadBytes(this.bytesRead); } return bytesRead; @@ -183,11 +180,13 @@ public long skip(final long byteCount) throws IOException { if (timeToResponseInitiated == -1) { timeToResponseInitiated = tempTime; } - if (skipped == -1 && timeToResponseLastRead == -1) { + // InputStream.skip will return 0 for both end of stream and for 0 bytes skipped. + boolean endOfStream = (skipped == 0 && byteCount != 0); + if (endOfStream && timeToResponseLastRead == -1) { timeToResponseLastRead = tempTime; networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead); } else { - bytesRead += skipped; + incrementBytesRead(skipped); networkMetricBuilder.setResponsePayloadBytes(bytesRead); } return skipped; @@ -197,4 +196,12 @@ public long skip(final long byteCount) throws IOException { throw e; } } + + private void incrementBytesRead(long bytesRead) { + if (this.bytesRead == -1) { + this.bytesRead = bytesRead; + } else { + this.bytesRead += bytesRead; + } + } } diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java index 8a7ecb2131b..e1f45c45329 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java @@ -30,6 +30,7 @@ import com.google.firebase.perf.v1.NetworkRequestMetric.NetworkClientErrorReason; import java.io.IOException; import java.io.InputStream; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,10 +41,14 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -/** Unit tests for {@link com.google.firebase.perf.network.InstrHttpInputStream}. */ +/** + * Unit tests for {@link com.google.firebase.perf.network.InstrHttpInputStream}. + * + * @noinspection ResultOfMethodCallIgnored + */ @RunWith(RobolectricTestRunner.class) public class InstrHttpInputStreamTest extends FirebasePerformanceTestBase { - + private AutoCloseable closeable; @Mock InputStream mInputStream; @Mock TransportManager transportManager; @Mock Timer timer; @@ -53,12 +58,17 @@ public class InstrHttpInputStreamTest extends FirebasePerformanceTestBase { @Before public void setUp() { - MockitoAnnotations.initMocks(this); + closeable = MockitoAnnotations.openMocks(this); when(timer.getMicros()).thenReturn((long) 1000); when(timer.getDurationMicros()).thenReturn((long) 2000); networkMetricBuilder = NetworkRequestMetricBuilder.builder(transportManager); } + @After + public void releaseMocks() throws Exception { + closeable.close(); + } + @Test public void testAvailable() throws IOException { int availableVal = 7; @@ -80,7 +90,7 @@ public void testClose() throws IOException { } @Test - public void testMark() throws IOException { + public void testMark() { int markInput = 256; new InstrHttpInputStream(mInputStream, networkMetricBuilder, timer).mark(markInput); @@ -89,7 +99,7 @@ public void testMark() throws IOException { } @Test - public void testMarkSupported() throws IOException { + public void testMarkSupported() { when(mInputStream.markSupported()).thenReturn(true); boolean ret = new InstrHttpInputStream(mInputStream, networkMetricBuilder, timer).markSupported(); @@ -108,6 +118,20 @@ public void testRead() throws IOException { verify(mInputStream).read(); } + @Test + public void testReadBufferOffsetZero() throws IOException { + byte[] b = new byte[0]; + int off = 0; + int len = 0; + when(mInputStream.read(b, off, len)).thenReturn(len); + int ret = new InstrHttpInputStream(mInputStream, networkMetricBuilder, timer).read(b, off, len); + + NetworkRequestMetric metric = networkMetricBuilder.build(); + assertThat(ret).isEqualTo(0); + assertThat(metric.getResponsePayloadBytes()).isEqualTo(0); + verify(mInputStream).read(b, off, len); + } + @Test public void testReadBufferOffsetCount() throws IOException { byte[] buffer = new byte[] {(byte) 0xe0}; diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md index 48987a62df5..70ad76eb6fe 100644 --- a/firebase-sessions/CHANGELOG.md +++ b/firebase-sessions/CHANGELOG.md @@ -1,7 +1,36 @@ # Unreleased + +# 2.1.1 +* [unchanged] Updated to keep SDK versions aligned. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-sessions` library. The Kotlin extensions library has no additional +updates. + +# 2.1.0 +* [changed] Add warning for known issue b/328687152 +* [changed] Use Dagger for dependency injection +* [changed] Updated datastore dependency to v1.1.3 to + fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8). + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-sessions` library. The Kotlin extensions library has no additional +updates. + +# 2.0.9 * [fixed] Make AQS resilient to background init in multi-process apps. + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-sessions` library. The Kotlin extensions library has no additional +updates. + # 2.0.7 * [fixed] Removed extraneous logs that risk leaking internal identifiers. diff --git a/firebase-sessions/benchmark/README.md b/firebase-sessions/benchmark/README.md new file mode 100644 index 00000000000..13a9941c665 --- /dev/null +++ b/firebase-sessions/benchmark/README.md @@ -0,0 +1,5 @@ +# Firebase Sessions Macrobenchmark + +## Setup + +## Run diff --git a/firebase-sessions/benchmark/benchmark.gradle.kts b/firebase-sessions/benchmark/benchmark.gradle.kts new file mode 100644 index 00000000000..115e73c66ed --- /dev/null +++ b/firebase-sessions/benchmark/benchmark.gradle.kts @@ -0,0 +1,63 @@ +/* + * Copyright 2025 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. + */ + +plugins { + id("com.android.test") + id("org.jetbrains.kotlin.android") +} + +android { + val compileSdkVersion: Int by rootProject + val targetSdkVersion: Int by rootProject + + namespace = "com.google.firebase.benchmark.sessions" + compileSdk = compileSdkVersion + + defaultConfig { + targetSdk = targetSdkVersion + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + create("benchmark") { + isDebuggable = true + signingConfig = signingConfigs["debug"] + matchingFallbacks += "release" + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { jvmTarget = "1.8" } + + targetProjectPath = ":firebase-sessions:test-app" + experimentalProperties["android.experimental.self-instrumenting"] = true +} + +dependencies { + implementation(libs.androidx.test.junit) + implementation(libs.androidx.benchmark.macro) +} + +androidComponents { + beforeVariants(selector().all()) { variantBuilder -> + variantBuilder.enable = (variantBuilder.buildType == "benchmark") + } +} diff --git a/firebase-sessions/benchmark/src/main/AndroidManifest.xml b/firebase-sessions/benchmark/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..4f78f2c185d --- /dev/null +++ b/firebase-sessions/benchmark/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/firebase-sessions/benchmark/src/main/kotlin/com/google/firebase/benchmark/sessions/StartupBenchmark.kt b/firebase-sessions/benchmark/src/main/kotlin/com/google/firebase/benchmark/sessions/StartupBenchmark.kt new file mode 100644 index 00000000000..fac6f1a4977 --- /dev/null +++ b/firebase-sessions/benchmark/src/main/kotlin/com/google/firebase/benchmark/sessions/StartupBenchmark.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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.firebase.benchmark.sessions + +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.StartupTimingMetric +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class StartupBenchmark { + @get:Rule val benchmarkRule = MacrobenchmarkRule() + + @Test + fun startup() = + benchmarkRule.measureRepeated( + packageName = "com.google.firebase.testing.sessions", + metrics = listOf(StartupTimingMetric()), + iterations = 5, + startupMode = StartupMode.COLD, + ) { + pressHome() + startActivityAndWait() + } +} diff --git a/firebase-sessions/firebase-sessions.gradle.kts b/firebase-sessions/firebase-sessions.gradle.kts index 15d22381e31..b136a281660 100644 --- a/firebase-sessions/firebase-sessions.gradle.kts +++ b/firebase-sessions/firebase-sessions.gradle.kts @@ -18,6 +18,7 @@ plugins { id("firebase-library") + id("firebase-vendor") id("kotlin-android") id("kotlin-kapt") } @@ -67,12 +68,18 @@ dependencies { exclude(group = "com.google.firebase", module = "firebase-common") exclude(group = "com.google.firebase", module = "firebase-components") } - implementation("androidx.datastore:datastore-preferences:1.0.0") - implementation("com.google.android.datatransport:transport-api:3.2.0") + api("com.google.firebase:firebase-annotations:16.2.0") api("com.google.firebase:firebase-encoders:17.0.0") api("com.google.firebase:firebase-encoders-json:18.0.1") + + implementation("com.google.android.datatransport:transport-api:3.2.0") + implementation(libs.javax.inject) implementation(libs.androidx.annotation) + implementation(libs.androidx.datastore.preferences) + + vendor(libs.dagger.dagger) { exclude(group = "javax.inject", module = "javax.inject") } + compileOnly(libs.errorprone.annotations) runtimeOnly("com.google.firebase:firebase-installations:18.0.0") { @@ -85,6 +92,7 @@ dependencies { } kapt(project(":encoders:firebase-encoders-processor")) + kapt(libs.dagger.compiler) testImplementation(project(":integ-testing")) { exclude(group = "com.google.firebase", module = "firebase-common") diff --git a/firebase-sessions/gradle.properties b/firebase-sessions/gradle.properties index c9bd869d4cd..0a3e66d5a26 100644 --- a/firebase-sessions/gradle.properties +++ b/firebase-sessions/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=2.0.9 -latestReleasedVersion=2.0.8 +version=2.1.2 +latestReleasedVersion=2.1.1 diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt index a11b20a7d5c..496cc70d36d 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt @@ -21,6 +21,8 @@ import com.google.android.datatransport.Encoding import com.google.android.datatransport.Event import com.google.android.datatransport.TransportFactory import com.google.firebase.inject.Provider +import javax.inject.Inject +import javax.inject.Singleton /** * The [EventGDTLoggerInterface] is for testing purposes so that we can mock EventGDTLogger in other @@ -38,19 +40,17 @@ internal fun interface EventGDTLoggerInterface { * * @hide */ -internal class EventGDTLogger(private val transportFactoryProvider: Provider) : +@Singleton +internal class EventGDTLogger +@Inject +constructor(private val transportFactoryProvider: Provider) : EventGDTLoggerInterface { // Logs a [SessionEvent] to FireLog override fun log(sessionEvent: SessionEvent) { transportFactoryProvider .get() - .getTransport( - AQS_LOG_SOURCE, - SessionEvent::class.java, - Encoding.of("json"), - this::encode, - ) + .getTransport(AQS_LOG_SOURCE, SessionEvent::class.java, Encoding.of("json"), this::encode) .send(Event.ofData(sessionEvent)) } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index 0dec3b98150..18b9961724b 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -20,18 +20,24 @@ import android.app.Application import android.util.Log import com.google.firebase.Firebase import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.app import com.google.firebase.sessions.api.FirebaseSessionsDependencies import com.google.firebase.sessions.settings.SessionsSettings +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** Responsible for initializing AQS */ -internal class FirebaseSessions( +@Singleton +internal class FirebaseSessions +@Inject +constructor( private val firebaseApp: FirebaseApp, private val settings: SessionsSettings, - backgroundDispatcher: CoroutineContext, + @Background backgroundDispatcher: CoroutineContext, lifecycleServiceBinder: SessionLifecycleServiceBinder, ) { diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt new file mode 100644 index 00000000000..5680c9cc0ec --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2025 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.firebase.sessions + +import android.content.Context +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.preferencesDataStoreFile +import com.google.android.datatransport.TransportFactory +import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.inject.Provider +import com.google.firebase.installations.FirebaseInstallationsApi +import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName +import com.google.firebase.sessions.settings.CrashlyticsSettingsFetcher +import com.google.firebase.sessions.settings.LocalOverrideSettings +import com.google.firebase.sessions.settings.RemoteSettings +import com.google.firebase.sessions.settings.RemoteSettingsFetcher +import com.google.firebase.sessions.settings.SessionsSettings +import com.google.firebase.sessions.settings.SettingsProvider +import dagger.Binds +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Qualifier +import javax.inject.Singleton +import kotlin.coroutines.CoroutineContext + +@Qualifier internal annotation class SessionConfigsDataStore + +@Qualifier internal annotation class SessionDetailsDataStore + +@Qualifier internal annotation class LocalOverrideSettingsProvider + +@Qualifier internal annotation class RemoteSettingsProvider + +/** + * Dagger component to provide [FirebaseSessions] and its dependencies. + * + * This gets configured and built in [FirebaseSessionsRegistrar.getComponents]. + */ +@Singleton +@Component(modules = [FirebaseSessionsComponent.MainModule::class]) +internal interface FirebaseSessionsComponent { + val firebaseSessions: FirebaseSessions + + val sessionDatastore: SessionDatastore + val sessionFirelogPublisher: SessionFirelogPublisher + val sessionGenerator: SessionGenerator + val sessionsSettings: SessionsSettings + + @Component.Builder + interface Builder { + @BindsInstance fun appContext(appContext: Context): Builder + + @BindsInstance + fun backgroundDispatcher(@Background backgroundDispatcher: CoroutineContext): Builder + + @BindsInstance fun blockingDispatcher(@Blocking blockingDispatcher: CoroutineContext): Builder + + @BindsInstance fun firebaseApp(firebaseApp: FirebaseApp): Builder + + @BindsInstance + fun firebaseInstallationsApi(firebaseInstallationsApi: FirebaseInstallationsApi): Builder + + @BindsInstance + fun transportFactoryProvider(transportFactoryProvider: Provider): Builder + + fun build(): FirebaseSessionsComponent + } + + @Module + interface MainModule { + @Binds @Singleton fun eventGDTLoggerInterface(impl: EventGDTLogger): EventGDTLoggerInterface + + @Binds @Singleton fun sessionDatastore(impl: SessionDatastoreImpl): SessionDatastore + + @Binds + @Singleton + fun sessionFirelogPublisher(impl: SessionFirelogPublisherImpl): SessionFirelogPublisher + + @Binds + @Singleton + fun sessionLifecycleServiceBinder( + impl: SessionLifecycleServiceBinderImpl + ): SessionLifecycleServiceBinder + + @Binds + @Singleton + fun crashlyticsSettingsFetcher(impl: RemoteSettingsFetcher): CrashlyticsSettingsFetcher + + @Binds + @Singleton + @LocalOverrideSettingsProvider + fun localOverrideSettings(impl: LocalOverrideSettings): SettingsProvider + + @Binds + @Singleton + @RemoteSettingsProvider + fun remoteSettings(impl: RemoteSettings): SettingsProvider + + companion object { + private const val TAG = "FirebaseSessions" + + @Provides @Singleton fun timeProvider(): TimeProvider = TimeProviderImpl + + @Provides @Singleton fun uuidGenerator(): UuidGenerator = UuidGeneratorImpl + + @Provides + @Singleton + fun applicationInfo(firebaseApp: FirebaseApp): ApplicationInfo = + SessionEvents.getApplicationInfo(firebaseApp) + + @Provides + @Singleton + @SessionConfigsDataStore + fun sessionConfigsDataStore(appContext: Context): DataStore = + PreferenceDataStoreFactory.create( + corruptionHandler = + ReplaceFileCorruptionHandler { ex -> + Log.w(TAG, "CorruptionException in settings DataStore in ${getProcessName()}.", ex) + emptyPreferences() + } + ) { + appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SETTINGS_CONFIG_NAME) + } + + @Provides + @Singleton + @SessionDetailsDataStore + fun sessionDetailsDataStore(appContext: Context): DataStore = + PreferenceDataStoreFactory.create( + corruptionHandler = + ReplaceFileCorruptionHandler { ex -> + Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex) + emptyPreferences() + } + ) { + appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SESSIONS_CONFIG_NAME) + } + } + } +} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index caad2de6ff8..5cb8de7a182 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -16,7 +16,10 @@ package com.google.firebase.sessions +import android.content.Context +import android.util.Log import androidx.annotation.Keep +import androidx.datastore.preferences.preferencesDataStore import com.google.android.datatransport.TransportFactory import com.google.firebase.FirebaseApp import com.google.firebase.annotations.concurrent.Background @@ -28,7 +31,6 @@ import com.google.firebase.components.Qualified.qualified import com.google.firebase.components.Qualified.unqualified import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.platforminfo.LibraryVersionComponent -import com.google.firebase.sessions.settings.SessionsSettings import kotlinx.coroutines.CoroutineDispatcher /** @@ -42,87 +44,66 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { listOf( Component.builder(FirebaseSessions::class.java) .name(LIBRARY_NAME) - .add(Dependency.required(firebaseApp)) - .add(Dependency.required(sessionsSettings)) - .add(Dependency.required(backgroundDispatcher)) - .add(Dependency.required(sessionLifecycleServiceBinder)) - .factory { container -> - FirebaseSessions( - container[firebaseApp], - container[sessionsSettings], - container[backgroundDispatcher], - container[sessionLifecycleServiceBinder], - ) - } + .add(Dependency.required(firebaseSessionsComponent)) + .factory { container -> container[firebaseSessionsComponent].firebaseSessions } .eagerInDefaultApp() .build(), - Component.builder(SessionGenerator::class.java) - .name("session-generator") - .factory { SessionGenerator(timeProvider = WallClock) } - .build(), - Component.builder(SessionFirelogPublisher::class.java) - .name("session-publisher") - .add(Dependency.required(firebaseApp)) - .add(Dependency.required(firebaseInstallationsApi)) - .add(Dependency.required(sessionsSettings)) - .add(Dependency.requiredProvider(transportFactory)) + Component.builder(FirebaseSessionsComponent::class.java) + .name("fire-sessions-component") + .add(Dependency.required(appContext)) .add(Dependency.required(backgroundDispatcher)) - .factory { container -> - SessionFirelogPublisherImpl( - container[firebaseApp], - container[firebaseInstallationsApi], - container[sessionsSettings], - EventGDTLogger(container.getProvider(transportFactory)), - container[backgroundDispatcher], - ) - } - .build(), - Component.builder(SessionsSettings::class.java) - .name("sessions-settings") - .add(Dependency.required(firebaseApp)) .add(Dependency.required(blockingDispatcher)) - .add(Dependency.required(backgroundDispatcher)) - .add(Dependency.required(firebaseInstallationsApi)) - .factory { container -> - SessionsSettings( - container[firebaseApp], - container[blockingDispatcher], - container[backgroundDispatcher], - container[firebaseInstallationsApi], - ) - } - .build(), - Component.builder(SessionDatastore::class.java) - .name("sessions-datastore") .add(Dependency.required(firebaseApp)) - .add(Dependency.required(backgroundDispatcher)) + .add(Dependency.required(firebaseInstallationsApi)) + .add(Dependency.requiredProvider(transportFactory)) .factory { container -> - SessionDatastoreImpl( - container[firebaseApp].applicationContext, - container[backgroundDispatcher], - ) + DaggerFirebaseSessionsComponent.builder() + .appContext(container[appContext]) + .backgroundDispatcher(container[backgroundDispatcher]) + .blockingDispatcher(container[blockingDispatcher]) + .firebaseApp(container[firebaseApp]) + .firebaseInstallationsApi(container[firebaseInstallationsApi]) + .transportFactoryProvider(container.getProvider(transportFactory)) + .build() } .build(), - Component.builder(SessionLifecycleServiceBinder::class.java) - .name("sessions-service-binder") - .add(Dependency.required(firebaseApp)) - .factory { container -> SessionLifecycleServiceBinderImpl(container[firebaseApp]) } - .build(), LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), ) private companion object { - private const val LIBRARY_NAME = "fire-sessions" + const val TAG = "FirebaseSessions" + const val LIBRARY_NAME = "fire-sessions" + + val appContext = unqualified(Context::class.java) + val firebaseApp = unqualified(FirebaseApp::class.java) + val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java) + val backgroundDispatcher = qualified(Background::class.java, CoroutineDispatcher::class.java) + val blockingDispatcher = qualified(Blocking::class.java, CoroutineDispatcher::class.java) + val transportFactory = unqualified(TransportFactory::class.java) + val firebaseSessionsComponent = unqualified(FirebaseSessionsComponent::class.java) + + init { + try { + ::preferencesDataStore.javaClass + } catch (ex: NoClassDefFoundError) { + Log.w( + TAG, + """ + Your app is experiencing a known issue in the Android Gradle plugin, see https://issuetracker.google.com/328687152 + + It affects Java-only apps using AGP version 8.3.2 and under. To avoid the issue, either: + + 1. Upgrade Android Gradle plugin to 8.4.0+ + Follow the guide at https://developer.android.com/build/agp-upgrade-assistant + + 2. Or, add the Kotlin plugin to your app + Follow the guide at https://developer.android.com/kotlin/add-kotlin - private val firebaseApp = unqualified(FirebaseApp::class.java) - private val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java) - private val backgroundDispatcher = - qualified(Background::class.java, CoroutineDispatcher::class.java) - private val blockingDispatcher = - qualified(Blocking::class.java, CoroutineDispatcher::class.java) - private val transportFactory = unqualified(TransportFactory::class.java) - private val sessionsSettings = unqualified(SessionsSettings::class.java) - private val sessionLifecycleServiceBinder = - unqualified(SessionLifecycleServiceBinder::class.java) + 3. Or, do the technical workaround described in https://issuetracker.google.com/issues/328687152#comment3 + """ + .trimIndent(), + ) + } + } } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt index 72e80469880..65d1dfbbc60 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt @@ -74,7 +74,7 @@ internal object ProcessDetailsProvider { /** Gets the app's current process name. If it could not be found, returns an empty string. */ internal fun getProcessName(): String { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { return Process.myProcessName() } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt index 736761617fd..2c4f243f942 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt @@ -16,20 +16,19 @@ package com.google.firebase.sessions -import android.content.Context import android.util.Log import androidx.datastore.core.DataStore -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore import com.google.firebase.Firebase +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.app -import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName import java.io.IOException import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -53,13 +52,16 @@ internal interface SessionDatastore { companion object { val instance: SessionDatastore - get() = Firebase.app[SessionDatastore::class.java] + get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionDatastore } } -internal class SessionDatastoreImpl( - private val context: Context, - private val backgroundDispatcher: CoroutineContext, +@Singleton +internal class SessionDatastoreImpl +@Inject +constructor( + @Background private val backgroundDispatcher: CoroutineContext, + @SessionDetailsDataStore private val dataStore: DataStore, ) : SessionDatastore { /** Most recent session from datastore is updated asynchronously whenever it changes */ @@ -70,7 +72,7 @@ internal class SessionDatastoreImpl( } private val firebaseSessionDataFlow: Flow = - context.dataStore.data + dataStore.data .catch { exception -> Log.e(TAG, "Error reading stored session data.", exception) emit(emptyPreferences()) @@ -86,14 +88,11 @@ internal class SessionDatastoreImpl( override fun updateSessionId(sessionId: String) { CoroutineScope(backgroundDispatcher).launch { try { - context.dataStore.edit { preferences -> + dataStore.edit { preferences -> preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId } } catch (e: IOException) { - Log.w( - TAG, - "Failed to update session Id: $e", - ) + Log.w(TAG, "Failed to update session Id: $e") } } } @@ -101,21 +100,9 @@ internal class SessionDatastoreImpl( override fun getCurrentSessionId() = currentSessionFromDatastore.get()?.sessionId private fun mapSessionsData(preferences: Preferences): FirebaseSessionsData = - FirebaseSessionsData( - preferences[FirebaseSessionDataKeys.SESSION_ID], - ) + FirebaseSessionsData(preferences[FirebaseSessionDataKeys.SESSION_ID]) private companion object { private const val TAG = "FirebaseSessionsRepo" - - private val Context.dataStore: DataStore by - preferencesDataStore( - name = SessionDataStoreConfigs.SESSIONS_CONFIG_NAME, - corruptionHandler = - ReplaceFileCorruptionHandler { ex -> - Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex) - emptyPreferences() - }, - ) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt index d63d49e3fe5..6e4b6153f8d 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt @@ -19,10 +19,13 @@ package com.google.firebase.sessions import android.util.Log import com.google.firebase.Firebase import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.app import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.api.FirebaseSessionsDependencies import com.google.firebase.sessions.settings.SessionsSettings +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -35,7 +38,7 @@ internal fun interface SessionFirelogPublisher { companion object { val instance: SessionFirelogPublisher - get() = Firebase.app[SessionFirelogPublisher::class.java] + get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionFirelogPublisher } } @@ -44,12 +47,15 @@ internal fun interface SessionFirelogPublisher { * * @hide */ -internal class SessionFirelogPublisherImpl( +@Singleton +internal class SessionFirelogPublisherImpl +@Inject +constructor( private val firebaseApp: FirebaseApp, private val firebaseInstallations: FirebaseInstallationsApi, private val sessionSettings: SessionsSettings, private val eventGDTLogger: EventGDTLoggerInterface, - private val backgroundDispatcher: CoroutineContext, + @Background private val backgroundDispatcher: CoroutineContext, ) : SessionFirelogPublisher { /** diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt index 3b4c3124c98..4c4775e8b24 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt @@ -19,7 +19,8 @@ package com.google.firebase.sessions import com.google.errorprone.annotations.CanIgnoreReturnValue import com.google.firebase.Firebase import com.google.firebase.app -import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton /** * [SessionDetails] is a data class responsible for storing information about the current Session. @@ -35,10 +36,10 @@ internal data class SessionDetails( * The [SessionGenerator] is responsible for generating the Session ID, and keeping the * [SessionDetails] up to date with the latest values. */ -internal class SessionGenerator( - private val timeProvider: TimeProvider, - private val uuidGenerator: () -> UUID = UUID::randomUUID -) { +@Singleton +internal class SessionGenerator +@Inject +constructor(private val timeProvider: TimeProvider, private val uuidGenerator: UuidGenerator) { private val firstSessionId = generateSessionId() private var sessionIndex = -1 @@ -59,15 +60,15 @@ internal class SessionGenerator( sessionId = if (sessionIndex == 0) firstSessionId else generateSessionId(), firstSessionId, sessionIndex, - sessionStartTimestampUs = timeProvider.currentTimeUs() + sessionStartTimestampUs = timeProvider.currentTimeUs(), ) return currentSession } - private fun generateSessionId() = uuidGenerator().toString().replace("-", "").lowercase() + private fun generateSessionId() = uuidGenerator.next().toString().replace("-", "").lowercase() internal companion object { val instance: SessionGenerator - get() = Firebase.app[SessionGenerator::class.java] + get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionGenerator } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt index bde6d138fbe..85930dc5455 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt @@ -128,7 +128,6 @@ internal class SessionLifecycleService : Service() { /** Generates a new session id and sends it everywhere it's needed */ private fun newSession() { try { - // TODO(mrober): Consider migrating to Dagger, or update [FirebaseSessionsRegistrar]. SessionGenerator.instance.generateNewSession() Log.d(TAG, "Generated new session.") broadcastSession() @@ -194,6 +193,7 @@ internal class SessionLifecycleService : Service() { handlerThread.start() messageHandler = MessageHandler(handlerThread.looper) messenger = Messenger(messageHandler) + Log.d(TAG, "Service created on process ${android.os.Process.myPid()}") } /** Called when a new [SessionLifecycleClient] binds to this service. */ diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt index 97a7d6b73ae..094a76ee51c 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt @@ -21,7 +21,8 @@ import android.content.Intent import android.content.ServiceConnection import android.os.Messenger import android.util.Log -import com.google.firebase.FirebaseApp +import javax.inject.Inject +import javax.inject.Singleton /** Interface for binding with the [SessionLifecycleService]. */ internal fun interface SessionLifecycleServiceBinder { @@ -32,11 +33,12 @@ internal fun interface SessionLifecycleServiceBinder { fun bindToService(callback: Messenger, serviceConnection: ServiceConnection) } -internal class SessionLifecycleServiceBinderImpl(private val firebaseApp: FirebaseApp) : - SessionLifecycleServiceBinder { +@Singleton +internal class SessionLifecycleServiceBinderImpl +@Inject +constructor(private val appContext: Context) : SessionLifecycleServiceBinder { override fun bindToService(callback: Messenger, serviceConnection: ServiceConnection) { - val appContext: Context = firebaseApp.applicationContext.applicationContext Intent(appContext, SessionLifecycleService::class.java).also { intent -> Log.d(TAG, "Binding service to application.") // This is necessary for the onBind() to be called by each process diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt index 706285de337..b66b09af19f 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt @@ -23,11 +23,12 @@ import kotlin.time.Duration.Companion.milliseconds /** Time provider interface, for testing purposes. */ internal interface TimeProvider { fun elapsedRealtime(): Duration + fun currentTimeUs(): Long } -/** "Wall clock" time provider. */ -internal object WallClock : TimeProvider { +/** "Wall clock" time provider implementation. */ +internal object TimeProviderImpl : TimeProvider { /** * Gets the [Duration] elapsed in "wall clock" time since device boot. * diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt new file mode 100644 index 00000000000..8c5b153fef2 --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 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.firebase.sessions + +import java.util.UUID + +/** UUID generator interface. */ +internal fun interface UuidGenerator { + fun next(): UUID +} + +/** Generate random UUIDs using [UUID.randomUUID]. */ +internal object UuidGeneratorImpl : UuidGenerator { + override fun next(): UUID = UUID.randomUUID() +} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt index 37e7acc949b..f13d0ffde2e 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt @@ -19,20 +19,19 @@ package com.google.firebase.sessions.settings import android.content.Context import android.content.pm.PackageManager import android.os.Bundle +import javax.inject.Inject +import javax.inject.Singleton import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration -internal class LocalOverrideSettings(context: Context) : SettingsProvider { - @Suppress("DEPRECATION") // TODO(mrober): Use ApplicationInfoFlags when target sdk set to 33 +@Singleton +internal class LocalOverrideSettings @Inject constructor(appContext: Context) : SettingsProvider { private val metadata = - context.packageManager - .getApplicationInfo( - context.packageName, - PackageManager.GET_META_DATA, - ) + appContext.packageManager + .getApplicationInfo(appContext.packageName, PackageManager.GET_META_DATA) .metaData - ?: Bundle.EMPTY // Default to an empty bundle, meaning no cached values. + ?: Bundle.EMPTY // Default to an empty bundle override val sessionEnabled: Boolean? get() = diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt index 1e6015a5c0d..67a48bc7924 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt @@ -19,11 +19,13 @@ package com.google.firebase.sessions.settings import android.os.Build import android.util.Log import androidx.annotation.VisibleForTesting -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.InstallationId +import dagger.Lazy +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -34,14 +36,19 @@ import kotlinx.coroutines.sync.withLock import org.json.JSONException import org.json.JSONObject -internal class RemoteSettings( - private val backgroundDispatcher: CoroutineContext, +@Singleton +internal class RemoteSettings +@Inject +constructor( + @Background private val backgroundDispatcher: CoroutineContext, private val firebaseInstallationsApi: FirebaseInstallationsApi, private val appInfo: ApplicationInfo, private val configsFetcher: CrashlyticsSettingsFetcher, - dataStore: DataStore, + private val lazySettingsCache: Lazy, ) : SettingsProvider { - private val settingsCache by lazy { SettingsCache(dataStore) } + private val settingsCache: SettingsCache + get() = lazySettingsCache.get() + private val fetchInProgress = Mutex() override val sessionEnabled: Boolean? diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt index a0896c24e7e..92d530f2fa1 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt @@ -17,10 +17,13 @@ package com.google.firebase.sessions.settings import android.net.Uri +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.sessions.ApplicationInfo import java.io.BufferedReader import java.io.InputStreamReader import java.net.URL +import javax.inject.Inject +import javax.inject.Singleton import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.withContext @@ -30,20 +33,22 @@ internal fun interface CrashlyticsSettingsFetcher { suspend fun doConfigFetch( headerOptions: Map, onSuccess: suspend (JSONObject) -> Unit, - onFailure: suspend (msg: String) -> Unit + onFailure: suspend (msg: String) -> Unit, ) } -internal class RemoteSettingsFetcher( +@Singleton +internal class RemoteSettingsFetcher +@Inject +constructor( private val appInfo: ApplicationInfo, - private val blockingDispatcher: CoroutineContext, - private val baseUrl: String = FIREBASE_SESSIONS_BASE_URL_STRING, + @Background private val blockingDispatcher: CoroutineContext, ) : CrashlyticsSettingsFetcher { @Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls. override suspend fun doConfigFetch( headerOptions: Map, onSuccess: suspend (JSONObject) -> Unit, - onFailure: suspend (String) -> Unit + onFailure: suspend (String) -> Unit, ) = withContext(blockingDispatcher) { try { @@ -78,7 +83,7 @@ internal class RemoteSettingsFetcher( val uri = Uri.Builder() .scheme("https") - .authority(baseUrl) + .authority(FIREBASE_SESSIONS_BASE_URL_STRING) .appendPath("spi") .appendPath("v2") .appendPath("platforms") diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt index fd2ee5dbddd..d319bebb7a2 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt @@ -16,64 +16,24 @@ package com.google.firebase.sessions.settings -import android.content.Context -import android.util.Log -import androidx.datastore.core.DataStore -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.emptyPreferences -import androidx.datastore.preferences.preferencesDataStore import com.google.firebase.Firebase -import com.google.firebase.FirebaseApp import com.google.firebase.app -import com.google.firebase.installations.FirebaseInstallationsApi -import com.google.firebase.sessions.ApplicationInfo -import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName -import com.google.firebase.sessions.SessionDataStoreConfigs -import com.google.firebase.sessions.SessionEvents -import kotlin.coroutines.CoroutineContext +import com.google.firebase.sessions.FirebaseSessionsComponent +import com.google.firebase.sessions.LocalOverrideSettingsProvider +import com.google.firebase.sessions.RemoteSettingsProvider +import javax.inject.Inject +import javax.inject.Singleton import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes /** [SessionsSettings] manages all the configs that are relevant to the sessions library. */ -internal class SessionsSettings( - private val localOverrideSettings: SettingsProvider, - private val remoteSettings: SettingsProvider, +@Singleton +internal class SessionsSettings +@Inject +constructor( + @LocalOverrideSettingsProvider private val localOverrideSettings: SettingsProvider, + @RemoteSettingsProvider private val remoteSettings: SettingsProvider, ) { - private constructor( - context: Context, - blockingDispatcher: CoroutineContext, - backgroundDispatcher: CoroutineContext, - firebaseInstallationsApi: FirebaseInstallationsApi, - appInfo: ApplicationInfo, - ) : this( - localOverrideSettings = LocalOverrideSettings(context), - remoteSettings = - RemoteSettings( - backgroundDispatcher, - firebaseInstallationsApi, - appInfo, - configsFetcher = - RemoteSettingsFetcher( - appInfo, - blockingDispatcher, - ), - dataStore = context.dataStore, - ), - ) - - constructor( - firebaseApp: FirebaseApp, - blockingDispatcher: CoroutineContext, - backgroundDispatcher: CoroutineContext, - firebaseInstallationsApi: FirebaseInstallationsApi, - ) : this( - firebaseApp.applicationContext, - blockingDispatcher, - backgroundDispatcher, - firebaseInstallationsApi, - SessionEvents.getApplicationInfo(firebaseApp), - ) // Order of preference for all the configs below: // 1. Honor local overrides @@ -140,19 +100,7 @@ internal class SessionsSettings( } internal companion object { - private const val TAG = "SessionsSettings" - val instance: SessionsSettings - get() = Firebase.app[SessionsSettings::class.java] - - private val Context.dataStore: DataStore by - preferencesDataStore( - name = SessionDataStoreConfigs.SETTINGS_CONFIG_NAME, - corruptionHandler = - ReplaceFileCorruptionHandler { ex -> - Log.w(TAG, "CorruptionException in settings DataStore in ${getProcessName()}.", ex) - emptyPreferences() - }, - ) + get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionsSettings } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt index 33b6a4fe7c8..2e60e51650a 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt @@ -25,7 +25,10 @@ import androidx.datastore.preferences.core.doublePreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey +import com.google.firebase.sessions.SessionConfigsDataStore import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking @@ -37,7 +40,10 @@ internal data class SessionConfigs( val cacheUpdatedTime: Long?, ) -internal class SettingsCache(private val dataStore: DataStore) { +@Singleton +internal class SettingsCache +@Inject +constructor(@SessionConfigsDataStore private val dataStore: DataStore) { private lateinit var sessionConfigs: SessionConfigs init { @@ -54,7 +60,7 @@ internal class SettingsCache(private val dataStore: DataStore) { sessionSamplingRate = preferences[SAMPLING_RATE], sessionRestartTimeout = preferences[RESTART_TIMEOUT_SECONDS], cacheDuration = preferences[CACHE_DURATION_SECONDS], - cacheUpdatedTime = preferences[CACHE_UPDATED_TIME] + cacheUpdatedTime = preferences[CACHE_UPDATED_TIME], ) } @@ -105,10 +111,7 @@ internal class SettingsCache(private val dataStore: DataStore) { updateSessionConfigs(preferences) } } catch (e: IOException) { - Log.w( - TAG, - "Failed to remove config values: $e", - ) + Log.w(TAG, "Failed to remove config values: $e") } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt index 7f29fb66ae7..7126bae4dbf 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt @@ -18,8 +18,8 @@ package com.google.firebase.sessions import com.google.common.truth.Truth.assertThat import com.google.firebase.sessions.testing.FakeTimeProvider +import com.google.firebase.sessions.testing.FakeUuidGenerator import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_TIMESTAMP_US -import java.util.UUID import org.junit.Test class SessionGeneratorTest { @@ -41,9 +41,7 @@ class SessionGeneratorTest { @Test(expected = UninitializedPropertyAccessException::class) fun currentSession_beforeGenerate_throwsUninitialized() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) sessionGenerator.currentSession } @@ -51,9 +49,7 @@ class SessionGeneratorTest { @Test fun hasGenerateSession_beforeGenerate_returnsFalse() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) assertThat(sessionGenerator.hasGenerateSession).isFalse() } @@ -61,9 +57,7 @@ class SessionGeneratorTest { @Test fun hasGenerateSession_afterGenerate_returnsTrue() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) sessionGenerator.generateNewSession() @@ -73,9 +67,7 @@ class SessionGeneratorTest { @Test fun generateNewSession_generatesValidSessionIds() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) sessionGenerator.generateNewSession() @@ -91,10 +83,7 @@ class SessionGeneratorTest { @Test fun generateNewSession_generatesValidSessionDetails() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - uuidGenerator = UUIDs()::next, - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = FakeUuidGenerator()) sessionGenerator.generateNewSession() @@ -117,10 +106,7 @@ class SessionGeneratorTest { @Test fun generateNewSession_incrementsSessionIndex_keepsFirstSessionId() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - uuidGenerator = UUIDs()::next, - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = FakeUuidGenerator()) val firstSessionDetails = sessionGenerator.generateNewSession() @@ -170,22 +156,9 @@ class SessionGeneratorTest { ) } - private class UUIDs(val names: List = listOf(UUID_1, UUID_2, UUID_3)) { - var index = -1 - - fun next(): UUID { - index = (index + 1).coerceAtMost(names.size - 1) - return UUID.fromString(names[index]) - } - } - - @Suppress("SpellCheckingInspection") // UUIDs are not words. companion object { - const val UUID_1 = "11111111-1111-1111-1111-111111111111" const val SESSION_ID_1 = "11111111111111111111111111111111" - const val UUID_2 = "22222222-2222-2222-2222-222222222222" const val SESSION_ID_2 = "22222222222222222222222222222222" - const val UUID_3 = "CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC" const val SESSION_ID_3 = "cccccccccccccccccccccccccccccccc" } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt index b038e68081c..12a017a7462 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt @@ -31,6 +31,7 @@ import com.google.firebase.sessions.api.SessionSubscriber.SessionDetails import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeSessionLifecycleServiceBinder import com.google.firebase.sessions.testing.FakeSessionSubscriber +import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -47,21 +48,21 @@ import org.robolectric.Shadows.shadowOf @RunWith(RobolectricTestRunner::class) internal class SessionLifecycleClientTest { private lateinit var fakeService: FakeSessionLifecycleServiceBinder - private lateinit var lifecycleServiceBinder: FakeSessionLifecycleServiceBinder + private lateinit var lifecycleServiceBinder: SessionLifecycleServiceBinder @Before fun setUp() { - val firebaseApp = - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build(), - ) - fakeService = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] - lifecycleServiceBinder = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build(), + ) + + fakeService = FirebaseSessionsFakeComponent.instance.fakeSessionLifecycleServiceBinder + lifecycleServiceBinder = FirebaseSessionsFakeComponent.instance.sessionLifecycleServiceBinder } @After diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt index 682a9ddfbbb..ccd933f1213 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt @@ -16,7 +16,6 @@ package com.google.firebase.sessions -import android.content.Context import android.content.Intent import android.os.Handler import android.os.Looper @@ -30,10 +29,8 @@ import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions import com.google.firebase.initialize import com.google.firebase.sessions.testing.FakeFirebaseApp -import com.google.firebase.sessions.testing.FakeFirelogPublisher -import com.google.firebase.sessions.testing.FakeSessionDatastore +import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent import java.time.Duration -import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Before import org.junit.Test @@ -46,14 +43,11 @@ import org.robolectric.annotation.LooperMode import org.robolectric.annotation.LooperMode.Mode.PAUSED import org.robolectric.shadows.ShadowSystemClock -@OptIn(ExperimentalCoroutinesApi::class) @MediumTest @LooperMode(PAUSED) @RunWith(RobolectricTestRunner::class) internal class SessionLifecycleServiceTest { - - lateinit var service: ServiceController - lateinit var firebaseApp: FirebaseApp + private lateinit var service: ServiceController data class CallbackMessage(val code: Int, val sessionId: String?) @@ -68,16 +62,14 @@ internal class SessionLifecycleServiceTest { @Before fun setUp() { - val context = ApplicationProvider.getApplicationContext() - firebaseApp = - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build() - ) + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build(), + ) service = createService() } @@ -99,7 +91,7 @@ internal class SessionLifecycleServiceTest { @Test fun binding_callbackOnInitialBindWhenSessionIdSet() { val client = TestCallbackHandler() - firebaseApp.get(FakeSessionDatastore::class.java).updateSessionId("123") + FirebaseSessionsFakeComponent.instance.fakeSessionDatastore.updateSessionId("123") bindToService(client) @@ -222,11 +214,9 @@ internal class SessionLifecycleServiceTest { } private fun createServiceLaunchIntent(client: TestCallbackHandler) = - Intent( - ApplicationProvider.getApplicationContext(), - SessionLifecycleService::class.java - ) - .apply { putExtra(SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, Messenger(client)) } + Intent(ApplicationProvider.getApplicationContext(), SessionLifecycleService::class.java).apply { + putExtra(SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, Messenger(client)) + } private fun createService() = Robolectric.buildService(SessionLifecycleService::class.java).create() @@ -237,7 +227,7 @@ internal class SessionLifecycleServiceTest { } private fun getUploadedSessions() = - firebaseApp.get(FakeFirelogPublisher::class.java).loggedSessions + FirebaseSessionsFakeComponent.instance.fakeFirelogPublisher.loggedSessions private fun getSessionId(msg: Message) = msg.data?.getString(SessionLifecycleService.SESSION_UPDATE_EXTRA) diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt index 189e13fed89..62e650d90c8 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt @@ -31,6 +31,7 @@ import com.google.firebase.sessions.api.SessionSubscriber import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeSessionLifecycleServiceBinder import com.google.firebase.sessions.testing.FakeSessionSubscriber +import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher @@ -46,7 +47,7 @@ import org.robolectric.Shadows @RunWith(AndroidJUnit4::class) internal class SessionsActivityLifecycleCallbacksTest { private lateinit var fakeService: FakeSessionLifecycleServiceBinder - private lateinit var lifecycleServiceBinder: FakeSessionLifecycleServiceBinder + private lateinit var lifecycleServiceBinder: SessionLifecycleServiceBinder private val fakeActivity = Activity() @Before @@ -63,17 +64,17 @@ internal class SessionsActivityLifecycleCallbacksTest { ) ) - val firebaseApp = - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build(), - ) - fakeService = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] - lifecycleServiceBinder = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build(), + ) + + fakeService = FirebaseSessionsFakeComponent.instance.fakeSessionLifecycleServiceBinder + lifecycleServiceBinder = FirebaseSessionsFakeComponent.instance.sessionLifecycleServiceBinder } @After diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt index 6a3a4a1f8c3..e4fb0b00148 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt @@ -22,11 +22,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp import com.google.firebase.concurrent.TestOnlyExecutors +import com.google.firebase.installations.FirebaseInstallationsApi +import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.SessionEvents import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeFirebaseInstallations import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher -import com.google.firebase.sessions.testing.TestSessionEventData.TEST_APPLICATION_INFO +import kotlin.coroutines.CoroutineContext import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers @@ -55,19 +57,18 @@ class RemoteSettingsTest { val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") val fakeFetcher = FakeRemoteConfigFetcher() - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext - val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) runCurrent() @@ -97,16 +98,17 @@ class RemoteSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) runCurrent() @@ -138,16 +140,17 @@ class RemoteSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) val fetchedResponse = JSONObject(VALID_RESPONSE) @@ -190,16 +193,17 @@ class RemoteSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) val fetchedResponse = JSONObject(VALID_RESPONSE) @@ -248,26 +252,24 @@ class RemoteSettingsTest { val context = firebaseApp.applicationContext val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") val fakeFetcherWithDelay = - FakeRemoteConfigFetcher( - JSONObject(VALID_RESPONSE), - networkDelay = 3.seconds, - ) + FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE), networkDelay = 3.seconds) fakeFetcherWithDelay.responseJSONObject .getJSONObject("app_quality") .put("sampling_rate", 0.125) val remoteSettingsWithDelay = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), - configsFetcher = fakeFetcherWithDelay, - dataStore = + fakeFetcherWithDelay, + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) // Do the first fetch. This one should fetched the configsFetcher. @@ -290,30 +292,12 @@ class RemoteSettingsTest { assertThat(remoteSettingsWithDelay.samplingRate).isEqualTo(0.125) } - @Test - fun remoteSettingsFetcher_badFetch_callsOnFailure() = runTest { - var failure: String? = null - - RemoteSettingsFetcher( - TEST_APPLICATION_INFO, - TestOnlyExecutors.blocking().asCoroutineDispatcher() + coroutineContext, - baseUrl = "this.url.is.invalid", - ) - .doConfigFetch( - headerOptions = emptyMap(), - onSuccess = {}, - onFailure = { failure = it }, - ) - - assertThat(failure).isNotNull() - } - @After fun cleanUp() { FirebaseApp.clearInstancesForTest() } - private companion object { + internal companion object { const val SESSION_TEST_CONFIGS_NAME = "firebase_session_settings_test" const val VALID_RESPONSE = @@ -334,5 +318,30 @@ class RemoteSettingsTest { } } """ + + /** + * Build an instance of [RemoteSettings] using the Dagger factory. + * + * This is needed because the SDK vendors Dagger to a difference namespace, but it does not for + * these unit tests. The [RemoteSettings.lazySettingsCache] has type [dagger.Lazy] in these + * tests, but type `com.google.firebase.sessions.dagger.Lazy` in the SDK. This method to build + * the instance is the easiest I could find that does not need any reference to [dagger.Lazy] in + * the test code. + */ + fun buildRemoteSettings( + backgroundDispatcher: CoroutineContext, + firebaseInstallationsApi: FirebaseInstallationsApi, + appInfo: ApplicationInfo, + configsFetcher: CrashlyticsSettingsFetcher, + settingsCache: SettingsCache, + ): RemoteSettings = + RemoteSettings_Factory.create( + { backgroundDispatcher }, + { firebaseInstallationsApi }, + { appInfo }, + { configsFetcher }, + { settingsCache }, + ) + .get() } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt index f74eac409e5..12f40e7cca8 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt @@ -107,16 +107,17 @@ class SessionsSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) val remoteSettings = - RemoteSettings( + RemoteSettingsTest.buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) val sessionsSettings = @@ -149,16 +150,17 @@ class SessionsSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) val remoteSettings = - RemoteSettings( + RemoteSettingsTest.buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) val sessionsSettings = @@ -197,16 +199,17 @@ class SessionsSettingsTest { fakeFetcher.responseJSONObject = JSONObject(invalidResponse) val remoteSettings = - RemoteSettings( + RemoteSettingsTest.buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) val sessionsSettings = diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt new file mode 100644 index 00000000000..88f1f816c12 --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 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.firebase.sessions.testing + +import com.google.firebase.sessions.UuidGenerator +import java.util.UUID + +/** Fake implementation of [UuidGenerator] to provide uuids of the given names in order. */ +internal class FakeUuidGenerator(private val names: List = listOf(UUID_1, UUID_2, UUID_3)) : + UuidGenerator { + private var index = -1 + + override fun next(): UUID { + index = (index + 1).coerceAtMost(names.size - 1) + return UUID.fromString(names[index]) + } + + companion object { + const val UUID_1 = "11111111-1111-1111-1111-111111111111" + const val UUID_2 = "22222222-2222-2222-2222-222222222222" + const val UUID_3 = "CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC" + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt new file mode 100644 index 00000000000..b3431f71840 --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2025 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.firebase.sessions.testing + +import com.google.firebase.Firebase +import com.google.firebase.app +import com.google.firebase.sessions.FirebaseSessions +import com.google.firebase.sessions.FirebaseSessionsComponent +import com.google.firebase.sessions.SessionDatastore +import com.google.firebase.sessions.SessionFirelogPublisher +import com.google.firebase.sessions.SessionGenerator +import com.google.firebase.sessions.SessionLifecycleServiceBinder +import com.google.firebase.sessions.settings.SessionsSettings +import com.google.firebase.sessions.settings.SettingsProvider + +/** Fake component to manage [FirebaseSessions] and related, often faked, dependencies. */ +@Suppress("MemberVisibilityCanBePrivate") // Keep access to fakes open for convenience +internal class FirebaseSessionsFakeComponent : FirebaseSessionsComponent { + // TODO(mrober): Move tests that need DI to integration tests, and remove this component. + + // Fakes, access these instances to setup test cases, e.g., add interval to fake time provider. + val fakeTimeProvider = FakeTimeProvider() + val fakeUuidGenerator = FakeUuidGenerator() + val fakeSessionDatastore = FakeSessionDatastore() + val fakeFirelogPublisher = FakeFirelogPublisher() + val fakeSessionLifecycleServiceBinder = FakeSessionLifecycleServiceBinder() + + // Settings providers, default to fake, set these to real instances for relevant test cases. + var localOverrideSettings: SettingsProvider = FakeSettingsProvider() + var remoteSettings: SettingsProvider = FakeSettingsProvider() + + override val firebaseSessions: FirebaseSessions + get() = throw NotImplementedError("FirebaseSessions not implemented, use integration tests.") + + override val sessionDatastore: SessionDatastore = fakeSessionDatastore + + override val sessionFirelogPublisher: SessionFirelogPublisher = fakeFirelogPublisher + + override val sessionGenerator: SessionGenerator by lazy { + SessionGenerator(timeProvider = fakeTimeProvider, uuidGenerator = fakeUuidGenerator) + } + + override val sessionsSettings: SessionsSettings by lazy { + SessionsSettings(localOverrideSettings, remoteSettings) + } + + val sessionLifecycleServiceBinder: SessionLifecycleServiceBinder + get() = fakeSessionLifecycleServiceBinder + + companion object { + val instance: FirebaseSessionsFakeComponent + get() = Firebase.app[FirebaseSessionsFakeComponent::class.java] + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt index 9755a5e12d0..8dc6454931e 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt @@ -16,102 +16,37 @@ package com.google.firebase.sessions.testing -import androidx.annotation.Keep -import com.google.android.datatransport.TransportFactory -import com.google.firebase.FirebaseApp -import com.google.firebase.annotations.concurrent.Background -import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.components.Component import com.google.firebase.components.ComponentRegistrar import com.google.firebase.components.Dependency -import com.google.firebase.components.Qualified.qualified import com.google.firebase.components.Qualified.unqualified -import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.platforminfo.LibraryVersionComponent import com.google.firebase.sessions.BuildConfig import com.google.firebase.sessions.FirebaseSessions -import com.google.firebase.sessions.SessionDatastore -import com.google.firebase.sessions.SessionFirelogPublisher -import com.google.firebase.sessions.SessionGenerator -import com.google.firebase.sessions.SessionLifecycleServiceBinder -import com.google.firebase.sessions.WallClock -import com.google.firebase.sessions.settings.SessionsSettings -import kotlinx.coroutines.CoroutineDispatcher +import com.google.firebase.sessions.FirebaseSessionsComponent /** * [ComponentRegistrar] for setting up Fake components for [FirebaseSessions] and its internal * dependencies for unit tests. - * - * @hide */ -@Keep internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { override fun getComponents() = listOf( - Component.builder(SessionGenerator::class.java) - .name("session-generator") - .factory { SessionGenerator(timeProvider = WallClock) } - .build(), - Component.builder(FakeFirelogPublisher::class.java) - .name("fake-session-publisher") - .factory { FakeFirelogPublisher() } - .build(), - Component.builder(SessionFirelogPublisher::class.java) - .name("session-publisher") - .add(Dependency.required(fakeFirelogPublisher)) - .factory { container -> container.get(fakeFirelogPublisher) } - .build(), - Component.builder(SessionsSettings::class.java) - .name("sessions-settings") - .add(Dependency.required(firebaseApp)) - .add(Dependency.required(blockingDispatcher)) - .add(Dependency.required(backgroundDispatcher)) - .factory { container -> - SessionsSettings( - container.get(firebaseApp), - container.get(blockingDispatcher), - container.get(backgroundDispatcher), - fakeFirebaseInstallations, - ) - } + Component.builder(FirebaseSessionsComponent::class.java) + .name("fire-sessions-component") + .add(Dependency.required(firebaseSessionsFakeComponent)) + .factory { container -> container.get(firebaseSessionsFakeComponent) } .build(), - Component.builder(FakeSessionDatastore::class.java) - .name("fake-sessions-datastore") - .factory { FakeSessionDatastore() } - .build(), - Component.builder(SessionDatastore::class.java) - .name("sessions-datastore") - .add(Dependency.required(fakeDatastore)) - .factory { container -> container.get(fakeDatastore) } - .build(), - Component.builder(FakeSessionLifecycleServiceBinder::class.java) - .name("fake-sessions-service-binder") - .factory { FakeSessionLifecycleServiceBinder() } - .build(), - Component.builder(SessionLifecycleServiceBinder::class.java) - .name("sessions-service-binder") - .add(Dependency.required(fakeServiceBinder)) - .factory { container -> container.get(fakeServiceBinder) } + Component.builder(FirebaseSessionsFakeComponent::class.java) + .name("fire-sessions-fake-component") + .factory { FirebaseSessionsFakeComponent() } .build(), LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), ) private companion object { - private const val LIBRARY_NAME = "fire-sessions" - - private val firebaseApp = unqualified(FirebaseApp::class.java) - private val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java) - private val backgroundDispatcher = - qualified(Background::class.java, CoroutineDispatcher::class.java) - private val blockingDispatcher = - qualified(Blocking::class.java, CoroutineDispatcher::class.java) - private val transportFactory = unqualified(TransportFactory::class.java) - private val fakeFirelogPublisher = unqualified(FakeFirelogPublisher::class.java) - private val fakeDatastore = unqualified(FakeSessionDatastore::class.java) - private val fakeServiceBinder = unqualified(FakeSessionLifecycleServiceBinder::class.java) - private val sessionGenerator = unqualified(SessionGenerator::class.java) - private val sessionsSettings = unqualified(SessionsSettings::class.java) + const val LIBRARY_NAME = "fire-sessions" - private val fakeFirebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val firebaseSessionsFakeComponent = unqualified(FirebaseSessionsFakeComponent::class.java) } } diff --git a/firebase-sessions/test-app/src/main/AndroidManifest.xml b/firebase-sessions/test-app/src/main/AndroidManifest.xml index 3e1f4840cb3..9965842a01e 100644 --- a/firebase-sessions/test-app/src/main/AndroidManifest.xml +++ b/firebase-sessions/test-app/src/main/AndroidManifest.xml @@ -43,6 +43,11 @@ android:process=":second" android:theme="@style/Theme.Widget_test_app.NoActionBar" /> + + + @@ -51,6 +56,12 @@ android:name="firebase_sessions_sessions_restart_timeout" android:value="5" /> + + + + - - = Build.VERSION_CODES.P) Application.getProcessName() else "unknown" + private fun logProcessDetails() { + val pid = android.os.Process.myPid() + val uid = android.os.Process.myUid() + val activity = javaClass.name + val process = getProcessName() + Log.i(TAG, "activity: $activity process: $process, pid: $pid, uid: $uid") + } + + private fun logFirebaseDetails() { + val activity = javaClass.name + val firebaseApps = FirebaseApp.getApps(this) + val defaultFirebaseApp = FirebaseApp.getInstance() + Log.i( + TAG, + "activity: $activity firebase: ${defaultFirebaseApp.name} appsCount: ${firebaseApps.count()}" + ) + } + + private fun setProcessAttribute() { + FirebasePerformance.getInstance().putAttribute("process_name", getProcessName()) + } + companion object { val TAG = "BaseActivity" } diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt index 88488a4cc92..f5a965da7d4 100644 --- a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt @@ -16,6 +16,7 @@ package com.google.firebase.testing.sessions +import android.app.Application import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT import android.content.Intent.FLAG_ACTIVITY_NEW_TASK @@ -26,14 +27,20 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.perf.FirebasePerformance import com.google.firebase.testing.sessions.databinding.FragmentFirstBinding import java.util.Date import java.util.Locale +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** A simple [Fragment] subclass as the default destination in the navigation. */ class FirstFragment : Fragment() { val crashlytics = FirebaseCrashlytics.getInstance() + val performance = FirebasePerformance.getInstance() private var _binding: FragmentFirstBinding? = null @@ -64,6 +71,14 @@ class FirstFragment : Fragment() { Thread.sleep(1_000) } } + binding.createTrace.setOnClickListener { + lifecycleScope.launch(Dispatchers.IO) { + val performanceTrace = performance.newTrace("test_trace") + performanceTrace.start() + delay(1000) + performanceTrace.stop() + } + } binding.buttonForegroundProcess.setOnClickListener { if (binding.buttonForegroundProcess.getText().startsWith("Start")) { ForegroundService.startService(requireContext(), "Starting service at ${getDateText()}") @@ -89,6 +104,7 @@ class FirstFragment : Fragment() { intent.addFlags(FLAG_ACTIVITY_NEW_TASK) startActivity(intent) } + binding.processName.text = getProcessName() } override fun onResume() { @@ -111,5 +127,9 @@ class FirstFragment : Fragment() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date()) else "unknown" + + fun getProcessName(): String = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) Application.getProcessName() + else "unknown" } } diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt index 9272510d0f3..6c2fd3c06b0 100644 --- a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt @@ -22,10 +22,14 @@ import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.os.Build import android.os.Bundle import android.widget.Button +import android.widget.TextView +import androidx.lifecycle.lifecycleScope +import com.google.firebase.perf.FirebasePerformance +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** Second activity from the MainActivity that runs on a different process. */ class SecondActivity : BaseActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) @@ -38,12 +42,21 @@ class SecondActivity : BaseActivity() { findViewById