diff --git a/packages/firestore/lite/pipelines/pipelines.ts b/packages/firestore/lite/pipelines/pipelines.ts index cc7f91e750c..1b46bcbff7b 100644 --- a/packages/firestore/lite/pipelines/pipelines.ts +++ b/packages/firestore/lite/pipelines/pipelines.ts @@ -50,6 +50,8 @@ export type { export { PipelineSource } from '../../src/lite-api/pipeline-source'; +export { OneOf } from '../../src/util/types'; + export { PipelineResult, PipelineSnapshot @@ -60,51 +62,45 @@ export { Pipeline } from '../../src/lite-api/pipeline'; export { execute } from '../../src/lite-api/pipeline_impl'; export { - Stage, - FindNearestOptions, - AddFields, - Aggregate, - Distinct, - CollectionSource, - CollectionGroupSource, - DatabaseSource, - DocumentsSource, - Where, - FindNearest, - Limit, - Offset, - Select, - Sort, - GenericStage -} from '../../src/lite-api/stage'; + StageOptions, + CollectionStageOptions, + CollectionGroupStageOptions, + DatabaseStageOptions, + DocumentsStageOptions, + AddFieldsStageOptions, + RemoveFieldsStageOptions, + SelectStageOptions, + WhereStageOptions, + OffsetStageOptions, + LimitStageOptions, + DistinctStageOptions, + AggregateStageOptions, + FindNearestStageOptions, + ReplaceWithStageOptions, + SampleStageOptions, + UnionStageOptions, + UnnestStageOptions, + SortStageOptions +} from '../../src/lite-api/stage_options'; export { - Expr, + Expression, field, and, array, - arrayOffset, constant, add, subtract, multiply, - avg, - bitAnd, - substr, - constantVector, - bitLeftShift, - bitNot, + average, + substring, count, mapMerge, mapRemove, - bitOr, ifError, isAbsent, isError, or, - rand, - bitRightShift, - bitXor, divide, isNotNan, map, @@ -112,42 +108,40 @@ export { isNull, mod, documentId, - eq, - neq, - lt, + equal, + notEqual, + lessThan, countIf, - lte, - gt, - gte, + lessThanOrEqual, + greaterThan, + greaterThanOrEqual, arrayConcat, arrayContains, arrayContainsAny, arrayContainsAll, arrayLength, - eqAny, - notEqAny, + equalAny, + notEqualAny, xor, - cond, + conditional, not, logicalMaximum, logicalMinimum, exists, isNan, reverse, - replaceFirst, - replaceAll, byteLength, charLength, like, regexContains, regexMatch, - strContains, + stringContains, startsWith, endsWith, toLower, toUpper, trim, - strConcat, + stringConcat, mapGet, countAll, minimum, @@ -163,17 +157,17 @@ export { unixSecondsToTimestamp, timestampToUnixSeconds, timestampAdd, - timestampSub, + timestampSubtract, ascending, descending, - ExprWithAlias, + AliasedExpression, Field, Constant, - FunctionExpr, + FunctionExpression, Ordering, - ExprType, + ExpressionType, AggregateWithAlias, Selectable, - BooleanExpr, + BooleanExpression, AggregateFunction } from '../../src/lite-api/expressions'; diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 7c88da9343c..f3b9711359b 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -8,6 +8,7 @@ "author": "Firebase (https://firebase.google.com/)", "scripts": { "bundle": "rollup -c", + "compile": "tsc --emitDeclarationOnly --declaration -p tsconfig.json", "prebuild": "tsc --emitDeclarationOnly --declaration -p tsconfig.json; yarn api-report", "build": "run-p --npm-path npm build:lite build:main", "build:release": "yarn build && yarn typings:public", @@ -95,12 +96,12 @@ "require": "./dist/lite/pipelines.node.cjs.js", "import": "./dist/lite/pipelines.node.mjs" }, - "react-native": "./dist/lite/pipelines.rn.esm2017.js", + "react-native": "./dist/lite/pipelines.rn.esm.js", "browser": { "require": "./dist/lite/pipelines.browser.cjs.js", - "import": "./dist/lite/pipelines.browser.esm2017.js" + "import": "./dist/lite/pipelines.browser.esm.js" }, - "default": "./dist/lite/pipelines.browser.esm2017.js" + "default": "./dist/lite/pipelines.browser.esm.js" }, "./pipelines": { "types": "./pipelines/pipelines.d.ts", @@ -108,24 +109,28 @@ "require": "./dist/pipelines.node.cjs.js", "import": "./dist/pipelines.node.mjs" }, - "react-native": "./dist/index.rn.esm2017.js", + "react-native": "./dist/index.rn.esm.js", "browser": { "require": "./dist/pipelines.cjs.js", - "import": "./dist/pipelines.esm2017.js" + "import": "./dist/pipelines.esm.js" }, - "default": "./dist/pipelines.esm2017.js" + "default": "./dist/pipelines.esm.js" }, "./package.json": "./package.json" }, "main": "dist/node-cjs/index.node.cjs.js", "main-esm": "dist/node-esm/index.node.mjs", "react-native": "dist/index.rn.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", + "browser": "dist/browser-esm2017/index.esm.js", + "module": "dist/browser-esm2017/index.esm.js", "license": "Apache-2.0", "files": [ "dist", - "lite/package.json" + "lite/package.json", + "pipelines/package.json", + "pipelines/pipelines.d.ts", + "lite/pipelines/package.json", + "lite/pipelines/pipelines.d.ts" ], "dependencies": { "@firebase/component": "0.7.0", diff --git a/packages/firestore/rollup.config.js b/packages/firestore/rollup.config.js index 1020e05be23..c9222f69ab4 100644 --- a/packages/firestore/rollup.config.js +++ b/packages/firestore/rollup.config.js @@ -178,8 +178,8 @@ const allBuilds = [ output: [ { dir: 'dist/', - entryFileNames: '[name].esm2017.js', - chunkFileNames: 'common-[hash].esm2017.js', + entryFileNames: '[name].esm.js', + chunkFileNames: 'common-[hash].esm.js', format: 'es', sourcemap: true } diff --git a/packages/firestore/rollup.config.lite.js b/packages/firestore/rollup.config.lite.js index 746502c7808..6bf9297e4d8 100644 --- a/packages/firestore/rollup.config.lite.js +++ b/packages/firestore/rollup.config.lite.js @@ -187,8 +187,8 @@ const allBuilds = [ output: [ { dir: 'dist/lite/', - entryFileNames: '[name].esm2017.js', - chunkFileNames: 'common-[hash].esm2017.js', + entryFileNames: '[name].esm.js', + chunkFileNames: 'common-[hash].esm.js', format: 'es', sourcemap: true } @@ -207,8 +207,8 @@ const allBuilds = [ input: ['./lite/index.ts', './lite/pipelines/pipelines.ts'], output: { dir: 'dist/lite/', - entryFileNames: '[name].rn.esm2017.js', - chunkFileNames: 'common-[hash].rn.esm2017.js', + entryFileNames: '[name].rn.esm.js', + chunkFileNames: 'common-[hash].rn.esm.js', format: 'es', sourcemap: true }, diff --git a/packages/firestore/src/api/pipeline_impl.ts b/packages/firestore/src/api/pipeline_impl.ts index ba6e08105bb..843a3696f71 100644 --- a/packages/firestore/src/api/pipeline_impl.ts +++ b/packages/firestore/src/api/pipeline_impl.ts @@ -17,11 +17,20 @@ import { Pipeline } from '../api/pipeline'; import { firestoreClientExecutePipeline } from '../core/firestore_client'; +import { + StructuredPipeline, + StructuredPipelineOptions +} from '../core/structured_pipeline'; import { Pipeline as LitePipeline } from '../lite-api/pipeline'; import { PipelineResult, PipelineSnapshot } from '../lite-api/pipeline-result'; import { PipelineSource } from '../lite-api/pipeline-source'; +import { PipelineExecuteOptions } from '../lite-api/pipeline_options'; import { Stage } from '../lite-api/stage'; -import { newUserDataReader } from '../lite-api/user_data_reader'; +import { + newUserDataReader, + UserDataReader, + UserDataSource +} from '../lite-api/user_data_reader'; import { cast } from '../util/input_validation'; import { ensureFirestoreConfigured, Firestore } from './database'; @@ -68,45 +77,86 @@ declare module './database' { * @param pipeline The pipeline to execute. * @return A Promise representing the asynchronous pipeline execution. */ -export function execute(pipeline: LitePipeline): Promise { +export function execute(pipeline: LitePipeline): Promise; +export function execute( + options: PipelineExecuteOptions +): Promise; +export function execute( + pipelineOrOptions: LitePipeline | PipelineExecuteOptions +): Promise { + const options: PipelineExecuteOptions = !( + pipelineOrOptions instanceof LitePipeline + ) + ? pipelineOrOptions + : { + pipeline: pipelineOrOptions + }; + + const { pipeline, rawOptions, ...rest } = options; + const firestore = cast(pipeline._db, Firestore); const client = ensureFirestoreConfigured(firestore); - return firestoreClientExecutePipeline(client, pipeline).then(result => { - // Get the execution time from the first result. - // firestoreClientExecutePipeline returns at least one PipelineStreamElement - // even if the returned document set is empty. - const executionTime = - result.length > 0 ? result[0].executionTime?.toTimestamp() : undefined; - const docs = result - // Currently ignore any response from ExecutePipeline that does - // not contain any document data in the `fields` property. - .filter(element => !!element.fields) - .map( - element => - new PipelineResult( - pipeline._userDataWriter, - element.key?.path - ? new DocumentReference(firestore, null, element.key) - : undefined, - element.fields, - element.createTime?.toTimestamp(), - element.updateTime?.toTimestamp() - ) - ); + const udr = new UserDataReader( + firestore._databaseId, + /* ignoreUndefinedProperties */ true + ); + const context = udr.createContext(UserDataSource.Argument, 'execute'); + + const structuredPipelineOptions = new StructuredPipelineOptions( + rest, + rawOptions + ); + structuredPipelineOptions._readUserData(context); - return new PipelineSnapshot(pipeline, docs, executionTime); - }); + const structuredPipeline: StructuredPipeline = new StructuredPipeline( + pipeline, + structuredPipelineOptions + ); + + return firestoreClientExecutePipeline(client, structuredPipeline).then( + result => { + // Get the execution time from the first result. + // firestoreClientExecutePipeline returns at least one PipelineStreamElement + // even if the returned document set is empty. + const executionTime = + result.length > 0 ? result[0].executionTime?.toTimestamp() : undefined; + + const docs = result + // Currently ignore any response from ExecutePipeline that does + // not contain any document data in the `fields` property. + .filter(element => !!element.fields) + .map( + element => + new PipelineResult( + pipeline._userDataWriter, + element.fields!, + element.key?.path + ? new DocumentReference(firestore, null, element.key) + : undefined, + element.createTime?.toTimestamp(), + element.updateTime?.toTimestamp() + ) + ); + + return new PipelineSnapshot(pipeline, docs, executionTime); + } + ); } // Augment the Firestore class with the pipeline() factory method Firestore.prototype.pipeline = function (): PipelineSource { - return new PipelineSource(this._databaseId, (stages: Stage[]) => { - return new Pipeline( - this, - newUserDataReader(this), - new ExpUserDataWriter(this), - stages - ); - }); + const userDataReader = newUserDataReader(this); + return new PipelineSource( + this._databaseId, + userDataReader, + (stages: Stage[]) => { + return new Pipeline( + this, + userDataReader, + new ExpUserDataWriter(this), + stages + ); + } + ); }; diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index c82add0642a..86e075a4ca4 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -31,10 +31,12 @@ import { import { LiteUserDataWriter } from '../lite-api/reference_impl'; import { DocumentSnapshot as LiteDocumentSnapshot, - fieldPathFromArgument, FirestoreDataConverter as LiteFirestoreDataConverter } from '../lite-api/snapshot'; -import { UntypedFirestoreDataConverter } from '../lite-api/user_data_reader'; +import { + fieldPathFromArgument, + UntypedFirestoreDataConverter +} from '../lite-api/user_data_reader'; import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; import { fromBundledQuery } from '../local/local_serializer'; import { documentKeySet } from '../model/collections'; diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts index b632025f374..ee9850fe70e 100644 --- a/packages/firestore/src/api_pipelines.ts +++ b/packages/firestore/src/api_pipelines.ts @@ -17,6 +17,8 @@ export { PipelineSource } from './lite-api/pipeline-source'; +export { OneOf } from './util/types'; + export { PipelineResult, PipelineSnapshot, @@ -27,9 +29,32 @@ export { Pipeline } from './api/pipeline'; export { execute } from './api/pipeline_impl'; +export { PipelineExecuteOptions } from './lite-api/pipeline_options'; + +export { + StageOptions, + CollectionStageOptions, + CollectionGroupStageOptions, + DatabaseStageOptions, + DocumentsStageOptions, + AddFieldsStageOptions, + RemoveFieldsStageOptions, + SelectStageOptions, + WhereStageOptions, + OffsetStageOptions, + LimitStageOptions, + DistinctStageOptions, + AggregateStageOptions, + FindNearestStageOptions, + ReplaceWithStageOptions, + SampleStageOptions, + UnionStageOptions, + UnnestStageOptions, + SortStageOptions +} from './lite-api/stage_options'; + export { Stage, - FindNearestOptions, AddFields, Aggregate, Distinct, @@ -43,7 +68,7 @@ export { Offset, Select, Sort, - GenericStage + RawStage } from './lite-api/stage'; export { @@ -54,46 +79,44 @@ export { multiply, divide, mod, - eq, - neq, - lt, - lte, - gt, - gte, + equal, + notEqual, + lessThan, + lessThanOrEqual, + greaterThan, + greaterThanOrEqual, arrayConcat, arrayContains, arrayContainsAny, arrayContainsAll, arrayLength, - eqAny, - notEqAny, + equalAny, + notEqualAny, xor, - cond, + conditional, not, logicalMaximum, logicalMinimum, exists, isNan, reverse, - replaceFirst, - replaceAll, byteLength, charLength, like, regexContains, regexMatch, - strContains, + stringContains, startsWith, endsWith, toLower, toUpper, trim, - strConcat, + stringConcat, mapGet, countAll, count, sum, - avg, + average, and, or, minimum, @@ -109,19 +132,12 @@ export { unixSecondsToTimestamp, timestampToUnixSeconds, timestampAdd, - timestampSub, + timestampSubtract, ascending, descending, countIf, - bitAnd, - bitOr, - bitXor, - bitNot, - bitLeftShift, - bitRightShift, - rand, array, - arrayOffset, + arrayGet, isError, ifError, isAbsent, @@ -132,21 +148,40 @@ export { mapRemove, mapMerge, documentId, - substr, - Expr, - ExprWithAlias, + substring, + countDistinct, + ceil, + floor, + exp, + pow, + round, + collectionId, + ln, + log, + sqrt, + stringReverse, + length as len, + abs, + concat, + currentTimestamp, + error, + ifAbsent, + join, + log10, + arraySum, + Expression, + AliasedExpression, Field, - Constant, - FunctionExpr, - Ordering + FunctionExpression, + Ordering, + BooleanExpression, + AggregateFunction } from './lite-api/expressions'; export type { - ExprType, + ExpressionType, AggregateWithAlias, - Selectable, - BooleanExpr, - AggregateFunction + Selectable } from './lite-api/expressions'; export { _internalPipelineToExecutePipelineRequestProto } from './remote/internal_serializer'; diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 775089178a3..009e7b2aba2 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -23,7 +23,6 @@ import { CredentialsProvider } from '../api/credentials'; import { User } from '../auth/user'; -import { Pipeline } from '../lite-api/pipeline'; import { LocalStore } from '../local/local_store'; import { localStoreConfigureFieldIndexes, @@ -88,6 +87,7 @@ import { removeSnapshotsInSyncListener } from './event_manager'; import { newQueryForPath, Query } from './query'; +import { StructuredPipeline } from './structured_pipeline'; import { SyncEngine } from './sync_engine'; import { syncEngineListen, @@ -558,7 +558,7 @@ export function firestoreClientRunAggregateQuery( export function firestoreClientExecutePipeline( client: FirestoreClient, - pipeline: Pipeline + pipeline: StructuredPipeline ): Promise { const deferred = new Deferred(); diff --git a/packages/firestore/src/core/options_util.ts b/packages/firestore/src/core/options_util.ts new file mode 100644 index 00000000000..eef30b6d84c --- /dev/null +++ b/packages/firestore/src/core/options_util.ts @@ -0,0 +1,92 @@ +// 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. + +import { ParseContext } from '../api/parse_context'; +import { parseData } from '../lite-api/user_data_reader'; +import { ObjectValue } from '../model/object_value'; +import { FieldPath } from '../model/path'; +import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; +import { isPlainObject } from '../util/input_validation'; +import { mapToArray } from '../util/obj'; +export type OptionsDefinitions = Record; +export interface OptionDefinition { + serverName: string; + nestedOptions?: OptionsDefinitions; +} + +export class OptionsUtil { + constructor(private optionDefinitions: OptionsDefinitions) {} + + private _getKnownOptions( + options: Record, + context: ParseContext + ): ObjectValue { + const knownOptions: ObjectValue = ObjectValue.empty(); + + // SERIALIZE KNOWN OPTIONS + for (const knownOptionKey in this.optionDefinitions) { + if (this.optionDefinitions.hasOwnProperty(knownOptionKey)) { + const optionDefinition: OptionDefinition = + this.optionDefinitions[knownOptionKey]; + + if (knownOptionKey in options) { + const optionValue: unknown = options[knownOptionKey]; + let protoValue: Value | undefined = undefined; + + if (optionDefinition.nestedOptions && isPlainObject(optionValue)) { + const nestedUtil = new OptionsUtil(optionDefinition.nestedOptions); + protoValue = { + mapValue: { + fields: nestedUtil.getOptionsProto(context, optionValue) + } + }; + } else if (optionValue) { + protoValue = parseData(optionValue, context) ?? undefined; + } + + if (protoValue) { + knownOptions.set( + FieldPath.fromServerFormat(optionDefinition.serverName), + protoValue + ); + } + } + } + } + + return knownOptions; + } + + getOptionsProto( + context: ParseContext, + knownOptions: Record, + optionsOverride?: Record + ): ApiClientObjectMap | undefined { + const result: ObjectValue = this._getKnownOptions(knownOptions, context); + + // APPLY OPTIONS OVERRIDES + if (optionsOverride) { + const optionsMap = new Map( + mapToArray(optionsOverride, (value, key) => [ + FieldPath.fromServerFormat(key), + value !== undefined ? parseData(value, context) : null + ]) + ); + result.setAll(optionsMap); + } + + // Return MapValue from `result` or empty map value + return result.value.mapValue.fields ?? {}; + } +} diff --git a/packages/firestore/src/core/pipeline-util.ts b/packages/firestore/src/core/pipeline-util.ts index cb3342eca4e..3cf754e46f8 100644 --- a/packages/firestore/src/core/pipeline-util.ts +++ b/packages/firestore/src/core/pipeline-util.ts @@ -18,12 +18,12 @@ import { Firestore } from '../lite-api/database'; import { Constant, - BooleanExpr, + BooleanExpression, and, or, Ordering, - lt, - gt, + lessThan, + greaterThan, field } from '../lite-api/expressions'; import { Pipeline } from '../lite-api/pipeline'; @@ -50,7 +50,7 @@ import { /* eslint @typescript-eslint/no-explicit-any: 0 */ -export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpr { +export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpression { if (f instanceof FieldFilterInternal) { const fieldValue = field(f.field.toString()); if (isNanValue(f.value)) { @@ -72,32 +72,32 @@ export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpr { case Operator.LESS_THAN: return and( fieldValue.exists(), - fieldValue.lt(Constant._fromProto(value)) + fieldValue.lessThan(Constant._fromProto(value)) ); case Operator.LESS_THAN_OR_EQUAL: return and( fieldValue.exists(), - fieldValue.lte(Constant._fromProto(value)) + fieldValue.lessThanOrEqual(Constant._fromProto(value)) ); case Operator.GREATER_THAN: return and( fieldValue.exists(), - fieldValue.gt(Constant._fromProto(value)) + fieldValue.greaterThan(Constant._fromProto(value)) ); case Operator.GREATER_THAN_OR_EQUAL: return and( fieldValue.exists(), - fieldValue.gte(Constant._fromProto(value)) + fieldValue.greaterThanOrEqual(Constant._fromProto(value)) ); case Operator.EQUAL: return and( fieldValue.exists(), - fieldValue.eq(Constant._fromProto(value)) + fieldValue.equal(Constant._fromProto(value)) ); case Operator.NOT_EQUAL: return and( fieldValue.exists(), - fieldValue.neq(Constant._fromProto(value)) + fieldValue.notEqual(Constant._fromProto(value)) ); case Operator.ARRAY_CONTAINS: return and( @@ -109,11 +109,11 @@ export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpr { Constant._fromProto(val) ); if (!values) { - return and(fieldValue.exists(), fieldValue.eqAny([])); + return and(fieldValue.exists(), fieldValue.equalAny([])); } else if (values.length === 1) { - return and(fieldValue.exists(), fieldValue.eq(values[0])); + return and(fieldValue.exists(), fieldValue.equal(values[0])); } else { - return and(fieldValue.exists(), fieldValue.eqAny(values)); + return and(fieldValue.exists(), fieldValue.equalAny(values)); } } case Operator.ARRAY_CONTAINS_ANY: { @@ -127,11 +127,11 @@ export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpr { Constant._fromProto(val) ); if (!values) { - return and(fieldValue.exists(), fieldValue.notEqAny([])); + return and(fieldValue.exists(), fieldValue.notEqualAny([])); } else if (values.length === 1) { - return and(fieldValue.exists(), fieldValue.neq(values[0])); + return and(fieldValue.exists(), fieldValue.notEqual(values[0])); } else { - return and(fieldValue.exists(), fieldValue.notEqAny(values)); + return and(fieldValue.exists(), fieldValue.notEqualAny(values)); } } default: @@ -161,7 +161,8 @@ function reverseOrderings(orderings: Ordering[]): Ordering[] { o => new Ordering( o.expr, - o.direction === 'ascending' ? 'descending' : 'ascending' + o.direction === 'ascending' ? 'descending' : 'ascending', + undefined ) ); } @@ -249,9 +250,9 @@ function whereConditionsFromCursor( bound: Bound, orderings: Ordering[], position: 'before' | 'after' -): BooleanExpr { +): BooleanExpression { // The filterFunc is either greater than or less than - const filterFunc = position === 'before' ? lt : gt; + const filterFunc = position === 'before' ? lessThan : greaterThan; const cursors = bound.position.map(value => Constant._fromProto(value)); const size = cursors.length; @@ -259,11 +260,11 @@ function whereConditionsFromCursor( let value = cursors[size - 1]; // Add condition for last bound - let condition: BooleanExpr = filterFunc(field, value); + let condition: BooleanExpression = filterFunc(field, value); if (bound.inclusive) { // When the cursor bound is inclusive, then the last bound // can be equal to the value, otherwise it's not equal - condition = or(condition, field.eq(value)); + condition = or(condition, field.equal(value)); } // Iterate backwards over the remaining bounds, adding @@ -275,7 +276,10 @@ function whereConditionsFromCursor( // For each field in the orderings, the condition is either // a) lt|gt the cursor value, // b) or equal the cursor value and lt|gt the cursor values for other fields - condition = or(filterFunc(field, value), and(field.eq(value), condition)); + condition = or( + filterFunc(field, value), + and(field.equal(value), condition) + ); } return condition; diff --git a/packages/firestore/src/core/structured_pipeline.ts b/packages/firestore/src/core/structured_pipeline.ts new file mode 100644 index 00000000000..ac8ee4284f6 --- /dev/null +++ b/packages/firestore/src/core/structured_pipeline.ts @@ -0,0 +1,67 @@ +/** + * @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. + */ + +import { ParseContext } from '../api/parse_context'; +import { UserData } from '../lite-api/user_data_reader'; +import { + ApiClientObjectMap, + firestoreV1ApiClientInterfaces, + Pipeline as PipelineProto, + StructuredPipeline as StructuredPipelineProto +} from '../protos/firestore_proto_api'; +import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; + +import { OptionsUtil } from './options_util'; + +export class StructuredPipelineOptions implements UserData { + proto: ApiClientObjectMap | undefined; + + readonly optionsUtil = new OptionsUtil({ + indexMode: { + serverName: 'index_mode' + } + }); + + constructor( + private _userOptions: Record = {}, + private _optionsOverride: Record = {} + ) {} + + _readUserData(context: ParseContext): void { + this.proto = this.optionsUtil.getOptionsProto( + context, + this._userOptions, + this._optionsOverride + ); + } +} + +export class StructuredPipeline + implements ProtoSerializable +{ + constructor( + private pipeline: ProtoSerializable, + private options: StructuredPipelineOptions + ) {} + + _toProto(serializer: JsonProtoSerializer): StructuredPipelineProto { + return { + pipeline: this.pipeline._toProto(serializer), + options: this.options.proto + }; + } +} diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index fa0fbf68064..df9c7a2e7de 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -25,8 +25,7 @@ import { JsonProtoSerializer, ProtoValueSerializable, toMapValue, - toStringValue, - UserData + toStringValue } from '../remote/serializer'; import { hardAssert } from '../util/assert'; import { isPlainObject } from '../util/input_validation'; @@ -35,15 +34,11 @@ import { isString } from '../util/types'; import { Bytes } from './bytes'; import { documentId as documentIdFieldPath, FieldPath } from './field_path'; +import { vector } from './field_value_impl'; import { GeoPoint } from './geo_point'; import { DocumentReference } from './reference'; import { Timestamp } from './timestamp'; -import { - fieldPathFromArgument, - parseData, - UserDataReader, - UserDataSource -} from './user_data_reader'; +import { fieldPathFromArgument, parseData, UserData } from './user_data_reader'; import { VectorValue } from './vector_value'; /** @@ -51,13 +46,13 @@ import { VectorValue } from './vector_value'; * * An enumeration of the different types of expressions. */ -export type ExprType = +export type ExpressionType = | 'Field' | 'Constant' | 'Function' | 'AggregateFunction' - | 'ListOfExprs' - | 'ExprWithAlias'; + | 'ListOfExpressions' + | 'AliasedExpression'; /** * Converts a value to an Expr, Returning either a Constant, MapFunction, @@ -67,19 +62,18 @@ export type ExprType = * @internal * @param value */ -function valueToDefaultExpr(value: unknown): Expr { - let result: Expr | undefined; - if (value instanceof Expr) { +function valueToDefaultExpr(value: unknown): Expression { + let result: Expression | undefined; + if (value instanceof Expression) { return value; } else if (isPlainObject(value)) { - result = map(value as Record); + result = _map(value as Record, undefined); } else if (value instanceof Array) { result = array(value); } else { - result = new Constant(value); + result = _constant(value, undefined); } - result._createdFromLiteral = true; return result; } @@ -91,13 +85,15 @@ function valueToDefaultExpr(value: unknown): Expr { * @internal * @param value */ -function vectorToExpr(value: VectorValue | number[] | Expr): Expr { - if (value instanceof Expr) { +function vectorToExpr(value: VectorValue | number[] | Expression): Expression { + if (value instanceof Expression) { return value; + } else if (value instanceof VectorValue) { + return constant(value); + } else if (Array.isArray(value)) { + return constant(vector(value)); } else { - const result = constantVector(value); - result._createdFromLiteral = true; - return result; + throw new Error('Unsupported value: ' + typeof value); } } @@ -111,10 +107,9 @@ function vectorToExpr(value: VectorValue | number[] | Expr): Expr { * @internal * @param value */ -function fieldOrExpression(value: unknown): Expr { +function fieldOrExpression(value: unknown): Expression { if (isString(value)) { const result = field(value); - result._createdFromLiteral = true; return result; } else { return valueToDefaultExpr(value); @@ -137,16 +132,10 @@ function fieldOrExpression(value: unknown): Expr { * The `Expr` class provides a fluent API for building expressions. You can chain together * method calls to create complex expressions. */ -export abstract class Expr implements ProtoValueSerializable, UserData { - abstract readonly exprType: ExprType; +export abstract class Expression implements ProtoValueSerializable, UserData { + abstract readonly expressionType: ExpressionType; - /** - * @internal - * @private - * Indicates if this expression was created from a literal value passed - * by the caller. - */ - _createdFromLiteral: boolean = false; + abstract readonly _methodName?: string; /** * @private @@ -159,10 +148,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @private * @internal */ - abstract _readUserData( - dataReader: UserDataReader, - context?: ParseContext - ): void; + abstract _readUserData(context: ParseContext): void; /** * Creates an expression that adds this expression to another expression. @@ -176,12 +162,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param others Optional additional expressions or literals to add to this expression. * @return A new `Expr` representing the addition operation. */ - add(second: Expr | unknown, ...others: Array): FunctionExpr { - const values = [second, ...others]; - return new FunctionExpr('add', [ - this, - ...values.map(value => valueToDefaultExpr(value)) - ]); + add(second: Expression | unknown): FunctionExpression { + return new FunctionExpression( + 'add', + [this, valueToDefaultExpr(second)], + 'add' + ); } /** @@ -192,10 +178,10 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("price").subtract(field("discount")); * ``` * - * @param other The expression to subtract from this expression. + * @param subtrahend The expression to subtract from this expression. * @return A new `Expr` representing the subtraction operation. */ - subtract(other: Expr): FunctionExpr; + subtract(subtrahend: Expression): FunctionExpression; /** * Creates an expression that subtracts a constant value from this expression. @@ -205,12 +191,16 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("total").subtract(20); * ``` * - * @param other The constant value to subtract. + * @param subtrahend The constant value to subtract. * @return A new `Expr` representing the subtraction operation. */ - subtract(other: number): FunctionExpr; - subtract(other: number | Expr): FunctionExpr { - return new FunctionExpr('subtract', [this, valueToDefaultExpr(other)]); + subtract(subtrahend: number): FunctionExpression; + subtract(subtrahend: number | Expression): FunctionExpression { + return new FunctionExpression( + 'subtract', + [this, valueToDefaultExpr(subtrahend)], + 'subtract' + ); } /** @@ -225,15 +215,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param others Optional additional expressions or literals to multiply by. * @return A new `Expr` representing the multiplication operation. */ - multiply( - second: Expr | number, - ...others: Array - ): FunctionExpr { - return new FunctionExpr('multiply', [ - this, - valueToDefaultExpr(second), - ...others.map(value => valueToDefaultExpr(value)) - ]); + multiply(second: Expression | number): FunctionExpression { + return new FunctionExpression( + 'multiply', + [this, valueToDefaultExpr(second)], + 'multiply' + ); } /** @@ -244,10 +231,10 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("total").divide(field("count")); * ``` * - * @param other The expression to divide by. + * @param divisor The expression to divide by. * @return A new `Expr` representing the division operation. */ - divide(other: Expr): FunctionExpr; + divide(divisor: Expression): FunctionExpression; /** * Creates an expression that divides this expression by a constant value. @@ -257,12 +244,16 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("value").divide(10); * ``` * - * @param other The constant value to divide by. + * @param divisor The constant value to divide by. * @return A new `Expr` representing the division operation. */ - divide(other: number): FunctionExpr; - divide(other: number | Expr): FunctionExpr { - return new FunctionExpr('divide', [this, valueToDefaultExpr(other)]); + divide(divisor: number): FunctionExpression; + divide(divisor: number | Expression): FunctionExpression { + return new FunctionExpression( + 'divide', + [this, valueToDefaultExpr(divisor)], + 'divide' + ); } /** @@ -276,7 +267,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param expression The expression to divide by. * @return A new `Expr` representing the modulo operation. */ - mod(expression: Expr): FunctionExpr; + mod(expression: Expression): FunctionExpression; /** * Creates an expression that calculates the modulo (remainder) of dividing this expression by a constant value. @@ -289,9 +280,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param value The constant value to divide by. * @return A new `Expr` representing the modulo operation. */ - mod(value: number): FunctionExpr; - mod(other: number | Expr): FunctionExpr { - return new FunctionExpr('mod', [this, valueToDefaultExpr(other)]); + mod(value: number): FunctionExpression; + mod(other: number | Expression): FunctionExpression { + return new FunctionExpression( + 'mod', + [this, valueToDefaultExpr(other)], + 'mod' + ); } /** @@ -299,28 +294,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'age' field is equal to 21 - * field("age").eq(21); + * field("age").equal(21); * ``` * * @param expression The expression to compare for equality. * @return A new `Expr` representing the equality comparison. */ - eq(expression: Expr): BooleanExpr; + equal(expression: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is equal to a constant value. * * ```typescript * // Check if the 'city' field is equal to "London" - * field("city").eq("London"); + * field("city").equal("London"); * ``` * * @param value The constant value to compare for equality. * @return A new `Expr` representing the equality comparison. */ - eq(value: unknown): BooleanExpr; - eq(other: unknown): BooleanExpr { - return new BooleanExpr('eq', [this, valueToDefaultExpr(other)]); + equal(value: unknown): BooleanExpression; + equal(other: unknown): BooleanExpression { + return new BooleanExpression( + 'equal', + [this, valueToDefaultExpr(other)], + 'equal' + ); } /** @@ -328,28 +327,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'status' field is not equal to "completed" - * field("status").neq("completed"); + * field("status").notEqual("completed"); * ``` * * @param expression The expression to compare for inequality. * @return A new `Expr` representing the inequality comparison. */ - neq(expression: Expr): BooleanExpr; + notEqual(expression: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is not equal to a constant value. * * ```typescript * // Check if the 'country' field is not equal to "USA" - * field("country").neq("USA"); + * field("country").notEqual("USA"); * ``` * * @param value The constant value to compare for inequality. * @return A new `Expr` representing the inequality comparison. */ - neq(value: unknown): BooleanExpr; - neq(other: unknown): BooleanExpr { - return new BooleanExpr('neq', [this, valueToDefaultExpr(other)]); + notEqual(value: unknown): BooleanExpression; + notEqual(other: unknown): BooleanExpression { + return new BooleanExpression( + 'not_equal', + [this, valueToDefaultExpr(other)], + 'notEqual' + ); } /** @@ -357,28 +360,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'age' field is less than 'limit' - * field("age").lt(field('limit')); + * field("age").lessThan(field('limit')); * ``` * * @param experession The expression to compare for less than. * @return A new `Expr` representing the less than comparison. */ - lt(experession: Expr): BooleanExpr; + lessThan(experession: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is less than a constant value. * * ```typescript * // Check if the 'price' field is less than 50 - * field("price").lt(50); + * field("price").lessThan(50); * ``` * * @param value The constant value to compare for less than. * @return A new `Expr` representing the less than comparison. */ - lt(value: unknown): BooleanExpr; - lt(other: unknown): BooleanExpr { - return new BooleanExpr('lt', [this, valueToDefaultExpr(other)]); + lessThan(value: unknown): BooleanExpression; + lessThan(other: unknown): BooleanExpression { + return new BooleanExpression( + 'less_than', + [this, valueToDefaultExpr(other)], + 'lessThan' + ); } /** @@ -387,28 +394,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'quantity' field is less than or equal to 20 - * field("quantity").lte(constant(20)); + * field("quantity").lessThan(constant(20)); * ``` * * @param expression The expression to compare for less than or equal to. * @return A new `Expr` representing the less than or equal to comparison. */ - lte(expression: Expr): BooleanExpr; + lessThanOrEqual(expression: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is less than or equal to a constant value. * * ```typescript * // Check if the 'score' field is less than or equal to 70 - * field("score").lte(70); + * field("score").lessThan(70); * ``` * * @param value The constant value to compare for less than or equal to. * @return A new `Expr` representing the less than or equal to comparison. */ - lte(value: unknown): BooleanExpr; - lte(other: unknown): BooleanExpr { - return new BooleanExpr('lte', [this, valueToDefaultExpr(other)]); + lessThanOrEqual(value: unknown): BooleanExpression; + lessThanOrEqual(other: unknown): BooleanExpression { + return new BooleanExpression( + 'less_than_or_equal', + [this, valueToDefaultExpr(other)], + 'lessThanOrEqual' + ); } /** @@ -416,28 +427,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'age' field is greater than the 'limit' field - * field("age").gt(field("limit")); + * field("age").greaterThan(field("limit")); * ``` * * @param expression The expression to compare for greater than. * @return A new `Expr` representing the greater than comparison. */ - gt(expression: Expr): BooleanExpr; + greaterThan(expression: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is greater than a constant value. * * ```typescript * // Check if the 'price' field is greater than 100 - * field("price").gt(100); + * field("price").greaterThan(100); * ``` * * @param value The constant value to compare for greater than. * @return A new `Expr` representing the greater than comparison. */ - gt(value: unknown): BooleanExpr; - gt(other: unknown): BooleanExpr { - return new BooleanExpr('gt', [this, valueToDefaultExpr(other)]); + greaterThan(value: unknown): BooleanExpression; + greaterThan(other: unknown): BooleanExpression { + return new BooleanExpression( + 'greater_than', + [this, valueToDefaultExpr(other)], + 'greaterThan' + ); } /** @@ -446,13 +461,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'quantity' field is greater than or equal to field 'requirement' plus 1 - * field("quantity").gte(field('requirement').add(1)); + * field("quantity").greaterThanOrEqual(field('requirement').add(1)); * ``` * * @param expression The expression to compare for greater than or equal to. * @return A new `Expr` representing the greater than or equal to comparison. */ - gte(expression: Expr): BooleanExpr; + greaterThanOrEqual(expression: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is greater than or equal to a constant @@ -460,15 +475,19 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'score' field is greater than or equal to 80 - * field("score").gte(80); + * field("score").greaterThanOrEqual(80); * ``` * * @param value The constant value to compare for greater than or equal to. * @return A new `Expr` representing the greater than or equal to comparison. */ - gte(value: unknown): BooleanExpr; - gte(other: unknown): BooleanExpr { - return new BooleanExpr('gte', [this, valueToDefaultExpr(other)]); + greaterThanOrEqual(value: unknown): BooleanExpression; + greaterThanOrEqual(other: unknown): BooleanExpression { + return new BooleanExpression( + 'greater_than_or_equal', + [this, valueToDefaultExpr(other)], + 'greaterThanOrEqual' + ); } /** @@ -483,12 +502,16 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `Expr` representing the concatenated array. */ arrayConcat( - secondArray: Expr | unknown[], - ...otherArrays: Array - ): FunctionExpr { + secondArray: Expression | unknown[], + ...otherArrays: Array + ): FunctionExpression { const elements = [secondArray, ...otherArrays]; const exprValues = elements.map(value => valueToDefaultExpr(value)); - return new FunctionExpr('array_concat', [this, ...exprValues]); + return new FunctionExpression( + 'array_concat', + [this, ...exprValues], + 'arrayConcat' + ); } /** @@ -502,7 +525,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param expression The element to search for in the array. * @return A new `Expr` representing the 'array_contains' comparison. */ - arrayContains(expression: Expr): BooleanExpr; + arrayContains(expression: Expression): BooleanExpression; /** * Creates an expression that checks if an array contains a specific value. @@ -515,12 +538,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param value The element to search for in the array. * @return A new `Expr` representing the 'array_contains' comparison. */ - arrayContains(value: unknown): BooleanExpr; - arrayContains(element: unknown): BooleanExpr { - return new BooleanExpr('array_contains', [ - this, - valueToDefaultExpr(element) - ]); + arrayContains(value: unknown): BooleanExpression; + arrayContains(element: unknown): BooleanExpression { + return new BooleanExpression( + 'array_contains', + [this, valueToDefaultExpr(element)], + 'arrayContains' + ); } /** @@ -534,7 +558,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param values The elements to check for in the array. * @return A new `Expr` representing the 'array_contains_all' comparison. */ - arrayContainsAll(values: Array): BooleanExpr; + arrayContainsAll(values: Array): BooleanExpression; /** * Creates an expression that checks if an array contains all the specified elements. @@ -547,12 +571,16 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param arrayExpression The elements to check for in the array. * @return A new `Expr` representing the 'array_contains_all' comparison. */ - arrayContainsAll(arrayExpression: Expr): BooleanExpr; - arrayContainsAll(values: unknown[] | Expr): BooleanExpr { + arrayContainsAll(arrayExpression: Expression): BooleanExpression; + arrayContainsAll(values: unknown[] | Expression): BooleanExpression { const normalizedExpr = Array.isArray(values) - ? new ListOfExprs(values.map(valueToDefaultExpr)) + ? new ListOfExprs(values.map(valueToDefaultExpr), 'arrayContainsAll') : values; - return new BooleanExpr('array_contains_all', [this, normalizedExpr]); + return new BooleanExpression( + 'array_contains_all', + [this, normalizedExpr], + 'arrayContainsAll' + ); } /** @@ -566,7 +594,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param values The elements to check for in the array. * @return A new `Expr` representing the 'array_contains_any' comparison. */ - arrayContainsAny(values: Array): BooleanExpr; + arrayContainsAny(values: Array): BooleanExpression; /** * Creates an expression that checks if an array contains any of the specified elements. @@ -580,12 +608,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param arrayExpression The elements to check for in the array. * @return A new `Expr` representing the 'array_contains_any' comparison. */ - arrayContainsAny(arrayExpression: Expr): BooleanExpr; - arrayContainsAny(values: Array | Expr): BooleanExpr { + arrayContainsAny(arrayExpression: Expression): BooleanExpression; + arrayContainsAny( + values: Array | Expression + ): BooleanExpression { const normalizedExpr = Array.isArray(values) - ? new ListOfExprs(values.map(valueToDefaultExpr)) + ? new ListOfExprs(values.map(valueToDefaultExpr), 'arrayContainsAny') : values; - return new BooleanExpr('array_contains_any', [this, normalizedExpr]); + return new BooleanExpression( + 'array_contains_any', + [this, normalizedExpr], + 'arrayContainsAny' + ); + } + + /** + * Creates an expression that reverses an array. + * + * ```typescript + * // Reverse the value of the 'myArray' field. + * field("myArray").arrayReverse(); + * ``` + * + * @return A new {@code Expr} representing the reversed array. + */ + arrayReverse(): FunctionExpression { + return new FunctionExpression('array_reverse', [this]); } /** @@ -598,8 +646,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the length of the array. */ - arrayLength(): FunctionExpr { - return new FunctionExpr('array_length', [this]); + arrayLength(): FunctionExpression { + return new FunctionExpression('array_length', [this], 'arrayLength'); } /** @@ -608,13 +656,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * field("category").eqAny("Electronics", field("primaryType")); + * field("category").equalAny("Electronics", field("primaryType")); * ``` * * @param values The values or expressions to check against. * @return A new `Expr` representing the 'IN' comparison. */ - eqAny(values: Array): BooleanExpr; + equalAny(values: Array): BooleanExpression; /** * Creates an expression that checks if this expression is equal to any of the provided values or @@ -622,18 +670,18 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * field("category").eqAny(array(["Electronics", field("primaryType")])); + * field("category").equalAny(array(["Electronics", field("primaryType")])); * ``` * * @param arrayExpression An expression that evaluates to an array of values to check against. * @return A new `Expr` representing the 'IN' comparison. */ - eqAny(arrayExpression: Expr): BooleanExpr; - eqAny(others: unknown[] | Expr): BooleanExpr { + equalAny(arrayExpression: Expression): BooleanExpression; + equalAny(others: unknown[] | Expression): BooleanExpression { const exprOthers = Array.isArray(others) - ? new ListOfExprs(others.map(valueToDefaultExpr)) + ? new ListOfExprs(others.map(valueToDefaultExpr), 'equalAny') : others; - return new BooleanExpr('eq_any', [this, exprOthers]); + return new BooleanExpression('equal_any', [this, exprOthers], 'equalAny'); } /** @@ -642,31 +690,35 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * field("status").notEqAny(["pending", field("rejectedStatus")]); + * field("status").notEqualAny(["pending", field("rejectedStatus")]); * ``` * * @param values The values or expressions to check against. - * @return A new `Expr` representing the 'NotEqAny' comparison. + * @return A new `Expr` representing the 'notEqualAny' comparison. */ - notEqAny(values: Array): BooleanExpr; + notEqualAny(values: Array): BooleanExpression; /** * Creates an expression that checks if this expression is not equal to any of the values in the evaluated expression. * * ```typescript * // Check if the 'status' field is not equal to any value in the field 'rejectedStatuses' - * field("status").notEqAny(field('rejectedStatuses')); + * field("status").notEqualAny(field('rejectedStatuses')); * ``` * * @param arrayExpression The values or expressions to check against. - * @return A new `Expr` representing the 'NotEqAny' comparison. + * @return A new `Expr` representing the 'notEqualAny' comparison. */ - notEqAny(arrayExpression: Expr): BooleanExpr; - notEqAny(others: unknown[] | Expr): BooleanExpr { + notEqualAny(arrayExpression: Expression): BooleanExpression; + notEqualAny(others: unknown[] | Expression): BooleanExpression { const exprOthers = Array.isArray(others) - ? new ListOfExprs(others.map(valueToDefaultExpr)) + ? new ListOfExprs(others.map(valueToDefaultExpr), 'notEqualAny') : others; - return new BooleanExpr('not_eq_any', [this, exprOthers]); + return new BooleanExpression( + 'not_equal_any', + [this, exprOthers], + 'notEqualAny' + ); } /** @@ -679,8 +731,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the 'isNaN' check. */ - isNan(): BooleanExpr { - return new BooleanExpr('is_nan', [this]); + isNan(): BooleanExpression { + return new BooleanExpression('is_nan', [this], 'isNan'); } /** @@ -693,8 +745,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the 'isNull' check. */ - isNull(): BooleanExpr { - return new BooleanExpr('is_null', [this]); + isNull(): BooleanExpression { + return new BooleanExpression('is_null', [this], 'isNull'); } /** @@ -707,8 +759,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the 'exists' check. */ - exists(): BooleanExpr { - return new BooleanExpr('exists', [this]); + exists(): BooleanExpression { + return new BooleanExpression('exists', [this], 'exists'); } /** @@ -721,8 +773,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the length of the string. */ - charLength(): FunctionExpr { - return new FunctionExpr('char_length', [this]); + charLength(): FunctionExpression { + return new FunctionExpression('char_length', [this], 'charLength'); } /** @@ -736,7 +788,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new `Expr` representing the 'like' comparison. */ - like(pattern: string): FunctionExpr; + like(pattern: string): BooleanExpression; /** * Creates an expression that performs a case-sensitive string comparison. @@ -749,9 +801,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new `Expr` representing the 'like' comparison. */ - like(pattern: Expr): FunctionExpr; - like(stringOrExpr: string | Expr): FunctionExpr { - return new FunctionExpr('like', [this, valueToDefaultExpr(stringOrExpr)]); + like(pattern: Expression): BooleanExpression; + like(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'like', + [this, valueToDefaultExpr(stringOrExpr)], + 'like' + ); } /** @@ -766,7 +822,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The regular expression to use for the search. * @return A new `Expr` representing the 'contains' comparison. */ - regexContains(pattern: string): BooleanExpr; + regexContains(pattern: string): BooleanExpression; /** * Creates an expression that checks if a string contains a specified regular expression as a @@ -780,12 +836,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The regular expression to use for the search. * @return A new `Expr` representing the 'contains' comparison. */ - regexContains(pattern: Expr): BooleanExpr; - regexContains(stringOrExpr: string | Expr): BooleanExpr { - return new BooleanExpr('regex_contains', [ - this, - valueToDefaultExpr(stringOrExpr) - ]); + regexContains(pattern: Expression): BooleanExpression; + regexContains(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'regex_contains', + [this, valueToDefaultExpr(stringOrExpr)], + 'regexContains' + ); } /** @@ -799,7 +856,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The regular expression to use for the match. * @return A new `Expr` representing the regular expression match. */ - regexMatch(pattern: string): BooleanExpr; + regexMatch(pattern: string): BooleanExpression; /** * Creates an expression that checks if a string matches a specified regular expression. @@ -812,12 +869,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The regular expression to use for the match. * @return A new `Expr` representing the regular expression match. */ - regexMatch(pattern: Expr): BooleanExpr; - regexMatch(stringOrExpr: string | Expr): BooleanExpr { - return new BooleanExpr('regex_match', [ - this, - valueToDefaultExpr(stringOrExpr) - ]); + regexMatch(pattern: Expression): BooleanExpression; + regexMatch(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'regex_match', + [this, valueToDefaultExpr(stringOrExpr)], + 'regexMatch' + ); } /** @@ -825,31 +883,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'description' field contains "example". - * field("description").strContains("example"); + * field("description").stringContains("example"); * ``` * * @param substring The substring to search for. * @return A new `Expr` representing the 'contains' comparison. */ - strContains(substring: string): BooleanExpr; + stringContains(substring: string): BooleanExpression; /** * Creates an expression that checks if a string contains the string represented by another expression. * * ```typescript * // Check if the 'description' field contains the value of the 'keyword' field. - * field("description").strContains(field("keyword")); + * field("description").stringContains(field("keyword")); * ``` * * @param expr The expression representing the substring to search for. * @return A new `Expr` representing the 'contains' comparison. */ - strContains(expr: Expr): BooleanExpr; - strContains(stringOrExpr: string | Expr): BooleanExpr { - return new BooleanExpr('str_contains', [ - this, - valueToDefaultExpr(stringOrExpr) - ]); + stringContains(expr: Expression): BooleanExpression; + stringContains(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'string_contains', + [this, valueToDefaultExpr(stringOrExpr)], + 'stringContains' + ); } /** @@ -863,7 +922,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param prefix The prefix to check for. * @return A new `Expr` representing the 'starts with' comparison. */ - startsWith(prefix: string): BooleanExpr; + startsWith(prefix: string): BooleanExpression; /** * Creates an expression that checks if a string starts with a given prefix (represented as an @@ -877,12 +936,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param prefix The prefix expression to check for. * @return A new `Expr` representing the 'starts with' comparison. */ - startsWith(prefix: Expr): BooleanExpr; - startsWith(stringOrExpr: string | Expr): BooleanExpr { - return new BooleanExpr('starts_with', [ - this, - valueToDefaultExpr(stringOrExpr) - ]); + startsWith(prefix: Expression): BooleanExpression; + startsWith(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'starts_with', + [this, valueToDefaultExpr(stringOrExpr)], + 'startsWith' + ); } /** @@ -896,7 +956,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param suffix The postfix to check for. * @return A new `Expr` representing the 'ends with' comparison. */ - endsWith(suffix: string): BooleanExpr; + endsWith(suffix: string): BooleanExpression; /** * Creates an expression that checks if a string ends with a given postfix (represented as an @@ -910,12 +970,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param suffix The postfix expression to check for. * @return A new `Expr` representing the 'ends with' comparison. */ - endsWith(suffix: Expr): BooleanExpr; - endsWith(stringOrExpr: string | Expr): BooleanExpr { - return new BooleanExpr('ends_with', [ - this, - valueToDefaultExpr(stringOrExpr) - ]); + endsWith(suffix: Expression): BooleanExpression; + endsWith(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'ends_with', + [this, valueToDefaultExpr(stringOrExpr)], + 'endsWith' + ); } /** @@ -928,8 +989,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the lowercase string. */ - toLower(): FunctionExpr { - return new FunctionExpr('to_lower', [this]); + toLower(): FunctionExpression { + return new FunctionExpression('to_lower', [this], 'toLower'); } /** @@ -942,8 +1003,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the uppercase string. */ - toUpper(): FunctionExpr { - return new FunctionExpr('to_upper', [this]); + toUpper(): FunctionExpression { + return new FunctionExpression('to_upper', [this], 'toUpper'); } /** @@ -956,8 +1017,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the trimmed string. */ - trim(): FunctionExpr { - return new FunctionExpr('trim', [this]); + trim(): FunctionExpression { + return new FunctionExpression('trim', [this], 'trim'); } /** @@ -965,20 +1026,45 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * field("firstName").strConcat(constant(" "), field("lastName")); + * field("firstName").stringConcat(constant(" "), field("lastName")); * ``` * * @param secondString The additional expression or string literal to concatenate. * @param otherStrings Optional additional expressions or string literals to concatenate. * @return A new `Expr` representing the concatenated string. */ - strConcat( - secondString: Expr | string, - ...otherStrings: Array - ): FunctionExpr { + stringConcat( + secondString: Expression | string, + ...otherStrings: Array + ): FunctionExpression { const elements = [secondString, ...otherStrings]; const exprs = elements.map(valueToDefaultExpr); - return new FunctionExpr('str_concat', [this, ...exprs]); + return new FunctionExpression( + 'string_concat', + [this, ...exprs], + 'stringConcat' + ); + } + + /** + * Creates an expression that concatenates expression results together. + * + * ```typescript + * // Combine the 'firstName', ' ', and 'lastName' fields into a single value. + * field("firstName").concat(constant(" "), field("lastName")); + * ``` + * + * @param second The additional expression or literal to concatenate. + * @param others Optional additional expressions or literals to concatenate. + * @return A new `Expr` representing the concatenated value. + */ + concat( + second: Expression | unknown, + ...others: Array + ): FunctionExpression { + const elements = [second, ...others]; + const exprs = elements.map(valueToDefaultExpr); + return new FunctionExpression('concat', [this, ...exprs], 'concat'); } /** @@ -991,94 +1077,78 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the reversed string. */ - reverse(): FunctionExpr { - return new FunctionExpr('reverse', [this]); + reverse(): FunctionExpression { + return new FunctionExpression('reverse', [this], 'reverse'); } /** - * Creates an expression that replaces the first occurrence of a substring within this string expression with another substring. + * Creates an expression that calculates the length of this string expression in bytes. * * ```typescript - * // Replace the first occurrence of "hello" with "hi" in the 'message' field - * field("message").replaceFirst("hello", "hi"); + * // Calculate the length of the 'myString' field in bytes. + * field("myString").byteLength(); * ``` * - * @param find The substring to search for. - * @param replace The substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. + * @return A new {@code Expr} representing the length of the string in bytes. */ - replaceFirst(find: string, replace: string): FunctionExpr; + byteLength(): FunctionExpression { + return new FunctionExpression('byte_length', [this], 'byteLength'); + } /** - * Creates an expression that replaces the first occurrence of a substring within this string expression with another substring, - * where the substring to find and the replacement substring are specified by expressions. + * Creates an expression that computes the ceiling of a numeric value. * * ```typescript - * // Replace the first occurrence of the value in 'findField' with the value in 'replaceField' in the 'message' field - * field("message").replaceFirst(field("findField"), field("replaceField")); + * // Compute the ceiling of the 'price' field. + * field("price").ceil(); * ``` * - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. - */ - replaceFirst(find: Expr, replace: Expr): FunctionExpr; - replaceFirst(find: Expr | string, replace: Expr | string): FunctionExpr { - return new FunctionExpr('replace_first', [ - this, - valueToDefaultExpr(find), - valueToDefaultExpr(replace) - ]); + * @return A new {@code Expr} representing the ceiling of the numeric value. + */ + ceil(): FunctionExpression { + return new FunctionExpression('ceil', [this]); } /** - * Creates an expression that replaces all occurrences of a substring within this string expression with another substring. + * Creates an expression that computes the floor of a numeric value. * * ```typescript - * // Replace all occurrences of "hello" with "hi" in the 'message' field - * field("message").replaceAll("hello", "hi"); + * // Compute the floor of the 'price' field. + * field("price").floor(); * ``` * - * @param find The substring to search for. - * @param replace The substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. + * @return A new {@code Expr} representing the floor of the numeric value. */ - replaceAll(find: string, replace: string): FunctionExpr; + floor(): FunctionExpression { + return new FunctionExpression('floor', [this]); + } /** - * Creates an expression that replaces all occurrences of a substring within this string expression with another substring, - * where the substring to find and the replacement substring are specified by expressions. + * Creates an expression that computes the absolute value of a numeric value. * * ```typescript - * // Replace all occurrences of the value in 'findField' with the value in 'replaceField' in the 'message' field - * field("message").replaceAll(field("findField"), field("replaceField")); + * // Compute the absolute value of the 'price' field. + * field("price").abs(); * ``` * - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. - */ - replaceAll(find: Expr, replace: Expr): FunctionExpr; - replaceAll(find: Expr | string, replace: Expr | string): FunctionExpr { - return new FunctionExpr('replace_all', [ - this, - valueToDefaultExpr(find), - valueToDefaultExpr(replace) - ]); + * @return A new {@code Expr} representing the absolute value of the numeric value. + */ + abs(): FunctionExpression { + return new FunctionExpression('abs', [this]); } /** - * Creates an expression that calculates the length of this string expression in bytes. + * Creates an expression that computes e to the power of this expression. * * ```typescript - * // Calculate the length of the 'myString' field in bytes. - * field("myString").byteLength(); + * // Compute e to the power of the 'value' field. + * field("value").exp(); * ``` * - * @return A new {@code Expr} representing the length of the string in bytes. + * @return A new {@code Expr} representing the exp of the numeric value. */ - byteLength(): FunctionExpr { - return new FunctionExpr('byte_length', [this]); + exp(): FunctionExpression { + return new FunctionExpression('exp', [this]); } /** @@ -1092,8 +1162,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param subfield The key to access in the map. * @return A new `Expr` representing the value associated with the given key in the map. */ - mapGet(subfield: string): FunctionExpr { - return new FunctionExpr('map_get', [this, constant(subfield)]); + mapGet(subfield: string): FunctionExpression { + return new FunctionExpression( + 'map_get', + [this, constant(subfield)], + 'mapGet' + ); } /** @@ -1108,7 +1182,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'count' aggregation. */ count(): AggregateFunction { - return new AggregateFunction('count', [this]); + return new AggregateFunction('count', [this], 'count'); } /** @@ -1122,7 +1196,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'sum' aggregation. */ sum(): AggregateFunction { - return new AggregateFunction('sum', [this]); + return new AggregateFunction('sum', [this], 'sum'); } /** @@ -1131,13 +1205,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Calculate the average age of users - * field("age").avg().as("averageAge"); + * field("age").average().as("averageAge"); * ``` * - * @return A new `AggregateFunction` representing the 'avg' aggregation. + * @return A new `AggregateFunction` representing the 'average' aggregation. */ - avg(): AggregateFunction { - return new AggregateFunction('avg', [this]); + average(): AggregateFunction { + return new AggregateFunction('average', [this], 'average'); } /** @@ -1148,10 +1222,10 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("price").minimum().as("lowestPrice"); * ``` * - * @return A new `AggregateFunction` representing the 'min' aggregation. + * @return A new `AggregateFunction` representing the 'minimum' aggregation. */ minimum(): AggregateFunction { - return new AggregateFunction('minimum', [this]); + return new AggregateFunction('minimum', [this], 'minimum'); } /** @@ -1162,10 +1236,24 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("score").maximum().as("highestScore"); * ``` * - * @return A new `AggregateFunction` representing the 'max' aggregation. + * @return A new `AggregateFunction` representing the 'maximum' aggregation. */ maximum(): AggregateFunction { - return new AggregateFunction('maximum', [this]); + return new AggregateFunction('maximum', [this], 'maximum'); + } + + /** + * Creates an aggregation that counts the number of distinct values of the expression or field. + * + * ```typescript + * // Count the distinct number of products + * field("productId").countDistinct().as("distinctProducts"); + * ``` + * + * @return A new `AggregateFunction` representing the 'count_distinct' aggregation. + */ + countDistinct(): AggregateFunction { + return new AggregateFunction('count_distinct', [this], 'countDistinct'); } /** @@ -1178,17 +1266,18 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @param second The second expression or literal to compare with. * @param others Optional additional expressions or literals to compare with. - * @return A new {@code Expr} representing the logical max operation. + * @return A new {@code Expr} representing the logical maximum operation. */ logicalMaximum( - second: Expr | unknown, - ...others: Array - ): FunctionExpr { + second: Expression | unknown, + ...others: Array + ): FunctionExpression { const values = [second, ...others]; - return new FunctionExpr('logical_maximum', [ - this, - ...values.map(valueToDefaultExpr) - ]); + return new FunctionExpression( + 'maximum', + [this, ...values.map(valueToDefaultExpr)], + 'logicalMaximum' + ); } /** @@ -1201,17 +1290,18 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @param second The second expression or literal to compare with. * @param others Optional additional expressions or literals to compare with. - * @return A new {@code Expr} representing the logical min operation. + * @return A new {@code Expr} representing the logical minimum operation. */ logicalMinimum( - second: Expr | unknown, - ...others: Array - ): FunctionExpr { + second: Expression | unknown, + ...others: Array + ): FunctionExpression { const values = [second, ...others]; - return new FunctionExpr('logical_minimum', [ - this, - ...values.map(valueToDefaultExpr) - ]); + return new FunctionExpression( + 'minimum', + [this, ...values.map(valueToDefaultExpr)], + 'minimum' + ); } /** @@ -1224,8 +1314,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the length of the vector. */ - vectorLength(): FunctionExpr { - return new FunctionExpr('vector_length', [this]); + vectorLength(): FunctionExpression { + return new FunctionExpression('vector_length', [this], 'vectorLength'); } /** @@ -1239,7 +1329,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vectorExpression The other vector (represented as an Expr) to compare against. * @return A new `Expr` representing the cosine distance between the two vectors. */ - cosineDistance(vectorExpression: Expr): FunctionExpr; + cosineDistance(vectorExpression: Expression): FunctionExpression; /** * Calculates the Cosine distance between two vectors. * @@ -1251,9 +1341,15 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vector The other vector (as a VectorValue) to compare against. * @return A new `Expr` representing the Cosine* distance between the two vectors. */ - cosineDistance(vector: VectorValue | number[]): FunctionExpr; - cosineDistance(other: Expr | VectorValue | number[]): FunctionExpr { - return new FunctionExpr('cosine_distance', [this, vectorToExpr(other)]); + cosineDistance(vector: VectorValue | number[]): FunctionExpression; + cosineDistance( + other: Expression | VectorValue | number[] + ): FunctionExpression { + return new FunctionExpression( + 'cosine_distance', + [this, vectorToExpr(other)], + 'cosineDistance' + ); } /** @@ -1267,7 +1363,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vectorExpression The other vector (as an array of numbers) to calculate with. * @return A new `Expr` representing the dot product between the two vectors. */ - dotProduct(vectorExpression: Expr): FunctionExpr; + dotProduct(vectorExpression: Expression): FunctionExpression; /** * Calculates the dot product between two vectors. @@ -1280,9 +1376,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vector The other vector (as an array of numbers) to calculate with. * @return A new `Expr` representing the dot product between the two vectors. */ - dotProduct(vector: VectorValue | number[]): FunctionExpr; - dotProduct(other: Expr | VectorValue | number[]): FunctionExpr { - return new FunctionExpr('dot_product', [this, vectorToExpr(other)]); + dotProduct(vector: VectorValue | number[]): FunctionExpression; + dotProduct(other: Expression | VectorValue | number[]): FunctionExpression { + return new FunctionExpression( + 'dot_product', + [this, vectorToExpr(other)], + 'dotProduct' + ); } /** @@ -1296,7 +1396,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vectorExpression The other vector (as an array of numbers) to calculate with. * @return A new `Expr` representing the Euclidean distance between the two vectors. */ - euclideanDistance(vectorExpression: Expr): FunctionExpr; + euclideanDistance(vectorExpression: Expression): FunctionExpression; /** * Calculates the Euclidean distance between two vectors. @@ -1309,9 +1409,15 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vector The other vector (as a VectorValue) to compare against. * @return A new `Expr` representing the Euclidean distance between the two vectors. */ - euclideanDistance(vector: VectorValue | number[]): FunctionExpr; - euclideanDistance(other: Expr | VectorValue | number[]): FunctionExpr { - return new FunctionExpr('euclidean_distance', [this, vectorToExpr(other)]); + euclideanDistance(vector: VectorValue | number[]): FunctionExpression; + euclideanDistance( + other: Expression | VectorValue | number[] + ): FunctionExpression { + return new FunctionExpression( + 'euclidean_distance', + [this, vectorToExpr(other)], + 'euclideanDistance' + ); } /** @@ -1325,8 +1431,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the timestamp. */ - unixMicrosToTimestamp(): FunctionExpr { - return new FunctionExpr('unix_micros_to_timestamp', [this]); + unixMicrosToTimestamp(): FunctionExpression { + return new FunctionExpression( + 'unix_micros_to_timestamp', + [this], + 'unixMicrosToTimestamp' + ); } /** @@ -1339,8 +1449,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the number of microseconds since epoch. */ - timestampToUnixMicros(): FunctionExpr { - return new FunctionExpr('timestamp_to_unix_micros', [this]); + timestampToUnixMicros(): FunctionExpression { + return new FunctionExpression( + 'timestamp_to_unix_micros', + [this], + 'timestampToUnixMicros' + ); } /** @@ -1354,8 +1468,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the timestamp. */ - unixMillisToTimestamp(): FunctionExpr { - return new FunctionExpr('unix_millis_to_timestamp', [this]); + unixMillisToTimestamp(): FunctionExpression { + return new FunctionExpression( + 'unix_millis_to_timestamp', + [this], + 'unixMillisToTimestamp' + ); } /** @@ -1368,8 +1486,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the number of milliseconds since epoch. */ - timestampToUnixMillis(): FunctionExpr { - return new FunctionExpr('timestamp_to_unix_millis', [this]); + timestampToUnixMillis(): FunctionExpression { + return new FunctionExpression( + 'timestamp_to_unix_millis', + [this], + 'timestampToUnixMillis' + ); } /** @@ -1383,8 +1505,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the timestamp. */ - unixSecondsToTimestamp(): FunctionExpr { - return new FunctionExpr('unix_seconds_to_timestamp', [this]); + unixSecondsToTimestamp(): FunctionExpression { + return new FunctionExpression( + 'unix_seconds_to_timestamp', + [this], + 'unixSecondsToTimestamp' + ); } /** @@ -1397,8 +1523,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the number of seconds since epoch. */ - timestampToUnixSeconds(): FunctionExpr { - return new FunctionExpr('timestamp_to_unix_seconds', [this]); + timestampToUnixSeconds(): FunctionExpression { + return new FunctionExpression( + 'timestamp_to_unix_seconds', + [this], + 'timestampToUnixSeconds' + ); } /** @@ -1413,7 +1543,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param amount The expression evaluates to amount of the unit. * @return A new {@code Expr} representing the resulting timestamp. */ - timestampAdd(unit: Expr, amount: Expr): FunctionExpr; + timestampAdd(unit: Expression, amount: Expression): FunctionExpression; /** * Creates an expression that adds a specified amount of time to this timestamp expression. @@ -1430,23 +1560,23 @@ export abstract class Expr implements ProtoValueSerializable, UserData { timestampAdd( unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number - ): FunctionExpr; + ): FunctionExpression; timestampAdd( unit: - | Expr + | Expression | 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: Expr | number - ): FunctionExpr { - return new FunctionExpr('timestamp_add', [ - this, - valueToDefaultExpr(unit), - valueToDefaultExpr(amount) - ]); + amount: Expression | number + ): FunctionExpression { + return new FunctionExpression( + 'timestamp_add', + [this, valueToDefaultExpr(unit), valueToDefaultExpr(amount)], + 'timestampAdd' + ); } /** @@ -1454,238 +1584,47 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Subtract some duration determined by field 'unit' and 'amount' from the 'timestamp' field. - * field("timestamp").timestampSub(field("unit"), field("amount")); + * field("timestamp").timestampSubtract(field("unit"), field("amount")); * ``` * * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. * @param amount The expression evaluates to amount of the unit. * @return A new {@code Expr} representing the resulting timestamp. */ - timestampSub(unit: Expr, amount: Expr): FunctionExpr; + timestampSubtract(unit: Expression, amount: Expression): FunctionExpression; /** * Creates an expression that subtracts a specified amount of time from this timestamp expression. * * ```typescript * // Subtract 1 day from the 'timestamp' field. - * field("timestamp").timestampSub("day", 1); + * field("timestamp").timestampSubtract("day", 1); * ``` * * @param unit The unit of time to subtract (e.g., "day", "hour"). * @param amount The amount of time to subtract. * @return A new {@code Expr} representing the resulting timestamp. */ - timestampSub( + timestampSubtract( unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number - ): FunctionExpr; - timestampSub( + ): FunctionExpression; + timestampSubtract( unit: - | Expr + | Expression | 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: Expr | number - ): FunctionExpr { - return new FunctionExpr('timestamp_sub', [ - this, - valueToDefaultExpr(unit), - valueToDefaultExpr(amount) - ]); - } - - /** - * @beta - * - * Creates an expression that applies a bitwise AND operation between this expression and a constant. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 0xFF. - * field("field1").bitAnd(0xFF); - * ``` - * - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise AND operation. - */ - bitAnd(otherBits: number | Bytes): FunctionExpr; - /** - * @beta - * - * Creates an expression that applies a bitwise AND operation between two expressions. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 'field2'. - * field("field1").bitAnd(field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise AND operation. - */ - bitAnd(bitsExpression: Expr): FunctionExpr; - bitAnd(bitsOrExpression: number | Expr | Bytes): FunctionExpr { - return new FunctionExpr('bit_and', [ - this, - valueToDefaultExpr(bitsOrExpression) - ]); - } - - /** - * @beta - * - * Creates an expression that applies a bitwise OR operation between this expression and a constant. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 0xFF. - * field("field1").bitOr(0xFF); - * ``` - * - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise OR operation. - */ - bitOr(otherBits: number | Bytes): FunctionExpr; - /** - * @beta - * - * Creates an expression that applies a bitwise OR operation between two expressions. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 'field2'. - * field("field1").bitOr(field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise OR operation. - */ - bitOr(bitsExpression: Expr): FunctionExpr; - bitOr(bitsOrExpression: number | Expr | Bytes): FunctionExpr { - return new FunctionExpr('bit_or', [ - this, - valueToDefaultExpr(bitsOrExpression) - ]); - } - - /** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between this expression and a constant. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 0xFF. - * field("field1").bitXor(0xFF); - * ``` - * - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ - bitXor(otherBits: number | Bytes): FunctionExpr; - /** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between two expressions. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 'field2'. - * field("field1").bitXor(field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ - bitXor(bitsExpression: Expr): FunctionExpr; - bitXor(bitsOrExpression: number | Expr | Bytes): FunctionExpr { - return new FunctionExpr('bit_xor', [ - this, - valueToDefaultExpr(bitsOrExpression) - ]); - } - - /** - * @beta - * - * Creates an expression that applies a bitwise NOT operation to this expression. - * - * ```typescript - * // Calculate the bitwise NOT of 'field1'. - * field("field1").bitNot(); - * ``` - * - * @return A new {@code Expr} representing the bitwise NOT operation. - */ - bitNot(): FunctionExpr { - return new FunctionExpr('bit_not', [this]); - } - - /** - * @beta - * - * Creates an expression that applies a bitwise left shift operation to this expression. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 2 bits. - * field("field1").bitLeftShift(2); - * ``` - * - * @param y The operand constant representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ - bitLeftShift(y: number): FunctionExpr; - /** - * @beta - * - * Creates an expression that applies a bitwise left shift operation to this expression. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 'field2' bits. - * field("field1").bitLeftShift(field("field2")); - * ``` - * - * @param numberExpr The operand expression representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ - bitLeftShift(numberExpr: Expr): FunctionExpr; - bitLeftShift(numberExpr: number | Expr): FunctionExpr { - return new FunctionExpr('bit_left_shift', [ - this, - valueToDefaultExpr(numberExpr) - ]); - } - - /** - * @beta - * - * Creates an expression that applies a bitwise right shift operation to this expression. - * - * ```typescript - * // Calculate the bitwise right shift of 'field1' by 2 bits. - * field("field1").bitRightShift(2); - * ``` - * - * @param right The operand constant representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise right shift operation. - */ - bitRightShift(y: number): FunctionExpr; - /** - * @beta - * - * Creates an expression that applies a bitwise right shift operation to this expression. - * - * ```typescript - * // Calculate the bitwise right shift of 'field1' by 'field2' bits. - * field("field1").bitRightShift(field("field2")); - * ``` - * - * @param numberExpr The operand expression representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise right shift operation. - */ - bitRightShift(numberExpr: Expr): FunctionExpr; - bitRightShift(numberExpr: number | Expr): FunctionExpr { - return new FunctionExpr('bit_right_shift', [ - this, - valueToDefaultExpr(numberExpr) - ]); + amount: Expression | number + ): FunctionExpression { + return new FunctionExpression( + 'timestamp_subtract', + [this, valueToDefaultExpr(unit), valueToDefaultExpr(amount)], + 'timestampSubtract' + ); } /** @@ -1700,8 +1639,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the documentId operation. */ - documentId(): FunctionExpr { - return new FunctionExpr('document_id', [this]); + documentId(): FunctionExpression { + return new FunctionExpression('document_id', [this], 'documentId'); } /** @@ -1713,7 +1652,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param length Length of the substring. If not provided, the substring will * end at the end of the input. */ - substr(position: number, length?: number): FunctionExpr; + substring(position: number, length?: number): FunctionExpression; /** * @beta @@ -1724,17 +1663,24 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param length An expression returning the length of the substring. If not provided the * substring will end at the end of the input. */ - substr(position: Expr, length?: Expr): FunctionExpr; - substr(position: Expr | number, length?: Expr | number): FunctionExpr { + substring(position: Expression, length?: Expression): FunctionExpression; + substring( + position: Expression | number, + length?: Expression | number + ): FunctionExpression { const positionExpr = valueToDefaultExpr(position); if (length === undefined) { - return new FunctionExpr('substr', [this, positionExpr]); + return new FunctionExpression( + 'substring', + [this, positionExpr], + 'substring' + ); } else { - return new FunctionExpr('substr', [ - this, - positionExpr, - valueToDefaultExpr(length) - ]); + return new FunctionExpression( + 'substring', + [this, positionExpr, valueToDefaultExpr(length)], + 'substring' + ); } } @@ -1746,13 +1692,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Return the value in the 'tags' field array at index `1`. - * field('tags').arrayOffset(1); + * field('tags').arrayGet(1); * ``` * * @param offset The index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. + * @return A new Expr representing the 'arrayGet' operation. */ - arrayOffset(offset: number): FunctionExpr; + arrayGet(offset: number): FunctionExpression; /** * @beta @@ -1763,15 +1709,19 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * ```typescript * // Return the value in the tags field array at index specified by field * // 'favoriteTag'. - * field('tags').arrayOffset(field('favoriteTag')); + * field('tags').arrayGet(field('favoriteTag')); * ``` * * @param offsetExpr An Expr evaluating to the index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. - */ - arrayOffset(offsetExpr: Expr): FunctionExpr; - arrayOffset(offset: Expr | number): FunctionExpr { - return new FunctionExpr('array_offset', [this, valueToDefaultExpr(offset)]); + * @return A new Expr representing the 'arrayGet' operation. + */ + arrayGet(offsetExpr: Expression): FunctionExpression; + arrayGet(offset: Expression | number): FunctionExpression { + return new FunctionExpression( + 'array_get', + [this, valueToDefaultExpr(offset)], + 'arrayGet' + ); } /** @@ -1786,8 +1736,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code BooleanExpr} representing the 'isError' check. */ - isError(): BooleanExpr { - return new BooleanExpr('is_error', [this]); + isError(): BooleanExpression { + return new BooleanExpression('is_error', [this], 'isError'); } /** @@ -1799,14 +1749,14 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * ```typescript * // Returns the first item in the title field arrays, or returns * // the entire title field if the array is empty or the field is another type. - * field("title").arrayOffset(0).ifError(field("title")); + * field("title").arrayGet(0).ifError(field("title")); * ``` * * @param catchExpr The catch expression that will be evaluated and * returned if this expression produces an error. * @return A new {@code Expr} representing the 'ifError' operation. */ - ifError(catchExpr: Expr): FunctionExpr; + ifError(catchExpr: Expression): FunctionExpression; /** * @beta @@ -1817,16 +1767,20 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * ```typescript * // Returns the first item in the title field arrays, or returns * // "Default Title" - * field("title").arrayOffset(0).ifError("Default Title"); + * field("title").arrayGet(0).ifError("Default Title"); * ``` * * @param catchValue The value that will be returned if this expression * produces an error. * @return A new {@code Expr} representing the 'ifError' operation. */ - ifError(catchValue: unknown): FunctionExpr; - ifError(catchValue: unknown): FunctionExpr { - return new FunctionExpr('if_error', [this, valueToDefaultExpr(catchValue)]); + ifError(catchValue: unknown): FunctionExpression; + ifError(catchValue: unknown): FunctionExpression { + return new FunctionExpression( + 'if_error', + [this, valueToDefaultExpr(catchValue)], + 'ifError' + ); } /** @@ -1842,8 +1796,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code BooleanExpr} representing the 'isAbsent' check. */ - isAbsent(): BooleanExpr { - return new BooleanExpr('is_absent', [this]); + isAbsent(): BooleanExpression { + return new BooleanExpression('is_absent', [this], 'isAbsent'); } /** @@ -1858,8 +1812,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code BooleanExpr} representing the 'isNotNull' check. */ - isNotNull(): BooleanExpr { - return new BooleanExpr('is_not_null', [this]); + isNotNull(): BooleanExpression { + return new BooleanExpression('is_not_null', [this], 'isNotNull'); } /** @@ -1874,8 +1828,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the 'isNaN' check. */ - isNotNan(): BooleanExpr { - return new BooleanExpr('is_not_nan', [this]); + isNotNan(): BooleanExpression { + return new BooleanExpression('is_not_nan', [this], 'isNotNan'); } /** @@ -1891,7 +1845,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param key The name of the key to remove from the input map. * @returns A new {@code FirestoreFunction} representing the 'mapRemove' operation. */ - mapRemove(key: string): FunctionExpr; + mapRemove(key: string): FunctionExpression; /** * @beta * @@ -1905,12 +1859,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param keyExpr An expression that produces the name of the key to remove from the input map. * @returns A new {@code FirestoreFunction} representing the 'mapRemove' operation. */ - mapRemove(keyExpr: Expr): FunctionExpr; - mapRemove(stringExpr: Expr | string): FunctionExpr { - return new FunctionExpr('map_remove', [ - this, - valueToDefaultExpr(stringExpr) - ]); + mapRemove(keyExpr: Expression): FunctionExpression; + mapRemove(stringExpr: Expression | string): FunctionExpression { + return new FunctionExpression( + 'map_remove', + [this, valueToDefaultExpr(stringExpr)], + 'mapRemove' + ); } /** @@ -1921,7 +1876,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * ``` * // Merges the map in the settings field with, a map literal, and a map in * // that is conditionally returned by another expression - * field('settings').mapMerge({ enabled: true }, cond(field('isAdmin'), { admin: true}, {}) + * field('settings').mapMerge({ enabled: true }, conditional(field('isAdmin'), { admin: true}, {}) * ``` * * @param secondMap A required second map to merge. Represented as a literal or @@ -1932,119 +1887,389 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @returns A new {@code FirestoreFunction} representing the 'mapMerge' operation. */ mapMerge( - secondMap: Record | Expr, - ...otherMaps: Array | Expr> - ): FunctionExpr { + secondMap: Record | Expression, + ...otherMaps: Array | Expression> + ): FunctionExpression { const secondMapExpr = valueToDefaultExpr(secondMap); const otherMapExprs = otherMaps.map(valueToDefaultExpr); - return new FunctionExpr('map_merge', [ - this, - secondMapExpr, - ...otherMapExprs - ]); + return new FunctionExpression( + 'map_merge', + [this, secondMapExpr, ...otherMapExprs], + 'mapMerge' + ); } /** - * Creates an {@link Ordering} that sorts documents in ascending order based on this expression. + * Creates an expression that returns the value of this expression raised to the power of another expression. * * ```typescript - * // Sort documents by the 'name' field in ascending order - * pipeline().collection("users") - * .sort(field("name").ascending()); + * // Raise the value of the 'base' field to the power of the 'exponent' field. + * field("base").pow(field("exponent")); * ``` * - * @return A new `Ordering` for ascending sorting. + * @param exponent The expression to raise this expression to the power of. + * @return A new `Expr` representing the power operation. */ - ascending(): Ordering { - return ascending(this); - } + pow(exponent: Expression): FunctionExpression; /** - * Creates an {@link Ordering} that sorts documents in descending order based on this expression. + * Creates an expression that returns the value of this expression raised to the power of a constant value. * * ```typescript - * // Sort documents by the 'createdAt' field in descending order - * firestore.pipeline().collection("users") - * .sort(field("createdAt").descending()); + * // Raise the value of the 'base' field to the power of 2. + * field("base").pow(2); * ``` * - * @return A new `Ordering` for descending sorting. + * @param exponent The constant value to raise this expression to the power of. + * @return A new `Expr` representing the power operation. */ - descending(): Ordering { - return descending(this); + pow(exponent: number): FunctionExpression; + pow(exponent: number | Expression): FunctionExpression { + return new FunctionExpression('pow', [this, valueToDefaultExpr(exponent)]); } /** - * Assigns an alias to this expression. + * Creates an expression that rounds a numeric value to the nearest whole number. * - * Aliases are useful for renaming fields in the output of a stage or for giving meaningful - * names to calculated values. + * ```typescript + * // Round the value of the 'price' field. + * field("price").round(); + * ``` + * + * @return A new `Expr` representing the rounded value. + */ + round(): FunctionExpression; + /** + * Creates an expression that rounds a numeric value to the specified number of decimal places. * * ```typescript - * // Calculate the total price and assign it the alias "totalPrice" and add it to the output. - * firestore.pipeline().collection("items") - * .addFields(field("price").multiply(field("quantity")).as("totalPrice")); + * // Round the value of the 'price' field to two decimal places. + * field("price").round(2); * ``` * - * @param name The alias to assign to this expression. - * @return A new {@link ExprWithAlias} that wraps this - * expression and associates it with the provided alias. + * @param decimalPlaces A constant specifying the rounding precision in decimal places. + * + * @return A new `Expr` representing the rounded value. */ - as(name: string): ExprWithAlias { - return new ExprWithAlias(this, name); - } -} - -/** - * @beta - * - * An interface that represents a selectable expression. - */ -export interface Selectable { - selectable: true; - readonly alias: string; - readonly expr: Expr; -} - -/** - * @beta - * - * A class that represents an aggregate function. - */ -export class AggregateFunction implements ProtoValueSerializable, UserData { - exprType: ExprType = 'AggregateFunction'; - + round(decimalPlaces: number): FunctionExpression; /** - * @internal - * @private - * Indicates if this expression was created from a literal value passed - * by the caller. + * Creates an expression that rounds a numeric value to the specified number of decimal places. + * + * ```typescript + * // Round the value of the 'price' field to two decimal places. + * field("price").round(constant(2)); + * ``` + * + * @param decimalPlaces An expression specifying the rounding precision in decimal places. + * + * @return A new `Expr` representing the rounded value. */ - _createdFromLiteral: boolean = false; - - constructor(private name: string, private params: Expr[]) {} + round(decimalPlaces: Expression): FunctionExpression; + round(decimalPlaces?: number | Expression): FunctionExpression { + if (decimalPlaces === undefined) { + return new FunctionExpression('round', [this]); + } else { + return new FunctionExpression( + 'round', + [this, valueToDefaultExpr(decimalPlaces)], + 'round' + ); + } + } /** - * Assigns an alias to this AggregateFunction. The alias specifies the name that - * the aggregated value will have in the output document. + * Creates an expression that returns the collection ID from a path. * * ```typescript - * // Calculate the average price of all items and assign it the alias "averagePrice". - * firestore.pipeline().collection("items") - * .aggregate(field("price").avg().as("averagePrice")); + * // Get the collection ID from a path. + * field("__path__").collectionId(); * ``` * - * @param name The alias to assign to this AggregateFunction. - * @return A new {@link AggregateWithAlias} that wraps this - * AggregateFunction and associates it with the provided alias. + * @return A new {@code Expr} representing the collectionId operation. */ - as(name: string): AggregateWithAlias { - return new AggregateWithAlias(this, name); + collectionId(): FunctionExpression { + return new FunctionExpression('collection_id', [this]); } /** - * @private - * @internal + * Creates an expression that calculates the length of a string, array, map, vector, or bytes. + * + * ```typescript + * // Get the length of the 'name' field. + * field("name").length(); + * + * // Get the number of items in the 'cart' array. + * field("cart").length(); + * ``` + * + * @return A new `Expr` representing the length of the string, array, map, vector, or bytes. + */ + length(): FunctionExpression { + return new FunctionExpression('length', [this]); + } + + /** + * Creates an expression that computes the natural logarithm of a numeric value. + * + * ```typescript + * // Compute the natural logarithm of the 'value' field. + * field("value").ln(); + * ``` + * + * @return A new {@code Expr} representing the natural logarithm of the numeric value. + */ + ln(): FunctionExpression { + return new FunctionExpression('ln', [this]); + } + + /** + * Creates an expression that computes the square root of a numeric value. + * + * ```typescript + * // Compute the square root of the 'value' field. + * field("value").sqrt(); + * ``` + * + * @return A new {@code Expr} representing the square root of the numeric value. + */ + sqrt(): FunctionExpression { + return new FunctionExpression('sqrt', [this]); + } + + /** + * Creates an expression that reverses a string. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * field("myString").stringReverse(); + * ``` + * + * @return A new {@code Expr} representing the reversed string. + */ + stringReverse(): FunctionExpression { + return new FunctionExpression('string_reverse', [this]); + } + + /** + * Creates an expression that returns the `elseValue` argument if this expression results in an absent value, else + * return the result of the this expression evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * field("optional_field").ifAbsent("default_value") + * ``` + * + * @param elseValue The value that will be returned if this Expression evaluates to an absent value. + * @return A new [Expression] representing the ifAbsent operation. + */ + ifAbsent(elseValue: unknown): Expression; + + /** + * Creates an expression that returns the `elseValue` argument if this expression results in an absent value, else + * return the result of this expression evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or if that is + * // absent, then returns the value of the field ` + * field("optional_field").ifAbsent(field('default_field')) + * ``` + * + * @param elseExpression The Expression that will be evaluated if this Expression evaluates to an absent value. + * @return A new [Expression] representing the ifAbsent operation. + */ + ifAbsent(elseExpression: unknown): Expression; + + ifAbsent(elseValueOrExpression: Expression | unknown): Expression { + return new FunctionExpression( + 'if_absent', + [this, valueToDefaultExpr(elseValueOrExpression)], + 'ifAbsent' + ); + } + + /** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with the delimiter from the 'separator' field. + * field("tags").join(field("separator")) + * ``` + * + * @param delimiterExpression The expression that evaluates to the delimiter string. + * @return A new Expression representing the join operation. + */ + join(delimiterExpression: Expression): Expression; + + /** + * Creates an expression that joins the elements of an array field into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with a comma and space. + * field("tags").join(", ") + * ``` + * + * @param delimiter The string to use as a delimiter. + * @return A new Expression representing the join operation. + */ + join(delimiter: string): Expression; + + join(delimeterValueOrExpression: string | Expression): Expression { + return new FunctionExpression( + 'join', + [this, valueToDefaultExpr(delimeterValueOrExpression)], + 'join' + ); + } + + /** + * Creates an expression that computes the base-10 logarithm of a numeric value. + * + * ```typescript + * // Compute the base-10 logarithm of the 'value' field. + * field("value").log10(); + * ``` + * + * @return A new {@code Expr} representing the base-10 logarithm of the numeric value. + */ + log10(): FunctionExpression { + return new FunctionExpression('log10', [this]); + } + + /** + * Creates an expression that computes the sum of the elements in an array. + * + * ```typescript + * // Compute the sum of the elements in the 'scores' field. + * field("scores").arraySum(); + * ``` + * + * @return A new {@code Expr} representing the sum of the elements in the array. + */ + arraySum(): FunctionExpression { + return new FunctionExpression('sum', [this]); + } + + // TODO(new-expression): Add new expression method definitions above this line + + /** + * Creates an {@link Ordering} that sorts documents in ascending order based on this expression. + * + * ```typescript + * // Sort documents by the 'name' field in ascending order + * pipeline().collection("users") + * .sort(field("name").ascending()); + * ``` + * + * @return A new `Ordering` for ascending sorting. + */ + ascending(): Ordering { + return ascending(this); + } + + /** + * Creates an {@link Ordering} that sorts documents in descending order based on this expression. + * + * ```typescript + * // Sort documents by the 'createdAt' field in descending order + * firestore.pipeline().collection("users") + * .sort(field("createdAt").descending()); + * ``` + * + * @return A new `Ordering` for descending sorting. + */ + descending(): Ordering { + return descending(this); + } + + /** + * 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. + * + * ```typescript + * // Calculate the total price and assign it the alias "totalPrice" and add it to the output. + * firestore.pipeline().collection("items") + * .addFields(field("price").multiply(field("quantity")).as("totalPrice")); + * ``` + * + * @param name The alias to assign to this expression. + * @return A new {@link AliasedExpression} that wraps this + * expression and associates it with the provided alias. + */ + as(name: string): AliasedExpression { + return new AliasedExpression(this, name, 'as'); + } +} + +/** + * @beta + * + * An interface that represents a selectable expression. + */ +export interface Selectable { + selectable: true; + /** + * @private + * @internal + */ + readonly alias: string; + /** + * @private + * @internal + */ + readonly expr: Expression; +} + +/** + * @beta + * + * A class that represents an aggregate function. + */ +export class AggregateFunction implements ProtoValueSerializable, UserData { + exprType: ExpressionType = 'AggregateFunction'; + + constructor(name: string, params: Expression[]); + /** + * INTERNAL Constructor with method name for validation. + * @hideconstructor + * @param name + * @param params + * @param _methodName + */ + constructor( + name: string, + params: Expression[], + _methodName: string | undefined + ); + constructor( + private name: string, + private params: Expression[], + readonly _methodName?: string + ) {} + + /** + * Assigns an alias to this AggregateFunction. The alias specifies the name that + * the aggregated value will have in the output document. + * + * ```typescript + * // Calculate the average price of all items and assign it the alias "averagePrice". + * firestore.pipeline().collection("items") + * .aggregate(field("price").average().as("averagePrice")); + * ``` + * + * @param name The alias to assign to this AggregateFunction. + * @return A new {@link AggregateWithAlias} that wraps this + * AggregateFunction and associates it with the provided alias. + */ + as(name: string): AggregateWithAlias { + return new AggregateWithAlias(this, name, 'as'); + } + + /** + * @private + * @internal */ _toProto(serializer: JsonProtoSerializer): ProtoValue { return { @@ -2061,13 +2286,12 @@ export class AggregateFunction implements ProtoValueSerializable, UserData { * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, this.name); + _readUserData(context: ParseContext): void { + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; this.params.forEach(expr => { - return expr._readUserData(dataReader, context); + return expr._readUserData(context); }); } } @@ -2078,66 +2302,53 @@ export class AggregateFunction implements ProtoValueSerializable, UserData { * An AggregateFunction with alias. */ export class AggregateWithAlias implements UserData { - constructor(readonly aggregate: AggregateFunction, readonly alias: string) {} - - /** - * @internal - * @private - * Indicates if this expression was created from a literal value passed - * by the caller. - */ - _createdFromLiteral: boolean = false; + constructor( + readonly aggregate: AggregateFunction, + readonly alias: string, + readonly _methodName: string | undefined + ) {} /** * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, 'as'); - this.aggregate._readUserData(dataReader, context); + _readUserData(context: ParseContext): void { + this.aggregate._readUserData(context); } } /** * @beta */ -export class ExprWithAlias implements Selectable, UserData { - exprType: ExprType = 'ExprWithAlias'; +export class AliasedExpression implements Selectable, UserData { + exprType: ExpressionType = 'AliasedExpression'; selectable = true as const; - /** - * @internal - * @private - * Indicates if this expression was created from a literal value passed - * by the caller. - */ - _createdFromLiteral: boolean = false; - - constructor(readonly expr: Expr, readonly alias: string) {} + constructor( + readonly expr: Expression, + readonly alias: string, + readonly _methodName: string | undefined + ) {} /** * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, 'as'); - this.expr._readUserData(dataReader, context); + _readUserData(context: ParseContext): void { + this.expr._readUserData(context); } } /** * @internal */ -class ListOfExprs extends Expr implements UserData { - exprType: ExprType = 'ListOfExprs'; +class ListOfExprs extends Expression implements UserData { + expressionType: ExpressionType = 'ListOfExpressions'; - constructor(private exprs: Expr[]) { + constructor( + private exprs: Expression[], + readonly _methodName: string | undefined + ) { super(); } @@ -2157,8 +2368,8 @@ class ListOfExprs extends Expr implements UserData { * @private * @internal */ - _readUserData(dataReader: UserDataReader): void { - this.exprs.forEach((expr: Expr) => expr._readUserData(dataReader)); + _readUserData(context: ParseContext): void { + this.exprs.forEach((expr: Expression) => expr._readUserData(context)); } } @@ -2180,8 +2391,8 @@ class ListOfExprs extends Expr implements UserData { * const cityField = field("address.city"); * ``` */ -export class Field extends Expr implements Selectable { - readonly exprType: ExprType = 'Field'; +export class Field extends Expression implements Selectable { + readonly expressionType: ExpressionType = 'Field'; selectable = true as const; /** @@ -2190,19 +2401,22 @@ export class Field extends Expr implements Selectable { * @hideconstructor * @param fieldPath */ - constructor(private fieldPath: InternalFieldPath) { + constructor( + private fieldPath: InternalFieldPath, + readonly _methodName: string | undefined + ) { super(); } - fieldName(): string { + get fieldName(): string { return this.fieldPath.canonicalString(); } get alias(): string { - return this.fieldName(); + return this.fieldName; } - get expr(): Expr { + get expr(): Expression { return this; } @@ -2220,7 +2434,7 @@ export class Field extends Expr implements Selectable { * @private * @internal */ - _readUserData(dataReader: UserDataReader): void {} + _readUserData(context: ParseContext): void {} } /** @@ -2243,18 +2457,25 @@ export class Field extends Expr implements Selectable { export function field(name: string): Field; export function field(path: FieldPath): Field; export function field(nameOrPath: string | FieldPath): Field { + return _field(nameOrPath, 'field'); +} + +export function _field( + nameOrPath: string | FieldPath, + methodName: string | undefined +): Field { if (typeof nameOrPath === 'string') { if (DOCUMENT_KEY_NAME === nameOrPath) { - return new Field(documentIdFieldPath()._internalPath); + return new Field(documentIdFieldPath()._internalPath, methodName); } - return new Field(fieldPathFromArgument('field', nameOrPath)); + return new Field(fieldPathFromArgument('field', nameOrPath), methodName); } else { - return new Field(nameOrPath._internalPath); + return new Field(nameOrPath._internalPath, methodName); } } /** - * @beta + * @internal * * Represents a constant value that can be used in a Firestore pipeline expression. * @@ -2268,8 +2489,8 @@ export function field(nameOrPath: string | FieldPath): Field { * const hello = constant("hello"); * ``` */ -export class Constant extends Expr { - readonly exprType: ExprType = 'Constant'; +export class Constant extends Expression { + readonly expressionType: ExpressionType = 'Constant'; private _protoValue?: ProtoValue; @@ -2279,7 +2500,10 @@ export class Constant extends Expr { * @hideconstructor * @param value The value of the constant. */ - constructor(private value: unknown) { + constructor( + private value: unknown, + readonly _methodName: string | undefined + ) { super(); } @@ -2288,7 +2512,7 @@ export class Constant extends Expr { * @internal */ static _fromProto(value: ProtoValue): Constant { - const result = new Constant(value); + const result = new Constant(value, undefined); result._protoValue = value; return result; } @@ -2297,7 +2521,7 @@ export class Constant extends Expr { * @private * @internal */ - _toProto(serializer: JsonProtoSerializer): ProtoValue { + _toProto(_: JsonProtoSerializer): ProtoValue { hardAssert( this._protoValue !== undefined, 0x00ed, @@ -2310,12 +2534,10 @@ export class Constant extends Expr { * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, 'constant'); - + _readUserData(context: ParseContext): void { + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; if (isFirestoreValue(this._protoValue)) { return; } else { @@ -2330,7 +2552,7 @@ export class Constant extends Expr { * @param value The number value. * @return A new `Constant` instance. */ -export function constant(value: number): Constant; +export function constant(value: number): Expression; /** * Creates a `Constant` instance for a string value. @@ -2338,15 +2560,15 @@ export function constant(value: number): Constant; * @param value The string value. * @return A new `Constant` instance. */ -export function constant(value: string): Constant; +export function constant(value: string): Expression; /** - * Creates a `Constant` instance for a boolean value. + * Creates a `BooleanExpression` instance for a boolean value. * * @param value The boolean value. * @return A new `Constant` instance. */ -export function constant(value: boolean): Constant; +export function constant(value: boolean): BooleanExpression; /** * Creates a `Constant` instance for a null value. @@ -2354,7 +2576,7 @@ export function constant(value: boolean): Constant; * @param value The null value. * @return A new `Constant` instance. */ -export function constant(value: null): Constant; +export function constant(value: null): Expression; /** * Creates a `Constant` instance for a GeoPoint value. @@ -2362,7 +2584,7 @@ export function constant(value: null): Constant; * @param value The GeoPoint value. * @return A new `Constant` instance. */ -export function constant(value: GeoPoint): Constant; +export function constant(value: GeoPoint): Expression; /** * Creates a `Constant` instance for a Timestamp value. @@ -2370,7 +2592,7 @@ export function constant(value: GeoPoint): Constant; * @param value The Timestamp value. * @return A new `Constant` instance. */ -export function constant(value: Timestamp): Constant; +export function constant(value: Timestamp): Expression; /** * Creates a `Constant` instance for a Date value. @@ -2378,7 +2600,7 @@ export function constant(value: Timestamp): Constant; * @param value The Date value. * @return A new `Constant` instance. */ -export function constant(value: Date): Constant; +export function constant(value: Date): Expression; /** * Creates a `Constant` instance for a Bytes value. @@ -2386,7 +2608,7 @@ export function constant(value: Date): Constant; * @param value The Bytes value. * @return A new `Constant` instance. */ -export function constant(value: Bytes): Constant; +export function constant(value: Bytes): Expression; /** * Creates a `Constant` instance for a DocumentReference value. @@ -2394,7 +2616,7 @@ export function constant(value: Bytes): Constant; * @param value The DocumentReference value. * @return A new `Constant` instance. */ -export function constant(value: DocumentReference): Constant; +export function constant(value: DocumentReference): Expression; /** * Creates a `Constant` instance for a Firestore proto value. @@ -2404,7 +2626,7 @@ export function constant(value: DocumentReference): Constant; * @param value The Firestore proto value. * @return A new `Constant` instance. */ -export function constant(value: ProtoValue): Constant; +export function constant(value: ProtoValue): Expression; /** * Creates a `Constant` instance for a VectorValue value. @@ -2412,28 +2634,26 @@ export function constant(value: ProtoValue): Constant; * @param value The VectorValue value. * @return A new `Constant` instance. */ -export function constant(value: VectorValue): Constant; +export function constant(value: VectorValue): Expression; -export function constant(value: unknown): Constant { - return new Constant(value); +export function constant(value: unknown): Expression | BooleanExpression { + return _constant(value, 'constant'); } /** - * Creates a `Constant` instance for a VectorValue value. - * - * ```typescript - * // Create a Constant instance for a vector value - * const vectorConstant = constantVector([1, 2, 3]); - * ``` - * - * @param value The VectorValue value. - * @return A new `Constant` instance. - */ -export function constantVector(value: number[] | VectorValue): Constant { - if (value instanceof VectorValue) { - return new Constant(value); + * @internal + * @private + * @param value + * @param methodName + */ +export function _constant( + value: unknown, + methodName: string | undefined +): Constant | BooleanExpression { + if (typeof value === 'boolean') { + return new BooleanConstant(value, methodName); } else { - return new Constant(new VectorValue(value as number[])); + return new Constant(value, methodName); } } @@ -2442,21 +2662,22 @@ export function constantVector(value: number[] | VectorValue): Constant { * @internal * @private */ -export class MapValue extends Expr { - constructor(private plainObject: Map) { +export class MapValue extends Expression { + constructor( + private plainObject: Map, + readonly _methodName: string | undefined + ) { super(); } - exprType: ExprType = 'Constant'; - - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, '_map'); + expressionType: ExpressionType = 'Constant'; + _readUserData(context: ParseContext): void { + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; this.plainObject.forEach(expr => { - expr._readUserData(dataReader, context); + expr._readUserData(context); }); } @@ -2471,13 +2692,23 @@ export class MapValue extends Expr { * This class defines the base class for Firestore {@link 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 {@link and}, {@link eq}, - * or the methods on {@link Expr} ({@link Expr#eq}, {@link Expr#lt}, etc.) to construct new Function instances. + * Typically, you would not use this class or its children directly. Use either the functions like {@link and}, {@link equal}, + * or the methods on {@link Expression} ({@link Expression#equal}, {@link Expression#lessThan}, etc.) to construct new Function instances. */ -export class FunctionExpr extends Expr { - readonly exprType: ExprType = 'Function'; +export class FunctionExpression extends Expression { + readonly expressionType: ExpressionType = 'Function'; - constructor(private name: string, private params: Expr[]) { + constructor(name: string, params: Expression[]); + constructor( + name: string, + params: Expression[], + _methodName: string | undefined + ); + constructor( + private name: string, + private params: Expression[], + readonly _methodName?: string + ) { super(); } @@ -2498,13 +2729,12 @@ export class FunctionExpr extends Expr { * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, this.name); + _readUserData(context: ParseContext): void { + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; this.params.forEach(expr => { - return expr._readUserData(dataReader, context); + return expr._readUserData(context); }); } } @@ -2514,7 +2744,7 @@ export class FunctionExpr extends Expr { * * An interface that represents a filter condition. */ -export class BooleanExpr extends FunctionExpr { +export class BooleanExpression extends FunctionExpression { filterable: true = true; /** @@ -2523,13 +2753,13 @@ export class BooleanExpr extends FunctionExpr { * * ```typescript * // Find the count of documents with a score greater than 90 - * field("score").gt(90).countIf().as("highestScore"); + * field("score").greaterThan(90).countIf().as("highestScore"); * ``` * * @return A new `AggregateFunction` representing the 'countIf' aggregation. */ countIf(): AggregateFunction { - return new AggregateFunction('count_if', [this]); + return new AggregateFunction('count_if', [this], 'countIf'); } /** @@ -2542,424 +2772,102 @@ export class BooleanExpr extends FunctionExpr { * * @return A new {@code Expr} representing the negated filter condition. */ - not(): BooleanExpr { - return new BooleanExpr('not', [this]); + not(): BooleanExpression { + return new BooleanExpression('not', [this], 'not'); + } + + /** + * Creates a conditional expression that evaluates to the 'then' expression + * if `this` expression evaluates to `true`, + * or evaluates to the 'else' expression if `this` expressions evaluates `false`. + * + * ```typescript + * // If 'age' is greater than 18, return "Adult"; otherwise, return "Minor". + * field("age").greaterThanOrEqual(18).conditional(constant("Adult"), constant("Minor")); + * ``` + * + * @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 {@code Expr} representing the conditional expression. + */ + conditional(thenExpr: Expression, elseExpr: Expression): FunctionExpression { + return new FunctionExpression( + 'conditional', + [this, thenExpr, elseExpr], + 'conditional' + ); + } + + /** + * @beta + * + * Creates an expression that returns the `catch` argument if there is an + * error, else return the result of this expression. + * + * ```typescript + * // Create an expression that protects against a divide by zero error + * // but always returns a boolean expression. + * constant(50).divide('length').gt(1).ifError(constant(false)); + * ``` + * + * @param catchValue The value that will be returned if this expression + * produces an error. + * @return A new {@code Expr} representing the 'ifError' operation. + */ + ifError(catchValue: BooleanExpression): BooleanExpression { + return new BooleanExpression('if_error', [this, catchValue], 'ifError'); } } /** - * @beta - * Creates an aggregation that counts the number of stage inputs where the provided - * boolean expression evaluates to true. + * @private + * @internal * - * ```typescript - * // Count the number of documents where 'is_active' field equals true - * countIf(field("is_active").eq(true)).as("numActiveDocuments"); - * ``` - * - * @param booleanExpr - The boolean expression to evaluate on each input. - * @returns A new `AggregateFunction` representing the 'countIf' aggregation. - */ -export function countIf(booleanExpr: BooleanExpr): AggregateFunction { - return booleanExpr.countIf(); -} - -/** - * @beta - * Creates an expression that return a pseudo-random value of type double in the - * range of [0, 1), inclusive of 0 and exclusive of 1. - * - * @returns A new `Expr` representing the 'rand' function. - */ -export function rand(): FunctionExpr { - return new FunctionExpr('rand', []); -} - -/** - * @beta - * - * Creates an expression that applies a bitwise AND operation between a field and a constant. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 0xFF. - * bitAnd("field1", 0xFF); - * ``` - * - * @param field The left operand field name. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise AND operation. - */ -export function bitAnd(field: string, otherBits: number | Bytes): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise AND operation between a field and an expression. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 'field2'. - * bitAnd("field1", field("field2")); - * ``` - * - * @param field The left operand field name. - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise AND operation. - */ -export function bitAnd(field: string, bitsExpression: Expr): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise AND operation between an expression and a constant. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 0xFF. - * bitAnd(field("field1"), 0xFF); - * ``` - * - * @param bitsExpression An expression returning bits. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise AND operation. - */ -export function bitAnd( - bitsExpression: Expr, - otherBits: number | Bytes -): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise AND operation between two expressions. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 'field2'. - * bitAnd(field("field1"), field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @param otherBitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise AND operation. + * To return a BooleanExpr as a constant, we need to break the pattern that expects a BooleanExpr to be a + * "pipeline function". Instead of building on serialization logic built into BooleanExpr, + * we override methods with those of an internally kept Constant value. */ -export function bitAnd( - bitsExpression: Expr, - otherBitsExpression: Expr -): FunctionExpr; -export function bitAnd( - bits: string | Expr, - bitsOrExpression: number | Expr | Bytes -): FunctionExpr { - return fieldOrExpression(bits).bitAnd(valueToDefaultExpr(bitsOrExpression)); -} +export class BooleanConstant extends BooleanExpression { + private readonly _internalConstant: Constant; -/** - * @beta - * - * Creates an expression that applies a bitwise OR operation between a field and a constant. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 0xFF. - * bitOr("field1", 0xFF); - * ``` - * - * @param field The left operand field name. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise OR operation. - */ -export function bitOr(field: string, otherBits: number | Bytes): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise OR operation between a field and an expression. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 'field2'. - * bitOr("field1", field("field2")); - * ``` - * - * @param field The left operand field name. - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise OR operation. - */ -export function bitOr(field: string, bitsExpression: Expr): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise OR operation between an expression and a constant. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 0xFF. - * bitOr(field("field1"), 0xFF); - * ``` - * - * @param bitsExpression An expression returning bits. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise OR operation. - */ -export function bitOr( - bitsExpression: Expr, - otherBits: number | Bytes -): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise OR operation between two expressions. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 'field2'. - * bitOr(field("field1"), field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @param otherBitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise OR operation. - */ -export function bitOr( - bitsExpression: Expr, - otherBitsExpression: Expr -): FunctionExpr; -export function bitOr( - bits: string | Expr, - bitsOrExpression: number | Expr | Bytes -): FunctionExpr { - return fieldOrExpression(bits).bitOr(valueToDefaultExpr(bitsOrExpression)); -} + constructor(value: boolean, readonly _methodName?: string) { + super('', []); -/** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between a field and a constant. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 0xFF. - * bitXor("field1", 0xFF); - * ``` - * - * @param field The left operand field name. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ -export function bitXor(field: string, otherBits: number | Bytes): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between a field and an expression. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 'field2'. - * bitXor("field1", field("field2")); - * ``` - * - * @param field The left operand field name. - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ -export function bitXor(field: string, bitsExpression: Expr): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between an expression and a constant. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 0xFF. - * bitXor(field("field1"), 0xFF); - * ``` - * - * @param bitsExpression An expression returning bits. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ -export function bitXor( - bitsExpression: Expr, - otherBits: number | Bytes -): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between two expressions. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 'field2'. - * bitXor(field("field1"), field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @param otherBitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ -export function bitXor( - bitsExpression: Expr, - otherBitsExpression: Expr -): FunctionExpr; -export function bitXor( - bits: string | Expr, - bitsOrExpression: number | Expr | Bytes -): FunctionExpr { - return fieldOrExpression(bits).bitXor(valueToDefaultExpr(bitsOrExpression)); -} + this._internalConstant = new Constant(value, _methodName); + } -/** - * @beta - * - * Creates an expression that applies a bitwise NOT operation to a field. - * - * ```typescript - * // Calculate the bitwise NOT of 'field1'. - * bitNot("field1"); - * ``` - * - * @param field The operand field name. - * @return A new {@code Expr} representing the bitwise NOT operation. - */ -export function bitNot(field: string): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise NOT operation to an expression. - * - * ```typescript - * // Calculate the bitwise NOT of 'field1'. - * bitNot(field("field1")); - * ``` - * - * @param bitsValueExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise NOT operation. - */ -export function bitNot(bitsValueExpression: Expr): FunctionExpr; -export function bitNot(bits: string | Expr): FunctionExpr { - return fieldOrExpression(bits).bitNot(); -} + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return this._internalConstant._toProto(serializer); + } -/** - * @beta - * - * Creates an expression that applies a bitwise left shift operation between a field and a constant. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 2 bits. - * bitLeftShift("field1", 2); - * ``` - * - * @param field The left operand field name. - * @param y The right operand constant representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ -export function bitLeftShift(field: string, y: number): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise left shift operation between a field and an expression. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 'field2' bits. - * bitLeftShift("field1", field("field2")); - * ``` - * - * @param field The left operand field name. - * @param numberExpr The right operand expression representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ -export function bitLeftShift(field: string, numberExpr: Expr): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise left shift operation between an expression and a constant. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 2 bits. - * bitLeftShift(field("field1"), 2); - * ``` - * - * @param xValue An expression returning bits. - * @param y The right operand constant representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ -export function bitLeftShift(xValue: Expr, y: number): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise left shift operation between two expressions. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 'field2' bits. - * bitLeftShift(field("field1"), field("field2")); - * ``` - * - * @param xValue An expression returning bits. - * @param numberExpr The right operand expression representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ -export function bitLeftShift(xValue: Expr, numberExpr: Expr): FunctionExpr; -export function bitLeftShift( - xValue: string | Expr, - numberExpr: number | Expr -): FunctionExpr { - return fieldOrExpression(xValue).bitLeftShift(valueToDefaultExpr(numberExpr)); + /** + * @private + * @internal + */ + _readUserData(context: ParseContext): void { + return this._internalConstant._readUserData(context); + } } /** * @beta - * - * Creates an expression that applies a bitwise right shift operation between a field and a constant. - * - * ```typescript - * // Calculate the bitwise right shift of 'field1' by 2 bits. - * bitRightShift("field1", 2); - * ``` - * - * @param field The left operand field name. - * @param y The right operand constant representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise right shift operation. - */ -export function bitRightShift(field: string, y: number): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise right shift operation between a field and an expression. - * - * ```typescript - * // Calculate the bitwise right shift of 'field1' by 'field2' bits. - * bitRightShift("field1", field("field2")); - * ``` - * - * @param field The left operand field name. - * @param numberExpr The right operand expression representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise right shift operation. - */ -export function bitRightShift(field: string, numberExpr: Expr): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise right shift operation between an expression and a constant. - * - * ```typescript - * // Calculate the bitwise right shift of 'field1' by 2 bits. - * bitRightShift(field("field1"), 2); - * ``` - * - * @param xValue An expression returning bits. - * @param y The right operand constant representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise right shift operation. - */ -export function bitRightShift(xValue: Expr, y: number): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise right shift operation between two expressions. + * Creates an aggregation that counts the number of stage inputs where the provided + * boolean expression evaluates to true. * * ```typescript - * // Calculate the bitwise right shift of 'field1' by 'field2' bits. - * bitRightShift(field("field1"), field("field2")); + * // Count the number of documents where 'is_active' field equals true + * countIf(field("is_active").equal(true)).as("numActiveDocuments"); * ``` * - * @param xValue An expression returning bits. - * @param right The right operand expression representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise right shift operation. + * @param booleanExpr - The boolean expression to evaluate on each input. + * @returns A new `AggregateFunction` representing the 'countIf' aggregation. */ -export function bitRightShift(xValue: Expr, numberExpr: Expr): FunctionExpr; -export function bitRightShift( - xValue: string | Expr, - numberExpr: number | Expr -): FunctionExpr { - return fieldOrExpression(xValue).bitRightShift( - valueToDefaultExpr(numberExpr) - ); +export function countIf(booleanExpr: BooleanExpression): AggregateFunction { + return booleanExpr.countIf(); } /** @@ -2970,14 +2878,17 @@ export function bitRightShift( * * ```typescript * // Return the value in the tags field array at index 1. - * arrayOffset('tags', 1); + * arrayGet('tags', 1); * ``` * * @param arrayField The name of the array field. * @param offset The index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. + * @return A new Expr representing the 'arrayGet' operation. */ -export function arrayOffset(arrayField: string, offset: number): FunctionExpr; +export function arrayGet( + arrayField: string, + offset: number +): FunctionExpression; /** * @beta @@ -2988,14 +2899,17 @@ export function arrayOffset(arrayField: string, offset: number): FunctionExpr; * ```typescript * // Return the value in the tags field array at index specified by field * // 'favoriteTag'. - * arrayOffset('tags', field('favoriteTag')); + * arrayGet('tags', field('favoriteTag')); * ``` * * @param arrayField The name of the array field. * @param offsetExpr An Expr evaluating to the index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. + * @return A new Expr representing the 'arrayGet' operation. */ -export function arrayOffset(arrayField: string, offsetExpr: Expr): FunctionExpr; +export function arrayGet( + arrayField: string, + offsetExpr: Expression +): FunctionExpression; /** * @beta @@ -3005,17 +2919,17 @@ export function arrayOffset(arrayField: string, offsetExpr: Expr): FunctionExpr; * * ```typescript * // Return the value in the tags field array at index 1. - * arrayOffset(field('tags'), 1); + * arrayGet(field('tags'), 1); * ``` * * @param arrayExpression An Expr evaluating to an array. * @param offset The index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. + * @return A new Expr representing the 'arrayGet' operation. */ -export function arrayOffset( - arrayExpression: Expr, +export function arrayGet( + arrayExpression: Expression, offset: number -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -3026,22 +2940,22 @@ export function arrayOffset( * ```typescript * // Return the value in the tags field array at index specified by field * // 'favoriteTag'. - * arrayOffset(field('tags'), field('favoriteTag')); + * arrayGet(field('tags'), field('favoriteTag')); * ``` * * @param arrayExpression An Expr evaluating to an array. * @param offsetExpr An Expr evaluating to the index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. - */ -export function arrayOffset( - arrayExpression: Expr, - offsetExpr: Expr -): FunctionExpr; -export function arrayOffset( - array: Expr | string, - offset: Expr | number -): FunctionExpr { - return fieldOrExpression(array).arrayOffset(valueToDefaultExpr(offset)); + * @return A new Expr representing the 'arrayGet' operation. + */ +export function arrayGet( + arrayExpression: Expression, + offsetExpr: Expression +): FunctionExpression; +export function arrayGet( + array: Expression | string, + offset: Expression | number +): FunctionExpression { + return fieldOrExpression(array).arrayGet(valueToDefaultExpr(offset)); } /** @@ -3057,10 +2971,34 @@ export function arrayOffset( * @param value The expression to check. * @return A new {@code Expr} representing the 'isError' check. */ -export function isError(value: Expr): BooleanExpr { +export function isError(value: Expression): BooleanExpression { return value.isError(); } +/** + * @beta + * + * Creates an expression that returns the `catch` argument if there is an + * error, else return the result of the `try` argument evaluation. + * + * This overload is useful when a BooleanExpression is required. + * + * ```typescript + * // Create an expression that protects against a divide by zero error + * // but always returns a boolean expression. + * ifError(constant(50).divide('length').gt(1), constant(false)); + * ``` + * + * @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 {@code Expr} representing the 'ifError' operation. + */ +export function ifError( + tryExpr: BooleanExpression, + catchExpr: BooleanExpression +): BooleanExpression; + /** * @beta * @@ -3070,7 +3008,7 @@ export function isError(value: Expr): BooleanExpr { * ```typescript * // Returns the first item in the title field arrays, or returns * // the entire title field if the array is empty or the field is another type. - * ifError(field("title").arrayOffset(0), field("title")); + * ifError(field("title").arrayGet(0), field("title")); * ``` * * @param tryExpr The try expression. @@ -3078,7 +3016,10 @@ export function isError(value: Expr): BooleanExpr { * returned if the tryExpr produces an error. * @return A new {@code Expr} representing the 'ifError' operation. */ -export function ifError(tryExpr: Expr, catchExpr: Expr): FunctionExpr; +export function ifError( + tryExpr: Expression, + catchExpr: Expression +): FunctionExpression; /** * @beta @@ -3089,7 +3030,7 @@ export function ifError(tryExpr: Expr, catchExpr: Expr): FunctionExpr; * ```typescript * // Returns the first item in the title field arrays, or returns * // "Default Title" - * ifError(field("title").arrayOffset(0), "Default Title"); + * ifError(field("title").arrayGet(0), "Default Title"); * ``` * * @param tryExpr The try expression. @@ -3097,9 +3038,22 @@ export function ifError(tryExpr: Expr, catchExpr: Expr): FunctionExpr; * error. * @return A new {@code Expr} representing the 'ifError' operation. */ -export function ifError(tryExpr: Expr, catchValue: unknown): FunctionExpr; -export function ifError(tryExpr: Expr, catchValue: unknown): FunctionExpr { - return tryExpr.ifError(valueToDefaultExpr(catchValue)); +export function ifError( + tryExpr: Expression, + catchValue: unknown +): FunctionExpression; +export function ifError( + tryExpr: Expression, + catchValue: unknown +): FunctionExpression { + if ( + tryExpr instanceof BooleanExpression && + catchValue instanceof BooleanExpression + ) { + return tryExpr.ifError(catchValue); + } else { + return tryExpr.ifError(valueToDefaultExpr(catchValue)); + } } /** @@ -3116,7 +3070,7 @@ export function ifError(tryExpr: Expr, catchValue: unknown): FunctionExpr { * @param value The expression to check. * @return A new {@code Expr} representing the 'isAbsent' check. */ -export function isAbsent(value: Expr): BooleanExpr; +export function isAbsent(value: Expression): BooleanExpression; /** * @beta @@ -3132,8 +3086,8 @@ export function isAbsent(value: Expr): BooleanExpr; * @param field The field to check. * @return A new {@code Expr} representing the 'isAbsent' check. */ -export function isAbsent(field: string): BooleanExpr; -export function isAbsent(value: Expr | string): BooleanExpr { +export function isAbsent(field: string): BooleanExpression; +export function isAbsent(value: Expression | string): BooleanExpression { return fieldOrExpression(value).isAbsent(); } @@ -3150,7 +3104,7 @@ export function isAbsent(value: Expr | string): BooleanExpr { * @param value The expression to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNull(value: Expr): BooleanExpr; +export function isNull(value: Expression): BooleanExpression; /** * @beta @@ -3165,8 +3119,8 @@ export function isNull(value: Expr): BooleanExpr; * @param value The name of the field to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNull(value: string): BooleanExpr; -export function isNull(value: Expr | string): BooleanExpr { +export function isNull(value: string): BooleanExpression; +export function isNull(value: Expression | string): BooleanExpression { return fieldOrExpression(value).isNull(); } @@ -3183,7 +3137,7 @@ export function isNull(value: Expr | string): BooleanExpr { * @param value The expression to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNotNull(value: Expr): BooleanExpr; +export function isNotNull(value: Expression): BooleanExpression; /** * @beta @@ -3198,8 +3152,8 @@ export function isNotNull(value: Expr): BooleanExpr; * @param value The name of the field to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNotNull(value: string): BooleanExpr; -export function isNotNull(value: Expr | string): BooleanExpr { +export function isNotNull(value: string): BooleanExpression; +export function isNotNull(value: Expression | string): BooleanExpression { return fieldOrExpression(value).isNotNull(); } @@ -3216,7 +3170,7 @@ export function isNotNull(value: Expr | string): BooleanExpr { * @param value The expression to check. * @return A new {@code Expr} representing the 'isNotNaN' check. */ -export function isNotNan(value: Expr): BooleanExpr; +export function isNotNan(value: Expression): BooleanExpression; /** * @beta @@ -3231,8 +3185,8 @@ export function isNotNan(value: Expr): BooleanExpr; * @param value The name of the field to check. * @return A new {@code Expr} representing the 'isNotNaN' check. */ -export function isNotNan(value: string): BooleanExpr; -export function isNotNan(value: Expr | string): BooleanExpr { +export function isNotNan(value: string): BooleanExpression; +export function isNotNan(value: Expression | string): BooleanExpression { return fieldOrExpression(value).isNotNan(); } @@ -3249,7 +3203,7 @@ export function isNotNan(value: Expr | string): BooleanExpr { * @param mapField The name of a field containing a map value. * @param key The name of the key to remove from the input map. */ -export function mapRemove(mapField: string, key: string): FunctionExpr; +export function mapRemove(mapField: string, key: string): FunctionExpression; /** * @beta * @@ -3263,7 +3217,7 @@ export function mapRemove(mapField: string, key: string): FunctionExpr; * @param mapExpr An expression return a map value. * @param key The name of the key to remove from the input map. */ -export function mapRemove(mapExpr: Expr, key: string): FunctionExpr; +export function mapRemove(mapExpr: Expression, key: string): FunctionExpression; /** * @beta * @@ -3277,7 +3231,10 @@ export function mapRemove(mapExpr: Expr, key: string): FunctionExpr; * @param mapField The name of a field containing a map value. * @param keyExpr An expression that produces the name of the key to remove from the input map. */ -export function mapRemove(mapField: string, keyExpr: Expr): FunctionExpr; +export function mapRemove( + mapField: string, + keyExpr: Expression +): FunctionExpression; /** * @beta * @@ -3291,12 +3248,15 @@ export function mapRemove(mapField: string, keyExpr: Expr): FunctionExpr; * @param mapExpr An expression return a map value. * @param keyExpr An expression that produces the name of the key to remove from the input map. */ -export function mapRemove(mapExpr: Expr, keyExpr: Expr): FunctionExpr; +export function mapRemove( + mapExpr: Expression, + keyExpr: Expression +): FunctionExpression; export function mapRemove( - mapExpr: Expr | string, - stringExpr: Expr | string -): FunctionExpr { + mapExpr: Expression | string, + stringExpr: Expression | string +): FunctionExpression { return fieldOrExpression(mapExpr).mapRemove(valueToDefaultExpr(stringExpr)); } @@ -3308,7 +3268,7 @@ export function mapRemove( * ``` * // Merges the map in the settings field with, a map literal, and a map in * // that is conditionally returned by another expression - * mapMerge('settings', { enabled: true }, cond(field('isAdmin'), { admin: true}, {}) + * mapMerge('settings', { enabled: true }, conditional(field('isAdmin'), { admin: true}, {}) * ``` * * @param mapField Name of a field containing a map value that will be merged. @@ -3319,9 +3279,9 @@ export function mapRemove( */ export function mapMerge( mapField: string, - secondMap: Record | Expr, - ...otherMaps: Array | Expr> -): FunctionExpr; + secondMap: Record | Expression, + ...otherMaps: Array | Expression> +): FunctionExpression; /** * @beta @@ -3331,7 +3291,7 @@ export function mapMerge( * ``` * // Merges the map in the settings field with, a map literal, and a map in * // that is conditionally returned by another expression - * mapMerge(field('settings'), { enabled: true }, cond(field('isAdmin'), { admin: true}, {}) + * mapMerge(field('settings'), { enabled: true }, conditional(field('isAdmin'), { admin: true}, {}) * ``` * * @param firstMap An expression or literal map value that will be merged. @@ -3341,16 +3301,16 @@ export function mapMerge( * as a literal or an expression that returns a map. */ export function mapMerge( - firstMap: Record | Expr, - secondMap: Record | Expr, - ...otherMaps: Array | Expr> -): FunctionExpr; + firstMap: Record | Expression, + secondMap: Record | Expression, + ...otherMaps: Array | Expression> +): FunctionExpression; export function mapMerge( - firstMap: string | Record | Expr, - secondMap: Record | Expr, - ...otherMaps: Array | Expr> -): FunctionExpr { + firstMap: string | Record | Expression, + secondMap: Record | Expression, + ...otherMaps: Array | Expression> +): FunctionExpression { const secondMapExpr = valueToDefaultExpr(secondMap); const otherMapExprs = otherMaps.map(valueToDefaultExpr); return fieldOrExpression(firstMap).mapMerge(secondMapExpr, ...otherMapExprs); @@ -3370,7 +3330,7 @@ export function mapMerge( */ export function documentId( documentPath: string | DocumentReference -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -3384,11 +3344,11 @@ export function documentId( * * @return A new {@code Expr} representing the documentId operation. */ -export function documentId(documentPathExpr: Expr): FunctionExpr; +export function documentId(documentPathExpr: Expression): FunctionExpression; export function documentId( - documentPath: Expr | string | DocumentReference -): FunctionExpr { + documentPath: Expression | string | DocumentReference +): FunctionExpression { // @ts-ignore const documentPathExpr = valueToDefaultExpr(documentPath); return documentPathExpr.documentId(); @@ -3403,11 +3363,11 @@ export function documentId( * @param position Index of the first character of the substring. * @param length Length of the substring. */ -export function substr( +export function substring( field: string, position: number, length?: number -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -3418,11 +3378,11 @@ export function substr( * @param position Index of the first character of the substring. * @param length Length of the substring. */ -export function substr( - input: Expr, +export function substring( + input: Expression, position: number, length?: number -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -3433,11 +3393,11 @@ export function substr( * @param position An expression that returns the index of the first character of the substring. * @param length An expression that returns the length of the substring. */ -export function substr( +export function substring( field: string, - position: Expr, - length?: Expr -): FunctionExpr; + position: Expression, + length?: Expression +): FunctionExpression; /** * @beta @@ -3448,22 +3408,22 @@ export function substr( * @param position An expression that returns the index of the first character of the substring. * @param length An expression that returns the length of the substring. */ -export function substr( - input: Expr, - position: Expr, - length?: Expr -): FunctionExpr; - -export function substr( - field: Expr | string, - position: Expr | number, - length?: Expr | number -): FunctionExpr { +export function substring( + input: Expression, + position: Expression, + length?: Expression +): FunctionExpression; + +export function substring( + field: Expression | string, + position: Expression | number, + length?: Expression | number +): FunctionExpression { const fieldExpr = fieldOrExpression(field); const positionExpr = valueToDefaultExpr(position); const lengthExpr = length === undefined ? undefined : valueToDefaultExpr(length); - return fieldExpr.substr(positionExpr, lengthExpr); + return fieldExpr.substring(positionExpr, lengthExpr); } /** @@ -3482,10 +3442,9 @@ export function substr( * @return A new {@code Expr} representing the addition operation. */ export function add( - first: Expr, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + first: Expression, + second: Expression | unknown +): FunctionExpression; /** * @beta @@ -3504,19 +3463,14 @@ export function add( */ export function add( fieldName: string, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + second: Expression | unknown +): FunctionExpression; export function add( - first: Expr | string, - second: Expr | unknown, - ...others: Array -): FunctionExpr { - return fieldOrExpression(first).add( - valueToDefaultExpr(second), - ...others.map(value => valueToDefaultExpr(value)) - ); + first: Expression | string, + second: Expression | unknown +): FunctionExpression { + return fieldOrExpression(first).add(valueToDefaultExpr(second)); } /** @@ -3533,7 +3487,10 @@ export function add( * @param right The expression to subtract. * @return A new {@code Expr} representing the subtraction operation. */ -export function subtract(left: Expr, right: Expr): FunctionExpr; +export function subtract( + left: Expression, + right: Expression +): FunctionExpression; /** * @beta @@ -3549,7 +3506,10 @@ export function subtract(left: Expr, right: Expr): FunctionExpr; * @param value The constant value to subtract. * @return A new {@code Expr} representing the subtraction operation. */ -export function subtract(expression: Expr, value: unknown): FunctionExpr; +export function subtract( + expression: Expression, + value: unknown +): FunctionExpression; /** * @beta @@ -3565,7 +3525,10 @@ export function subtract(expression: Expr, value: unknown): FunctionExpr; * @param expression The expression to subtract. * @return A new {@code Expr} representing the subtraction operation. */ -export function subtract(fieldName: string, expression: Expr): FunctionExpr; +export function subtract( + fieldName: string, + expression: Expression +): FunctionExpression; /** * @beta @@ -3581,11 +3544,11 @@ export function subtract(fieldName: string, expression: Expr): FunctionExpr; * @param value The constant value to subtract. * @return A new {@code Expr} representing the subtraction operation. */ -export function subtract(fieldName: string, value: unknown): FunctionExpr; +export function subtract(fieldName: string, value: unknown): FunctionExpression; export function subtract( - left: Expr | string, - right: Expr | unknown -): FunctionExpr { + left: Expression | string, + right: Expression | unknown +): FunctionExpression { const normalizedLeft = typeof left === 'string' ? field(left) : left; const normalizedRight = valueToDefaultExpr(right); return normalizedLeft.subtract(normalizedRight); @@ -3607,10 +3570,9 @@ export function subtract( * @return A new {@code Expr} representing the multiplication operation. */ export function multiply( - first: Expr, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + first: Expression, + second: Expression | unknown +): FunctionExpression; /** * @beta @@ -3629,19 +3591,14 @@ export function multiply( */ export function multiply( fieldName: string, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + second: Expression | unknown +): FunctionExpression; export function multiply( - first: Expr | string, - second: Expr | unknown, - ...others: Array -): FunctionExpr { - return fieldOrExpression(first).multiply( - valueToDefaultExpr(second), - ...others.map(valueToDefaultExpr) - ); + first: Expression | string, + second: Expression | unknown +): FunctionExpression { + return fieldOrExpression(first).multiply(valueToDefaultExpr(second)); } /** @@ -3658,7 +3615,7 @@ export function multiply( * @param right The expression to divide by. * @return A new {@code Expr} representing the division operation. */ -export function divide(left: Expr, right: Expr): FunctionExpr; +export function divide(left: Expression, right: Expression): FunctionExpression; /** * @beta @@ -3674,7 +3631,10 @@ export function divide(left: Expr, right: Expr): FunctionExpr; * @param value The constant value to divide by. * @return A new {@code Expr} representing the division operation. */ -export function divide(expression: Expr, value: unknown): FunctionExpr; +export function divide( + expression: Expression, + value: unknown +): FunctionExpression; /** * @beta @@ -3690,7 +3650,10 @@ export function divide(expression: Expr, value: unknown): FunctionExpr; * @param expressions The expression to divide by. * @return A new {@code Expr} representing the division operation. */ -export function divide(fieldName: string, expressions: Expr): FunctionExpr; +export function divide( + fieldName: string, + expressions: Expression +): FunctionExpression; /** * @beta @@ -3706,11 +3669,11 @@ export function divide(fieldName: string, expressions: Expr): FunctionExpr; * @param value The constant value to divide by. * @return A new {@code Expr} representing the division operation. */ -export function divide(fieldName: string, value: unknown): FunctionExpr; +export function divide(fieldName: string, value: unknown): FunctionExpression; export function divide( - left: Expr | string, - right: Expr | unknown -): FunctionExpr { + left: Expression | string, + right: Expression | unknown +): FunctionExpression { const normalizedLeft = typeof left === 'string' ? field(left) : left; const normalizedRight = valueToDefaultExpr(right); return normalizedLeft.divide(normalizedRight); @@ -3730,7 +3693,7 @@ export function divide( * @param right The divisor expression. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(left: Expr, right: Expr): FunctionExpr; +export function mod(left: Expression, right: Expression): FunctionExpression; /** * @beta @@ -3746,7 +3709,7 @@ export function mod(left: Expr, right: Expr): FunctionExpr; * @param value The divisor constant. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(expression: Expr, value: unknown): FunctionExpr; +export function mod(expression: Expression, value: unknown): FunctionExpression; /** * @beta @@ -3762,7 +3725,10 @@ export function mod(expression: Expr, value: unknown): FunctionExpr; * @param expression The divisor expression. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(fieldName: string, expression: Expr): FunctionExpr; +export function mod( + fieldName: string, + expression: Expression +): FunctionExpression; /** * @beta @@ -3778,8 +3744,11 @@ export function mod(fieldName: string, expression: Expr): FunctionExpr; * @param value The divisor constant. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(fieldName: string, value: unknown): FunctionExpr; -export function mod(left: Expr | string, right: Expr | unknown): FunctionExpr { +export function mod(fieldName: string, value: unknown): FunctionExpression; +export function mod( + left: Expression | string, + right: Expression | unknown +): FunctionExpression { const normalizedLeft = typeof left === 'string' ? field(left) : left; const normalizedRight = valueToDefaultExpr(right); return normalizedLeft.mod(normalizedRight); @@ -3798,8 +3767,14 @@ export function mod(left: Expr | string, right: Expr | unknown): FunctionExpr { * @param elements The input map to evaluate in the expression. * @return A new {@code Expr} representing the map function. */ -export function map(elements: Record): FunctionExpr { - const result: Expr[] = []; +export function map(elements: Record): FunctionExpression { + return _map(elements, 'map'); +} +export function _map( + elements: Record, + methodName: string | undefined +): FunctionExpression { + const result: Expression[] = []; for (const key in elements) { if (Object.prototype.hasOwnProperty.call(elements, key)) { const value = elements[key]; @@ -3807,7 +3782,7 @@ export function map(elements: Record): FunctionExpr { result.push(valueToDefaultExpr(value)); } } - return new FunctionExpr('map', result); + return new FunctionExpression('map', result, 'map'); } /** @@ -3822,14 +3797,14 @@ export function map(elements: Record): FunctionExpr { * @param plainObject */ export function _mapValue(plainObject: Record): MapValue { - const result: Map = new Map(); + const result: Map = new Map(); for (const key in plainObject) { if (Object.prototype.hasOwnProperty.call(plainObject, key)) { const value = plainObject[key]; result.set(key, valueToDefaultExpr(value)); } } - return new MapValue(result); + return new MapValue(result, undefined); } /** @@ -3845,10 +3820,17 @@ export function _mapValue(plainObject: Record): MapValue { * @param elements The input array to evaluate in the expression. * @return A new {@code Expr} representing the array function. */ -export function array(elements: unknown[]): FunctionExpr { - return new FunctionExpr( +export function array(elements: unknown[]): FunctionExpression { + return _array(elements, 'array'); +} +export function _array( + elements: unknown[], + methodName: string | undefined +): FunctionExpression { + return new FunctionExpression( 'array', - elements.map(element => valueToDefaultExpr(element)) + elements.map(element => valueToDefaultExpr(element)), + methodName ); } @@ -3859,14 +3841,14 @@ export function array(elements: unknown[]): FunctionExpr { * * ```typescript * // Check if the 'age' field is equal to an expression - * eq(field("age"), field("minAge").add(10)); + * equal(field("age"), field("minAge").add(10)); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the equality comparison. */ -export function eq(left: Expr, right: Expr): BooleanExpr; +export function equal(left: Expression, right: Expression): BooleanExpression; /** * @beta @@ -3875,14 +3857,17 @@ export function eq(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'age' field is equal to 21 - * eq(field("age"), 21); + * equal(field("age"), 21); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the equality comparison. */ -export function eq(expression: Expr, value: unknown): BooleanExpr; +export function equal( + expression: Expression, + value: unknown +): BooleanExpression; /** * @beta @@ -3891,14 +3876,17 @@ export function eq(expression: Expr, value: unknown): BooleanExpr; * * ```typescript * // Check if the 'age' field is equal to the 'limit' field - * eq("age", field("limit")); + * equal("age", field("limit")); * ``` * * @param fieldName The field name to compare. * @param expression The expression to compare to. * @return A new `Expr` representing the equality comparison. */ -export function eq(fieldName: string, expression: Expr): BooleanExpr; +export function equal( + fieldName: string, + expression: Expression +): BooleanExpression; /** * @beta @@ -3907,18 +3895,21 @@ export function eq(fieldName: string, expression: Expr): BooleanExpr; * * ```typescript * // Check if the 'city' field is equal to string constant "London" - * eq("city", "London"); + * equal("city", "London"); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the equality comparison. */ -export function eq(fieldName: string, value: unknown): BooleanExpr; -export function eq(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function equal(fieldName: string, value: unknown): BooleanExpression; +export function equal( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.eq(rightExpr); + return leftExpr.equal(rightExpr); } /** @@ -3928,14 +3919,17 @@ export function eq(left: Expr | string, right: unknown): BooleanExpr { * * ```typescript * // Check if the 'status' field is not equal to field 'finalState' - * neq(field("status"), field("finalState")); + * notEqual(field("status"), field("finalState")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the inequality comparison. */ -export function neq(left: Expr, right: Expr): BooleanExpr; +export function notEqual( + left: Expression, + right: Expression +): BooleanExpression; /** * @beta @@ -3944,14 +3938,17 @@ export function neq(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'status' field is not equal to "completed" - * neq(field("status"), "completed"); + * notEqual(field("status"), "completed"); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the inequality comparison. */ -export function neq(expression: Expr, value: unknown): BooleanExpr; +export function notEqual( + expression: Expression, + value: unknown +): BooleanExpression; /** * @beta @@ -3960,14 +3957,17 @@ export function neq(expression: Expr, value: unknown): BooleanExpr; * * ```typescript * // Check if the 'status' field is not equal to the value of 'expectedStatus' - * neq("status", field("expectedStatus")); + * notEqual("status", field("expectedStatus")); * ``` * * @param fieldName The field name to compare. * @param expression The expression to compare to. * @return A new `Expr` representing the inequality comparison. */ -export function neq(fieldName: string, expression: Expr): BooleanExpr; +export function notEqual( + fieldName: string, + expression: Expression +): BooleanExpression; /** * @beta @@ -3976,18 +3976,21 @@ export function neq(fieldName: string, expression: Expr): BooleanExpr; * * ```typescript * // Check if the 'country' field is not equal to "USA" - * neq("country", "USA"); + * notEqual("country", "USA"); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the inequality comparison. */ -export function neq(fieldName: string, value: unknown): BooleanExpr; -export function neq(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function notEqual(fieldName: string, value: unknown): BooleanExpression; +export function notEqual( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.neq(rightExpr); + return leftExpr.notEqual(rightExpr); } /** @@ -3997,14 +4000,17 @@ export function neq(left: Expr | string, right: unknown): BooleanExpr { * * ```typescript * // Check if the 'age' field is less than 30 - * lt(field("age"), field("limit")); + * lessThan(field("age"), field("limit")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the less than comparison. */ -export function lt(left: Expr, right: Expr): BooleanExpr; +export function lessThan( + left: Expression, + right: Expression +): BooleanExpression; /** * @beta @@ -4013,14 +4019,17 @@ export function lt(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'age' field is less than 30 - * lt(field("age"), 30); + * lessThan(field("age"), 30); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the less than comparison. */ -export function lt(expression: Expr, value: unknown): BooleanExpr; +export function lessThan( + expression: Expression, + value: unknown +): BooleanExpression; /** * @beta @@ -4029,14 +4038,17 @@ export function lt(expression: Expr, value: unknown): BooleanExpr; * * ```typescript * // Check if the 'age' field is less than the 'limit' field - * lt("age", field("limit")); + * lessThan("age", field("limit")); * ``` * * @param fieldName The field name to compare. * @param expression The expression to compare to. * @return A new `Expr` representing the less than comparison. */ -export function lt(fieldName: string, expression: Expr): BooleanExpr; +export function lessThan( + fieldName: string, + expression: Expression +): BooleanExpression; /** * @beta @@ -4045,18 +4057,21 @@ export function lt(fieldName: string, expression: Expr): BooleanExpr; * * ```typescript * // Check if the 'price' field is less than 50 - * lt("price", 50); + * lessThan("price", 50); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the less than comparison. */ -export function lt(fieldName: string, value: unknown): BooleanExpr; -export function lt(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function lessThan(fieldName: string, value: unknown): BooleanExpression; +export function lessThan( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.lt(rightExpr); + return leftExpr.lessThan(rightExpr); } /** @@ -4067,14 +4082,17 @@ export function lt(left: Expr | string, right: unknown): BooleanExpr { * * ```typescript * // Check if the 'quantity' field is less than or equal to 20 - * lte(field("quantity"), field("limit")); + * lessThan(field("quantity"), field("limit")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(left: Expr, right: Expr): BooleanExpr; +export function lessThanOrEqual( + left: Expression, + right: Expression +): BooleanExpression; /** * @beta @@ -4083,28 +4101,34 @@ export function lte(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'quantity' field is less than or equal to 20 - * lte(field("quantity"), 20); + * lessThan(field("quantity"), 20); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(expression: Expr, value: unknown): BooleanExpr; +export function lessThanOrEqual( + expression: Expression, + value: unknown +): BooleanExpression; /** * Creates an expression that checks if a field's value is less than or equal to an expression. * * ```typescript * // Check if the 'quantity' field is less than or equal to the 'limit' field - * lte("quantity", field("limit")); + * lessThan("quantity", field("limit")); * ``` * * @param fieldName The field name to compare. * @param expression The expression to compare to. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(fieldName: string, expression: Expr): BooleanExpr; +export function lessThanOrEqual( + fieldName: string, + expression: Expression +): BooleanExpression; /** * @beta @@ -4113,18 +4137,24 @@ export function lte(fieldName: string, expression: Expr): BooleanExpr; * * ```typescript * // Check if the 'score' field is less than or equal to 70 - * lte("score", 70); + * lessThan("score", 70); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(fieldName: string, value: unknown): BooleanExpr; -export function lte(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function lessThanOrEqual( + fieldName: string, + value: unknown +): BooleanExpression; +export function lessThanOrEqual( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.lte(rightExpr); + return leftExpr.lessThanOrEqual(rightExpr); } /** @@ -4135,14 +4165,17 @@ export function lte(left: Expr | string, right: unknown): BooleanExpr { * * ```typescript * // Check if the 'age' field is greater than 18 - * gt(field("age"), Constant(9).add(9)); + * greaterThan(field("age"), Constant(9).add(9)); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the greater than comparison. */ -export function gt(left: Expr, right: Expr): BooleanExpr; +export function greaterThan( + left: Expression, + right: Expression +): BooleanExpression; /** * @beta @@ -4151,14 +4184,17 @@ export function gt(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'age' field is greater than 18 - * gt(field("age"), 18); + * greaterThan(field("age"), 18); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the greater than comparison. */ -export function gt(expression: Expr, value: unknown): BooleanExpr; +export function greaterThan( + expression: Expression, + value: unknown +): BooleanExpression; /** * @beta @@ -4167,14 +4203,17 @@ export function gt(expression: Expr, value: unknown): BooleanExpr; * * ```typescript * // Check if the value of field 'age' is greater than the value of field 'limit' - * gt("age", field("limit")); + * greaterThan("age", field("limit")); * ``` * * @param fieldName The field name to compare. * @param expression The expression to compare to. * @return A new `Expr` representing the greater than comparison. */ -export function gt(fieldName: string, expression: Expr): BooleanExpr; +export function greaterThan( + fieldName: string, + expression: Expression +): BooleanExpression; /** * @beta @@ -4183,18 +4222,24 @@ export function gt(fieldName: string, expression: Expr): BooleanExpr; * * ```typescript * // Check if the 'price' field is greater than 100 - * gt("price", 100); + * greaterThan("price", 100); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the greater than comparison. */ -export function gt(fieldName: string, value: unknown): BooleanExpr; -export function gt(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function greaterThan( + fieldName: string, + value: unknown +): BooleanExpression; +export function greaterThan( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.gt(rightExpr); + return leftExpr.greaterThan(rightExpr); } /** @@ -4205,14 +4250,17 @@ export function gt(left: Expr | string, right: unknown): BooleanExpr { * * ```typescript * // Check if the 'quantity' field is greater than or equal to the field "threshold" - * gte(field("quantity"), field("threshold")); + * greaterThanOrEqual(field("quantity"), field("threshold")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(left: Expr, right: Expr): BooleanExpr; +export function greaterThanOrEqual( + left: Expression, + right: Expression +): BooleanExpression; /** * @beta @@ -4222,14 +4270,17 @@ export function gte(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'quantity' field is greater than or equal to 10 - * gte(field("quantity"), 10); + * greaterThanOrEqual(field("quantity"), 10); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(expression: Expr, value: unknown): BooleanExpr; +export function greaterThanOrEqual( + expression: Expression, + value: unknown +): BooleanExpression; /** * @beta @@ -4238,14 +4289,17 @@ export function gte(expression: Expr, value: unknown): BooleanExpr; * * ```typescript * // Check if the value of field 'age' is greater than or equal to the value of field 'limit' - * gte("age", field("limit")); + * greaterThanOrEqual("age", field("limit")); * ``` * * @param fieldName The field name to compare. * @param value The expression to compare to. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(fieldName: string, value: Expr): BooleanExpr; +export function greaterThanOrEqual( + fieldName: string, + value: Expression +): BooleanExpression; /** * @beta @@ -4255,18 +4309,24 @@ export function gte(fieldName: string, value: Expr): BooleanExpr; * * ```typescript * // Check if the 'score' field is greater than or equal to 80 - * gte("score", 80); + * greaterThanOrEqual("score", 80); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(fieldName: string, value: unknown): BooleanExpr; -export function gte(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function greaterThanOrEqual( + fieldName: string, + value: unknown +): BooleanExpression; +export function greaterThanOrEqual( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.gte(rightExpr); + return leftExpr.greaterThanOrEqual(rightExpr); } /** @@ -4285,10 +4345,10 @@ export function gte(left: Expr | string, right: unknown): BooleanExpr { * @return A new {@code Expr} representing the concatenated array. */ export function arrayConcat( - firstArray: Expr, - secondArray: Expr | unknown[], - ...otherArrays: Array -): FunctionExpr; + firstArray: Expression, + secondArray: Expression | unknown[], + ...otherArrays: Array +): FunctionExpression; /** * @beta @@ -4307,15 +4367,15 @@ export function arrayConcat( */ export function arrayConcat( firstArrayField: string, - secondArray: Expr | unknown[], - ...otherArrays: Array -): FunctionExpr; + secondArray: Expression | unknown[], + ...otherArrays: Array +): FunctionExpression; export function arrayConcat( - firstArray: Expr | string, - secondArray: Expr | unknown[], - ...otherArrays: Array -): FunctionExpr { + firstArray: Expression | string, + secondArray: Expression | unknown[], + ...otherArrays: Array +): FunctionExpression { const exprValues = otherArrays.map(element => valueToDefaultExpr(element)); return fieldOrExpression(firstArray).arrayConcat( fieldOrExpression(secondArray), @@ -4337,7 +4397,10 @@ export function arrayConcat( * @param element The element to search for in the array. * @return A new {@code Expr} representing the 'array_contains' comparison. */ -export function arrayContains(array: Expr, element: Expr): FunctionExpr; +export function arrayContains( + array: Expression, + element: Expression +): FunctionExpression; /** * @beta @@ -4353,7 +4416,10 @@ export function arrayContains(array: Expr, element: Expr): FunctionExpr; * @param element The element to search for in the array. * @return A new {@code Expr} representing the 'array_contains' comparison. */ -export function arrayContains(array: Expr, element: unknown): FunctionExpr; +export function arrayContains( + array: Expression, + element: unknown +): FunctionExpression; /** * @beta @@ -4369,7 +4435,10 @@ export function arrayContains(array: Expr, element: unknown): FunctionExpr; * @param element The element to search for in the array. * @return A new {@code Expr} representing the 'array_contains' comparison. */ -export function arrayContains(fieldName: string, element: Expr): FunctionExpr; +export function arrayContains( + fieldName: string, + element: Expression +): FunctionExpression; /** * @beta @@ -4385,11 +4454,14 @@ export function arrayContains(fieldName: string, element: Expr): FunctionExpr; * @param element The element to search for in the array. * @return A new {@code Expr} representing the 'array_contains' comparison. */ -export function arrayContains(fieldName: string, element: unknown): BooleanExpr; export function arrayContains( - array: Expr | string, + fieldName: string, + element: unknown +): BooleanExpression; +export function arrayContains( + array: Expression | string, element: unknown -): BooleanExpr { +): BooleanExpression { const arrayExpr = fieldOrExpression(array); const elementExpr = valueToDefaultExpr(element); return arrayExpr.arrayContains(elementExpr); @@ -4411,9 +4483,9 @@ export function arrayContains( * @return A new {@code Expr} representing the 'array_contains_any' comparison. */ export function arrayContainsAny( - array: Expr, - values: Array -): BooleanExpr; + array: Expression, + values: Array +): BooleanExpression; /** * @beta @@ -4433,8 +4505,8 @@ export function arrayContainsAny( */ export function arrayContainsAny( fieldName: string, - values: Array -): BooleanExpr; + values: Array +): BooleanExpression; /** * @beta @@ -4451,7 +4523,10 @@ export function arrayContainsAny( * @param values An expression that evaluates to an array, whose elements to check for in the array. * @return A new {@code Expr} representing the 'array_contains_any' comparison. */ -export function arrayContainsAny(array: Expr, values: Expr): BooleanExpr; +export function arrayContainsAny( + array: Expression, + values: Expression +): BooleanExpression; /** * @beta @@ -4469,11 +4544,14 @@ export function arrayContainsAny(array: Expr, values: Expr): BooleanExpr; * @param values An expression that evaluates to an array, whose elements to check for in the array field. * @return A new {@code Expr} representing the 'array_contains_any' comparison. */ -export function arrayContainsAny(fieldName: string, values: Expr): BooleanExpr; export function arrayContainsAny( - array: Expr | string, - values: unknown[] | Expr -): BooleanExpr { + fieldName: string, + values: Expression +): BooleanExpression; +export function arrayContainsAny( + array: Expression | string, + values: unknown[] | Expression +): BooleanExpression { // @ts-ignore implementation accepts both types return fieldOrExpression(array).arrayContainsAny(values); } @@ -4493,9 +4571,9 @@ export function arrayContainsAny( * @return A new {@code Expr} representing the 'array_contains_all' comparison. */ export function arrayContainsAll( - array: Expr, - values: Array -): BooleanExpr; + array: Expression, + values: Array +): BooleanExpression; /** * @beta @@ -4514,8 +4592,8 @@ export function arrayContainsAll( */ export function arrayContainsAll( fieldName: string, - values: Array -): BooleanExpr; + values: Array +): BooleanExpression; /** * @beta @@ -4532,9 +4610,9 @@ export function arrayContainsAll( * @return A new {@code Expr} representing the 'array_contains_all' comparison. */ export function arrayContainsAll( - array: Expr, - arrayExpression: Expr -): BooleanExpr; + array: Expression, + arrayExpression: Expression +): BooleanExpression; /** * @beta @@ -4553,12 +4631,12 @@ export function arrayContainsAll( */ export function arrayContainsAll( fieldName: string, - arrayExpression: Expr -): BooleanExpr; + arrayExpression: Expression +): BooleanExpression; export function arrayContainsAll( - array: Expr | string, - values: unknown[] | Expr -): BooleanExpr { + array: Expression | string, + values: unknown[] | Expression +): BooleanExpression { // @ts-ignore implementation accepts both types return fieldOrExpression(array).arrayContainsAll(values); } @@ -4576,7 +4654,7 @@ export function arrayContainsAll( * @param fieldName The name of the field containing an array to calculate the length of. * @return A new {@code Expr} representing the length of the array. */ -export function arrayLength(fieldName: string): FunctionExpr; +export function arrayLength(fieldName: string): FunctionExpression; /** * @beta @@ -4591,8 +4669,8 @@ export function arrayLength(fieldName: string): FunctionExpr; * @param array The array expression to calculate the length of. * @return A new {@code Expr} representing the length of the array. */ -export function arrayLength(array: Expr): FunctionExpr; -export function arrayLength(array: Expr | string): FunctionExpr { +export function arrayLength(array: Expression): FunctionExpression; +export function arrayLength(array: Expression | string): FunctionExpression { return fieldOrExpression(array).arrayLength(); } @@ -4604,17 +4682,17 @@ export function arrayLength(array: Expr | string): FunctionExpr { * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * eqAny(field("category"), [constant("Electronics"), field("primaryType")]); + * equalAny(field("category"), [constant("Electronics"), field("primaryType")]); * ``` * * @param expression The expression whose results to compare. * @param values The values to check against. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function eqAny( - expression: Expr, - values: Array -): BooleanExpr; +export function equalAny( + expression: Expression, + values: Array +): BooleanExpression; /** * @beta @@ -4623,14 +4701,17 @@ export function eqAny( * * ```typescript * // Check if the 'category' field is set to a value in the disabledCategories field - * eqAny(field("category"), field('disabledCategories')); + * equalAny(field("category"), field('disabledCategories')); * ``` * * @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 {@code Expr} representing the 'IN' comparison. */ -export function eqAny(expression: Expr, arrayExpression: Expr): BooleanExpr; +export function equalAny( + expression: Expression, + arrayExpression: Expression +): BooleanExpression; /** * @beta @@ -4640,17 +4721,17 @@ export function eqAny(expression: Expr, arrayExpression: Expr): BooleanExpr; * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * eqAny("category", [constant("Electronics"), field("primaryType")]); + * equalAny("category", [constant("Electronics"), field("primaryType")]); * ``` * * @param fieldName The field to compare. * @param values The values to check against. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function eqAny( +export function equalAny( fieldName: string, - values: Array -): BooleanExpr; + values: Array +): BooleanExpression; /** * @beta @@ -4660,20 +4741,23 @@ export function eqAny( * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * eqAny("category", ["Electronics", field("primaryType")]); + * equalAny("category", ["Electronics", field("primaryType")]); * ``` * * @param fieldName The field to compare. * @param arrayExpression An expression that evaluates to an array, whose elements to check for equality to the input field. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function eqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; -export function eqAny( - element: Expr | string, - values: unknown[] | Expr -): BooleanExpr { +export function equalAny( + fieldName: string, + arrayExpression: Expression +): BooleanExpression; +export function equalAny( + element: Expression | string, + values: unknown[] | Expression +): BooleanExpression { // @ts-ignore implementation accepts both types - return fieldOrExpression(element).eqAny(values); + return fieldOrExpression(element).equalAny(values); } /** @@ -4684,17 +4768,17 @@ export function eqAny( * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notEqAny(field("status"), ["pending", field("rejectedStatus")]); + * notEqualAny(field("status"), ["pending", field("rejectedStatus")]); * ``` * * @param element The expression to compare. * @param values The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny( - element: Expr, - values: Array -): BooleanExpr; +export function notEqualAny( + element: Expression, + values: Array +): BooleanExpression; /** * @beta @@ -4704,17 +4788,17 @@ export function notEqAny( * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notEqAny("status", [constant("pending"), field("rejectedStatus")]); + * notEqualAny("status", [constant("pending"), field("rejectedStatus")]); * ``` * * @param fieldName The field name to compare. * @param values The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny( +export function notEqualAny( fieldName: string, - values: Array -): BooleanExpr; + values: Array +): BooleanExpression; /** * @beta @@ -4724,14 +4808,17 @@ export function notEqAny( * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of the field 'rejectedStatus' - * notEqAny(field("status"), ["pending", field("rejectedStatus")]); + * notEqualAny(field("status"), ["pending", field("rejectedStatus")]); * ``` * * @param element The expression to compare. * @param arrayExpression The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny(element: Expr, arrayExpression: Expr): BooleanExpr; +export function notEqualAny( + element: Expression, + arrayExpression: Expression +): BooleanExpression; /** * @beta @@ -4740,21 +4827,24 @@ export function notEqAny(element: Expr, arrayExpression: Expr): BooleanExpr; * * ```typescript * // Check if the 'status' field is not equal to any value in the field 'rejectedStatuses' - * notEqAny("status", field("rejectedStatuses")); + * notEqualAny("status", field("rejectedStatuses")); * ``` * * @param fieldName The field name to compare. * @param arrayExpression The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; +export function notEqualAny( + fieldName: string, + arrayExpression: Expression +): BooleanExpression; -export function notEqAny( - element: Expr | string, - values: unknown[] | Expr -): BooleanExpr { +export function notEqualAny( + element: Expression | string, + values: unknown[] | Expression +): BooleanExpression { // @ts-ignore implementation accepts both types - return fieldOrExpression(element).notEqAny(values); + return fieldOrExpression(element).notEqualAny(values); } /** @@ -4766,9 +4856,9 @@ export function notEqAny( * // Check if only one of the conditions is true: 'age' greater than 18, 'city' is "London", * // or 'status' is "active". * const condition = xor( - * gt("age", 18), - * eq("city", "London"), - * eq("status", "active")); + * greaterThan("age", 18), + * equal("city", "London"), + * equal("status", "active")); * ``` * * @param first The first condition. @@ -4777,11 +4867,15 @@ export function notEqAny( * @return A new {@code Expr} representing the logical 'XOR' operation. */ export function xor( - first: BooleanExpr, - second: BooleanExpr, - ...additionalConditions: BooleanExpr[] -): BooleanExpr { - return new BooleanExpr('xor', [first, second, ...additionalConditions]); + first: BooleanExpression, + second: BooleanExpression, + ...additionalConditions: BooleanExpression[] +): BooleanExpression { + return new BooleanExpression( + 'xor', + [first, second, ...additionalConditions], + 'xor' + ); } /** @@ -4792,8 +4886,8 @@ export function xor( * * ```typescript * // If 'age' is greater than 18, return "Adult"; otherwise, return "Minor". - * cond( - * gt("age", 18), constant("Adult"), constant("Minor")); + * conditional( + * greaterThan("age", 18), constant("Adult"), constant("Minor")); * ``` * * @param condition The condition to evaluate. @@ -4801,12 +4895,16 @@ export function xor( * @param elseExpr The expression to evaluate if the condition is false. * @return A new {@code Expr} representing the conditional expression. */ -export function cond( - condition: BooleanExpr, - thenExpr: Expr, - elseExpr: Expr -): FunctionExpr { - return new FunctionExpr('cond', [condition, thenExpr, elseExpr]); +export function conditional( + condition: BooleanExpression, + thenExpr: Expression, + elseExpr: Expression +): FunctionExpression { + return new FunctionExpression( + 'conditional', + [condition, thenExpr, elseExpr], + 'conditional' + ); } /** @@ -4816,13 +4914,13 @@ export function cond( * * ```typescript * // Find documents where the 'completed' field is NOT true - * not(eq("completed", true)); + * not(equal("completed", true)); * ``` * * @param booleanExpr The filter condition to negate. * @return A new {@code Expr} representing the negated filter condition. */ -export function not(booleanExpr: BooleanExpr): BooleanExpr { +export function not(booleanExpr: BooleanExpression): BooleanExpression { return booleanExpr.not(); } @@ -4841,13 +4939,13 @@ export function not(booleanExpr: BooleanExpr): BooleanExpr { * @param first The first operand expression. * @param second The second expression or literal. * @param others Optional additional expressions or literals. - * @return A new {@code Expr} representing the logical max operation. + * @return A new {@code Expr} representing the logical maximum operation. */ export function logicalMaximum( - first: Expr, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + first: Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression; /** * @beta @@ -4864,19 +4962,19 @@ export function logicalMaximum( * @param fieldName The first operand field name. * @param second The second expression or literal. * @param others Optional additional expressions or literals. - * @return A new {@code Expr} representing the logical max operation. + * @return A new {@code Expr} representing the logical maximum operation. */ export function logicalMaximum( fieldName: string, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + second: Expression | unknown, + ...others: Array +): FunctionExpression; export function logicalMaximum( - first: Expr | string, - second: Expr | unknown, - ...others: Array -): FunctionExpr { + first: Expression | string, + second: Expression | unknown, + ...others: Array +): FunctionExpression { return fieldOrExpression(first).logicalMaximum( valueToDefaultExpr(second), ...others.map(value => valueToDefaultExpr(value)) @@ -4898,13 +4996,13 @@ export function logicalMaximum( * @param first The first operand expression. * @param second The second expression or literal. * @param others Optional additional expressions or literals. - * @return A new {@code Expr} representing the logical min operation. + * @return A new {@code Expr} representing the logical minimum operation. */ export function logicalMinimum( - first: Expr, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + first: Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression; /** * @beta @@ -4922,19 +5020,19 @@ export function logicalMinimum( * @param fieldName The first operand field name. * @param second The second expression or literal. * @param others Optional additional expressions or literals. - * @return A new {@code Expr} representing the logical min operation. + * @return A new {@code Expr} representing the logical minimum operation. */ export function logicalMinimum( fieldName: string, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + second: Expression | unknown, + ...others: Array +): FunctionExpression; export function logicalMinimum( - first: Expr | string, - second: Expr | unknown, - ...others: Array -): FunctionExpr { + first: Expression | string, + second: Expression | unknown, + ...others: Array +): FunctionExpression { return fieldOrExpression(first).logicalMinimum( valueToDefaultExpr(second), ...others.map(value => valueToDefaultExpr(value)) @@ -4954,7 +5052,7 @@ export function logicalMinimum( * @param value An expression evaluates to the name of the field to check. * @return A new {@code Expr} representing the 'exists' check. */ -export function exists(value: Expr): BooleanExpr; +export function exists(value: Expression): BooleanExpression; /** * @beta @@ -4969,8 +5067,8 @@ export function exists(value: Expr): BooleanExpr; * @param fieldName The field name to check. * @return A new {@code Expr} representing the 'exists' check. */ -export function exists(fieldName: string): BooleanExpr; -export function exists(valueOrField: Expr | string): BooleanExpr { +export function exists(fieldName: string): BooleanExpression; +export function exists(valueOrField: Expression | string): BooleanExpression { return fieldOrExpression(valueOrField).exists(); } @@ -4987,7 +5085,7 @@ export function exists(valueOrField: Expr | string): BooleanExpr { * @param value The expression to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNan(value: Expr): BooleanExpr; +export function isNan(value: Expression): BooleanExpression; /** * @beta @@ -5002,8 +5100,8 @@ export function isNan(value: Expr): BooleanExpr; * @param fieldName The name of the field to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNan(fieldName: string): BooleanExpr; -export function isNan(value: Expr | string): BooleanExpr { +export function isNan(fieldName: string): BooleanExpression; +export function isNan(value: Expression | string): BooleanExpression { return fieldOrExpression(value).isNan(); } @@ -5020,7 +5118,7 @@ export function isNan(value: Expr | string): BooleanExpr { * @param stringExpression An expression evaluating to a string value, which will be reversed. * @return A new {@code Expr} representing the reversed string. */ -export function reverse(stringExpression: Expr): FunctionExpr; +export function reverse(stringExpression: Expression): FunctionExpression; /** * @beta @@ -5035,191 +5133,160 @@ export function reverse(stringExpression: Expr): FunctionExpr; * @param field The name of the field representing the string to reverse. * @return A new {@code Expr} representing the reversed string. */ -export function reverse(field: string): FunctionExpr; -export function reverse(expr: Expr | string): FunctionExpr { +export function reverse(field: string): FunctionExpression; +export function reverse(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).reverse(); } /** * @beta * - * Creates an expression that replaces the first occurrence of a substring within a string with another substring. + * Creates an expression that calculates the byte length of a string in UTF-8, or just the length of a Blob. * * ```typescript - * // Replace the first occurrence of "hello" with "hi" in the 'message' field. - * replaceFirst(field("message"), "hello", "hi"); + * // Calculate the length of the 'myString' field in bytes. + * byteLength(field("myString")); * ``` * - * @param value The expression representing the string to perform the replacement on. - * @param find The substring to search for. - * @param replace The substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. + * @param expr The expression representing the string. + * @return A new {@code Expr} representing the length of the string in bytes. */ -export function replaceFirst( - value: Expr, - find: string, - replace: string -): FunctionExpr; +export function byteLength(expr: Expression): FunctionExpression; /** * @beta * - * Creates an expression that replaces the first occurrence of a substring within a string with another substring, - * where the substring to find and the replacement substring are specified by expressions. + * 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. * * ```typescript - * // Replace the first occurrence of the value in 'findField' with the value in 'replaceField' in the 'message' field. - * replaceFirst(field("message"), field("findField"), field("replaceField")); + * // Calculate the length of the 'myString' field in bytes. + * byteLength("myString"); * ``` * - * @param value The expression representing the string to perform the replacement on. - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. + * @param fieldName The name of the field containing the string. + * @return A new {@code Expr} representing the length of the string in bytes. */ -export function replaceFirst( - value: Expr, - find: Expr, - replace: Expr -): FunctionExpr; +export function byteLength(fieldName: string): FunctionExpression; +export function byteLength(expr: Expression | string): FunctionExpression { + const normalizedExpr = fieldOrExpression(expr); + return normalizedExpr.byteLength(); +} /** - * @beta - * - * Creates an expression that replaces the first occurrence of a substring within a string represented by a field with another substring. + * Creates an expression that reverses an array. * * ```typescript - * // Replace the first occurrence of "hello" with "hi" in the 'message' field. - * replaceFirst("message", "hello", "hi"); + * // Reverse the value of the 'myArray' field. + * arrayReverse("myArray"); * ``` * - * @param fieldName The name of the field representing the string to perform the replacement on. - * @param find The substring to search for. - * @param replace The substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. + * @param fieldName The name of the field to reverse. + * @return A new {@code Expr} representing the reversed array. */ -export function replaceFirst( - fieldName: string, - find: string, - replace: string -): FunctionExpr; -export function replaceFirst( - value: Expr | string, - find: Expr | string, - replace: Expr | string -): FunctionExpr { - const normalizedValue = fieldOrExpression(value); - const normalizedFind = valueToDefaultExpr(find); - const normalizedReplace = valueToDefaultExpr(replace); - return normalizedValue.replaceFirst(normalizedFind, normalizedReplace); -} +export function arrayReverse(fieldName: string): FunctionExpression; /** - * @beta - * - * Creates an expression that replaces all occurrences of a substring within a string with another substring. + * Creates an expression that reverses an array. * * ```typescript - * // Replace all occurrences of "hello" with "hi" in the 'message' field. - * replaceAll(field("message"), "hello", "hi"); + * // Reverse the value of the 'myArray' field. + * arrayReverse(field("myArray")); * ``` * - * @param value The expression representing the string to perform the replacement on. - * @param find The substring to search for. - * @param replace The substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. + * @param arrayExpression An expression evaluating to an array value, which will be reversed. + * @return A new {@code Expr} representing the reversed array. */ -export function replaceAll( - value: Expr, - find: string, - replace: string -): FunctionExpr; +export function arrayReverse(arrayExpression: Expression): FunctionExpression; +export function arrayReverse(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).arrayReverse(); +} /** - * @beta - * - * Creates an expression that replaces all occurrences of a substring within a string with another substring, - * where the substring to find and the replacement substring are specified by expressions. + * Creates an expression that computes e to the power of the expression's result. * * ```typescript - * // Replace all occurrences of the value in 'findField' with the value in 'replaceField' in the 'message' field. - * replaceAll(field("message"), field("findField"), field("replaceField")); + * // Compute e to the power of 2. + * exp(constant(2)); * ``` * - * @param value The expression representing the string to perform the replacement on. - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. + * @return A new {@code Expr} representing the exp of the numeric value. */ -export function replaceAll( - value: Expr, - find: Expr, - replace: Expr -): FunctionExpr; +export function exp(expression: Expression): FunctionExpression; /** - * @beta - * - * Creates an expression that replaces all occurrences of a substring within a string represented by a field with another substring. + * Creates an expression that computes e to the power of the expression's result. * * ```typescript - * // Replace all occurrences of "hello" with "hi" in the 'message' field. - * replaceAll("message", "hello", "hi"); + * // Compute e to the power of the 'value' field. + * exp('value'); * ``` * - * @param fieldName The name of the field representing the string to perform the replacement on. - * @param find The substring to search for. - * @param replace The substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. + * @return A new {@code Expr} representing the exp of the numeric value. */ -export function replaceAll( - fieldName: string, - find: string, - replace: string -): FunctionExpr; -export function replaceAll( - value: Expr | string, - find: Expr | string, - replace: Expr | string -): FunctionExpr { - const normalizedValue = fieldOrExpression(value); - const normalizedFind = valueToDefaultExpr(find); - const normalizedReplace = valueToDefaultExpr(replace); - return normalizedValue.replaceAll(normalizedFind, normalizedReplace); +export function exp(fieldName: string): FunctionExpression; + +export function exp( + expressionOrFieldName: Expression | string +): FunctionExpression { + return fieldOrExpression(expressionOrFieldName).exp(); } /** - * @beta - * - * Creates an expression that calculates the byte length of a string in UTF-8, or just the length of a Blob. + * Creates an expression that computes the ceiling of a numeric value. * * ```typescript - * // Calculate the length of the 'myString' field in bytes. - * byteLength(field("myString")); + * // Compute the ceiling of the 'price' field. + * ceil("price"); * ``` * - * @param expr The expression representing the string. - * @return A new {@code Expr} representing the length of the string in bytes. + * @param fieldName The name of the field to compute the ceiling of. + * @return A new {@code Expr} representing the ceiling of the numeric value. */ -export function byteLength(expr: Expr): FunctionExpr; +export function ceil(fieldName: string): FunctionExpression; /** - * @beta - * - * 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. + * Creates an expression that computes the ceiling of a numeric value. * * ```typescript - * // Calculate the length of the 'myString' field in bytes. - * byteLength("myString"); + * // Compute the ceiling of the 'price' field. + * ceil(field("price")); * ``` * - * @param fieldName The name of the field containing the string. - * @return A new {@code Expr} representing the length of the string in bytes. + * @param expression An expression evaluating to a numeric value, which the ceiling will be computed for. + * @return A new {@code Expr} representing the ceiling of the numeric value. */ -export function byteLength(fieldName: string): FunctionExpr; -export function byteLength(expr: Expr | string): FunctionExpr { - const normalizedExpr = fieldOrExpression(expr); - return normalizedExpr.byteLength(); +export function ceil(expression: Expression): FunctionExpression; +export function ceil(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).ceil(); +} + +/** + * Creates an expression that computes the floor of a numeric value. + * + * @param expr The expression to compute the floor of. + * @return A new {@code Expr} representing the floor of the numeric value. + */ +export function floor(expr: Expression): FunctionExpression; + +/** + * Creates an expression that computes the floor of a numeric value. + * + * @param fieldName The name of the field to compute the floor of. + * @return A new {@code Expr} representing the floor of the numeric value. + */ +export function floor(fieldName: string): FunctionExpression; +export function floor(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).floor(); +} + +/** + * Creates an aggregation that counts the number of distinct values of a field. + * + * @param expr The expression or field to count distinct values of. + * @return A new `AggregateFunction` representing the 'count_distinct' aggregation. + */ +export function countDistinct(expr: Expression | string): AggregateFunction { + return fieldOrExpression(expr).countDistinct(); } /** @@ -5235,7 +5302,7 @@ export function byteLength(expr: Expr | string): FunctionExpr { * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the length of the string. */ -export function charLength(fieldName: string): FunctionExpr; +export function charLength(fieldName: string): FunctionExpression; /** * @beta @@ -5250,8 +5317,8 @@ export function charLength(fieldName: string): FunctionExpr; * @param stringExpression The expression representing the string to calculate the length of. * @return A new {@code Expr} representing the length of the string. */ -export function charLength(stringExpression: Expr): FunctionExpr; -export function charLength(value: Expr | string): FunctionExpr { +export function charLength(stringExpression: Expression): FunctionExpression; +export function charLength(value: Expression | string): FunctionExpression { const valueExpr = fieldOrExpression(value); return valueExpr.charLength(); } @@ -5271,7 +5338,7 @@ export function charLength(value: Expr | string): FunctionExpr { * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new {@code Expr} representing the 'like' comparison. */ -export function like(fieldName: string, pattern: string): BooleanExpr; +export function like(fieldName: string, pattern: string): BooleanExpression; /** * @beta @@ -5288,7 +5355,7 @@ export function like(fieldName: string, pattern: string): BooleanExpr; * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new {@code Expr} representing the 'like' comparison. */ -export function like(fieldName: string, pattern: Expr): BooleanExpr; +export function like(fieldName: string, pattern: Expression): BooleanExpression; /** * @beta @@ -5304,7 +5371,10 @@ export function like(fieldName: string, pattern: Expr): BooleanExpr; * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new {@code Expr} representing the 'like' comparison. */ -export function like(stringExpression: Expr, pattern: string): BooleanExpr; +export function like( + stringExpression: Expression, + pattern: string +): BooleanExpression; /** * @beta @@ -5320,11 +5390,14 @@ export function like(stringExpression: Expr, pattern: string): BooleanExpr; * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new {@code Expr} representing the 'like' comparison. */ -export function like(stringExpression: Expr, pattern: Expr): BooleanExpr; export function like( - left: Expr | string, - pattern: Expr | string -): FunctionExpr { + stringExpression: Expression, + pattern: Expression +): BooleanExpression; +export function like( + left: Expression | string, + pattern: Expression | string +): BooleanExpression { const leftExpr = fieldOrExpression(left); const patternExpr = valueToDefaultExpr(pattern); return leftExpr.like(patternExpr); @@ -5345,7 +5418,10 @@ export function like( * @param pattern The regular expression to use for the search. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function regexContains(fieldName: string, pattern: string): BooleanExpr; +export function regexContains( + fieldName: string, + pattern: string +): BooleanExpression; /** * @beta @@ -5362,7 +5438,10 @@ export function regexContains(fieldName: string, pattern: string): BooleanExpr; * @param pattern The regular expression to use for the search. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function regexContains(fieldName: string, pattern: Expr): BooleanExpr; +export function regexContains( + fieldName: string, + pattern: Expression +): BooleanExpression; /** * @beta @@ -5380,9 +5459,9 @@ export function regexContains(fieldName: string, pattern: Expr): BooleanExpr; * @return A new {@code Expr} representing the 'contains' comparison. */ export function regexContains( - stringExpression: Expr, + stringExpression: Expression, pattern: string -): BooleanExpr; +): BooleanExpression; /** * @beta @@ -5400,13 +5479,13 @@ export function regexContains( * @return A new {@code Expr} representing the 'contains' comparison. */ export function regexContains( - stringExpression: Expr, - pattern: Expr -): BooleanExpr; + stringExpression: Expression, + pattern: Expression +): BooleanExpression; export function regexContains( - left: Expr | string, - pattern: Expr | string -): BooleanExpr { + left: Expression | string, + pattern: Expression | string +): BooleanExpression { const leftExpr = fieldOrExpression(left); const patternExpr = valueToDefaultExpr(pattern); return leftExpr.regexContains(patternExpr); @@ -5426,7 +5505,10 @@ export function regexContains( * @param pattern The regular expression to use for the match. * @return A new {@code Expr} representing the regular expression match. */ -export function regexMatch(fieldName: string, pattern: string): BooleanExpr; +export function regexMatch( + fieldName: string, + pattern: string +): BooleanExpression; /** * @beta @@ -5442,7 +5524,10 @@ export function regexMatch(fieldName: string, pattern: string): BooleanExpr; * @param pattern The regular expression to use for the match. * @return A new {@code Expr} representing the regular expression match. */ -export function regexMatch(fieldName: string, pattern: Expr): BooleanExpr; +export function regexMatch( + fieldName: string, + pattern: Expression +): BooleanExpression; /** * @beta @@ -5460,9 +5545,9 @@ export function regexMatch(fieldName: string, pattern: Expr): BooleanExpr; * @return A new {@code Expr} representing the regular expression match. */ export function regexMatch( - stringExpression: Expr, + stringExpression: Expression, pattern: string -): BooleanExpr; +): BooleanExpression; /** * @beta @@ -5479,11 +5564,14 @@ export function regexMatch( * @param pattern The regular expression to use for the match. * @return A new {@code Expr} representing the regular expression match. */ -export function regexMatch(stringExpression: Expr, pattern: Expr): BooleanExpr; export function regexMatch( - left: Expr | string, - pattern: Expr | string -): BooleanExpr { + stringExpression: Expression, + pattern: Expression +): BooleanExpression; +export function regexMatch( + left: Expression | string, + pattern: Expression | string +): BooleanExpression { const leftExpr = fieldOrExpression(left); const patternExpr = valueToDefaultExpr(pattern); return leftExpr.regexMatch(patternExpr); @@ -5496,14 +5584,17 @@ export function regexMatch( * * ```typescript * // Check if the 'description' field contains "example". - * strContains("description", "example"); + * stringContains("description", "example"); * ``` * * @param fieldName The name of the field containing the string. * @param substring The substring to search for. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function strContains(fieldName: string, substring: string): BooleanExpr; +export function stringContains( + fieldName: string, + substring: string +): BooleanExpression; /** * @beta @@ -5512,14 +5603,17 @@ export function strContains(fieldName: string, substring: string): BooleanExpr; * * ```typescript * // Check if the 'description' field contains the value of the 'keyword' field. - * strContains("description", field("keyword")); + * stringContains("description", field("keyword")); * ``` * * @param fieldName The name of the field containing the string. * @param substring The expression representing the substring to search for. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function strContains(fieldName: string, substring: Expr): BooleanExpr; +export function stringContains( + fieldName: string, + substring: Expression +): BooleanExpression; /** * @beta @@ -5528,17 +5622,17 @@ export function strContains(fieldName: string, substring: Expr): BooleanExpr; * * ```typescript * // Check if the 'description' field contains "example". - * strContains(field("description"), "example"); + * stringContains(field("description"), "example"); * ``` * * @param stringExpression The expression representing the string to perform the comparison on. * @param substring The substring to search for. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function strContains( - stringExpression: Expr, +export function stringContains( + stringExpression: Expression, substring: string -): BooleanExpr; +): BooleanExpression; /** * @beta @@ -5547,24 +5641,24 @@ export function strContains( * * ```typescript * // Check if the 'description' field contains the value of the 'keyword' field. - * strContains(field("description"), field("keyword")); + * stringContains(field("description"), field("keyword")); * ``` * * @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 {@code Expr} representing the 'contains' comparison. */ -export function strContains( - stringExpression: Expr, - substring: Expr -): BooleanExpr; -export function strContains( - left: Expr | string, - substring: Expr | string -): BooleanExpr { +export function stringContains( + stringExpression: Expression, + substring: Expression +): BooleanExpression; +export function stringContains( + left: Expression | string, + substring: Expression | string +): BooleanExpression { const leftExpr = fieldOrExpression(left); const substringExpr = valueToDefaultExpr(substring); - return leftExpr.strContains(substringExpr); + return leftExpr.stringContains(substringExpr); } /** @@ -5581,7 +5675,10 @@ export function strContains( * @param prefix The prefix to check for. * @return A new {@code Expr} representing the 'starts with' comparison. */ -export function startsWith(fieldName: string, prefix: string): BooleanExpr; +export function startsWith( + fieldName: string, + prefix: string +): BooleanExpression; /** * @beta @@ -5597,7 +5694,10 @@ export function startsWith(fieldName: string, prefix: string): BooleanExpr; * @param prefix The expression representing the prefix. * @return A new {@code Expr} representing the 'starts with' comparison. */ -export function startsWith(fieldName: string, prefix: Expr): BooleanExpr; +export function startsWith( + fieldName: string, + prefix: Expression +): BooleanExpression; /** * @beta @@ -5613,7 +5713,10 @@ export function startsWith(fieldName: string, prefix: Expr): BooleanExpr; * @param prefix The prefix to check for. * @return A new {@code Expr} representing the 'starts with' comparison. */ -export function startsWith(stringExpression: Expr, prefix: string): BooleanExpr; +export function startsWith( + stringExpression: Expression, + prefix: string +): BooleanExpression; /** * @beta @@ -5629,11 +5732,14 @@ export function startsWith(stringExpression: Expr, prefix: string): BooleanExpr; * @param prefix The prefix to check for. * @return A new {@code Expr} representing the 'starts with' comparison. */ -export function startsWith(stringExpression: Expr, prefix: Expr): BooleanExpr; export function startsWith( - expr: Expr | string, - prefix: Expr | string -): BooleanExpr { + stringExpression: Expression, + prefix: Expression +): BooleanExpression; +export function startsWith( + expr: Expression | string, + prefix: Expression | string +): BooleanExpression { return fieldOrExpression(expr).startsWith(valueToDefaultExpr(prefix)); } @@ -5651,7 +5757,7 @@ export function startsWith( * @param suffix The postfix to check for. * @return A new {@code Expr} representing the 'ends with' comparison. */ -export function endsWith(fieldName: string, suffix: string): BooleanExpr; +export function endsWith(fieldName: string, suffix: string): BooleanExpression; /** * @beta @@ -5667,7 +5773,10 @@ export function endsWith(fieldName: string, suffix: string): BooleanExpr; * @param suffix The expression representing the postfix. * @return A new {@code Expr} representing the 'ends with' comparison. */ -export function endsWith(fieldName: string, suffix: Expr): BooleanExpr; +export function endsWith( + fieldName: string, + suffix: Expression +): BooleanExpression; /** * @beta @@ -5683,7 +5792,10 @@ export function endsWith(fieldName: string, suffix: Expr): BooleanExpr; * @param suffix The postfix to check for. * @return A new {@code Expr} representing the 'ends with' comparison. */ -export function endsWith(stringExpression: Expr, suffix: string): BooleanExpr; +export function endsWith( + stringExpression: Expression, + suffix: string +): BooleanExpression; /** * @beta @@ -5699,11 +5811,14 @@ export function endsWith(stringExpression: Expr, suffix: string): BooleanExpr; * @param suffix The postfix to check for. * @return A new {@code Expr} representing the 'ends with' comparison. */ -export function endsWith(stringExpression: Expr, suffix: Expr): BooleanExpr; export function endsWith( - expr: Expr | string, - suffix: Expr | string -): BooleanExpr { + stringExpression: Expression, + suffix: Expression +): BooleanExpression; +export function endsWith( + expr: Expression | string, + suffix: Expression | string +): BooleanExpression { return fieldOrExpression(expr).endsWith(valueToDefaultExpr(suffix)); } @@ -5720,7 +5835,7 @@ export function endsWith( * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the lowercase string. */ -export function toLower(fieldName: string): FunctionExpr; +export function toLower(fieldName: string): FunctionExpression; /** * @beta @@ -5735,8 +5850,8 @@ export function toLower(fieldName: string): FunctionExpr; * @param stringExpression The expression representing the string to convert to lowercase. * @return A new {@code Expr} representing the lowercase string. */ -export function toLower(stringExpression: Expr): FunctionExpr; -export function toLower(expr: Expr | string): FunctionExpr { +export function toLower(stringExpression: Expression): FunctionExpression; +export function toLower(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).toLower(); } @@ -5753,7 +5868,7 @@ export function toLower(expr: Expr | string): FunctionExpr { * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the uppercase string. */ -export function toUpper(fieldName: string): FunctionExpr; +export function toUpper(fieldName: string): FunctionExpression; /** * @beta @@ -5768,8 +5883,8 @@ export function toUpper(fieldName: string): FunctionExpr; * @param stringExpression The expression representing the string to convert to uppercase. * @return A new {@code Expr} representing the uppercase string. */ -export function toUpper(stringExpression: Expr): FunctionExpr; -export function toUpper(expr: Expr | string): FunctionExpr { +export function toUpper(stringExpression: Expression): FunctionExpression; +export function toUpper(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).toUpper(); } @@ -5786,7 +5901,7 @@ export function toUpper(expr: Expr | string): FunctionExpr { * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the trimmed string. */ -export function trim(fieldName: string): FunctionExpr; +export function trim(fieldName: string): FunctionExpression; /** * @beta @@ -5801,8 +5916,8 @@ export function trim(fieldName: string): FunctionExpr; * @param stringExpression The expression representing the string to trim. * @return A new {@code Expr} representing the trimmed string. */ -export function trim(stringExpression: Expr): FunctionExpr; -export function trim(expr: Expr | string): FunctionExpr { +export function trim(stringExpression: Expression): FunctionExpression; +export function trim(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).trim(); } @@ -5813,7 +5928,7 @@ export function trim(expr: Expr | string): FunctionExpr { * * ```typescript * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * strConcat("firstName", " ", field("lastName")); + * stringConcat("firstName", " ", field("lastName")); * ``` * * @param fieldName The field name containing the initial string value. @@ -5821,11 +5936,11 @@ export function trim(expr: Expr | string): FunctionExpr { * @param otherStrings Optional additional expressions or literals (typically strings) to concatenate. * @return A new {@code Expr} representing the concatenated string. */ -export function strConcat( +export function stringConcat( fieldName: string, - secondString: Expr | string, - ...otherStrings: Array -): FunctionExpr; + secondString: Expression | string, + ...otherStrings: Array +): FunctionExpression; /** * @beta @@ -5833,7 +5948,7 @@ export function strConcat( * * ```typescript * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * strConcat(field("firstName"), " ", field("lastName")); + * stringConcat(field("firstName"), " ", field("lastName")); * ``` * * @param firstString The initial string expression to concatenate to. @@ -5841,17 +5956,17 @@ export function strConcat( * @param otherStrings Optional additional expressions or literals (typically strings) to concatenate. * @return A new {@code Expr} representing the concatenated string. */ -export function strConcat( - firstString: Expr, - secondString: Expr | string, - ...otherStrings: Array -): FunctionExpr; -export function strConcat( - first: string | Expr, - second: string | Expr, - ...elements: Array -): FunctionExpr { - return fieldOrExpression(first).strConcat( +export function stringConcat( + firstString: Expression, + secondString: Expression | string, + ...otherStrings: Array +): FunctionExpression; +export function stringConcat( + first: string | Expression, + second: string | Expression, + ...elements: Array +): FunctionExpression { + return fieldOrExpression(first).stringConcat( valueToDefaultExpr(second), ...elements.map(valueToDefaultExpr) ); @@ -5871,7 +5986,7 @@ export function strConcat( * @param subField The key to access in the map. * @return A new {@code Expr} representing the value associated with the given key in the map. */ -export function mapGet(fieldName: string, subField: string): FunctionExpr; +export function mapGet(fieldName: string, subField: string): FunctionExpression; /** * @beta @@ -5887,11 +6002,14 @@ export function mapGet(fieldName: string, subField: string): FunctionExpr; * @param subField The key to access in the map. * @return A new {@code Expr} representing the value associated with the given key in the map. */ -export function mapGet(mapExpression: Expr, subField: string): FunctionExpr; export function mapGet( - fieldOrExpr: string | Expr, + mapExpression: Expression, + subField: string +): FunctionExpression; +export function mapGet( + fieldOrExpr: string | Expression, subField: string -): FunctionExpr { +): FunctionExpression { return fieldOrExpression(fieldOrExpr).mapGet(subField); } @@ -5908,7 +6026,7 @@ export function mapGet( * @return A new {@code AggregateFunction} representing the 'countAll' aggregation. */ export function countAll(): AggregateFunction { - return new AggregateFunction('count', []); + return new AggregateFunction('count', [], 'count'); } /** @@ -5919,13 +6037,13 @@ export function countAll(): AggregateFunction { * * ```typescript * // Count the number of items where the price is greater than 10 - * count(field("price").gt(10)).as("expensiveItemCount"); + * count(field("price").greaterThan(10)).as("expensiveItemCount"); * ``` * * @param expression The expression to count. * @return A new {@code AggregateFunction} representing the 'count' aggregation. */ -export function count(expression: Expr): AggregateFunction; +export function count(expression: Expression): AggregateFunction; /** * Creates an aggregation that counts the number of stage inputs where the input field exists. @@ -5939,7 +6057,7 @@ export function count(expression: Expr): AggregateFunction; * @return A new {@code AggregateFunction} representing the 'count' aggregation. */ export function count(fieldName: string): AggregateFunction; -export function count(value: Expr | string): AggregateFunction { +export function count(value: Expression | string): AggregateFunction { return fieldOrExpression(value).count(); } @@ -5957,7 +6075,7 @@ export function count(value: Expr | string): AggregateFunction { * @param expression The expression to sum up. * @return A new {@code AggregateFunction} representing the 'sum' aggregation. */ -export function sum(expression: Expr): AggregateFunction; +export function sum(expression: Expression): AggregateFunction; /** * @beta @@ -5974,7 +6092,7 @@ export function sum(expression: Expr): AggregateFunction; * @return A new {@code AggregateFunction} representing the 'sum' aggregation. */ export function sum(fieldName: string): AggregateFunction; -export function sum(value: Expr | string): AggregateFunction { +export function sum(value: Expression | string): AggregateFunction { return fieldOrExpression(value).sum(); } @@ -5986,13 +6104,13 @@ export function sum(value: Expr | string): AggregateFunction { * * ```typescript * // Calculate the average age of users - * avg(field("age")).as("averageAge"); + * average(field("age")).as("averageAge"); * ``` * * @param expression The expression representing the values to average. - * @return A new {@code AggregateFunction} representing the 'avg' aggregation. + * @return A new {@code AggregateFunction} representing the 'average' aggregation. */ -export function avg(expression: Expr): AggregateFunction; +export function average(expression: Expression): AggregateFunction; /** * @beta @@ -6002,15 +6120,15 @@ export function avg(expression: Expr): AggregateFunction; * * ```typescript * // Calculate the average age of users - * avg("age").as("averageAge"); + * average("age").as("averageAge"); * ``` * * @param fieldName The name of the field containing numeric values to average. - * @return A new {@code AggregateFunction} representing the 'avg' aggregation. + * @return A new {@code AggregateFunction} representing the 'average' aggregation. */ -export function avg(fieldName: string): AggregateFunction; -export function avg(value: Expr | string): AggregateFunction { - return fieldOrExpression(value).avg(); +export function average(fieldName: string): AggregateFunction; +export function average(value: Expression | string): AggregateFunction { + return fieldOrExpression(value).average(); } /** @@ -6025,9 +6143,9 @@ export function avg(value: Expr | string): AggregateFunction { * ``` * * @param expression The expression to find the minimum value of. - * @return A new {@code AggregateFunction} representing the 'min' aggregation. + * @return A new {@code AggregateFunction} representing the 'minimum' aggregation. */ -export function minimum(expression: Expr): AggregateFunction; +export function minimum(expression: Expression): AggregateFunction; /** * @beta @@ -6040,10 +6158,10 @@ export function minimum(expression: Expr): AggregateFunction; * ``` * * @param fieldName The name of the field to find the minimum value of. - * @return A new {@code AggregateFunction} representing the 'min' aggregation. + * @return A new {@code AggregateFunction} representing the 'minimum' aggregation. */ export function minimum(fieldName: string): AggregateFunction; -export function minimum(value: Expr | string): AggregateFunction { +export function minimum(value: Expression | string): AggregateFunction { return fieldOrExpression(value).minimum(); } @@ -6059,9 +6177,9 @@ export function minimum(value: Expr | string): AggregateFunction { * ``` * * @param expression The expression to find the maximum value of. - * @return A new {@code AggregateFunction} representing the 'max' aggregation. + * @return A new {@code AggregateFunction} representing the 'maximum' aggregation. */ -export function maximum(expression: Expr): AggregateFunction; +export function maximum(expression: Expression): AggregateFunction; /** * @beta @@ -6074,10 +6192,10 @@ export function maximum(expression: Expr): AggregateFunction; * ``` * * @param fieldName The name of the field to find the maximum value of. - * @return A new {@code AggregateFunction} representing the 'max' aggregation. + * @return A new {@code AggregateFunction} representing the 'maximum' aggregation. */ export function maximum(fieldName: string): AggregateFunction; -export function maximum(value: Expr | string): AggregateFunction { +export function maximum(value: Expression | string): AggregateFunction { return fieldOrExpression(value).maximum(); } @@ -6098,7 +6216,7 @@ export function maximum(value: Expr | string): AggregateFunction { export function cosineDistance( fieldName: string, vector: number[] | VectorValue -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6116,8 +6234,8 @@ export function cosineDistance( */ export function cosineDistance( fieldName: string, - vectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression +): FunctionExpression; /** * @beta @@ -6134,9 +6252,9 @@ export function cosineDistance( * @return A new {@code Expr} representing the cosine distance between the two vectors. */ export function cosineDistance( - vectorExpression: Expr, - vector: number[] | Expr -): FunctionExpr; + vectorExpression: Expression, + vector: number[] | VectorValue +): FunctionExpression; /** * @beta @@ -6153,13 +6271,13 @@ export function cosineDistance( * @return A new {@code Expr} representing the cosine distance between the two vectors. */ export function cosineDistance( - vectorExpression: Expr, - otherVectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression, + otherVectorExpression: Expression +): FunctionExpression; export function cosineDistance( - expr: Expr | string, - other: Expr | number[] | VectorValue -): FunctionExpr { + expr: Expression | string, + other: Expression | number[] | VectorValue +): FunctionExpression { const expr1 = fieldOrExpression(expr); const expr2 = vectorToExpr(other); return expr1.cosineDistance(expr2); @@ -6182,7 +6300,7 @@ export function cosineDistance( export function dotProduct( fieldName: string, vector: number[] | VectorValue -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6200,8 +6318,8 @@ export function dotProduct( */ export function dotProduct( fieldName: string, - vectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression +): FunctionExpression; /** * @beta @@ -6218,9 +6336,9 @@ export function dotProduct( * @return A new {@code Expr} representing the dot product between the two vectors. */ export function dotProduct( - vectorExpression: Expr, + vectorExpression: Expression, vector: number[] | VectorValue -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6237,13 +6355,13 @@ export function dotProduct( * @return A new {@code Expr} representing the dot product between the two vectors. */ export function dotProduct( - vectorExpression: Expr, - otherVectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression, + otherVectorExpression: Expression +): FunctionExpression; export function dotProduct( - expr: Expr | string, - other: Expr | number[] | VectorValue -): FunctionExpr { + expr: Expression | string, + other: Expression | number[] | VectorValue +): FunctionExpression { const expr1 = fieldOrExpression(expr); const expr2 = vectorToExpr(other); return expr1.dotProduct(expr2); @@ -6266,7 +6384,7 @@ export function dotProduct( export function euclideanDistance( fieldName: string, vector: number[] | VectorValue -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6284,8 +6402,8 @@ export function euclideanDistance( */ export function euclideanDistance( fieldName: string, - vectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression +): FunctionExpression; /** * @beta @@ -6303,9 +6421,9 @@ export function euclideanDistance( * @return A new {@code Expr} representing the Euclidean distance between the two vectors. */ export function euclideanDistance( - vectorExpression: Expr, + vectorExpression: Expression, vector: number[] | VectorValue -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6322,13 +6440,13 @@ export function euclideanDistance( * @return A new {@code Expr} representing the Euclidean distance between the two vectors. */ export function euclideanDistance( - vectorExpression: Expr, - otherVectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression, + otherVectorExpression: Expression +): FunctionExpression; export function euclideanDistance( - expr: Expr | string, - other: Expr | number[] | VectorValue -): FunctionExpr { + expr: Expression | string, + other: Expression | number[] | VectorValue +): FunctionExpression { const expr1 = fieldOrExpression(expr); const expr2 = vectorToExpr(other); return expr1.euclideanDistance(expr2); @@ -6347,7 +6465,7 @@ export function euclideanDistance( * @param vectorExpression The expression representing the Firestore Vector. * @return A new {@code Expr} representing the length of the array. */ -export function vectorLength(vectorExpression: Expr): FunctionExpr; +export function vectorLength(vectorExpression: Expression): FunctionExpression; /** * @beta @@ -6362,8 +6480,8 @@ export function vectorLength(vectorExpression: Expr): FunctionExpr; * @param fieldName The name of the field representing the Firestore Vector. * @return A new {@code Expr} representing the length of the array. */ -export function vectorLength(fieldName: string): FunctionExpr; -export function vectorLength(expr: Expr | string): FunctionExpr { +export function vectorLength(fieldName: string): FunctionExpression; +export function vectorLength(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).vectorLength(); } @@ -6381,7 +6499,7 @@ export function vectorLength(expr: Expr | string): FunctionExpr { * @param expr The expression representing the number of microseconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixMicrosToTimestamp(expr: Expr): FunctionExpr; +export function unixMicrosToTimestamp(expr: Expression): FunctionExpression; /** * @beta @@ -6397,8 +6515,10 @@ export function unixMicrosToTimestamp(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the number of microseconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixMicrosToTimestamp(fieldName: string): FunctionExpr; -export function unixMicrosToTimestamp(expr: Expr | string): FunctionExpr { +export function unixMicrosToTimestamp(fieldName: string): FunctionExpression; +export function unixMicrosToTimestamp( + expr: Expression | string +): FunctionExpression { return fieldOrExpression(expr).unixMicrosToTimestamp(); } @@ -6415,7 +6535,7 @@ export function unixMicrosToTimestamp(expr: Expr | string): FunctionExpr { * @param expr The expression representing the timestamp. * @return A new {@code Expr} representing the number of microseconds since epoch. */ -export function timestampToUnixMicros(expr: Expr): FunctionExpr; +export function timestampToUnixMicros(expr: Expression): FunctionExpression; /** * @beta @@ -6430,8 +6550,10 @@ export function timestampToUnixMicros(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the timestamp. * @return A new {@code Expr} representing the number of microseconds since epoch. */ -export function timestampToUnixMicros(fieldName: string): FunctionExpr; -export function timestampToUnixMicros(expr: Expr | string): FunctionExpr { +export function timestampToUnixMicros(fieldName: string): FunctionExpression; +export function timestampToUnixMicros( + expr: Expression | string +): FunctionExpression { return fieldOrExpression(expr).timestampToUnixMicros(); } @@ -6449,7 +6571,7 @@ export function timestampToUnixMicros(expr: Expr | string): FunctionExpr { * @param expr The expression representing the number of milliseconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixMillisToTimestamp(expr: Expr): FunctionExpr; +export function unixMillisToTimestamp(expr: Expression): FunctionExpression; /** * @beta @@ -6465,8 +6587,10 @@ export function unixMillisToTimestamp(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the number of milliseconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixMillisToTimestamp(fieldName: string): FunctionExpr; -export function unixMillisToTimestamp(expr: Expr | string): FunctionExpr { +export function unixMillisToTimestamp(fieldName: string): FunctionExpression; +export function unixMillisToTimestamp( + expr: Expression | string +): FunctionExpression { const normalizedExpr = fieldOrExpression(expr); return normalizedExpr.unixMillisToTimestamp(); } @@ -6484,7 +6608,7 @@ export function unixMillisToTimestamp(expr: Expr | string): FunctionExpr { * @param expr The expression representing the timestamp. * @return A new {@code Expr} representing the number of milliseconds since epoch. */ -export function timestampToUnixMillis(expr: Expr): FunctionExpr; +export function timestampToUnixMillis(expr: Expression): FunctionExpression; /** * @beta @@ -6499,8 +6623,10 @@ export function timestampToUnixMillis(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the timestamp. * @return A new {@code Expr} representing the number of milliseconds since epoch. */ -export function timestampToUnixMillis(fieldName: string): FunctionExpr; -export function timestampToUnixMillis(expr: Expr | string): FunctionExpr { +export function timestampToUnixMillis(fieldName: string): FunctionExpression; +export function timestampToUnixMillis( + expr: Expression | string +): FunctionExpression { const normalizedExpr = fieldOrExpression(expr); return normalizedExpr.timestampToUnixMillis(); } @@ -6519,7 +6645,7 @@ export function timestampToUnixMillis(expr: Expr | string): FunctionExpr { * @param expr The expression representing the number of seconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixSecondsToTimestamp(expr: Expr): FunctionExpr; +export function unixSecondsToTimestamp(expr: Expression): FunctionExpression; /** * @beta @@ -6535,8 +6661,10 @@ export function unixSecondsToTimestamp(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the number of seconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixSecondsToTimestamp(fieldName: string): FunctionExpr; -export function unixSecondsToTimestamp(expr: Expr | string): FunctionExpr { +export function unixSecondsToTimestamp(fieldName: string): FunctionExpression; +export function unixSecondsToTimestamp( + expr: Expression | string +): FunctionExpression { const normalizedExpr = fieldOrExpression(expr); return normalizedExpr.unixSecondsToTimestamp(); } @@ -6554,7 +6682,7 @@ export function unixSecondsToTimestamp(expr: Expr | string): FunctionExpr { * @param expr The expression representing the timestamp. * @return A new {@code Expr} representing the number of seconds since epoch. */ -export function timestampToUnixSeconds(expr: Expr): FunctionExpr; +export function timestampToUnixSeconds(expr: Expression): FunctionExpression; /** * @beta @@ -6569,8 +6697,10 @@ export function timestampToUnixSeconds(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the timestamp. * @return A new {@code Expr} representing the number of seconds since epoch. */ -export function timestampToUnixSeconds(fieldName: string): FunctionExpr; -export function timestampToUnixSeconds(expr: Expr | string): FunctionExpr { +export function timestampToUnixSeconds(fieldName: string): FunctionExpression; +export function timestampToUnixSeconds( + expr: Expression | string +): FunctionExpression { const normalizedExpr = fieldOrExpression(expr); return normalizedExpr.timestampToUnixSeconds(); } @@ -6591,10 +6721,10 @@ export function timestampToUnixSeconds(expr: Expr | string): FunctionExpr { * @return A new {@code Expr} representing the resulting timestamp. */ export function timestampAdd( - timestamp: Expr, - unit: Expr, - amount: Expr -): FunctionExpr; + timestamp: Expression, + unit: Expression, + amount: Expression +): FunctionExpression; /** * @beta @@ -6612,10 +6742,10 @@ export function timestampAdd( * @return A new {@code Expr} representing the resulting timestamp. */ export function timestampAdd( - timestamp: Expr, + timestamp: Expression, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6636,19 +6766,19 @@ export function timestampAdd( fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): FunctionExpr; +): FunctionExpression; export function timestampAdd( - timestamp: Expr | string, + timestamp: Expression | string, unit: - | Expr + | Expression | 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: Expr | number -): FunctionExpr { + amount: Expression | number +): FunctionExpression { const normalizedTimestamp = fieldOrExpression(timestamp); const normalizedUnit = valueToDefaultExpr(unit); const normalizedAmount = valueToDefaultExpr(amount); @@ -6662,7 +6792,7 @@ export function timestampAdd( * * ```typescript * // Subtract some duration determined by field 'unit' and 'amount' from the 'timestamp' field. - * timestampSub(field("timestamp"), field("unit"), field("amount")); + * timestampSubtract(field("timestamp"), field("unit"), field("amount")); * ``` * * @param timestamp The expression representing the timestamp. @@ -6670,11 +6800,11 @@ export function timestampAdd( * @param amount The expression evaluates to amount of the unit. * @return A new {@code Expr} representing the resulting timestamp. */ -export function timestampSub( - timestamp: Expr, - unit: Expr, - amount: Expr -): FunctionExpr; +export function timestampSubtract( + timestamp: Expression, + unit: Expression, + amount: Expression +): FunctionExpression; /** * @beta @@ -6683,7 +6813,7 @@ export function timestampSub( * * ```typescript * // Subtract 1 day from the 'timestamp' field. - * timestampSub(field("timestamp"), "day", 1); + * timestampSubtract(field("timestamp"), "day", 1); * ``` * * @param timestamp The expression representing the timestamp. @@ -6691,11 +6821,11 @@ export function timestampSub( * @param amount The amount of time to subtract. * @return A new {@code Expr} representing the resulting timestamp. */ -export function timestampSub( - timestamp: Expr, +export function timestampSubtract( + timestamp: Expression, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6704,7 +6834,7 @@ export function timestampSub( * * ```typescript * // Subtract 1 day from the 'timestamp' field. - * timestampSub("timestamp", "day", 1); + * timestampSubtract("timestamp", "day", 1); * ``` * * @param fieldName The name of the field representing the timestamp. @@ -6712,27 +6842,65 @@ export function timestampSub( * @param amount The amount of time to subtract. * @return A new {@code Expr} representing the resulting timestamp. */ -export function timestampSub( +export function timestampSubtract( fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): FunctionExpr; -export function timestampSub( - timestamp: Expr | string, +): FunctionExpression; +export function timestampSubtract( + timestamp: Expression | string, unit: - | Expr + | Expression | 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: Expr | number -): FunctionExpr { + amount: Expression | number +): FunctionExpression { const normalizedTimestamp = fieldOrExpression(timestamp); const normalizedUnit = valueToDefaultExpr(unit); const normalizedAmount = valueToDefaultExpr(amount); - return normalizedTimestamp.timestampSub(normalizedUnit, normalizedAmount); + return normalizedTimestamp.timestampSubtract( + normalizedUnit, + normalizedAmount + ); +} + +/** + * @beta + * + * Creates an expression that evaluates to the current server timestamp. + * + * ```typescript + * // Get the current server timestamp + * currentTimestamp() + * ``` + * + * @return A new Expression representing the current server timestamp. + */ +export function currentTimestamp(): FunctionExpression { + return new FunctionExpression('current_timestamp', [], 'currentTimestamp'); +} + +/** + * Creates an expression that raises an error with the given message. This could be useful for + * debugging purposes. + * + * ```typescript + * // Raise an error with the message "simulating an evaluation error". + * error("simulating an evaluation error") + * ``` + * + * @return A new Expression representing the error() operation. + */ +export function error(message: string): Expression { + return new FunctionExpression( + 'error', + [constant(message)], + 'currentTimestamp' + ); } /** @@ -6743,7 +6911,7 @@ export function timestampSub( * ```typescript * // Check if the 'age' field is greater than 18 AND the 'city' field is "London" AND * // the 'status' field is "active" - * const condition = and(gt("age", 18), eq("city", "London"), eq("status", "active")); + * const condition = and(greaterThan("age", 18), equal("city", "London"), equal("status", "active")); * ``` * * @param first The first filter condition. @@ -6752,11 +6920,11 @@ export function timestampSub( * @return A new {@code Expr} representing the logical 'AND' operation. */ export function and( - first: BooleanExpr, - second: BooleanExpr, - ...more: BooleanExpr[] -): BooleanExpr { - return new BooleanExpr('and', [first, second, ...more]); + first: BooleanExpression, + second: BooleanExpression, + ...more: BooleanExpression[] +): BooleanExpression { + return new BooleanExpression('and', [first, second, ...more], 'and'); } /** @@ -6767,7 +6935,7 @@ export function and( * ```typescript * // Check if the 'age' field is greater than 18 OR the 'city' field is "London" OR * // the 'status' field is "active" - * const condition = or(gt("age", 18), eq("city", "London"), eq("status", "active")); + * const condition = or(greaterThan("age", 18), equal("city", "London"), equal("status", "active")); * ``` * * @param first The first filter condition. @@ -6776,13 +6944,638 @@ export function and( * @return A new {@code Expr} representing the logical 'OR' operation. */ export function or( - first: BooleanExpr, - second: BooleanExpr, - ...more: BooleanExpr[] -): BooleanExpr { - return new BooleanExpr('or', [first, second, ...more]); + first: BooleanExpression, + second: BooleanExpression, + ...more: BooleanExpression[] +): BooleanExpression { + return new BooleanExpression('or', [first, second, ...more], 'xor'); +} + +/** + * Creates an expression that returns the value of the base expression raised to the power of the exponent expression. + * + * ```typescript + * // Raise the value of the 'base' field to the power of the 'exponent' field. + * pow(field("base"), field("exponent")); + * ``` + * + * @param base The expression to raise to the power of the exponent. + * @param exponent The expression to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: Expression, exponent: Expression): FunctionExpression; + +/** + * Creates an expression that returns the value of the base expression raised to the power of the exponent. + * + * ```typescript + * // Raise the value of the 'base' field to the power of 2. + * pow(field("base"), 2); + * ``` + * + * @param base The expression to raise to the power of the exponent. + * @param exponent The constant value to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: Expression, exponent: number): FunctionExpression; + +/** + * Creates an expression that returns the value of the base field raised to the power of the exponent expression. + * + * ```typescript + * // Raise the value of the 'base' field to the power of the 'exponent' field. + * pow("base", field("exponent")); + * ``` + * + * @param base The name of the field to raise to the power of the exponent. + * @param exponent The expression to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: string, exponent: Expression): FunctionExpression; + +/** + * Creates an expression that returns the value of the base field raised to the power of the exponent. + * + * ```typescript + * // Raise the value of the 'base' field to the power of 2. + * pow("base", 2); + * ``` + * + * @param base The name of the field to raise to the power of the exponent. + * @param exponent The constant value to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: string, exponent: number): FunctionExpression; +export function pow( + base: Expression | string, + exponent: Expression | number +): FunctionExpression { + return fieldOrExpression(base).pow(exponent as number); +} + +/** + * Creates an expression that rounds a numeric value to the nearest whole number. + * + * ```typescript + * // Round the value of the 'price' field. + * round("price"); + * ``` + * + * @param fieldName The name of the field to round. + * @return A new `Expr` representing the rounded value. + */ +export function round(fieldName: string): FunctionExpression; + +/** + * Creates an expression that rounds a numeric value to the nearest whole number. + * + * ```typescript + * // Round the value of the 'price' field. + * round(field("price")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which will be rounded. + * @return A new `Expr` representing the rounded value. + */ +export function round(expression: Expression): FunctionExpression; + +/** + * Creates an expression that rounds a numeric value to the specified number of decimal places. + * + * ```typescript + * // Round the value of the 'price' field to two decimal places. + * round("price", 2); + * ``` + * + * @param fieldName The name of the field to round. + * @param decimalPlaces A constant or expression specifying the rounding precision in decimal places. + * @return A new `Expr` representing the rounded value. + */ +export function round( + fieldName: string, + decimalPlaces: number | Expression +): FunctionExpression; + +/** + * Creates an expression that rounds a numeric value to the specified number of decimal places. + * + * ```typescript + * // Round the value of the 'price' field to two decimal places. + * round(field("price"), constant(2)); + * ``` + * + * @param expression An expression evaluating to a numeric value, which will be rounded. + * @param decimalPlaces A constant or expression specifying the rounding precision in decimal places. + * @return A new `Expr` representing the rounded value. + */ +export function round( + expression: Expression, + decimalPlaces: number | Expression +): FunctionExpression; +export function round( + expr: Expression | string, + decimalPlaces?: number | Expression +): FunctionExpression { + if (decimalPlaces === undefined) { + return fieldOrExpression(expr).round(); + } else { + return fieldOrExpression(expr).round(valueToDefaultExpr(decimalPlaces)); + } +} + +/** + * Creates an expression that returns the collection ID from a path. + * + * ```typescript + * // Get the collection ID from a path. + * collectionId("__name__"); + * ``` + * + * @param fieldName The name of the field to get the collection ID from. + * @return A new {@code Expr} representing the collectionId operation. + */ +export function collectionId(fieldName: string): FunctionExpression; + +/** + * Creates an expression that returns the collection ID from a path. + * + * ```typescript + * // Get the collection ID from a path. + * collectionId(field("__name__")); + * ``` + * + * @param expression An expression evaluating to a path, which the collection ID will be extracted from. + * @return A new {@code Expr} representing the collectionId operation. + */ +export function collectionId(expression: Expression): FunctionExpression; +export function collectionId(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).collectionId(); +} + +/** + * Creates an expression that calculates the length of a string, array, map, vector, or bytes. + * + * ```typescript + * // Get the length of the 'name' field. + * length("name"); + * + * // Get the number of items in the 'cart' array. + * length("cart"); + * ``` + * + * @param fieldName The name of the field to calculate the length of. + * @return A new `Expr` representing the length of the string, array, map, vector, or bytes. + */ +export function length(fieldName: string): FunctionExpression; + +/** + * Creates an expression that calculates the length of a string, array, map, vector, or bytes. + * + * ```typescript + * // Get the length of the 'name' field. + * length(field("name")); + * + * // Get the number of items in the 'cart' array. + * length(field("cart")); + * ``` + * + * @param expression An expression evaluating to a string, array, map, vector, or bytes, which the length will be calculated for. + * @return A new `Expr` representing the length of the string, array, map, vector, or bytes. + */ +export function length(expression: Expression): FunctionExpression; +export function length(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).length(); +} + +/** + * Creates an expression that computes the natural logarithm of a numeric value. + * + * ```typescript + * // Compute the natural logarithm of the 'value' field. + * ln("value"); + * ``` + * + * @param fieldName The name of the field to compute the natural logarithm of. + * @return A new `Expr` representing the natural logarithm of the numeric value. + */ +export function ln(fieldName: string): FunctionExpression; + +/** + * Creates an expression that computes the natural logarithm of a numeric value. + * + * ```typescript + * // Compute the natural logarithm of the 'value' field. + * ln(field("value")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the natural logarithm will be computed for. + * @return A new `Expr` representing the natural logarithm of the numeric value. + */ +export function ln(expression: Expression): FunctionExpression; +export function ln(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).ln(); +} + +/** + * Creates an expression that computes the logarithm of an expression to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with base 10. + * log(field("value"), 10); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the logarithm will be computed for. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log(expression: Expression, base: number): FunctionExpression; +/** + * Creates an expression that computes the logarithm of an expression to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with the base in the 'base' field. + * log(field("value"), field("base")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the logarithm will be computed for. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log( + expression: Expression, + base: Expression +): FunctionExpression; +/** + * Creates an expression that computes the logarithm of a field to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with base 10. + * log("value", 10); + * ``` + * + * @param fieldName The name of the field to compute the logarithm of. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log(fieldName: string, base: number): FunctionExpression; +/** + * Creates an expression that computes the logarithm of a field to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with the base in the 'base' field. + * log("value", field("base")); + * ``` + * + * @param fieldName The name of the field to compute the logarithm of. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log(fieldName: string, base: Expression): FunctionExpression; +export function log( + expr: Expression | string, + base: number | Expression +): FunctionExpression { + return new FunctionExpression('log', [ + fieldOrExpression(expr), + valueToDefaultExpr(base) + ]); +} + +/** + * Creates an expression that computes the square root of a numeric value. + * + * ```typescript + * // Compute the square root of the 'value' field. + * sqrt(field("value")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the square root will be computed for. + * @return A new {@code Expr} representing the square root of the numeric value. + */ +export function sqrt(expression: Expression): FunctionExpression; +/** + * Creates an expression that computes the square root of a numeric value. + * + * ```typescript + * // Compute the square root of the 'value' field. + * sqrt("value"); + * ``` + * + * @param fieldName The name of the field to compute the square root of. + * @return A new {@code Expr} representing the square root of the numeric value. + */ +export function sqrt(fieldName: string): FunctionExpression; +export function sqrt(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).sqrt(); +} + +/** + * Creates an expression that reverses a string. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * strReverse(field("myString")); + * ``` + * + * @param stringExpression An expression evaluating to a string value, which will be reversed. + * @return A new {@code Expr} representing the reversed string. + */ +export function stringReverse(stringExpression: Expression): FunctionExpression; + +/** + * Creates an expression that reverses a string value in the specified field. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * strReverse("myString"); + * ``` + * + * @param field The name of the field representing the string to reverse. + * @return A new {@code Expr} representing the reversed string. + */ +export function stringReverse(field: string): FunctionExpression; +export function stringReverse(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).stringReverse(); +} + +/** + * Creates an expression that concatenates strings, arrays, or blobs. Types cannot be mixed. + * + * ```typescript + * // Concatenate the 'firstName' and 'lastName' fields with a space in between. + * concat(field("firstName"), " ", field("lastName")) + * ``` + * + * @param first The first expressions to concatenate. + * @param second The second literal or expression to concatenate. + * @param others Additional literals or expressions to concatenate. + * @return A new `Expression` representing the concatenation. + */ +export function concat( + first: Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression; + +/** + * Creates an expression that concatenates strings, arrays, or blobs. Types cannot be mixed. + * + * ```typescript + * // Concatenate a field with a literal string. + * concat(field("firstName"), "Doe") + * ``` + * + * @param fieldName The name of a field to concatenate. + * @param second The second literal or expression to concatenate. + * @param others Additional literal or expressions to concatenate. + * @return A new `Expression` representing the concatenation. + */ +export function concat( + fieldName: string, + second: Expression | unknown, + ...others: Array +): FunctionExpression; + +export function concat( + fieldNameOrExpression: string | Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression { + return new FunctionExpression('concat', [ + fieldOrExpression(fieldNameOrExpression), + valueToDefaultExpr(second), + ...others.map(valueToDefaultExpr) + ]); +} + +/** + * Creates an expression that computes the absolute value of a numeric value. + * + * @param expr The expression to compute the absolute value of. + * @return A new {@code Expr} representing the absolute value of the numeric value. + */ +export function abs(expr: Expression): FunctionExpression; + +/** + * Creates an expression that computes the absolute value of a numeric value. + * + * @param fieldName The field to compute the absolute value of. + * @return A new {@code Expr} representing the absolute value of the numeric value. + */ +export function abs(fieldName: string): FunctionExpression; +export function abs(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).abs(); +} + +/** + * Creates an expression that returns the `elseExpr` argument if `ifExpr` is absent, else return + * the result of the `ifExpr` argument evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * ifAbsent(field("optional_field"), constant("default_value")) + * ``` + * + * @param ifExpr The expression to check for absence. + * @param elseExpr The expression that will be evaluated and returned if [ifExpr] is absent. + * @return A new Expression representing the ifAbsent operation. + */ +export function ifAbsent(ifExpr: Expression, elseExpr: Expression): Expression; + +/** + * Creates an expression that returns the `elseValue` argument if `ifExpr` is absent, else + * return the result of the `ifExpr` argument evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * ifAbsent(field("optional_field"), "default_value") + * ``` + * + * @param ifExpr The expression to check for absence. + * @param elseValue The value that will be returned if `ifExpr` evaluates to an absent value. + * @return A new [Expression] representing the ifAbsent operation. + */ +export function ifAbsent(ifExpr: Expression, elseValue: unknown): Expression; + +/** + * Creates an expression that returns the `elseExpr` argument if `ifFieldName` is absent, else + * return the value of the field. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns the value of + * // 'default_field' if 'optional_field' is absent. + * ifAbsent("optional_field", field("default_field")) + * ``` + * + * @param ifFieldName The field to check for absence. + * @param elseExpr The expression that will be evaluated and returned if `ifFieldName` is + * absent. + * @return A new Expression representing the ifAbsent operation. + */ +export function ifAbsent(ifFieldName: string, elseExpr: Expression): Expression; + +/** + * Creates an expression that returns the `elseValue` argument if `ifFieldName` is absent, else + * return the value of the field. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * ifAbsent("optional_field", "default_value") + * ``` + * + * @param ifFieldName The field to check for absence. + * @param elseValue The value that will be returned if [ifFieldName] is absent. + * @return A new Expression representing the ifAbsent operation. + */ +export function ifAbsent( + ifFieldName: string | Expression, + elseValue: Expression | unknown +): Expression; +export function ifAbsent( + fieldNameOrExpression: string | Expression, + elseValue: Expression | unknown +): Expression { + return fieldOrExpression(fieldNameOrExpression).ifAbsent( + valueToDefaultExpr(elseValue) + ); +} + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with a comma and space. + * join("tags", ", ") + * ``` + * + * @param arrayFieldName The name of the field containing the array. + * @param delimiter The string to use as a delimiter. + * @return A new Expression representing the join operation. + */ +export function join(arrayFieldName: string, delimiter: string): Expression; + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join an array of string using the delimiter from the 'separator' field. + * join(array(['foo', 'bar']), field("separator")) + * ``` + * + * @param arrayExpression An expression that evaluates to an array. + * @param delimiterExpression The expression that evaluates to the delimiter string. + * @return A new Expression representing the join operation. + */ +export function join( + arrayExpression: Expression, + delimiterExpression: Expression +): Expression; + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with a comma and space. + * join(field("tags"), ", ") + * ``` + * + * @param arrayExpression An expression that evaluates to an array. + * @param delimiter The string to use as a delimiter. + * @return A new Expression representing the join operation. + */ +export function join( + arrayExpression: Expression, + delimiter: string +): Expression; + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with the delimiter from the 'separator' field. + * join('tags', field("separator")) + * ``` + * + * @param arrayFieldName The name of the field containing the array. + * @param delimiterExpression The expression that evaluates to the delimiter string. + * @return A new Expression representing the join operation. + */ +export function join( + arrayFieldName: string, + delimiterExpression: Expression +): Expression; +export function join( + fieldNameOrExpression: string | Expression, + delimiterValueOrExpression: Expression | string +): Expression { + return fieldOrExpression(fieldNameOrExpression).join( + valueToDefaultExpr(delimiterValueOrExpression) + ); +} + +/** + * Creates an expression that computes the base-10 logarithm of a numeric value. + * + * ```typescript + * // Compute the base-10 logarithm of the 'value' field. + * log10("value"); + * ``` + * + * @param fieldName The name of the field to compute the base-10 logarithm of. + * @return A new `Expr` representing the base-10 logarithm of the numeric value. + */ +export function log10(fieldName: string): FunctionExpression; + +/** + * Creates an expression that computes the base-10 logarithm of a numeric value. + * + * ```typescript + * // Compute the base-10 logarithm of the 'value' field. + * log10(field("value")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the base-10 logarithm will be computed for. + * @return A new `Expr` representing the base-10 logarithm of the numeric value. + */ +export function log10(expression: Expression): FunctionExpression; +export function log10(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).log10(); } +/** + * Creates an expression that computes the sum of the elements in an array. + * + * ```typescript + * // Compute the sum of the elements in the 'scores' field. + * arraySum("scores"); + * ``` + * + * @param fieldName The name of the field to compute the sum of. + * @return A new `Expr` representing the sum of the elements in the array. + */ +export function arraySum(fieldName: string): FunctionExpression; + +/** + * Creates an expression that computes the sum of the elements in an array. + * + * ```typescript + * // Compute the sum of the elements in the 'scores' field. + * arraySum(field("scores")); + * ``` + * + * @param expression An expression evaluating to a numeric array, which the sum will be computed for. + * @return A new `Expr` representing the sum of the elements in the array. + */ +export function arraySum(expression: Expression): FunctionExpression; +export function arraySum(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).arraySum(); +} + +// TODO(new-expression): Add new top-level expression function definitions above this line + /** * @beta * @@ -6797,7 +7590,7 @@ export function or( * @param expr The expression to create an ascending ordering for. * @return A new `Ordering` for ascending sorting. */ -export function ascending(expr: Expr): Ordering; +export function ascending(expr: Expression): Ordering; /** * @beta @@ -6814,8 +7607,8 @@ export function ascending(expr: Expr): Ordering; * @return A new `Ordering` for ascending sorting. */ export function ascending(fieldName: string): Ordering; -export function ascending(field: Expr | string): Ordering { - return new Ordering(fieldOrExpression(field), 'ascending'); +export function ascending(field: Expression | string): Ordering { + return new Ordering(fieldOrExpression(field), 'ascending', 'ascending'); } /** @@ -6832,7 +7625,7 @@ export function ascending(field: Expr | string): Ordering { * @param expr The expression to create a descending ordering for. * @return A new `Ordering` for descending sorting. */ -export function descending(expr: Expr): Ordering; +export function descending(expr: Expression): Ordering; /** * @beta @@ -6849,8 +7642,8 @@ export function descending(expr: Expr): Ordering; * @return A new `Ordering` for descending sorting. */ export function descending(fieldName: string): Ordering; -export function descending(field: Expr | string): Ordering { - return new Ordering(fieldOrExpression(field), 'descending'); +export function descending(field: Expression | string): Ordering { + return new Ordering(fieldOrExpression(field), 'descending', 'descending'); } /** @@ -6862,18 +7655,11 @@ export function descending(field: Expr | string): Ordering { */ export class Ordering implements ProtoValueSerializable, UserData { constructor( - readonly expr: Expr, - readonly direction: 'ascending' | 'descending' + public readonly expr: Expression, + public readonly direction: 'ascending' | 'descending', + readonly _methodName: string | undefined ) {} - /** - * @internal - * @private - * Indicates if this expression was created from a literal value passed - * by the caller. - */ - _createdFromLiteral: boolean = false; - /** * @private * @internal @@ -6893,13 +7679,54 @@ export class Ordering implements ProtoValueSerializable, UserData { * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, 'constant'); - this.expr._readUserData(dataReader); + _readUserData(context: ParseContext): void { + this.expr._readUserData(context); } _protoValueType: 'ProtoValue' = 'ProtoValue'; } + +export function isSelectable(val: unknown): val is Selectable { + const candidate = val as Selectable; + return ( + candidate.selectable && isString(candidate.alias) && isExpr(candidate.expr) + ); +} + +export function isOrdering(val: unknown): val is Ordering { + const candidate = val as Ordering; + return ( + isExpr(candidate.expr) && + (candidate.direction === 'ascending' || + candidate.direction === 'descending') + ); +} + +export function isAliasedAggregate(val: unknown): val is AggregateWithAlias { + const candidate = val as AggregateWithAlias; + return ( + isString(candidate.alias) && + candidate.aggregate instanceof AggregateFunction + ); +} + +export function isExpr(val: unknown): val is Expression { + return val instanceof Expression; +} + +export function isBooleanExpr(val: unknown): val is BooleanExpression { + return val instanceof BooleanExpression; +} + +export function isField(val: unknown): val is Field { + return val instanceof Field; +} + +export function toField(value: string | Field): Field { + if (isString(value)) { + const result = field(value); + return result; + } else { + return value as Field; + } +} diff --git a/packages/firestore/src/lite-api/pipeline-result.ts b/packages/firestore/src/lite-api/pipeline-result.ts index 635636ac46b..c352ba48338 100644 --- a/packages/firestore/src/lite-api/pipeline-result.ts +++ b/packages/firestore/src/lite-api/pipeline-result.ts @@ -18,12 +18,12 @@ import { ObjectValue } from '../model/object_value'; import { isOptionalEqual } from '../util/misc'; -import { Field } from './expressions'; +import { Field, isField } from './expressions'; import { FieldPath } from './field_path'; import { Pipeline } from './pipeline'; import { DocumentData, DocumentReference, refEqual } from './reference'; -import { fieldPathFromArgument } from './snapshot'; import { Timestamp } from './timestamp'; +import { fieldPathFromArgument } from './user_data_reader'; import { AbstractUserDataWriter } from './user_data_writer'; export class PipelineSnapshot { @@ -40,14 +40,6 @@ export class PipelineSnapshot { this._results = results; } - /** - * The Pipeline on which you called `execute()` in order to get this - * `PipelineSnapshot`. - */ - get pipeline(): Pipeline { - return this._pipeline; - } - /** An array of all the results in the `PipelineSnapshot`. */ get results(): PipelineResult[] { return this._results; @@ -95,7 +87,7 @@ export class PipelineResult { * @internal * @private */ - readonly _fields: ObjectValue | undefined; + readonly _fields: ObjectValue; /** * @private @@ -104,16 +96,14 @@ export class PipelineResult { * @param userDataWriter The serializer used to encode/decode protobuf. * @param ref The reference to the document. * @param fields The fields of the Firestore `Document` Protobuf backing - * this document (or undefined if the document does not exist). - * @param readTime The time when this result was read (or undefined if - * the document exists only locally). + * this document. * @param createTime The time when the document was created if the result is a document, undefined otherwise. * @param updateTime The time when the document was last updated if the result is a document, undefined otherwise. */ constructor( userDataWriter: AbstractUserDataWriter, + fields: ObjectValue, ref?: DocumentReference, - fields?: ObjectValue, createTime?: Timestamp, updateTime?: Timestamp ) { @@ -164,10 +154,9 @@ export class PipelineResult { } /** - * Retrieves all fields in the result as an object. Returns 'undefined' if - * the document doesn't exist. + * Retrieves all fields in the result as an object. * - * @returns {T|undefined} An object containing all fields in the document or + * @returns {T} An object containing all fields in the document or * 'undefined' if the document doesn't exist. * * @example @@ -180,11 +169,7 @@ export class PipelineResult { * }); * ``` */ - data(): AppModelType | undefined { - if (this._fields === undefined) { - return undefined; - } - + data(): AppModelType { return this._userDataWriter.convertValue( this._fields.value ) as AppModelType; @@ -215,6 +200,9 @@ export class PipelineResult { if (this._fields === undefined) { return undefined; } + if (isField(fieldPath)) { + fieldPath = fieldPath.fieldName; + } const value = this._fields.field( fieldPathFromArgument('DocumentSnapshot.get', fieldPath) diff --git a/packages/firestore/src/lite-api/pipeline-source.ts b/packages/firestore/src/lite-api/pipeline-source.ts index 421fc759bfb..3f4d62cb0be 100644 --- a/packages/firestore/src/lite-api/pipeline-source.ts +++ b/packages/firestore/src/lite-api/pipeline-source.ts @@ -17,10 +17,16 @@ import { DatabaseId } from '../core/database_info'; import { toPipeline } from '../core/pipeline-util'; -import { FirestoreError, Code } from '../util/error'; +import { Code, FirestoreError } from '../util/error'; +import { isString } from '../util/types'; import { Pipeline } from './pipeline'; -import { CollectionReference, DocumentReference, Query } from './reference'; +import { + CollectionReference, + DocumentReference, + isCollectionReference, + Query +} from './reference'; import { CollectionGroupSource, CollectionSource, @@ -28,6 +34,13 @@ import { DocumentsSource, Stage } from './stage'; +import { + CollectionGroupStageOptions, + CollectionStageOptions, + DatabaseStageOptions, + DocumentsStageOptions +} from './stage_options'; +import { UserDataReader, UserDataSource } from './user_data_reader'; /** * Represents the source of a Firestore {@link Pipeline}. @@ -37,10 +50,13 @@ export class PipelineSource { /** * @internal * @private + * @param databaseId + * @param userDataReader * @param _createPipeline */ constructor( private databaseId: DatabaseId, + private userDataReader: UserDataReader, /** * @internal * @private @@ -49,44 +65,119 @@ export class PipelineSource { ) {} /** - * Set the pipeline's source to the collection specified by the given path. - * - * @param collectionPath A path to a collection that will be the source of this pipeline. + * Returns all documents from the entire collection. The collection can be nested. + * @param collection - Name or reference to the collection that will be used as the Pipeline source. */ - collection(collectionPath: string): PipelineType; - + collection(collection: string | CollectionReference): PipelineType; /** - * Set the pipeline's source to the collection specified by the given CollectionReference. - * - * @param collectionReference A CollectionReference for a collection that will be the source of this pipeline. - * The converter for this CollectionReference will be ignored and not have an effect on this pipeline. - * - * @throws {@FirestoreError} Thrown if the provided CollectionReference targets a different project or database than the pipeline. + * Returns all documents from the entire collection. The collection can be nested. + * @param options - Options defining how this CollectionStage is evaluated. */ - collection(collectionReference: CollectionReference): PipelineType; - collection(collection: CollectionReference | string): PipelineType { - if (collection instanceof CollectionReference) { - this._validateReference(collection); - return this._createPipeline([new CollectionSource(collection.path)]); - } else { - return this._createPipeline([new CollectionSource(collection)]); + collection(options: CollectionStageOptions): PipelineType; + collection( + collectionOrOptions: string | CollectionReference | CollectionStageOptions + ): PipelineType { + // Process argument union(s) from method overloads + const options = + isString(collectionOrOptions) || + isCollectionReference(collectionOrOptions) + ? {} + : collectionOrOptions; + const collectionRefOrString = + isString(collectionOrOptions) || + isCollectionReference(collectionOrOptions) + ? collectionOrOptions + : collectionOrOptions.collection; + + // Validate that a user provided reference is for the same Firestore DB + if (isCollectionReference(collectionRefOrString)) { + this._validateReference(collectionRefOrString); } + + // Convert user land convenience types to internal types + const normalizedCollection = isString(collectionRefOrString) + ? (collectionRefOrString as string) + : collectionRefOrString.path; + + // Create stage object + const stage = new CollectionSource(normalizedCollection, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'collection' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([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. + * Returns all documents from a collection ID regardless of the parent. + * @param collectionId - ID of the collection group to use as the Pipeline source. + */ + collectionGroup(collectionId: string): PipelineType; + /** + * Returns all documents from a collection ID regardless of the parent. + * @param options - Options defining how this CollectionGroupStage is evaluated. */ - collectionGroup(collectionId: string): PipelineType { - return this._createPipeline([new CollectionGroupSource(collectionId)]); + collectionGroup(options: CollectionGroupStageOptions): PipelineType; + collectionGroup( + collectionIdOrOptions: string | CollectionGroupStageOptions + ): PipelineType { + // Process argument union(s) from method overloads + let collectionId: string; + let options: {}; + if (isString(collectionIdOrOptions)) { + collectionId = collectionIdOrOptions; + options = {}; + } else { + ({ collectionId, ...options } = collectionIdOrOptions); + } + + // Create stage object + const stage = new CollectionGroupSource(collectionId, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'collectionGroup' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([stage]); } /** - * Set the pipeline's source to be all documents in this database. + * Returns all documents from the entire database. */ - database(): PipelineType { - return this._createPipeline([new DatabaseSource()]); + database(): PipelineType; + /** + * Returns all documents from the entire database. + * @param options - Options defining how a DatabaseStage is evaluated. + */ + database(options: DatabaseStageOptions): PipelineType; + database(options?: DatabaseStageOptions): PipelineType { + // Process argument union(s) from method overloads + options = options ?? {}; + + // Create stage object + const stage = new DatabaseSource(options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'database' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([stage]); } /** @@ -97,14 +188,52 @@ export class PipelineSource { * * @throws {@FirestoreError} Thrown if any of the provided DocumentReferences target a different project or database than the pipeline. */ - documents(docs: Array): PipelineType { - docs.forEach(doc => { - if (doc instanceof DocumentReference) { - this._validateReference(doc); - } - }); - - return this._createPipeline([DocumentsSource.of(docs)]); + documents(docs: Array): PipelineType; + + /** + * Set the pipeline's source to the documents specified by the given paths and DocumentReferences. + * + * @param options - Options defining how this DocumentsStage is evaluated. + * + * @throws {@FirestoreError} Thrown if any of the provided DocumentReferences target a different project or database than the pipeline. + */ + documents(options: DocumentsStageOptions): PipelineType; + documents( + docsOrOptions: Array | DocumentsStageOptions + ): PipelineType { + // Process argument union(s) from method overloads + let options: {}; + let docs: Array; + if (Array.isArray(docsOrOptions)) { + docs = docsOrOptions; + options = {}; + } else { + ({ docs, ...options } = docsOrOptions); + } + + // Validate that all user provided references are for the same Firestore DB + docs + .filter(v => v instanceof DocumentReference) + .forEach(dr => this._validateReference(dr as DocumentReference)); + + // Convert user land convenience types to internal types + const normalizedDocs: string[] = docs.map(doc => + isString(doc) ? doc : doc.path + ); + + // Create stage object + const stage = new DocumentsSource(normalizedDocs, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'documents' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([stage]); } /** diff --git a/packages/firestore/src/lite-api/pipeline.ts b/packages/firestore/src/lite-api/pipeline.ts index 8b5bebeddb8..0d542384000 100644 --- a/packages/firestore/src/lite-api/pipeline.ts +++ b/packages/firestore/src/lite-api/pipeline.ts @@ -15,62 +15,79 @@ * limitations under the License. */ -import { ObjectValue } from '../model/object_value'; import { Pipeline as ProtoPipeline, Stage as ProtoStage } from '../protos/firestore_proto_api'; import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; import { isPlainObject } from '../util/input_validation'; +import { + aliasedAggregateToMap, + fieldOrExpression, + selectablesToMap, + vectorToExpr +} from '../util/pipeline_util'; +import { isNumber, isString } from '../util/types'; import { Firestore } from './database'; import { _mapValue, AggregateFunction, AggregateWithAlias, - Expr, - ExprWithAlias, + BooleanExpression, + _constant, + Expression, Field, - BooleanExpr, + field, Ordering, Selectable, - field, - Constant + _field, + isSelectable, + isField, + isBooleanExpr, + isAliasedAggregate, + toField, + isOrdering, + isExpr } from './expressions'; import { AddFields, Aggregate, Distinct, FindNearest, - FindNearestOptions, - GenericStage, + RawStage, Limit, Offset, RemoveFields, Replace, + Sample, Select, Sort, - Sample, + Stage, Union, Unnest, - Stage, Where } from './stage'; import { - parseVectorValue, - UserDataReader, - UserDataSource -} from './user_data_reader'; + AddFieldsStageOptions, + AggregateStageOptions, + DistinctStageOptions, + FindNearestStageOptions, + LimitStageOptions, + OffsetStageOptions, + RemoveFieldsStageOptions, + ReplaceWithStageOptions, + SampleStageOptions, + SelectStageOptions, + SortStageOptions, + StageOptions, + UnionStageOptions, + UnnestStageOptions, + WhereStageOptions +} from './stage_options'; +import { UserDataReader, UserDataSource } from './user_data_reader'; import { AbstractUserDataWriter } from './user_data_writer'; -interface ReadableUserData { - _readUserData(dataReader: UserDataReader): void; -} - -function isReadableUserData(value: unknown): value is ReadableUserData { - return typeof (value as ReadableUserData)._readUserData === 'function'; -} - /** * @beta * @@ -110,10 +127,6 @@ function isReadableUserData(value: unknown): value is ReadableUserData { * .aggregate(avg(field("rating")).as("averageRating"))); * ``` */ - -/** - * Base-class implementation - */ export class Pipeline implements ProtoSerializable { /** * @internal @@ -148,8 +161,8 @@ export class Pipeline implements ProtoSerializable { * The added fields are defined using {@link Selectable}s, which can be: * * - {@link Field}: References an existing document field. - * - {@link Expr}: Either a literal value (see {@link Constant}) or a computed value - * (see {@FunctionExpr}) with an assigned alias using {@link Expr#as}. + * - {@link Expression}: Either a literal value (see {@link Constant}) or a computed value + * (see {@FunctionExpr}) with an assigned alias using {@link Expression#as}. * * Example: * @@ -165,15 +178,64 @@ export class Pipeline implements ProtoSerializable { * @param additionalFields Optional additional fields to add to the documents, specified as {@link Selectable}s. * @return A new Pipeline object with this stage appended to the stage list. */ - addFields(field: Selectable, ...additionalFields: Selectable[]): Pipeline { - return this._addStage( - new AddFields( - this.readUserData( - 'addFields', - this.selectablesToMap([field, ...additionalFields]) - ) - ) + addFields(field: Selectable, ...additionalFields: Selectable[]): Pipeline; + /** + * 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 (if there + * is name overlaps). + * + * The added fields are defined using {@link Selectable}s, which can be: + * + * - {@link Field}: References an existing document field. + * - {@link Expression}: Either a literal value (see {@link Constant}) or a computed value + * (see {@FunctionExpr}) with an assigned alias using {@link Expression#as}. + * + * Example: + * + * ```typescript + * firestore.pipeline().collection("books") + * .addFields( + * field("rating").as("bookRating"), // Rename 'rating' to 'bookRating' + * add(5, field("quantity")).as("totalCost") // Calculate 'totalCost' + * ); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + addFields(options: AddFieldsStageOptions): Pipeline; + addFields( + fieldOrOptions: Selectable | AddFieldsStageOptions, + ...additionalFields: Selectable[] + ): Pipeline { + // Process argument union(s) from method overloads + let fields: Selectable[]; + let options: {}; + if (isSelectable(fieldOrOptions)) { + fields = [fieldOrOptions, ...additionalFields]; + options = {}; + } else { + ({ fields, ...options } = fieldOrOptions); + } + + // Convert user land convenience types to internal types + const normalizedFields: Map = selectablesToMap(fields); + + // Create stage object + const stage = new AddFields(normalizedFields, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'addFields' ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -197,12 +259,55 @@ export class Pipeline implements ProtoSerializable { removeFields( fieldValue: Field | string, ...additionalFields: Array + ): Pipeline; + /** + * Remove fields from outputs of previous stages. + * + * Example: + * + * ```typescript + * firestore.pipeline().collection('books') + * // removes field 'rating' and 'cost' from the previous stage outputs. + * .removeFields( + * field('rating'), + * 'cost' + * ); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + removeFields(options: RemoveFieldsStageOptions): Pipeline; + removeFields( + fieldValueOrOptions: Field | string | RemoveFieldsStageOptions, + ...additionalFields: Array ): Pipeline { - const fieldExpressions = [fieldValue, ...additionalFields].map(f => - typeof f === 'string' ? field(f) : (f as Field) + // Process argument union(s) from method overloads + const options = + isField(fieldValueOrOptions) || isString(fieldValueOrOptions) + ? {} + : fieldValueOrOptions; + const fields: Array = + isField(fieldValueOrOptions) || isString(fieldValueOrOptions) + ? [fieldValueOrOptions, ...additionalFields] + : fieldValueOrOptions.fields; + + // Convert user land convenience types to internal types + const convertedFields: Field[] = fields.map(f => + isString(f) ? field(f) : (f as Field) ); - this.readUserData('removeFields', fieldExpressions); - return this._addStage(new RemoveFields(fieldExpressions)); + + // Create stage object + const stage = new RemoveFields(convertedFields, options); + + // User data must be read in the context of the API method to + // provide contextual errors + stage._readUserData( + this.userDataReader.createContext(UserDataSource.Argument, 'removeFields') + ); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -214,7 +319,7 @@ export class Pipeline implements ProtoSerializable { *
  • {@code string}: Name of an existing field
  • *
  • {@link Field}: References an existing field.
  • *
  • {@link Function}: Represents the result of a function with an assigned alias name using - * {@link Expr#as}
  • + * {@link Expression#as} * * *

    If no selections are provided, the output of this stage is empty. Use {@link @@ -224,7 +329,7 @@ export class Pipeline implements ProtoSerializable { *

    Example: * * ```typescript - * firestore.pipeline().collection("books") + * db.pipeline().collection("books") * .select( * "firstName", * field("lastName"), @@ -241,22 +346,111 @@ export class Pipeline implements ProtoSerializable { select( selection: Selectable | string, ...additionalSelections: Array + ): Pipeline; + /** + * Selects or creates a set of fields from the outputs of previous stages. + * + *

    The selected fields are defined using {@link Selectable} expressions, which can be: + * + *

      + *
    • {@code string}: Name of an existing field
    • + *
    • {@link Field}: References an existing field.
    • + *
    • {@link Function}: Represents the result of a function with an assigned alias name using + * {@link Expression#as}
    • + *
    + * + *

    If no selections are provided, the output of this stage is empty. Use {@link + * Pipeline#addFields} instead if only additions are + * desired. + * + *

    Example: + * + * ```typescript + * db.pipeline().collection("books") + * .select( + * "firstName", + * field("lastName"), + * field("address").toUppercase().as("upperAddress"), + * ); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + select(options: SelectStageOptions): Pipeline; + select( + selectionOrOptions: Selectable | string | SelectStageOptions, + ...additionalSelections: Array ): Pipeline { - let projections: Map = this.selectablesToMap([ - selection, - ...additionalSelections - ]); - projections = this.readUserData('select', projections); - return this._addStage(new Select(projections)); + // Process argument union(s) from method overloads + const options = + isSelectable(selectionOrOptions) || isString(selectionOrOptions) + ? {} + : selectionOrOptions; + + const selections: Array = + isSelectable(selectionOrOptions) || isString(selectionOrOptions) + ? [selectionOrOptions, ...additionalSelections] + : selectionOrOptions.selections; + + // Convert user land convenience types to internal types + const normalizedSelections: Map = + selectablesToMap(selections); + + // Create stage object + const stage = new Select(normalizedSelections, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'select' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** * Filters the documents from previous stages to only include those matching the specified {@link - * BooleanExpr}. + * BooleanExpression}. + * + *

    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 {@link + * BooleanExpression}, typically including but not limited to: + * + *

      + *
    • field comparators: {@link Function#eq}, {@link Function#lt} (less than), {@link + * Function#gt} (greater than), etc.
    • + *
    • logical operators: {@link Function#and}, {@link Function#or}, {@link Function#not}, etc.
    • + *
    • advanced functions: {@link Function#regexMatch}, {@link + * Function#arrayContains}, etc.
    • + *
    + * + *

    Example: + * + * ```typescript + * firestore.pipeline().collection("books") + * .where( + * and( + * gt(field("rating"), 4.0), // Filter for ratings greater than 4.0 + * field("genre").eq("Science Fiction") // Equivalent to gt("genre", "Science Fiction") + * ) + * ); + * ``` + * + * @param condition The {@link BooleanExpression} to apply. + * @return A new Pipeline object with this stage appended to the stage list. + */ + where(condition: BooleanExpression): Pipeline; + /** + * Filters the documents from previous stages to only include those matching the specified {@link + * BooleanExpression}. * *

    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 {@link - * BooleanExpr}, typically including but not limited to: + * BooleanExpression}, typically including but not limited to: * *

      *
    • field comparators: {@link Function#eq}, {@link Function#lt} (less than), {@link @@ -278,12 +472,30 @@ export class Pipeline implements ProtoSerializable { * ); * ``` * - * @param condition The {@link BooleanExpr} to apply. + * @param options - An object that specifies required and optional parameters for the stage. * @return A new Pipeline object with this stage appended to the stage list. */ - where(condition: BooleanExpr): Pipeline { - this.readUserData('where', condition); - return this._addStage(new Where(condition)); + where(options: WhereStageOptions): Pipeline; + where(conditionOrOptions: BooleanExpression | WhereStageOptions): Pipeline { + // Process argument union(s) from method overloads + const options = isBooleanExpr(conditionOrOptions) ? {} : conditionOrOptions; + const condition: BooleanExpression = isBooleanExpr(conditionOrOptions) + ? conditionOrOptions + : conditionOrOptions.condition; + + // Create stage object + const stage = new Where(condition, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'where' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -297,8 +509,8 @@ export class Pipeline implements ProtoSerializable { * * ```typescript * // Retrieve the second page of 20 results - * firestore.pipeline().collection("books") - * .sort(field("published").descending()) + * firestore.pipeline().collection('books') + * .sort(field('published').descending()) * .offset(20) // Skip the first 20 results * .limit(20); // Take the next 20 results * ``` @@ -306,8 +518,53 @@ export class Pipeline implements ProtoSerializable { * @param offset The number of documents to skip. * @return A new Pipeline object with this stage appended to the stage list. */ - offset(offset: number): Pipeline { - return this._addStage(new Offset(offset)); + offset(offset: number): Pipeline; + /** + * 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 {@link #limit} to control the + * size of each page. + * + *

      Example: + * + * ```typescript + * // Retrieve the second page of 20 results + * firestore.pipeline().collection('books') + * .sort(field('published').descending()) + * .offset(20) // Skip the first 20 results + * .limit(20); // Take the next 20 results + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + offset(options: OffsetStageOptions): Pipeline; + offset(offsetOrOptions: number | OffsetStageOptions): Pipeline { + // Process argument union(s) from method overloads + let options: {}; + let offset: number; + if (isNumber(offsetOrOptions)) { + options = {}; + offset = offsetOrOptions; + } else { + options = offsetOrOptions; + offset = offsetOrOptions.offset; + } + + // Create stage object + const stage = new Offset(offset, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'offset' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -327,30 +584,75 @@ export class Pipeline implements ProtoSerializable { * * ```typescript * // Limit the results to the top 10 highest-rated books - * firestore.pipeline().collection("books") - * .sort(field("rating").descending()) + * firestore.pipeline().collection('books') + * .sort(field('rating').descending()) * .limit(10); * ``` * * @param limit The maximum number of documents to return. * @return A new Pipeline object with this stage appended to the stage list. */ - limit(limit: number): Pipeline { - return this._addStage(new Limit(limit)); + limit(limit: number): Pipeline; + /** + * 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 {@link #offset} to retrieve specific pages of + * results.
      • + *
      • **Limiting Data Retrieval:** To prevent excessive data transfer and improve performance, + * especially when dealing with large collections.
      • + *
      + * + *

      Example: + * + * ```typescript + * // Limit the results to the top 10 highest-rated books + * firestore.pipeline().collection('books') + * .sort(field('rating').descending()) + * .limit(10); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + limit(options: LimitStageOptions): Pipeline; + limit(limitOrOptions: number | LimitStageOptions): Pipeline { + // Process argument union(s) from method overloads + const options = isNumber(limitOrOptions) ? {} : limitOrOptions; + const limit: number = isNumber(limitOrOptions) + ? limitOrOptions + : limitOrOptions.limit; + + // Create stage object + const stage = new Limit(limit, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'limit' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** * 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 {@link Expr} values ({@link Field}, {@link Function}, etc). + * unique combinations of {@link Expression} values ({@link Field}, {@link Function}, etc). * * The parameters to this stage are defined using {@link Selectable} expressions or strings: * * - {@code string}: Name of an existing field * - {@link Field}: References an existing document field. - * - {@link ExprWithAlias}: Represents the result of a function with an assigned alias name - * using {@link Expr#as}. + * - {@link AliasedExpr}: Represents the result of a function with an assigned alias name + * using {@link Expression#as}. * * Example: * @@ -370,15 +672,63 @@ export class Pipeline implements ProtoSerializable { distinct( group: string | Selectable, ...additionalGroups: Array + ): Pipeline; + /** + * 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 {@link Expression} values ({@link Field}, {@link Function}, etc). + * + * The parameters to this stage are defined using {@link Selectable} expressions or strings: + * + * - {@code string}: Name of an existing field + * - {@link Field}: References an existing document field. + * - {@link AliasedExpr}: Represents the result of a function with an assigned alias name + * using {@link Expression#as}. + * + * Example: + * + * ```typescript + * // Get a list of unique author names in uppercase and genre combinations. + * firestore.pipeline().collection("books") + * .distinct(toUppercase(field("author")).as("authorName"), field("genre"), "publishedAt") + * .select("authorName"); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + distinct(options: DistinctStageOptions): Pipeline; + distinct( + groupOrOptions: string | Selectable | DistinctStageOptions, + ...additionalGroups: Array ): Pipeline { - return this._addStage( - new Distinct( - this.readUserData( - 'distinct', - this.selectablesToMap([group, ...additionalGroups]) - ) - ) + // Process argument union(s) from method overloads + const options = + isString(groupOrOptions) || isSelectable(groupOrOptions) + ? {} + : groupOrOptions; + const groups: Array = + isString(groupOrOptions) || isSelectable(groupOrOptions) + ? [groupOrOptions, ...additionalGroups] + : groupOrOptions.groups; + + // Convert user land convenience types to internal types + const convertedGroups: Map = selectablesToMap(groups); + + // Create stage object + const stage = new Distinct(convertedGroups, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'distinct' ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -386,7 +736,7 @@ export class Pipeline implements ProtoSerializable { * *

      This stage allows you to calculate aggregate values over a set of documents. You define the * aggregations to perform using {@link AggregateWithAlias} expressions which are typically results of - * calling {@link Expr#as} on {@link AggregateFunction} instances. + * calling {@link Expression#as} on {@link AggregateFunction} instances. * *

      Example: * @@ -422,7 +772,7 @@ export class Pipeline implements ProtoSerializable { * specifying groups is the same as putting the entire inputs into one group.

    • *
    • **Accumulators:** One or more accumulation operations to perform within each group. These * are defined using {@link AggregateWithAlias} expressions, which are typically created by - * calling {@link Expr#as} on {@link AggregateFunction} instances. Each aggregation + * calling {@link Expression#as} on {@link AggregateFunction} instances. Each aggregation * calculates a value (e.g., sum, average, count) based on the documents within its group.
    • *
    * @@ -437,79 +787,108 @@ export class Pipeline implements ProtoSerializable { * }); * ``` * - * @param options An object that specifies the accumulators - * and optional grouping fields to perform. + * @param options - An object that specifies required and optional parameters for the stage. * @return A new {@code Pipeline} object with this stage appended to the stage * list. */ - aggregate(options: { - accumulators: AggregateWithAlias[]; - groups?: Array; - }): Pipeline; + aggregate(options: AggregateStageOptions): Pipeline; aggregate( - optionsOrTarget: - | AggregateWithAlias - | { - accumulators: AggregateWithAlias[]; - groups?: Array; - }, + targetOrOptions: AggregateWithAlias | AggregateStageOptions, ...rest: AggregateWithAlias[] ): Pipeline { - if ('accumulators' in optionsOrTarget) { - return this._addStage( - new Aggregate( - new Map( - optionsOrTarget.accumulators.map((target: AggregateWithAlias) => { - this.readUserData( - 'aggregate', - target as unknown as AggregateWithAlias - ); - return [ - (target as unknown as AggregateWithAlias).alias, - (target as unknown as AggregateWithAlias).aggregate - ]; - }) - ), - this.readUserData( - 'aggregate', - this.selectablesToMap(optionsOrTarget.groups || []) - ) - ) - ); - } else { - return this._addStage( - new Aggregate( - new Map( - [optionsOrTarget, ...rest].map(target => [ - (target as unknown as AggregateWithAlias).alias, - this.readUserData( - 'aggregate', - (target as unknown as AggregateWithAlias).aggregate - ) - ]) - ), - new Map() - ) - ); - } - } + // Process argument union(s) from method overloads + const options = isAliasedAggregate(targetOrOptions) ? {} : targetOrOptions; + const accumulators: AggregateWithAlias[] = isAliasedAggregate( + targetOrOptions + ) + ? [targetOrOptions, ...rest] + : targetOrOptions.accumulators; + const groups: Array = isAliasedAggregate( + targetOrOptions + ) + ? [] + : targetOrOptions.groups ?? []; - findNearest(options: FindNearestOptions): Pipeline { + // Convert user land convenience types to internal types + const convertedAccumulators: Map = + aliasedAggregateToMap(accumulators); + const convertedGroups: Map = selectablesToMap(groups); + + // Create stage object + const stage = new Aggregate( + convertedGroups, + convertedAccumulators, + options + ); + + // User data must be read in the context of the API method to + // provide contextual errors const parseContext = this.userDataReader.createContext( UserDataSource.Argument, - 'findNearest' + 'aggregate' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * Performs a vector proximity search on the documents from the previous stage, returning the + * K-nearest documents based on the specified query `vectorValue` and `distanceMeasure`. The + * returned documents will be sorted in order from nearest to furthest from the query `vectorValue`. + * + *

    Example: + * + * ```typescript + * // Find the 10 most similar books based on the book description. + * const bookDescription = "Lorem ipsum..."; + * const queryVector: number[] = ...; // compute embedding of `bookDescription` + * + * firestore.pipeline().collection("books") + * .findNearest({ + * field: 'embedding', + * vectorValue: queryVector, + * distanceMeasure: 'euclidean', + * limit: 10, // optional + * distanceField: 'computedDistance' // optional + * }); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + findNearest(options: FindNearestStageOptions): Pipeline { + // Convert user land convenience types to internal types + const field = toField(options.field); + const vectorValue = vectorToExpr(options.vectorValue); + const distanceField = options.distanceField + ? toField(options.distanceField) + : undefined; + const internalOptions = { + distanceField, + limit: options.limit, + rawOptions: options.rawOptions + }; + + // Create stage object + const stage = new FindNearest( + vectorValue, + field, + options.distanceMeasure, + internalOptions ); - const value = parseVectorValue(options.vectorValue, parseContext); - const vectorObjectValue = new ObjectValue(value); - return this._addStage( - new FindNearest( - options.field instanceof Field ? options.field : field(options.field), - vectorObjectValue, - options.distanceMeasure, - options.limit, - options.distanceField - ) + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'addFields' ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -537,11 +916,55 @@ export class Pipeline implements ProtoSerializable { * @param additionalOrderings Optional additional {@link Ordering} instances specifying the additional sorting criteria. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - sort(ordering: Ordering, ...additionalOrderings: Ordering[]): Pipeline { - // Ordering object - return this._addStage( - new Sort(this.readUserData('sort', [ordering, ...additionalOrderings])) + sort(ordering: Ordering, ...additionalOrderings: Ordering[]): Pipeline; + /** + * Sorts the documents from previous stages based on one or more {@link Ordering} criteria. + * + *

    This stage allows you to order the results of your pipeline. You can specify multiple {@link + * 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. + * + *

    Example: + * + * ```typescript + * // Sort books by rating in descending order, and then by title in ascending order for books + * // with the same rating + * firestore.pipeline().collection("books") + * .sort( + * Ordering.of(field("rating")).descending(), + * Ordering.of(field("title")) // Ascending order is the default + * ); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + sort(options: SortStageOptions): Pipeline; + sort( + orderingOrOptions: Ordering | SortStageOptions, + ...additionalOrderings: Ordering[] + ): Pipeline { + // Process argument union(s) from method overloads + const options = isOrdering(orderingOrOptions) ? {} : orderingOrOptions; + const orderings: Ordering[] = isOrdering(orderingOrOptions) + ? [orderingOrOptions, ...additionalOrderings] + : orderingOrOptions.orderings; + + // Create stage object + const stage = new Sort(orderings, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'sort' ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -576,7 +999,6 @@ export class Pipeline implements ProtoSerializable { * @return A new {@code Pipeline} object with this stage appended to the stage list. */ replaceWith(fieldName: string): Pipeline; - /** * Fully overwrites all fields in a document with those coming from a map. * @@ -610,14 +1032,74 @@ export class Pipeline implements ProtoSerializable { * // } * ``` * - * @param expr An {@link Expr} that when returned evaluates to a map. + * @param expr An {@link Expression} that when returned evaluates to a map. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - replaceWith(expr: Expr): Pipeline; - replaceWith(value: Expr | string): Pipeline { - const fieldExpr = typeof value === 'string' ? field(value) : value; - this.readUserData('replaceWith', fieldExpr); - return this._addStage(new Replace(fieldExpr, 'full_replace')); + replaceWith(expr: Expression): Pipeline; + /** + * Fully overwrites all fields in a document with those coming from a 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. + * + *

    Example: + * + * ```typescript + * // Input. + * // { + * // 'name': 'John Doe Jr.', + * // 'parents': { + * // 'father': 'John Doe Sr.', + * // 'mother': 'Jane Doe' + * // } + * // } + * + * // Emit parents as document. + * firestore.pipeline().collection('people').replaceWith(map({ + * foo: 'bar', + * info: { + * name: field('name') + * } + * })); + * + * // Output + * // { + * // 'father': 'John Doe Sr.', + * // 'mother': 'Jane Doe' + * // } + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + replaceWith(options: ReplaceWithStageOptions): Pipeline; + replaceWith( + valueOrOptions: Expression | string | ReplaceWithStageOptions + ): Pipeline { + // Process argument union(s) from method overloads + const options = + isString(valueOrOptions) || isExpr(valueOrOptions) ? {} : valueOrOptions; + const fieldNameOrExpr: string | Expression = + isString(valueOrOptions) || isExpr(valueOrOptions) + ? valueOrOptions + : valueOrOptions.map; + + // Convert user land convenience types to internal types + const mapExpr = fieldOrExpression(fieldNameOrExpr); + + // Create stage object + const stage = new Replace(mapExpr, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'replaceWith' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -655,24 +1137,39 @@ export class Pipeline implements ProtoSerializable { * firestore.pipeline().collection("books") * .sample({ percentage: 0.5 }); * - * @param options The {@code SampleOptions} specifies how sampling is performed. + * @param options - An object that specifies required and optional parameters for the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - sample(options: { percentage: number } | { documents: number }): Pipeline; - sample( - documentsOrOptions: number | { percentage: number } | { documents: number } - ): Pipeline { - if (typeof documentsOrOptions === 'number') { - return this._addStage(new Sample(documentsOrOptions, 'documents')); - } else if ('percentage' in documentsOrOptions) { - return this._addStage( - new Sample(documentsOrOptions.percentage, 'percent') - ); + sample(options: SampleStageOptions): Pipeline; + sample(documentsOrOptions: number | SampleStageOptions): Pipeline { + // Process argument union(s) from method overloads + const options = isNumber(documentsOrOptions) ? {} : documentsOrOptions; + let rate: number; + let mode: 'documents' | 'percent'; + if (isNumber(documentsOrOptions)) { + rate = documentsOrOptions; + mode = 'documents'; + } else if (isNumber(documentsOrOptions.documents)) { + rate = documentsOrOptions.documents; + mode = 'documents'; } else { - return this._addStage( - new Sample(documentsOrOptions.documents, 'documents') - ); + rate = documentsOrOptions.percentage!; + mode = 'percent'; } + + // Create stage object + const stage = new Sample(rate, mode, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'sample' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -693,8 +1190,50 @@ export class Pipeline implements ProtoSerializable { * @param other The other {@code Pipeline} that is part of union. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - union(other: Pipeline): Pipeline { - return this._addStage(new Union(other)); + union(other: Pipeline): Pipeline; + /** + * 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` {@code Pipeline} given in parameter. The order of documents + * emitted from this stage is undefined. + * + *

    Example: + * + * ```typescript + * // Emit documents from books collection and magazines collection. + * firestore.pipeline().collection('books') + * .union(firestore.pipeline().collection('magazines')); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + union(options: UnionStageOptions): Pipeline; + union(otherOrOptions: Pipeline | UnionStageOptions): Pipeline { + // Process argument union(s) from method overloads + let options: {}; + let otherPipeline: Pipeline; + if (isPipeline(otherOrOptions)) { + options = {}; + otherPipeline = otherOrOptions; + } else { + ({ other: otherPipeline, ...options } = otherOrOptions); + } + + // Create stage object + const stage = new Union(otherPipeline, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'union' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -729,64 +1268,133 @@ export class Pipeline implements ProtoSerializable { * @param indexField An optional string value specifying the field path to write the offset (starting at zero) into the array the un-nested element is from * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - unnest(selectable: Selectable, indexField?: string): Pipeline { - this.readUserData('unnest', selectable.expr); - - const alias = field(selectable.alias); - this.readUserData('unnest', alias); - - if (indexField) { - return this._addStage( - new Unnest(selectable.expr, alias, field(indexField)) - ); + unnest(selectable: Selectable, indexField?: string): Pipeline; + /** + * Produces a document for each element in an input array. + * + * For each previous stage document, this stage will emit zero or more augmented documents. The + * input array specified by the `selectable` parameter, will emit an augmented document for each input array element. The input array element will + * augment the previous stage document by setting the `alias` field with the array element value. + * + * When `selectable` evaluates to a non-array value (ex: number, null, absent), then the stage becomes a no-op for + * the current input document, returning it as is with the `alias` field absent. + * + * No documents are emitted when `selectable` evaluates to an empty array. + * + * Example: + * + * ```typescript + * // Input: + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": [ "comedy", "space", "adventure" ], ... } + * + * // Emit a book document for each tag of the book. + * firestore.pipeline().collection("books") + * .unnest(field("tags").as('tag'), 'tagIndex'); + * + * // Output: + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "comedy", "tagIndex": 0, ... } + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "space", "tagIndex": 1, ... } + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", "tagIndex": 2, ... } + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + unnest(options: UnnestStageOptions): Pipeline; + unnest( + selectableOrOptions: Selectable | UnnestStageOptions, + indexField?: string + ): Pipeline { + // Process argument union(s) from method overloads + let options: { indexField?: Field } & StageOptions; + let selectable: Selectable; + let indexFieldName: string | undefined; + if (isSelectable(selectableOrOptions)) { + options = {}; + selectable = selectableOrOptions; + indexFieldName = indexField; } else { - return this._addStage(new Unnest(selectable.expr, alias)); + ({ + selectable, + indexField: indexFieldName, + ...options + } = selectableOrOptions); + } + + // Convert user land convenience types to internal types + const alias = selectable.alias; + const expr = selectable.expr as Expression; + if (isString(indexFieldName)) { + options.indexField = _field(indexFieldName, 'unnest'); } + + // Create stage object + const stage = new Unnest(alias, expr, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'unnest' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** - * Adds a generic stage to the pipeline. + * Adds a raw stage to the pipeline. * *

    This method provides a flexible way to extend the pipeline's functionality by adding custom - * stages. Each generic stage is defined by a unique `name` and a set of `params` that control its + * stages. Each raw stage is defined by a unique `name` and a set of `params` that control its * behavior. * - *

    Example (Assuming there is no "where" stage available in SDK): + *

    Example (Assuming there is no 'where' stage available in SDK): * * ```typescript - * // Assume we don't have a built-in "where" stage - * firestore.pipeline().collection("books") - * .genericStage("where", [field("published").lt(1900)]) // Custom "where" stage - * .select("title", "author"); + * // Assume we don't have a built-in 'where' stage + * firestore.pipeline().collection('books') + * .rawStage('where', [field('published').lt(1900)]) // Custom 'where' stage + * .select('title', 'author'); * ``` * - * @param name The unique name of the generic stage to add. - * @param params A list of parameters to configure the generic stage's behavior. + * @param name - The unique name of the raw stage to add. + * @param params - A list of parameters to configure the raw stage's behavior. + * @param options - An object of key value pairs that specifies optional parameters for the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - genericStage(name: string, params: unknown[]): Pipeline { - // Convert input values to Expressions. - // We treat objects as mapValues and arrays as arrayValues, - // this is unlike the default conversion for objects and arrays - // passed to an expression. + rawStage( + name: string, + params: unknown[], + options?: { [key: string]: Expression | unknown } + ): Pipeline { + // Convert user land convenience types to internal types const expressionParams = params.map((value: unknown) => { - if (value instanceof Expr) { + if (value instanceof Expression) { return value; } else if (value instanceof AggregateFunction) { return value; } else if (isPlainObject(value)) { return _mapValue(value as Record); } else { - return new Constant(value); + return _constant(value, 'rawStage'); } }); - expressionParams.forEach(param => { - if (isReadableUserData(param)) { - param._readUserData(this.userDataReader); - } - }); - return this._addStage(new GenericStage(name, expressionParams)); + // Create stage object + const stage = new RawStage(name, expressionParams, options ?? {}); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'rawStage' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -811,47 +1419,6 @@ export class Pipeline implements ProtoSerializable { ); } - private selectablesToMap( - selectables: Array - ): Map { - const result = new Map(); - for (const selectable of selectables) { - if (typeof selectable === 'string') { - result.set(selectable as string, field(selectable)); - } else if (selectable instanceof Field) { - result.set(selectable.alias, selectable.expr); - } else if (selectable instanceof ExprWithAlias) { - result.set(selectable.alias, selectable.expr); - } - } - return result; - } - - /** - * Reads user data for each expression in the expressionMap. - * @param name Name of the calling function. Used for error messages when invalid user data is encountered. - * @param expressionMap - * @return the expressionMap argument. - * @private - */ - private readUserData< - T extends - | Map - | ReadableUserData[] - | ReadableUserData - >(name: string, expressionMap: T): T { - if (isReadableUserData(expressionMap)) { - expressionMap._readUserData(this.userDataReader); - } else if (Array.isArray(expressionMap)) { - expressionMap.forEach(readableData => - readableData._readUserData(this.userDataReader) - ); - } else { - expressionMap.forEach(expr => expr._readUserData(this.userDataReader)); - } - return expressionMap; - } - /** * @internal * @private @@ -870,3 +1437,7 @@ export class Pipeline implements ProtoSerializable { return new Pipeline(db, userDataReader, userDataWriter, stages); } } + +export function isPipeline(val: unknown): val is Pipeline { + return val instanceof Pipeline; +} diff --git a/packages/firestore/src/lite-api/pipeline_impl.ts b/packages/firestore/src/lite-api/pipeline_impl.ts index c1ca940a56b..397e27bc1b4 100644 --- a/packages/firestore/src/lite-api/pipeline_impl.ts +++ b/packages/firestore/src/lite-api/pipeline_impl.ts @@ -15,6 +15,10 @@ * limitations under the License. */ +import { + StructuredPipeline, + StructuredPipelineOptions +} from '../core/structured_pipeline'; import { invokeExecutePipeline } from '../remote/datastore'; import { getDatastore } from './components'; @@ -25,7 +29,11 @@ import { PipelineSource } from './pipeline-source'; import { DocumentReference } from './reference'; import { LiteUserDataWriter } from './reference_impl'; import { Stage } from './stage'; -import { newUserDataReader } from './user_data_reader'; +import { + newUserDataReader, + UserDataReader, + UserDataSource +} from './user_data_reader'; declare module './database' { interface Firestore { @@ -69,7 +77,22 @@ declare module './database' { */ export function execute(pipeline: Pipeline): Promise { const datastore = getDatastore(pipeline._db); - return invokeExecutePipeline(datastore, pipeline).then(result => { + + const udr = new UserDataReader( + pipeline._db._databaseId, + /* ignoreUndefinedProperties */ true + ); + const context = udr.createContext(UserDataSource.Argument, 'execute'); + + const structuredPipelineOptions = new StructuredPipelineOptions({}, {}); + structuredPipelineOptions._readUserData(context); + + const structuredPipeline: StructuredPipeline = new StructuredPipeline( + pipeline, + structuredPipelineOptions + ); + + return invokeExecutePipeline(datastore, structuredPipeline).then(result => { // Get the execution time from the first result. // firestoreClientExecutePipeline returns at least one PipelineStreamElement // even if the returned document set is empty. @@ -84,10 +107,10 @@ export function execute(pipeline: Pipeline): Promise { element => new PipelineResult( pipeline._userDataWriter, + element.fields!, element.key?.path ? new DocumentReference(pipeline._db, null, element.key) : undefined, - element.fields, element.createTime?.toTimestamp(), element.updateTime?.toTimestamp() ) @@ -100,7 +123,11 @@ export function execute(pipeline: Pipeline): Promise { Firestore.prototype.pipeline = function (): PipelineSource { const userDataWriter = new LiteUserDataWriter(this); const userDataReader = newUserDataReader(this); - return new PipelineSource(this._databaseId, (stages: Stage[]) => { - return new Pipeline(this, userDataReader, userDataWriter, stages); - }); + return new PipelineSource( + this._databaseId, + userDataReader, + (stages: Stage[]) => { + return new Pipeline(this, userDataReader, userDataWriter, stages); + } + ); }; diff --git a/packages/firestore/src/lite-api/pipeline_options.ts b/packages/firestore/src/lite-api/pipeline_options.ts new file mode 100644 index 00000000000..1ab8ead510f --- /dev/null +++ b/packages/firestore/src/lite-api/pipeline_options.ts @@ -0,0 +1,74 @@ +// 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. +import type { Pipeline } from './pipeline'; + +/** + * Options defining Pipeline execution. + */ +export interface PipelineExecuteOptions { + /** + * Pipeline to be evaluated. + */ + pipeline: Pipeline; + + /** + * Specify the index mode. + */ + indexMode?: 'recommended'; + + /** + * An escape hatch to set options not known at SDK build time. These values + * will be passed directly to the Firestore backend and not used by the SDK. + * + * The option name will be used as provided. And must match the name + * format used by the backend (hint: use a snake_case_name). + * + * Custom option values can be any type supported + * by Firestore (for example: string, boolean, number, map, …). Value types + * not known to the SDK will be rejected. + * + * Values specified in rawOptions will take precedence over any options + * with the same name set by the SDK. + * + * Override the `example_option`: + * ``` + * execute({ + * pipeline: myPipeline, + * rawOptions: { + * // Override `example_option`. This will not + * // merge with the existing `example_option` object. + * "example_option": { + * foo: "bar" + * } + * } + * } + * ``` + * + * `rawOptions` supports dot notation, if you want to override + * a nested option. + * ``` + * execute({ + * pipeline: myPipeline, + * rawOptions: { + * // Override `example_option.foo` and do not override + * // any other properties of `example_option`. + * "example_option.foo": "bar" + * } + * } + * ``` + */ + rawOptions?: { + [name: string]: unknown; + }; +} diff --git a/packages/firestore/src/lite-api/query.ts b/packages/firestore/src/lite-api/query.ts index f0a357b828c..f019f0d0936 100644 --- a/packages/firestore/src/lite-api/query.ts +++ b/packages/firestore/src/lite-api/query.ts @@ -52,8 +52,9 @@ import { import { FieldPath } from './field_path'; import { DocumentData, DocumentReference, Query } from './reference'; -import { DocumentSnapshot, fieldPathFromArgument } from './snapshot'; +import { DocumentSnapshot } from './snapshot'; import { + fieldPathFromArgument, newUserDataReader, parseQueryValue, UserDataReader diff --git a/packages/firestore/src/lite-api/reference.ts b/packages/firestore/src/lite-api/reference.ts index e6c5fd7b056..43eedecf8b4 100644 --- a/packages/firestore/src/lite-api/reference.ts +++ b/packages/firestore/src/lite-api/reference.ts @@ -440,6 +440,12 @@ export class CollectionReference< } } +export function isCollectionReference( + val: unknown +): val is CollectionReference { + return val instanceof CollectionReference; +} + /** * Gets a `CollectionReference` instance that refers to the collection at * the specified absolute path. diff --git a/packages/firestore/src/lite-api/snapshot.ts b/packages/firestore/src/lite-api/snapshot.ts index 66c3a1422e9..ba7b08cf9dd 100644 --- a/packages/firestore/src/lite-api/snapshot.ts +++ b/packages/firestore/src/lite-api/snapshot.ts @@ -15,15 +15,13 @@ * limitations under the License. */ -import { Compat, getModularInstance } from '@firebase/util'; +import { getModularInstance } from '@firebase/util'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; -import { FieldPath as InternalFieldPath } from '../model/path'; import { arrayEquals } from '../util/misc'; import { Firestore } from './database'; -import { Field } from './expressions'; import { FieldPath } from './field_path'; import { DocumentData, @@ -35,7 +33,7 @@ import { WithFieldValue } from './reference'; import { - fieldPathFromDotSeparatedString, + fieldPathFromArgument, UntypedFirestoreDataConverter } from './user_data_reader'; import { AbstractUserDataWriter } from './user_data_writer'; @@ -510,21 +508,3 @@ export function snapshotEqual( return false; } - -/** - * Helper that calls `fromDotSeparatedString()` but wraps any error thrown. - */ -export function fieldPathFromArgument( - methodName: string, - arg: string | FieldPath | Compat | Field -): InternalFieldPath { - if (typeof arg === 'string') { - return fieldPathFromDotSeparatedString(methodName, arg); - } else if (arg instanceof FieldPath) { - return arg._internalPath; - } else if (arg instanceof Field) { - return fieldPathFromDotSeparatedString(methodName, arg.fieldName()); - } else { - return arg._delegate._internalPath; - } -} diff --git a/packages/firestore/src/lite-api/stage.ts b/packages/firestore/src/lite-api/stage.ts index 31faaa00e76..5dd30eedba7 100644 --- a/packages/firestore/src/lite-api/stage.ts +++ b/packages/firestore/src/lite-api/stage.ts @@ -15,10 +15,12 @@ * limitations under the License. */ -import { ObjectValue } from '../model/object_value'; +import { ParseContext } from '../api/parse_context'; +import { OptionsUtil } from '../core/options_util'; import { - Stage as ProtoStage, - Value as ProtoValue + ApiClientObjectMap, + firestoreV1ApiClientInterfaces, + Stage as ProtoStage } from '../protos/firestore_proto_api'; import { toNumber } from '../remote/number_serializer'; import { @@ -32,50 +34,98 @@ import { hardAssert } from '../util/assert'; import { AggregateFunction, - Expr, + BooleanExpression, + Expression, Field, - BooleanExpr, - Ordering, - field + field, + Ordering } from './expressions'; import { Pipeline } from './pipeline'; -import { DocumentReference } from './reference'; -import { VectorValue } from './vector_value'; +import { StageOptions } from './stage_options'; +import { isUserData, UserData } from './user_data_reader'; /** * @beta */ -export interface Stage extends ProtoSerializable { - name: string; +export abstract class Stage implements ProtoSerializable, UserData { + /** + * Store optionsProto parsed by _readUserData. + * @private + * @internal + * @protected + */ + protected optionsProto: + | ApiClientObjectMap + | undefined = undefined; + protected knownOptions: Record; + protected rawOptions?: Record; + + constructor(options: StageOptions) { + ({ rawOptions: this.rawOptions, ...this.knownOptions } = options); + } + + _readUserData(context: ParseContext): void { + this.optionsProto = this._optionsUtil.getOptionsProto( + context, + this.knownOptions, + this.rawOptions + ); + } + + _toProto(_: JsonProtoSerializer): ProtoStage { + return { + name: this._name, + options: this.optionsProto + }; + } + + abstract get _optionsUtil(): OptionsUtil; + abstract get _name(): string; } /** * @beta */ -export class AddFields implements Stage { - name = 'add_fields'; +export class AddFields extends Stage { + get _name(): string { + return 'add_fields'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(private fields: Map) {} + constructor(private fields: Map, options: StageOptions) { + super(options); + } - /** - * @internal - * @private - */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, + ...super._toProto(serializer), args: [toMapValue(serializer, this.fields)] }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.fields, context); + } } /** * @beta */ -export class RemoveFields implements Stage { - name = 'remove_fields'; +export class RemoveFields extends Stage { + get _name(): string { + return 'remove_fields'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(private fields: Field[]) {} + constructor(private fields: Field[], options: StageOptions) { + super(options); + } /** * @internal @@ -83,22 +133,36 @@ export class RemoveFields implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, + ...super._toProto(serializer), args: this.fields.map(f => f._toProto(serializer)) }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.fields, context); + } } /** * @beta */ -export class Aggregate implements Stage { - name = 'aggregate'; +export class Aggregate extends Stage { + get _name(): string { + return 'aggregate'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } constructor( + private groups: Map, private accumulators: Map, - private groups: Map - ) {} + options: StageOptions + ) { + super(options); + } /** * @internal @@ -106,22 +170,36 @@ export class Aggregate implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, + ...super._toProto(serializer), args: [ toMapValue(serializer, this.accumulators), toMapValue(serializer, this.groups) ] }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.groups, context); + readUserDataHelper(this.accumulators, context); + } } /** * @beta */ -export class Distinct implements Stage { - name = 'distinct'; +export class Distinct extends Stage { + get _name(): string { + return 'distinct'; + } - constructor(private groups: Map) {} + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private groups: Map, options: StageOptions) { + super(options); + } /** * @internal @@ -129,22 +207,42 @@ export class Distinct implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, + ...super._toProto(serializer), args: [toMapValue(serializer, this.groups)] }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.groups, context); + } } /** * @beta */ -export class CollectionSource implements Stage { - name = 'collection'; +export class CollectionSource extends Stage { + get _name(): string { + return 'collection'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + forceIndex: { + serverName: 'force_index' + } + }); + } + + private formattedCollectionPath: string; + + constructor(collection: string, options: StageOptions) { + super(options); - constructor(private collectionPath: string) { - if (!this.collectionPath.startsWith('/')) { - this.collectionPath = '/' + this.collectionPath; - } + // prepend slash to collection string + this.formattedCollectionPath = collection.startsWith('/') + ? collection + : '/' + collection; } /** @@ -153,19 +251,35 @@ export class CollectionSource implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [{ referenceValue: this.collectionPath }] + ...super._toProto(serializer), + args: [{ referenceValue: this.formattedCollectionPath }] }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } } /** * @beta */ -export class CollectionGroupSource implements Stage { - name = 'collection_group'; +export class CollectionGroupSource extends Stage { + get _name(): string { + return 'collection_group'; + } - constructor(private collectionId: string) {} + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + forceIndex: { + serverName: 'force_index' + } + }); + } + + constructor(private collectionId: string, options: StageOptions) { + super(options); + } /** * @internal @@ -173,17 +287,26 @@ export class CollectionGroupSource implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, + ...super._toProto(serializer), args: [{ referenceValue: '' }, { stringValue: this.collectionId }] }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } } /** * @beta */ -export class DatabaseSource implements Stage { - name = 'database'; +export class DatabaseSource extends Stage { + get _name(): string { + return 'database'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } /** * @internal @@ -191,28 +314,33 @@ export class DatabaseSource implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name + ...super._toProto(serializer) }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } } /** * @beta */ -export class DocumentsSource implements Stage { - name = 'documents'; - - constructor(private docPaths: string[]) {} - - static of(refs: Array): DocumentsSource { - return new DocumentsSource( - refs.map(ref => - ref instanceof DocumentReference - ? '/' + ref.path - : ref.startsWith('/') - ? ref - : '/' + ref - ) +export class DocumentsSource extends Stage { + get _name(): string { + return 'documents'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + private formattedPaths: string[]; + + constructor(docPaths: string[], options: StageOptions) { + super(options); + this.formattedPaths = docPaths.map(path => + path.startsWith('/') ? path : '/' + path ); } @@ -222,21 +350,32 @@ export class DocumentsSource implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: this.docPaths.map(p => { + ...super._toProto(serializer), + args: this.formattedPaths.map(p => { return { referenceValue: p }; }) }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } } /** * @beta */ -export class Where implements Stage { - name = 'where'; +export class Where extends Stage { + get _name(): string { + return 'where'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(private condition: BooleanExpr) {} + constructor(private condition: BooleanExpression, options: StageOptions) { + super(options); + } /** * @internal @@ -244,87 +383,85 @@ export class Where implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [(this.condition as unknown as Expr)._toProto(serializer)] + ...super._toProto(serializer), + args: [this.condition._toProto(serializer)] }; } -} -/** - * @beta - */ -export interface FindNearestOptions { - field: Field | string; - vectorValue: VectorValue | number[]; - distanceMeasure: 'euclidean' | 'cosine' | 'dot_product'; - limit?: number; - distanceField?: string; + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.condition, context); + } } /** * @beta */ -export class FindNearest implements Stage { - name = 'find_nearest'; +export class FindNearest extends Stage { + get _name(): string { + return 'find_nearest'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + limit: { + serverName: 'limit' + }, + distanceField: { + serverName: 'distance_field' + } + }); + } - /** - * @private - * @internal - * - * @param _field - * @param _vectorValue - * @param _distanceMeasure - * @param _limit - * @param _distanceField - */ constructor( - private _field: Field, - private _vectorValue: ObjectValue, - private _distanceMeasure: 'euclidean' | 'cosine' | 'dot_product', - private _limit?: number, - private _distanceField?: string - ) {} + private vectorValue: Expression, + private field: Field, + private distanceMeasure: 'euclidean' | 'cosine' | 'dot_product', + options: StageOptions + ) { + super(options); + } /** * @private * @internal */ _toProto(serializer: JsonProtoSerializer): ProtoStage { - const options: { [k: string]: ProtoValue } = {}; - - if (this._limit) { - options.limit = toNumber(serializer, this._limit)!; - } - - if (this._distanceField) { - // eslint-disable-next-line camelcase - options.distance_field = field(this._distanceField)._toProto(serializer); - } - return { - name: this.name, + ...super._toProto(serializer), args: [ - this._field._toProto(serializer), - this._vectorValue.value, - toStringValue(this._distanceMeasure) - ], - options + this.field._toProto(serializer), + this.vectorValue._toProto(serializer), + toStringValue(this.distanceMeasure) + ] }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.vectorValue, context); + readUserDataHelper(this.field, context); + } } /** * @beta */ -export class Limit implements Stage { - name = 'limit'; +export class Limit extends Stage { + get _name(): string { + return 'limit'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(readonly limit: number) { + constructor(private limit: number, options: StageOptions) { hardAssert( !isNaN(limit) && limit !== Infinity && limit !== -Infinity, 0x882c, 'Invalid limit value' ); + super(options); } /** @@ -333,7 +470,7 @@ export class Limit implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, + ...super._toProto(serializer), args: [toNumber(serializer, this.limit)] }; } @@ -342,10 +479,17 @@ export class Limit implements Stage { /** * @beta */ -export class Offset implements Stage { - name = 'offset'; +export class Offset extends Stage { + get _name(): string { + return 'offset'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(private offset: number) {} + constructor(private offset: number, options: StageOptions) { + super(options); + } /** * @internal @@ -353,7 +497,7 @@ export class Offset implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, + ...super._toProto(serializer), args: [toNumber(serializer, this.offset)] }; } @@ -362,10 +506,20 @@ export class Offset implements Stage { /** * @beta */ -export class Select implements Stage { - name = 'select'; +export class Select extends Stage { + get _name(): string { + return 'select'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(private projections: Map) {} + constructor( + private selections: Map, + options: StageOptions + ) { + super(options); + } /** * @internal @@ -373,19 +527,32 @@ export class Select implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [toMapValue(serializer, this.projections)] + ...super._toProto(serializer), + args: [toMapValue(serializer, this.selections)] }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.selections, context); + } } /** * @beta */ -export class Sort implements Stage { - name = 'sort'; +export class Sort extends Stage { + get _name(): string { + return 'sort'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(private orders: Ordering[]) {} + constructor(private orderings: Ordering[], options: StageOptions) { + super(options); + } /** * @internal @@ -393,105 +560,162 @@ export class Sort implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: this.orders.map(o => o._toProto(serializer)) + ...super._toProto(serializer), + args: this.orderings.map(o => o._toProto(serializer)) }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.orderings, context); + } } /** * @beta */ -export class Sample implements Stage { - name = 'sample'; +export class Sample extends Stage { + get _name(): string { + return 'sample'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(private limit: number, private mode: string) {} + constructor( + private rate: number, + private mode: 'percent' | 'documents', + options: StageOptions + ) { + super(options); + } _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [toNumber(serializer, this.limit)!, toStringValue(this.mode)!] + ...super._toProto(serializer), + args: [toNumber(serializer, this.rate)!, toStringValue(this.mode)!] }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } } /** * @beta */ -export class Union implements Stage { - name = 'union'; +export class Union extends Stage { + get _name(): string { + return 'union'; + } - constructor(private _other: Pipeline) {} + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private other: Pipeline, options: StageOptions) { + super(options); + } _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [toPipelineValue(this._other._toProto(serializer))] + ...super._toProto(serializer), + args: [toPipelineValue(this.other._toProto(serializer))] }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } } /** * @beta */ -export class Unnest implements Stage { - name = 'unnest'; +export class Unnest extends Stage { + get _name(): string { + return 'unnest'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + indexField: { + serverName: 'index_field' + } + }); + } + constructor( - private expr: Expr, - private alias: Field, - private indexField?: Field - ) {} + private alias: string, + private expr: Expression, + options: StageOptions + ) { + super(options); + } _toProto(serializer: JsonProtoSerializer): ProtoStage { - const stageProto: ProtoStage = { - name: this.name, - args: [this.expr._toProto(serializer), this.alias._toProto(serializer)] + return { + ...super._toProto(serializer), + args: [ + this.expr._toProto(serializer), + field(this.alias)._toProto(serializer) + ] }; + } - if (this.indexField) { - stageProto.options = { - ['index_field']: this.indexField._toProto(serializer) - }; - } - - return stageProto; + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.expr, context); } } /** * @beta */ -export class Replace implements Stage { - name = 'replace_with'; +export class Replace extends Stage { + static readonly MODE = 'full_replace'; - constructor( - private field: Expr, - private mode: - | 'full_replace' - | 'merge_prefer_nest' - | 'merge_prefer_parent' = 'full_replace' - ) {} + get _name(): string { + return 'replace_with'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private map: Expression, options: StageOptions) { + super(options); + } _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [this.field._toProto(serializer), toStringValue(this.mode)] + ...super._toProto(serializer), + args: [this.map._toProto(serializer), toStringValue(Replace.MODE)] }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.map, context); + } } /** * @beta */ -export class GenericStage implements Stage { +export class RawStage extends Stage { /** * @private * @internal */ constructor( - public name: string, - private params: Array - ) {} + private name: string, + private params: Array, + rawOptions: Record + ) { + super({ rawOptions }); + } /** * @internal @@ -500,7 +724,41 @@ export class GenericStage implements Stage { _toProto(serializer: JsonProtoSerializer): ProtoStage { return { name: this.name, - args: this.params.map(o => o._toProto(serializer)) + args: this.params.map(o => o._toProto(serializer)), + options: this.optionsProto }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.params, context); + } + + get _name(): string { + return this.name; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } +} + +/** + * Helper to read user data across a number of different formats. + * @param name Name of the calling function. Used for error messages when invalid user data is encountered. + * @param expressionMap + * @return the expressionMap argument. + * @private + */ +function readUserDataHelper< + T extends Map | UserData[] | UserData +>(expressionMap: T, context: ParseContext): T { + if (isUserData(expressionMap)) { + expressionMap._readUserData(context); + } else if (Array.isArray(expressionMap)) { + expressionMap.forEach(readableData => readableData._readUserData(context)); + } else { + expressionMap.forEach(expr => expr._readUserData(context)); + } + return expressionMap; } diff --git a/packages/firestore/src/lite-api/stage_options.ts b/packages/firestore/src/lite-api/stage_options.ts new file mode 100644 index 00000000000..e45c39e023e --- /dev/null +++ b/packages/firestore/src/lite-api/stage_options.ts @@ -0,0 +1,280 @@ +import { OneOf } from '../util/types'; + +import { + AggregateWithAlias, + BooleanExpression, + Expression, + Field, + Ordering, + Selectable +} from './expressions'; +import { Pipeline } from './pipeline'; +import { CollectionReference, DocumentReference } from './reference'; +import { VectorValue } from './vector_value'; + +/** + * Options defining how a Stage is evaluated. + */ +export interface StageOptions { + /** + * An escape hatch to set options not known at SDK build time. These values + * will be passed directly to the Firestore backend and not used by the SDK. + * + * The option name will be used as provided. And must match the name + * format used by the backend (hint: use a snake_case_name). + * + * Raw option values can be any type supported + * by Firestore (for example: string, boolean, number, map, …). Value types + * not known to the SDK will be rejected. + * + * Values specified in rawOptions will take precedence over any options + * with the same name set by the SDK. + * + * `rawOptions` supports dot notation, if you want to override + * a nested option. + */ + rawOptions?: { + [name: string]: unknown; + }; +} +/** + * Options defining how a CollectionStage is evaluated. See {@link PipelineSource.collection}. + */ +export type CollectionStageOptions = StageOptions & { + /** + * Name or reference to the collection that will be used as the Pipeline source. + */ + collection: string | CollectionReference; + + /** + * Specifies the name of an index to be used for a query, overriding the query optimizer's default choice. + * This can be useful for performance tuning in specific scenarios where the default index selection + * does not yield optimal performance. + * + * @remarks This property is optional. When provided, it should be the exact name of the index to force. + */ + forceIndex?: string; +}; + +/** + * Defines the configuration options for a {@link CollectionGroupStage} within a pipeline. + * This type extends {@link StageOptions} and provides specific settings for how a collection group + * is identified and processed during pipeline execution. + * + * @see {@link PipelineSource.collectionGroup} to create a collection group stage. + */ +export type CollectionGroupStageOptions = StageOptions & { + /** + * ID of the collection group to use as the Pipeline source. + */ + collectionId: string; + + /** + * Specifies the name of an index to be used for a query, overriding the query optimizer's default choice. + * This can be useful for performance tuning in specific scenarios where the default index selection + * does not yield optimal performance. + * + * @remarks This property is optional. When provided, it should be the exact name of the index to force. + */ + forceIndex?: string; +}; +/** + * Options defining how a DatabaseStage is evaluated. See {@link PipelineSource.database}. + */ +export type DatabaseStageOptions = StageOptions & {}; +/** + * Options defining how a DocumentsStage is evaluated. See {@link PipelineSource.documents}. + */ +export type DocumentsStageOptions = StageOptions & { + /** + * An array of paths and DocumentReferences specifying the individual documents that will be the source of this pipeline. + * The converters for these DocumentReferences will be ignored and not have an effect on this pipeline. + * There must be at least one document specified in the array. + */ + docs: Array; +}; +/** + * Options defining how an AddFieldsStage is evaluated. See {@link Pipeline.addFields}. + */ +export type AddFieldsStageOptions = StageOptions & { + /** + * The fields to add to each document, specified as a {@link Selectable}. + * At least one field is required. + */ + fields: Selectable[]; +}; +/** + * Options defining how a RemoveFieldsStage is evaluated. See {@link Pipeline.removeFields}. + */ +export type RemoveFieldsStageOptions = StageOptions & { + /** + * The fields to remove from each document. + */ + fields: Array; +}; +/** + * Options defining how a SelectStage is evaluated. See {@link Pipeline.select}. + */ +export type SelectStageOptions = StageOptions & { + /** + * The fields to include in the output documents, specified as {@link Selectable} expression + * or as a string value indicating the field name. + */ + selections: Array; +}; +/** + * Options defining how a WhereStage is evaluated. See {@link Pipeline.where}. + */ +export type WhereStageOptions = StageOptions & { + /** + * The {@link BooleanExpression} to apply as a filter for each input document to this stage. + */ + condition: BooleanExpression; +}; +/** + * Options defining how an OffsetStage is evaluated. See {@link Pipeline.offset}. + */ +export type OffsetStageOptions = StageOptions & { + /** + * The number of documents to skip. + */ + offset: number; +}; +/** + * Options defining how a LimitStage is evaluated. See {@link Pipeline.limit}. + */ +export type LimitStageOptions = StageOptions & { + /** + * The maximum number of documents to return. + */ + limit: number; +}; +/** + * Options defining how a DistinctStage is evaluated. See {@link Pipeline.distinct}. + */ +export type DistinctStageOptions = StageOptions & { + /** + * The {@link Selectable} expressions or field names to consider when determining + * distinct value combinations (groups). + */ + groups: Array; +}; + +/** + * Options defining how an AggregateStage is evaluated. See {@link Pipeline.aggregate}. + */ +export type AggregateStageOptions = StageOptions & { + /** + * The {@link AliasedAggregate} values specifying aggregate operations to + * perform on the input documents. + */ + accumulators: AggregateWithAlias[]; + /** + * The {@link Selectable} expressions or field names to consider when determining + * distinct value combinations (groups), which will be aggregated over. + */ + groups?: Array; +}; +/** + * Options defining how a FindNearestStage is evaluated. See {@link Pipeline.findNearest}. + */ +export type FindNearestStageOptions = StageOptions & { + /** + * Specifies the field to be used. This can be a string representing the field path + * (e.g., 'fieldName', 'nested.fieldName') or an object of type {@link Field} + * representing a more complex field expression. + */ + field: Field | string; + /** + * Specifies the query vector value, to which the vector distance will be computed. + */ + vectorValue: VectorValue | number[]; + /** + * Specifies the method used to compute the distance between vectors. + * + * Possible values are: + * - `'euclidean'`: Euclidean distance. + * - `'cosine'`: Cosine similarity. + * - `'dot_product'`: Dot product. + */ + distanceMeasure: 'euclidean' | 'cosine' | 'dot_product'; + /** + * The maximum number of documents to return from the FindNearest stage. + */ + limit?: number; + /** + * If set, specifies the field on the output documents that will contain + * the computed vector distance for the document. If not set, the computed + * vector distance will not be returned. + */ + distanceField?: string; +}; +/** + * Options defining how a ReplaceWithStage is evaluated. See {@link Pipeline.replaceWith}. + */ +export type ReplaceWithStageOptions = StageOptions & { + /** + * The name of a field that contains a map or an {@link Expression} that + * evaluates to a map. + */ + map: Expression | string; +}; +/** + * Defines the options for evaluating a sample stage within a pipeline. + * This type combines common {@link StageOptions} with a specific configuration + * where only one of the defined sampling methods can be applied. + * + * See {@link Pipeline.sample} to create a sample stage.. + */ +export type SampleStageOptions = StageOptions & + OneOf<{ + /** + * If set, specifies the sample rate as a percentage of the + * input documents. + * + * Cannot be set when `documents: number` is set. + */ + percentage: number; + /** + * If set, specifies the sample rate as a total number of + * documents to sample from the input documents. + * + * Cannot be set when `percentage: number` is set. + */ + documents: number; + }>; +/** + * Options defining how a UnionStage is evaluated. See {@link Pipeline.union}. + */ +export type UnionStageOptions = StageOptions & { + /** + * Specifies the other Pipeline to union with. + */ + other: Pipeline; +}; + +/** + * Represents the specific options available for configuring an `UnnestStage` within a pipeline. + */ +export type UnnestStageOptions = StageOptions & { + /** + * A `Selectable` object that defines an array expression to be un-nested + * and the alias for the un-nested field. + */ + selectable: Selectable; + /** + * If set, specifies the field on the output documents that will contain the + * offset (starting at zero) that the element is from the original array. + */ + indexField?: string; +}; +/** + * Options defining how a SortStage is evaluated. See {@link Pipeline.sort}. + */ +export type SortStageOptions = StageOptions & { + /** + * Orderings specify how the input documents are sorted. + * One or more ordering are required. + */ + orderings: Ordering[]; +}; diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index 905b13edd6f..8ea2728b3a7 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -1079,3 +1079,11 @@ function fieldMaskContains( ): boolean { return haystack.some(v => v.isEqual(needle)); } + +export interface UserData { + _readUserData(context: ParseContext): void; +} + +export function isUserData(value: unknown): value is UserData { + return typeof (value as UserData)._readUserData === 'function'; +} diff --git a/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts b/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts index 709509c8a4e..0ccebd779f5 100644 --- a/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts +++ b/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts @@ -16,3 +16,10 @@ */ export { toByteStreamReader } from '../browser/byte_stream_reader'; + +// This is not included in the RN lite-bundle, but the rollup build +// will fail if these exports are not defined. +export { + buildDocumentSnapshotJsonBundle, + buildQuerySnapshotJsonBundle +} from '../browser/snapshot_to_json'; diff --git a/packages/firestore/src/remote/datastore.ts b/packages/firestore/src/remote/datastore.ts index 10566503ca3..081b8cf5c9a 100644 --- a/packages/firestore/src/remote/datastore.ts +++ b/packages/firestore/src/remote/datastore.ts @@ -20,7 +20,7 @@ import { User } from '../auth/user'; import { Aggregate } from '../core/aggregate'; import { DatabaseId } from '../core/database_info'; import { queryToAggregateTarget, Query, queryToTarget } from '../core/query'; -import { Pipeline } from '../lite-api/pipeline'; +import { StructuredPipeline } from '../core/structured_pipeline'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { Mutation } from '../model/mutation'; @@ -244,14 +244,12 @@ export async function invokeBatchGetDocumentsRpc( export async function invokeExecutePipeline( datastore: Datastore, - pipeline: Pipeline + structuredPipeline: StructuredPipeline ): Promise { const datastoreImpl = debugCast(datastore, DatastoreImpl); const executePipelineRequest: ProtoExecutePipelineRequest = { database: getEncodedDatabaseId(datastoreImpl.serializer), - structuredPipeline: { - pipeline: pipeline._toProto(datastoreImpl.serializer) - } + structuredPipeline: structuredPipeline._toProto(datastoreImpl.serializer) }; const response = await datastoreImpl.invokeStreamingRPC< @@ -264,17 +262,22 @@ export async function invokeExecutePipeline( executePipelineRequest ); - return response + const result: PipelineStreamElement[] = []; + response .filter(proto => !!proto.results) - .flatMap(proto => { + .forEach(proto => { if (proto.results!.length === 0) { - return fromPipelineResponse(datastoreImpl.serializer, proto); + result.push(fromPipelineResponse(datastoreImpl.serializer, proto)); } else { - return proto.results!.map(result => - fromPipelineResponse(datastoreImpl.serializer, proto, result) + return proto.results!.forEach(document => + result.push( + fromPipelineResponse(datastoreImpl.serializer, proto, document) + ) ); } }); + + return result; } export async function invokeRunQueryRpc( diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index d08e0d16874..f11781ac331 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import { ParseContext } from '../api/parse_context'; import { Aggregate } from '../core/aggregate'; import { Bound } from '../core/bound'; import { DatabaseId } from '../core/database_info'; @@ -41,7 +40,6 @@ import { TargetId } from '../core/types'; import { Bytes } from '../lite-api/bytes'; import { GeoPoint } from '../lite-api/geo_point'; import { Timestamp } from '../lite-api/timestamp'; -import { UserDataReader } from '../lite-api/user_data_reader'; import { TargetData, TargetPurpose } from '../local/target_data'; import { MutableDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; @@ -1475,10 +1473,6 @@ export function isProtoValueSerializable( ); } -export interface UserData { - _readUserData(dataReader: UserDataReader, context?: ParseContext): void; -} - export function toMapValue( serializer: JsonProtoSerializer, input: Map> diff --git a/packages/firestore/src/util/input_validation.ts b/packages/firestore/src/util/input_validation.ts index 7fd9967b5a0..f3b5dda6985 100644 --- a/packages/firestore/src/util/input_validation.ts +++ b/packages/firestore/src/util/input_validation.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { DocumentData } from '../lite-api/reference'; import { DocumentKey } from '../model/document_key'; import { ResourcePath } from '../model/path'; @@ -92,7 +93,7 @@ export function validateCollectionPath(path: ResourcePath): void { * Returns true if it's a non-null object without a custom prototype * (i.e. excludes Array, Date, etc.). */ -export function isPlainObject(input: unknown): boolean { +export function isPlainObject(input: unknown): input is DocumentData { return ( typeof input === 'object' && input !== null && diff --git a/packages/firestore/src/util/pipeline_util.ts b/packages/firestore/src/util/pipeline_util.ts new file mode 100644 index 00000000000..63b8fce1b48 --- /dev/null +++ b/packages/firestore/src/util/pipeline_util.ts @@ -0,0 +1,115 @@ +import { vector } from '../api'; +import { + _constant, + AggregateFunction, + AggregateWithAlias, + array, + constant, + Expression, + AliasedExpression, + field, + Field, + map, + Selectable +} from '../lite-api/expressions'; +import { VectorValue } from '../lite-api/vector_value'; + +import { isPlainObject } from './input_validation'; +import { isFirestoreValue } from './proto'; +import { isString } from './types'; + +export function selectablesToMap( + selectables: Array +): Map { + const result = new Map(); + for (const selectable of selectables) { + if (typeof selectable === 'string') { + result.set(selectable as string, field(selectable)); + } else if (selectable instanceof Field) { + result.set(selectable.alias, selectable.expr); + } else if (selectable instanceof AliasedExpression) { + result.set(selectable.alias, selectable.expr); + } + } + return result; +} + +export function aliasedAggregateToMap( + aliasedAggregatees: AggregateWithAlias[] +): Map { + return aliasedAggregatees.reduce( + (map: Map, selectable: AggregateWithAlias) => { + map.set(selectable.alias, selectable.aggregate as AggregateFunction); + return map; + }, + new Map() as Map + ); +} + +/** + * Converts a value to an Expr, Returning either a Constant, MapFunction, + * ArrayFunction, or the input itself (if it's already an expression). + * + * @private + * @internal + * @param value + */ +export function vectorToExpr( + value: VectorValue | number[] | Expression +): Expression { + if (value instanceof Expression) { + return value; + } else if (value instanceof VectorValue) { + const result = constant(value); + return result; + } else if (Array.isArray(value)) { + const result = constant(vector(value)); + return result; + } else { + throw new Error('Unsupported value: ' + typeof value); + } +} + +/** + * Converts a value to an Expr, Returning either a Constant, MapFunction, + * ArrayFunction, or the input itself (if it's already an expression). + * If the input is a string, it is assumed to be a field name, and a + * field(value) is returned. + * + * @private + * @internal + * @param value + */ +export function fieldOrExpression(value: unknown): Expression { + if (isString(value)) { + const result = field(value); + return result; + } else { + return valueToDefaultExpr(value); + } +} +/** + * Converts a value to an Expr, Returning either a Constant, MapFunction, + * ArrayFunction, or the input itself (if it's already an expression). + * + * @private + * @internal + * @param value + */ +export function valueToDefaultExpr(value: unknown): Expression { + let result: Expression | undefined; + if (isFirestoreValue(value)) { + return constant(value); + } + if (value instanceof Expression) { + return value; + } else if (isPlainObject(value)) { + result = map(value as Record); + } else if (value instanceof Array) { + result = array(value); + } else { + result = _constant(value, undefined); + } + + return result; +} diff --git a/packages/firestore/src/util/types.ts b/packages/firestore/src/util/types.ts index 361ebda1935..89ed50a240b 100644 --- a/packages/firestore/src/util/types.ts +++ b/packages/firestore/src/util/types.ts @@ -37,6 +37,10 @@ export function isNegativeZero(value: number): boolean { return value === 0 && 1 / value === 1 / -0; } +export function isNumber(value: unknown): value is number { + return typeof value === 'number'; +} + /** * Returns whether a value is an integer and in the safe integer range * @param value - The value to test for being an integer and in the safe range @@ -69,3 +73,18 @@ export interface DocumentLike { addEventListener(type: string, listener: EventListener): void; removeEventListener(type: string, listener: EventListener): void; } + +/** + * Utility type to create an type that only allows one + * property of the Type param T to be set. + * + * type XorY = OneOf<{ x: unknown, y: unknown}> + * let a = { x: "foo" } // OK + * let b = { y: "foo" } // OK + * let c = { a: "foo", y: "foo" } // Not OK + */ +export type OneOf = { + [K in keyof T]: Pick & { + [P in Exclude]?: undefined; + }; +}[keyof T]; diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index fb04f775972..11d13dbb07f 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -15,25 +15,10 @@ * limitations under the License. */ +import { FirebaseError } from '@firebase/util'; import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { - AggregateFunction, - ascending, - BooleanExpr, - byteLength, - constantVector, - FunctionExpr, - timestampAdd, - timestampToUnixMicros, - timestampToUnixMillis, - timestampToUnixSeconds, - toLower, - unixMicrosToTimestamp, - unixMillisToTimestamp, - vectorLength -} from '../../../src/lite-api/expressions'; import { PipelineSnapshot } from '../../../src/lite-api/pipeline-result'; import { addEqualityMatcher } from '../../util/equality_matcher'; import { Deferred } from '../../util/promise'; @@ -53,16 +38,16 @@ import { collection, documentId as documentIdFieldPath, writeBatch, - addDoc + addDoc, + DocumentReference, + deleteDoc } from '../util/firebase_export'; -import { apiDescribe, withTestCollection, itIf } from '../util/helpers'; +import { apiDescribe, withTestCollection } from '../util/helpers'; import { array, mod, pipelineResultEqual, sum, - replaceFirst, - replaceAll, descending, isNan, map, @@ -74,48 +59,39 @@ import { arrayContains, arrayContainsAny, count, - avg, + average, cosineDistance, not, countAll, dotProduct, endsWith, - eq, + equal, reverse, toUpper, euclideanDistance, - gt, + greaterThan, like, - lt, - strContains, + lessThan, + stringContains, divide, - lte, + lessThanOrEqual, arrayLength, - arrayConcat, mapGet, - neq, + notEqual, or, regexContains, regexMatch, startsWith, - strConcat, + stringConcat, subtract, - cond, - eqAny, + conditional, + equalAny, logicalMaximum, - notEqAny, + notEqualAny, multiply, countIf, - bitAnd, - bitOr, - bitXor, - bitNot, exists, - bitLeftShift, charLength, - bitRightShift, - rand, - arrayOffset, minimum, maximum, isError, @@ -125,27 +101,60 @@ import { isNull, isNotNull, isNotNan, - timestampSub, + timestampSubtract, mapRemove, mapMerge, documentId, - substr, + substring, logicalMinimum, xor, field, constant, _internalPipelineToExecutePipelineRequestProto, - FindNearestOptions + FindNearestStageOptions, + AggregateFunction, + arrayGet, + ascending, + BooleanExpression, + byteLength, + FunctionExpression, + timestampAdd, + timestampToUnixMicros, + timestampToUnixMillis, + timestampToUnixSeconds, + toLower, + unixMicrosToTimestamp, + unixMillisToTimestamp, + vectorLength, + countDistinct, + ceil, + floor, + exp, + pow, + round, + collectionId, + ln, + log, + sqrt, + stringReverse, + len as length, + abs, + concat, + error, + currentTimestamp, + ifAbsent, + join, + log10, + arraySum } from '../util/pipeline_export'; use(chaiAsPromised); setLogLevel('debug'); -const testUnsupportedFeatures: boolean | 'only' = false; const timestampDeltaMS = 1000; -apiDescribe('Pipelines', persistence => { +apiDescribe.only('Pipelines', persistence => { addEqualityMatcher(); let firestore: Firestore; @@ -333,6 +342,23 @@ apiDescribe('Pipelines', persistence => { await withTestCollectionPromise; }); + describe('console support', () => { + it('supports internal serialization to proto', async () => { + const pipeline = firestore + .pipeline() + .collection('books') + .where(equal('awards.hugo', true)) + .select( + 'title', + field('nestedField.level.1'), + mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') + ); + + const proto = _internalPipelineToExecutePipelineRequestProto(pipeline); + expect(proto).not.to.be.null; + }); + }); + describe('pipeline results', () => { it('empty snapshot as expected', async () => { const snapshot = await execute( @@ -348,7 +374,6 @@ apiDescribe('Pipelines', persistence => { .sort(ascending('__name__')); const snapshot = await execute(ppl); expect(snapshot.results.length).to.equal(10); - expect(snapshot.pipeline).to.equal(ppl); expectResults( snapshot, 'book1', @@ -448,7 +473,7 @@ apiDescribe('Pipelines', persistence => { const pipeline = firestore .pipeline() .collection(randomCol.path) - .aggregate(avg('rating').as('avgRating')); + .aggregate(average('rating').as('avgRating')); const snapshot = await execute(pipeline); const end = new Date().valueOf(); @@ -466,7 +491,7 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .aggregate({ - accumulators: [avg('rating').as('avgRating')], + accumulators: [average('rating').as('avgRating')], groups: ['genre'] }); @@ -524,31 +549,26 @@ apiDescribe('Pipelines', persistence => { await terminate(db2); }); - // Subcollections not currently supported in DBE - itIf(testUnsupportedFeatures)( - 'supports collection group as source', - async () => { - const randomSubCollectionId = Math.random().toString(16).slice(2); - const doc1 = await addDoc( - collection(randomCol, 'book1', randomSubCollectionId), - { order: 1 } - ); - const doc2 = await addDoc( - collection(randomCol, 'book2', randomSubCollectionId), - { order: 2 } - ); - const snapshot = await execute( - firestore - .pipeline() - .collectionGroup(randomSubCollectionId) - .sort(ascending('order')) - ); - expectResults(snapshot, doc1.id, doc2.id); - } - ); + it('supports collection group as source', async () => { + const randomSubCollectionId = Math.random().toString(16).slice(2); + const doc1 = await addDoc( + collection(randomCol, 'book1', randomSubCollectionId), + { order: 1 } + ); + const doc2 = await addDoc( + collection(randomCol, 'book2', randomSubCollectionId), + { order: 2 } + ); + const snapshot = await execute( + firestore + .pipeline() + .collectionGroup(randomSubCollectionId) + .sort(ascending('order')) + ); + expectResults(snapshot, doc1.id, doc2.id); + }); - // subcollections not currently supported in dbe - itIf(testUnsupportedFeatures)('supports database as source', async () => { + it('supports database as source', async () => { const randomId = Math.random().toString(16).slice(2); const doc1 = await addDoc(collection(randomCol, 'book1', 'sub'), { order: 1, @@ -562,7 +582,7 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .database() - .where(eq('randomId', randomId)) + .where(equal('randomId', randomId)) .sort(ascending('order')) ); expectResults(snapshot, doc1.id, doc2.id); @@ -585,8 +605,7 @@ apiDescribe('Pipelines', persistence => { Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])) ).as('bytes'), constant(doc(firestore, 'foo', 'bar')).as('documentReference'), - constantVector(vector([1, 2, 3])).as('vectorValue'), - constantVector([1, 2, 3]).as('vectorValue2'), + constant(vector([1, 2, 3])).as('vectorValue'), map({ 'number': 1, 'string': 'a string', @@ -643,7 +662,6 @@ apiDescribe('Pipelines', persistence => { 'bytes': Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])), 'documentReference': doc(firestore, 'foo', 'bar'), 'vectorValue': vector([1, 2, 3]), - 'vectorValue2': vector([1, 2, 3]), 'map': { 'number': 1, 'string': 'a string', @@ -749,7 +767,7 @@ apiDescribe('Pipelines', persistence => { ) .where( and( - eq('metadataArray', [ + equal('metadataArray', [ 1, 2, field('genre'), @@ -759,7 +777,7 @@ apiDescribe('Pipelines', persistence => { published: field('published') } ]), - eq('metadata', { + equal('metadata', { genre: field('genre'), rating: multiply('rating', 10), nestedArray: [field('title')], @@ -801,6 +819,30 @@ apiDescribe('Pipelines', persistence => { } }); }); + + it('supports boolean value constants as a BooleanExpression', async () => { + const snapshots = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + conditional(constant(true), constant('TRUE'), constant('FALSE')).as( + 'true' + ), + conditional( + constant(false), + constant('TRUE'), + constant('FALSE') + ).as('false') + ) + ); + + expectResults(snapshots, { + 'true': 'TRUE', + 'false': 'FALSE' + }); + }); }); describe('stages', () => { @@ -818,10 +860,41 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('genre', 'Science Fiction')) + .where(equal('genre', 'Science Fiction')) + .aggregate( + countAll().as('count'), + average('rating').as('avgRating'), + maximum('rating').as('maxRating'), + sum('rating').as('sumRating') + ) + ); + expectResults(snapshot, { + count: 2, + avgRating: 4.4, + maxRating: 4.6, + sumRating: 8.8 + }); + }); + + it('supports aggregate options', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate({ + accumulators: [countAll().as('count')] + }) + ); + expectResults(snapshot, { count: 10 }); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('genre', 'Science Fiction')) .aggregate( countAll().as('count'), - avg('rating').as('avgRating'), + average('rating').as('avgRating'), maximum('rating').as('maxRating'), sum('rating').as('sumRating') ) @@ -840,7 +913,7 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(lt('published', 1900)) + .where(lessThan('published', 1900)) .aggregate({ accumulators: [], groups: ['genre'] @@ -854,12 +927,12 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(lt(field('published'), 1984)) + .where(lessThan(field('published'), 1984)) .aggregate({ - accumulators: [avg('rating').as('avgRating')], + accumulators: [average('rating').as('avgRating')], groups: ['genre'] }) - .where(gt('avgRating', 4.3)) + .where(greaterThan('avgRating', 4.3)) .sort(field('avgRating').descending()) ); expectResults( @@ -895,7 +968,7 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .aggregate(countIf(field('rating').gt(4.3)).as('count')) + .aggregate(countIf(field('rating').greaterThan(4.3)).as('count')) ); const expectedResults = { count: 3 @@ -906,10 +979,20 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .aggregate(field('rating').gt(4.3).countIf().as('count')) + .aggregate(field('rating').greaterThan(4.3).countIf().as('count')) ); expectResults(snapshot, expectedResults); }); + + it('returns countDistinct accumulation', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(countDistinct('genre').as('distinctGenres')) + ); + expectResults(snapshot, { distinctGenres: 8 }); + }); }); describe('distinct stage', () => { @@ -935,6 +1018,34 @@ apiDescribe('Pipelines', persistence => { { genre: 'Southern Gothic', author: 'Harper Lee' } ); }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .distinct('genre', 'author') + .sort({ + orderings: [ + field('genre').ascending(), + field('author').ascending() + ] + }) + ); + expectResults( + snapshot, + { genre: 'Dystopian', author: 'George Orwell' }, + { genre: 'Dystopian', author: 'Margaret Atwood' }, + { genre: 'Fantasy', author: 'J.R.R. Tolkien' }, + { genre: 'Magical Realism', author: 'Gabriel García Márquez' }, + { genre: 'Modernist', author: 'F. Scott Fitzgerald' }, + { genre: 'Psychological Thriller', author: 'Fyodor Dostoevsky' }, + { genre: 'Romance', author: 'Jane Austen' }, + { genre: 'Science Fiction', author: 'Douglas Adams' }, + { genre: 'Science Fiction', author: 'Frank Herbert' }, + { genre: 'Southern Gothic', author: 'Harper Lee' } + ); + }); }); describe('select stage', () => { @@ -966,6 +1077,25 @@ apiDescribe('Pipelines', persistence => { { title: "The Handmaid's Tale", author: 'Margaret Atwood' } ); }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select({ selections: ['title', field('author').as('auth0r')] }) + .sort(field('auth0r').ascending()) + .limit(2) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + auth0r: 'Douglas Adams' + }, + { title: 'The Great Gatsby', auth0r: 'F. Scott Fitzgerald' } + ); + }); }); describe('addField stage', () => { @@ -1020,6 +1150,60 @@ apiDescribe('Pipelines', persistence => { } ); }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .addFields({ + fields: [constant('bar').as('foo')] + }) + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + foo: 'bar' + }, + { + title: 'The Great Gatsby', + author: 'F. Scott Fitzgerald', + foo: 'bar' + }, + { title: 'Dune', author: 'Frank Herbert', foo: 'bar' }, + { + title: 'Crime and Punishment', + author: 'Fyodor Dostoevsky', + foo: 'bar' + }, + { + title: 'One Hundred Years of Solitude', + author: 'Gabriel García Márquez', + foo: 'bar' + }, + { title: '1984', author: 'George Orwell', foo: 'bar' }, + { + title: 'To Kill a Mockingbird', + author: 'Harper Lee', + foo: 'bar' + }, + { + title: 'The Lord of the Rings', + author: 'J.R.R. Tolkien', + foo: 'bar' + }, + { title: 'Pride and Prejudice', author: 'Jane Austen', foo: 'bar' }, + { + title: "The Handmaid's Tale", + author: 'Margaret Atwood', + foo: 'bar' + } + ); + }); }); describe('removeFields stage', () => { @@ -1061,76 +1245,201 @@ apiDescribe('Pipelines', persistence => { } ); }); - }); - describe('where stage', () => { - it('where with and (2 conditions)', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .where( - and( - gt('rating', 4.5), - eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) - ) - ) - ); - expectResults(snapshot, 'book10', 'book4'); - }); - it('where with and (3 conditions)', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .where( - and( - gt('rating', 4.5), - eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), - lt('published', 1965) - ) - ) - ); - expectResults(snapshot, 'book4'); - }); - it('where with or', async () => { + it('supports options', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .where( - or( - eq('genre', 'Romance'), - eq('genre', 'Dystopian'), - eq('genre', 'Fantasy') - ) - ) - .sort(ascending('title')) - .select('title') + .select('title', 'author', 'genre') + .sort(field('author').ascending()) + .removeFields({ + fields: [field('author'), 'genre'] + }) + .sort(field('author').ascending()) ); expectResults( snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'The Great Gatsby' + }, + { title: 'Dune' }, + { + title: 'Crime and Punishment' + }, + { + title: 'One Hundred Years of Solitude' + }, { title: '1984' }, + { + title: 'To Kill a Mockingbird' + }, + { + title: 'The Lord of the Rings' + }, { title: 'Pride and Prejudice' }, - { title: "The Handmaid's Tale" }, - { title: 'The Lord of the Rings' } + { + title: "The Handmaid's Tale" + } ); }); + }); - it('where with xor', async () => { + describe('findNearest stage', () => { + it('can find nearest', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .where( - xor( - eq('genre', 'Romance'), - eq('genre', 'Dystopian'), - eq('genre', 'Fantasy'), - eq('published', 1949) - ) - ) - .select('title') + .select('title', 'author') + .sort(field('author').ascending()) + .removeFields(field('author')) + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'The Great Gatsby' + }, + { title: 'Dune' }, + { + title: 'Crime and Punishment' + }, + { + title: 'One Hundred Years of Solitude' + }, + { title: '1984' }, + { + title: 'To Kill a Mockingbird' + }, + { + title: 'The Lord of the Rings' + }, + { title: 'Pride and Prejudice' }, + { + title: "The Handmaid's Tale" + } + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'genre') + .sort(field('author').ascending()) + .removeFields({ + fields: [field('author'), 'genre'] + }) + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'The Great Gatsby' + }, + { title: 'Dune' }, + { + title: 'Crime and Punishment' + }, + { + title: 'One Hundred Years of Solitude' + }, + { title: '1984' }, + { + title: 'To Kill a Mockingbird' + }, + { + title: 'The Lord of the Rings' + }, + { title: 'Pride and Prejudice' }, + { + title: "The Handmaid's Tale" + } + ); + }); + }); + + describe('where stage', () => { + it('where with and (2 conditions)', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + and( + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + ) + ) + ); + expectResults(snapshot, 'book10', 'book4'); + }); + + it('where with and (3 conditions)', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + and( + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), + lessThan('published', 1965) + ) + ) + ); + expectResults(snapshot, 'book4'); + }); + + it('where with or', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + or( + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('genre', 'Fantasy') + ) + ) + .sort(ascending('title')) + .select('title') + ); + expectResults( + snapshot, + { title: '1984' }, + { title: 'Pride and Prejudice' }, + { title: "The Handmaid's Tale" }, + { title: 'The Lord of the Rings' } + ); + }); + + it('where with xor', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + xor( + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('genre', 'Fantasy'), + equal('published', 1949) + ) + ) + .select('title') ); expectResults( snapshot, @@ -1140,17 +1449,20 @@ apiDescribe('Pipelines', persistence => { ); }); - // it('where with boolean constant', async () => { - // const snapshot = await execute( - // firestore - // .pipeline() - // .collection(randomCol.path) - // .where(constant(true)) - // .sort(ascending('__name__')) - // .limit(2) - // ); - // expectResults(snapshot, 'book1', 'book10'); - // }); + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where({ + condition: and( + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + ) + }) + ); + expectResults(snapshot, 'book10', 'book4'); + }); }); describe('sort, offset, and limit stages', () => { @@ -1171,19 +1483,39 @@ apiDescribe('Pipelines', persistence => { { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' } ); }); + + it('sort, offset, and limit stages support options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort({ + orderings: [field('author').ascending()] + }) + .offset({ offset: 5 }) + .limit({ limit: 3 }) + .select('title', 'author') + ); + expectResults( + snapshot, + { title: '1984', author: 'George Orwell' }, + { title: 'To Kill a Mockingbird', author: 'Harper Lee' }, + { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' } + ); + }); }); - describe('generic stage', () => { + describe('raw stage', () => { it('can select fields', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .genericStage('select', [ + .rawStage('select', [ { title: field('title'), metadata: { - 'author': field('author') + author: field('author') } } ]) @@ -1206,9 +1538,9 @@ apiDescribe('Pipelines', persistence => { .sort(field('author').ascending()) .limit(1) .select('title', 'author') - .genericStage('add_fields', [ + .rawStage('add_fields', [ { - display: strConcat('title', ' - ', field('author')) + display: stringConcat('title', ' - ', field('author')) } ]) ); @@ -1225,7 +1557,7 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select('title', 'author') - .genericStage('where', [field('author').eq('Douglas Adams')]) + .rawStage('where', [field('author').equal('Douglas Adams')]) ); expectResults(snapshot, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1239,14 +1571,14 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select('title', 'author') - .genericStage('sort', [ + .rawStage('sort', [ { direction: 'ascending', expression: field('author') } ]) - .genericStage('offset', [3]) - .genericStage('limit', [1]) + .rawStage('offset', [3]) + .rawStage('limit', [1]) ); expectResults(snapshot, { author: 'Fyodor Dostoevsky', @@ -1260,8 +1592,8 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select('title', 'author', 'rating') - .genericStage('aggregate', [ - { averageRating: field('rating').avg() }, + .rawStage('aggregate', [ + { averageRating: field('rating').average() }, {} ]) ); @@ -1276,7 +1608,7 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select('title', 'author', 'rating') - .genericStage('distinct', [{ rating: field('rating') }]) + .rawStage('distinct', [{ rating: field('rating') }]) .sort(field('rating').descending()) ); expectResults( @@ -1304,6 +1636,38 @@ apiDescribe('Pipelines', persistence => { } ); }); + + it('can perform FindNearest query', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .rawStage( + 'find_nearest', + [ + field('embedding'), + vector([10, 1, 2, 1, 1, 1, 1, 1, 1, 1]), + 'euclidean' + ], + { + 'distance_field': field('computedDistance'), + limit: 2 + } + ) + .select('title', 'computedDistance') + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + computedDistance: 1 + }, + { + title: 'One Hundred Years of Solitude', + computedDistance: 12.041594578792296 + } + ); + }); }); describe('replaceWith stage', () => { @@ -1312,7 +1676,7 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .replaceWith('awards') ); expectResults(snapshot, { @@ -1327,7 +1691,7 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .replaceWith( map({ foo: 'bar', @@ -1342,6 +1706,21 @@ apiDescribe('Pipelines', persistence => { baz: { title: "The Hitchhiker's Guide to the Galaxy" } }); }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .replaceWith({ map: 'awards' }) + ); + expectResults(snapshot, { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }); + }); }); describe('sample stage', () => { @@ -1364,7 +1743,7 @@ apiDescribe('Pipelines', persistence => { it('run pipeline with sample limit of {percentage: 0.6}', async () => { let avgSize = 0; - const numIterations = 20; + const numIterations = 30; for (let i = 0; i < numIterations; i++) { const snapshot = await execute( firestore @@ -1413,6 +1792,39 @@ apiDescribe('Pipelines', persistence => { 'book9' ); }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .union({ other: firestore.pipeline().collection(randomCol.path) }) + .sort(field(documentIdFieldPath()).ascending()) + ); + expectResults( + snapshot, + 'book1', + 'book1', + 'book10', + 'book10', + 'book2', + 'book2', + 'book3', + 'book3', + 'book4', + 'book4', + 'book5', + 'book5', + 'book6', + 'book6', + 'book7', + 'book7', + 'book8', + 'book8', + 'book9', + 'book9' + ); + }); }); describe('unnest stage', () => { @@ -1421,8 +1833,8 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) - .unnest(field('tags').as('tag'), 'tagsIndex') + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(field('tags').as('tag')) .select( 'title', 'author', @@ -1484,13 +1896,14 @@ apiDescribe('Pipelines', persistence => { } ); }); - it('unnest an expr', async () => { + + it('unnest with index field', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) - .unnest(array([1, 2, 3]).as('copy')) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(field('tags').as('tag'), 'tagsIndex') .select( 'title', 'author', @@ -1498,9 +1911,10 @@ apiDescribe('Pipelines', persistence => { 'published', 'rating', 'tags', - 'copy', + 'tag', 'awards', - 'nestedField' + 'nestedField', + 'tagsIndex' ) ); expectResults( @@ -1512,13 +1926,14 @@ apiDescribe('Pipelines', persistence => { published: 1979, rating: 4.2, tags: ['comedy', 'space', 'adventure'], - copy: 1, + tag: 'comedy', awards: { hugo: true, nebula: false, others: { unknown: { year: 1980 } } }, - nestedField: { 'level.1': { 'level.2': true } } + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 0 }, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1527,13 +1942,14 @@ apiDescribe('Pipelines', persistence => { published: 1979, rating: 4.2, tags: ['comedy', 'space', 'adventure'], - copy: 2, + tag: 'space', awards: { hugo: true, nebula: false, others: { unknown: { year: 1980 } } }, - nestedField: { 'level.1': { 'level.2': true } } + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 1 }, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1542,38 +1958,184 @@ apiDescribe('Pipelines', persistence => { published: 1979, rating: 4.2, tags: ['comedy', 'space', 'adventure'], - copy: 3, + tag: 'adventure', awards: { hugo: true, nebula: false, others: { unknown: { year: 1980 } } }, - nestedField: { 'level.1': { 'level.2': true } } + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 2 } ); }); - }); - describe('findNearest stage', () => { - it('run pipeline with findNearest', async () => { - const measures: Array = [ - 'euclidean', - 'dot_product', - 'cosine' - ]; - for (const measure of measures) { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol) - .findNearest({ - field: 'embedding', - vectorValue: vector([10, 1, 3, 1, 2, 1, 1, 1, 1, 1]), - limit: 3, - distanceMeasure: measure - }) - .select('title') - ); + it('unnest an expr', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(array([1, 2, 3]).as('copy')) + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'copy', + 'awards', + 'nestedField' + ) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 1, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 2, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 3, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + } + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest({ + selectable: field('tags').as('tag'), + indexField: 'tagsIndex' + }) + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'tag', + 'awards', + 'nestedField', + 'tagsIndex' + ) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'comedy', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 0 + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'space', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 1 + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'adventure', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 2 + } + ); + }); + }); + + describe('findNearest stage', () => { + it('run pipeline with findNearest', async () => { + const measures: Array = [ + 'euclidean', + 'dot_product', + 'cosine' + ]; + for (const measure of measures) { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .findNearest({ + field: 'embedding', + vectorValue: vector([10, 1, 3, 1, 2, 1, 1, 1, 1, 1]), + limit: 3, + distanceMeasure: measure + }) + .select('title') + ); expectResults( snapshot, { @@ -1618,6 +2180,31 @@ apiDescribe('Pipelines', persistence => { }); }); + describe('error handling', () => { + it('error properties are propagated from the firestore backend', async () => { + try { + const myPipeline = firestore + .pipeline() + .collection(randomCol.path) + .rawStage('select', [ + // incorrect parameter type + field('title') + ]); + + await execute(myPipeline); + + expect.fail('expected pipeline.execute() to throw'); + } catch (e: unknown) { + expect(e instanceof FirebaseError).to.be.true; + const err = e as FirebaseError; + expect(err['code']).to.equal('invalid-argument'); + expect(typeof err['message']).to.equal('string'); + + expect(err['message']).to.match(/^3 INVALID_ARGUMENT: .*$/); + } + }); + }); + describe('function expressions', () => { it('logical max works', async () => { const snapshot = await execute( @@ -1663,36 +2250,44 @@ apiDescribe('Pipelines', persistence => { ); }); - it('cond works', async () => { + it('conditional works', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .select( 'title', - cond( - lt(field('published'), 1960), + conditional( + lessThan(field('published'), 1960), constant(1960), field('published') - ).as('published-safe') + ).as('published-safe'), + field('rating') + .greaterThanOrEqual(4.5) + .conditional(constant('great'), constant('good')) + .as('rating') ) .sort(field('title').ascending()) .limit(3) ); expectResults( snapshot, - { title: '1984', 'published-safe': 1960 }, - { title: 'Crime and Punishment', 'published-safe': 1960 }, - { title: 'Dune', 'published-safe': 1965 } + { title: '1984', 'published-safe': 1960, rating: 'good' }, + { + title: 'Crime and Punishment', + 'published-safe': 1960, + rating: 'good' + }, + { title: 'Dune', 'published-safe': 1965, rating: 'great' } ); }); - it('eqAny works', async () => { + it('equalAny works', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .where(eqAny('published', [1979, 1999, 1967])) + .where(equalAny('published', [1979, 1999, 1967])) .sort(descending('title')) .select('title') ); @@ -1703,13 +2298,13 @@ apiDescribe('Pipelines', persistence => { ); }); - it('notEqAny works', async () => { + it('notEqualAny works', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .where( - notEqAny( + notEqualAny( 'published', [1965, 1925, 1949, 1960, 1866, 1985, 1954, 1967, 1979] ) @@ -1765,7 +2360,7 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select(arrayLength('tags').as('tagsCount')) - .where(eq('tagsCount', 3)) + .where(equal('tagsCount', 3)) ); expect(snapshot.results.length).to.equal(10); }); @@ -1777,7 +2372,7 @@ apiDescribe('Pipelines', persistence => { .collection(randomCol.path) .sort(ascending('author')) .select( - field('author').strConcat(' - ', field('title')).as('bookInfo') + field('author').stringConcat(' - ', field('title')).as('bookInfo') ) .limit(1) ); @@ -1825,7 +2420,7 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(strContains('title', "'s")) + .where(stringContains('title', "'s")) .select('title') .sort(field('title').ascending()) ); @@ -1842,7 +2437,7 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select(charLength('title').as('titleLength'), field('title')) - .where(gt('titleLength', 20)) + .where(greaterThan('titleLength', 20)) .sort(field('title').ascending()) ); @@ -1906,14 +2501,14 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', 'To Kill a Mockingbird')) + .where(equal('title', 'To Kill a Mockingbird')) .select( add(field('rating'), 1).as('ratingPlusOne'), subtract(field('published'), 1900).as('yearsSince1900'), field('rating').multiply(10).as('ratingTimesTen'), divide('rating', 2).as('ratingDividedByTwo'), - multiply('rating', 10, 2).as('ratingTimes20'), - add('rating', 1, 2).as('ratingPlus3'), + multiply('rating', 20).as('ratingTimes20'), + add('rating', 3).as('ratingPlus3'), mod('rating', 2).as('ratingMod2') ) .limit(1) @@ -1936,9 +2531,9 @@ apiDescribe('Pipelines', persistence => { .collection(randomCol.path) .where( and( - gt('rating', 4.2), - lte(field('rating'), 4.5), - neq('genre', 'Science Fiction') + greaterThan('rating', 4.2), + lessThanOrEqual(field('rating'), 4.5), + notEqual('genre', 'Science Fiction') ) ) .select('rating', 'title') @@ -1962,8 +2557,11 @@ apiDescribe('Pipelines', persistence => { .collection(randomCol.path) .where( or( - and(gt('rating', 4.5), eq('genre', 'Science Fiction')), - lt('published', 1900) + and( + greaterThan('rating', 4.5), + equal('genre', 'Science Fiction') + ), + lessThan('published', 1900) ) ) .select('title') @@ -1987,10 +2585,16 @@ apiDescribe('Pipelines', persistence => { .select( isNull('rating').as('ratingIsNull'), isNan('rating').as('ratingIsNaN'), - isError(arrayOffset('title', 0)).as('isError'), - ifError(arrayOffset('title', 0), constant('was error')).as( + isError(divide(constant(1), constant(0))).as('isError'), + ifError(divide(constant(1), constant(0)), constant('was error')).as( 'ifError' ), + ifError( + divide(constant(1), constant(0)).greaterThan(1), + constant(true) + ) + .not() + .as('ifErrorBooleanExpression'), isAbsent('foo').as('isAbsent'), isNotNull('title').as('titleIsNotNull'), isNotNan('cost').as('costIsNotNan'), @@ -2003,6 +2607,7 @@ apiDescribe('Pipelines', persistence => { ratingIsNaN: false, isError: true, ifError: 'was error', + ifErrorBooleanExpression: false, isAbsent: true, titleIsNotNull: true, costIsNotNan: false, @@ -2019,10 +2624,15 @@ apiDescribe('Pipelines', persistence => { .select( field('rating').isNull().as('ratingIsNull'), field('rating').isNan().as('ratingIsNaN'), - arrayOffset('title', 0).isError().as('isError'), - arrayOffset('title', 0) + divide(constant(1), constant(0)).isError().as('isError'), + divide(constant(1), constant(0)) .ifError(constant('was error')) .as('ifError'), + divide(constant(1), constant(0)) + .greaterThan(1) + .ifError(constant(true)) + .not() + .as('ifErrorBooleanExpression'), field('foo').isAbsent().as('isAbsent'), field('title').isNotNull().as('titleIsNotNull'), field('cost').isNotNan().as('costIsNotNan') @@ -2033,6 +2643,7 @@ apiDescribe('Pipelines', persistence => { ratingIsNaN: false, isError: true, ifError: 'was error', + ifErrorBooleanExpression: false, isAbsent: true, titleIsNotNull: true, costIsNotNan: false @@ -2050,7 +2661,7 @@ apiDescribe('Pipelines', persistence => { field('awards').mapGet('others').as('others'), field('title') ) - .where(eq('hugoAward', true)) + .where(equal('hugoAward', true)) ); expectResults( snapshot, @@ -2059,25 +2670,25 @@ apiDescribe('Pipelines', persistence => { title: "The Hitchhiker's Guide to the Galaxy", others: { unknown: { year: 1980 } } }, - { hugoAward: true, title: 'Dune', others: null } + { hugoAward: true, title: 'Dune' } ); }); it('testDistanceFunctions', async () => { - const sourceVector = [0.1, 0.1]; - const targetVector = [0.5, 0.8]; + const sourceVector = vector([0.1, 0.1]); + const targetVector = vector([0.5, 0.8]); let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .select( - cosineDistance(constantVector(sourceVector), targetVector).as( + cosineDistance(constant(sourceVector), targetVector).as( 'cosineDistance' ), - dotProduct(constantVector(sourceVector), targetVector).as( + dotProduct(constant(sourceVector), targetVector).as( 'dotProductDistance' ), - euclideanDistance(constantVector(sourceVector), targetVector).as( + euclideanDistance(constant(sourceVector), targetVector).as( 'euclideanDistance' ) ) @@ -2095,13 +2706,13 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select( - constantVector(sourceVector) + constant(sourceVector) .cosineDistance(targetVector) .as('cosineDistance'), - constantVector(sourceVector) + constant(sourceVector) .dotProduct(targetVector) .as('dotProductDistance'), - constantVector(sourceVector) + constant(sourceVector) .euclideanDistance(targetVector) .as('euclideanDistance') ) @@ -2121,7 +2732,7 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .limit(1) - .select(vectorLength(constantVector([1, 2, 3])).as('vectorLength')) + .select(vectorLength(constant(vector([1, 2, 3]))).as('vectorLength')) ); expectResults(snapshot, { vectorLength: 3 @@ -2133,7 +2744,7 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('awards.hugo', true)) + .where(equal('awards.hugo', true)) .sort(descending('title')) .select('title', 'awards.hugo') ); @@ -2152,26 +2763,34 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('awards.hugo', true)) + .limit(1) + .replaceWith( + map({ + title: 'foo', + nested: { + level: { + '1': 'bar' + }, + 'level.1': { + 'level.2': 'baz' + } + } + }) + ) .select( 'title', - field('nestedField.level.1'), - mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') + field('nested.level.1'), + mapGet('nested', 'level.1').mapGet('level.2').as('nested') ) - .sort(descending('title')) - ); - expectResults( - snapshot, - { - title: "The Hitchhiker's Guide to the Galaxy", - 'nestedField.level.`1`': null, - nested: true - }, - { title: 'Dune', 'nestedField.level.`1`': null, nested: null } ); + expectResults(snapshot, { + title: 'foo', + 'nested.level.`1`': 'bar', + nested: 'baz' + }); }); - describe('genericFunction', () => { + describe('rawFunction', () => { it('add selectable', async () => { const snapshot = await execute( firestore @@ -2180,7 +2799,7 @@ apiDescribe('Pipelines', persistence => { .sort(descending('rating')) .limit(1) .select( - new FunctionExpr('add', [field('rating'), constant(1)]).as( + new FunctionExpression('add', [field('rating'), constant(1)]).as( 'rating' ) ) @@ -2196,9 +2815,9 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .where( - new BooleanExpr('and', [ - field('rating').gt(0), - field('title').charLength().lt(5), + new BooleanExpression('and', [ + field('rating').greaterThan(0), + field('title').charLength().lessThan(5), field('tags').arrayContains('propaganda') ]) ) @@ -2215,7 +2834,7 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .where( - new BooleanExpr('array_contains_any', [ + new BooleanExpression('array_contains_any', [ field('tags'), array(['politics']) ]) @@ -2233,9 +2852,9 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .aggregate( - new AggregateFunction('count_if', [field('rating').gte(4.5)]).as( - 'countOfBest' - ) + new AggregateFunction('count_if', [ + field('rating').greaterThanOrEqual(4.5) + ]).as('countOfBest') ) ); expectResults(snapshot, { @@ -2249,7 +2868,9 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .sort( - new FunctionExpr('char_length', [field('title')]).ascending(), + new FunctionExpression('char_length', [ + field('title') + ]).ascending(), descending('__name__') ) .limit(3) @@ -2270,45 +2891,6 @@ apiDescribe('Pipelines', persistence => { }); }); - itIf(testUnsupportedFeatures)('testReplaceFirst', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .where(eq('title', 'The Lord of the Rings')) - .limit(1) - .select(replaceFirst('title', 'o', '0').as('newName')) - ); - expectResults(snapshot, { newName: 'The L0rd of the Rings' }); - }); - - itIf(testUnsupportedFeatures)('testReplaceAll', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .where(eq('title', 'The Lord of the Rings')) - .limit(1) - .select(replaceAll('title', 'o', '0').as('newName')) - ); - expectResults(snapshot, { newName: 'The L0rd 0f the Rings' }); - }); - - it('supports Rand', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(10) - .select(rand().as('result')) - ); - expect(snapshot.results.length).to.equal(10); - snapshot.results.forEach(d => { - expect(d.get('result')).to.be.lt(1); - expect(d.get('result')).to.be.gte(0); - }); - }); - it('supports array', async () => { const snapshot = await execute( firestore @@ -2341,14 +2923,14 @@ apiDescribe('Pipelines', persistence => { }); }); - it('supports arrayOffset', async () => { + it('supports arrayGet', async () => { let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .sort(field('rating').descending()) .limit(3) - .select(arrayOffset('tags', 0).as('firstTag')) + .select(arrayGet('tags', 0).as('firstTag')) ); const expectedResults = [ { @@ -2369,7 +2951,7 @@ apiDescribe('Pipelines', persistence => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(3) - .select(field('tags').arrayOffset(0).as('firstTag')) + .select(field('tags').arrayGet(0).as('firstTag')) ); expectResults(snapshot, ...expectedResults); }); @@ -2521,12 +3103,16 @@ apiDescribe('Pipelines', persistence => { timestampAdd('timestamp', 'second', 10).as('plus10seconds'), timestampAdd('timestamp', 'microsecond', 10).as('plus10micros'), timestampAdd('timestamp', 'millisecond', 10).as('plus10millis'), - timestampSub('timestamp', 'day', 10).as('minus10days'), - timestampSub('timestamp', 'hour', 10).as('minus10hours'), - timestampSub('timestamp', 'minute', 10).as('minus10minutes'), - timestampSub('timestamp', 'second', 10).as('minus10seconds'), - timestampSub('timestamp', 'microsecond', 10).as('minus10micros'), - timestampSub('timestamp', 'millisecond', 10).as('minus10millis') + timestampSubtract('timestamp', 'day', 10).as('minus10days'), + timestampSubtract('timestamp', 'hour', 10).as('minus10hours'), + timestampSubtract('timestamp', 'minute', 10).as('minus10minutes'), + timestampSubtract('timestamp', 'second', 10).as('minus10seconds'), + timestampSubtract('timestamp', 'microsecond', 10).as( + 'minus10micros' + ), + timestampSubtract('timestamp', 'millisecond', 10).as( + 'minus10millis' + ) ) ); expectResults(snapshot, { @@ -2543,7 +3129,7 @@ apiDescribe('Pipelines', persistence => { minus10micros: new Timestamp(1741380234, 999990000), minus10millis: new Timestamp(1741380234, 990000000) }); - }); + }).timeout(10000); it('supports byteLength', async () => { const snapshot = await execute( @@ -2571,7 +3157,7 @@ apiDescribe('Pipelines', persistence => { .collection(randomCol) .limit(1) .select(constant(true).as('trueField')) - .select('trueField', not(eq('trueField', true)).as('falseField')) + .select('trueField', not(equal('trueField', true)).as('falseField')) ); expectResults(snapshot, { @@ -2579,187 +3165,523 @@ apiDescribe('Pipelines', persistence => { falseField: false }); }); - }); - describe('not yet implemented in backend', () => { - itIf(testUnsupportedFeatures)('supports Bit_and', async () => { + it('can reverse an array', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(bitAnd(constant(5), 12).as('result')) + .select(field('tags').arrayReverse().as('reversedTags')) ); expectResults(snapshot, { - result: 4 + reversedTags: ['adventure', 'space', 'comedy'] }); }); - itIf(testUnsupportedFeatures)('supports Bit_and', async () => { + it('can reverse an array with the top-level function', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(constant(5).bitAnd(12).as('result')) + .select(reverse('tags').as('reversedTags')) ); expectResults(snapshot, { - result: 4 + reversedTags: ['adventure', 'space', 'comedy'] }); }); - itIf(testUnsupportedFeatures)('supports Bit_or', async () => { - let snapshot = await execute( + it('can compute the ceiling of a numeric value', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(bitOr(constant(5), 12).as('result')) + .select(field('rating').ceil().as('ceilingRating')) ); expectResults(snapshot, { - result: 13 + ceilingRating: 5 }); - snapshot = await execute( + }); + + it('can compute the ceiling of a numeric value with the top-level function', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(constant(5).bitOr(12).as('result')) + .select(ceil('rating').as('ceilingRating')) ); expectResults(snapshot, { - result: 13 + ceilingRating: 5 }); }); - itIf(testUnsupportedFeatures)('supports Bit_xor', async () => { - let snapshot = await execute( + it('can compute the floor of a numeric value', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(bitXor(constant(5), 12).as('result')) + .select(field('rating').floor().as('floorRating')) ); expectResults(snapshot, { - result: 9 + floorRating: 4 }); - snapshot = await execute( + }); + + it('can compute the floor of a numeric value with the top-level function', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(constant(5).bitXor(12).as('result')) + .select(floor('rating').as('floorRating')) ); expectResults(snapshot, { - result: 9 + floorRating: 4 }); }); - itIf(testUnsupportedFeatures)('supports Bit_not', async () => { - let snapshot = await execute( + it('can compute e to the power of a numeric value', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) .limit(1) - .select( - bitNot(constant(Bytes.fromUint8Array(Uint8Array.of(0xfd)))).as( - 'result' - ) - ) + .select(field('rating').exp().as('expRating')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x02)) + expRating: 109.94717245212352 }); - snapshot = await execute( + }); + + it('can compute e to the power of a numeric value with the top-level function', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) .limit(1) - .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0xfd))) - .bitNot() - .as('result') - ) + .select(exp('rating').as('expRating')) + ); + expect(snapshot.results[0].get('expRating')).to.be.approximately( + 109.94717245212351, + 0.000001 ); - expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x02)) - }); }); - itIf(testUnsupportedFeatures)('supports Bit_left_shift', async () => { - let snapshot = await execute( + it('can compute the power of a numeric value', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select( - bitLeftShift( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), - 2 - ).as('result') - ) + .select(field('rating').pow(2).as('powerRating')) ); - expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x04)) - }); - snapshot = await execute( + expect(snapshot.results[0].get('powerRating')).to.be.approximately( + 17.64, + 0.0001 + ); + }); + + it('can compute the power of a numeric value with the top-level function', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) - .bitLeftShift(2) - .as('result') - ) + .select(pow('rating', 2).as('powerRating')) + ); + expect(snapshot.results[0].get('powerRating')).to.be.approximately( + 17.64, + 0.0001 ); - expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x04)) - }); }); - itIf(testUnsupportedFeatures)('supports Bit_right_shift', async () => { - let snapshot = await execute( + it('can round a numeric value', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select( - bitRightShift( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), - 2 - ).as('result') - ) + .select(field('rating').round().as('roundedRating')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + roundedRating: 4 }); - snapshot = await execute( + }); + + it('can round a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(round('rating').as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can round a numeric value away from zero for positive half-way values', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .addFields(constant(1.5).as('positiveHalf')) + .select(field('positiveHalf').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 2 + }); + }); + + it('can round a numeric value away from zero for negative half-way values', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) + .addFields(constant(-1.5).as('negativeHalf')) + .select(field('negativeHalf').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: -2 + }); + }); + + it('can round a numeric value to specified precision', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + foo: 4.123456 + }) + ) .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) - .bitRightShift(2) - .as('result') + field('foo').round(0).as('0'), + round('foo', 1).as('1'), + round('foo', constant(2)).as('2'), + round(field('foo'), 4).as('4') ) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + '0': 4, + '1': 4.1, + '2': 4.12, + '4': 4.1235 + }); + }); + + it('can get the collectionId from a path', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(field('__name__').collectionId().as('collectionId')) + ); + expectResults(snapshot, { + collectionId: randomCol.id }); }); - itIf(testUnsupportedFeatures)('supports Document_id', async () => { + it('can get the collectionId from a path with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(collectionId('__name__').as('collectionId')) + ); + expectResults(snapshot, { + collectionId: randomCol.id + }); + }); + + it('can compute the length of a string value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('title').length().as('titleLength')) + ); + expectResults(snapshot, { + titleLength: 36 + }); + }); + + it('can compute the length of a string value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(length('title').as('titleLength')) + ); + expectResults(snapshot, { + titleLength: 36 + }); + }); + + it('can compute the length of an array value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('tags').length().as('tagsLength')) + ); + expectResults(snapshot, { + tagsLength: 3 + }); + }); + + it('can compute the length of an array value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(length('tags').as('tagsLength')) + ); + expectResults(snapshot, { + tagsLength: 3 + }); + }); + + it('can compute the length of a map value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('awards').length().as('awardsLength')) + ); + expectResults(snapshot, { + awardsLength: 3 + }); + }); + + it('can compute the length of a vector value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('embedding').length().as('embeddingLength')) + ); + expectResults(snapshot, { + embeddingLength: 10 + }); + }); + + it('can compute the length of a bytes value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(constant('12é').as('value')) + .limit(1) + .select(field('value').byteLength().as('valueLength')) + ); + expectResults(snapshot, { + valueLength: 4 + }); + }); + + it('can compute the natural logarithm of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').ln().as('lnRating')) + ); + expect(snapshot.results[0]!.data().lnRating).to.be.closeTo(1.435, 0.001); + }); + + it('can compute the natural logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(ln('rating').as('lnRating')) + ); + expect(snapshot.results[0]!.data().lnRating).to.be.closeTo(1.435, 0.001); + }); + + it('can compute the natural logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(ln('rating').as('lnRating')) + ); + expectResults(snapshot, { + lnRating: 1.4350845252893227 + }); + }); + + it('can compute the logarithm of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(log(field('rating'), 10).as('logRating')) + ); + expectResults(snapshot, { + logRating: 0.6232492903979004 + }); + }); + + it('can compute the logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(log('rating', 10).as('logRating')) + ); + expectResults(snapshot, { + logRating: 0.6232492903979004 + }); + }); + + it('can round a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can round a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(round('rating').as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can compute the square root of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').sqrt().as('sqrtRating')) + ); + expectResults(snapshot, { + sqrtRating: 2.04939015319192 + }); + }); + + it('can compute the square root of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(sqrt('rating').as('sqrtRating')) + ); + expectResults(snapshot, { + sqrtRating: 2.04939015319192 + }); + }); + + it('can reverse a string', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('title').reverse().as('reversedTitle')) + ); + expectResults(snapshot, { + reversedTitle: "yxalaG eht ot ediuG s'rekihhctiH ehT" + }); + }); + + it('can reverse a string with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(stringReverse('title').as('reversedTitle')) + ); + expectResults(snapshot, { + reversedTitle: "yxalaG eht ot ediuG s'rekihhctiH ehT" + }); + }); + + it('supports Document_id', async () => { let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .sort(field('rating').descending()) .limit(1) - .select(documentId(field('__path__')).as('docId')) + .select( + documentId(field('__name__')).as('docId'), + documentId(field('__path__')).as('noDocId') + ) ); expectResults(snapshot, { - docId: 'book4' + docId: 'book4', + noDocId: null }); snapshot = await execute( firestore @@ -2767,21 +3689,21 @@ apiDescribe('Pipelines', persistence => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(1) - .select(field('__path__').documentId().as('docId')) + .select(field('__name__').documentId().as('docId')) ); expectResults(snapshot, { docId: 'book4' }); }); - itIf(testUnsupportedFeatures)('supports Substr', async () => { + it('supports substring', async () => { let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .sort(field('rating').descending()) .limit(1) - .select(substr('title', 9, 2).as('of')) + .select(substring('title', 9, 2).as('of')) ); expectResults(snapshot, { of: 'of' @@ -2792,93 +3714,65 @@ apiDescribe('Pipelines', persistence => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(1) - .select(field('title').substr(9, 2).as('of')) + .select(field('title').substring(9, 2).as('of')) ); expectResults(snapshot, { of: 'of' }); }); - itIf(testUnsupportedFeatures)( - 'supports Substr without length', - async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .sort(field('rating').descending()) - .limit(1) - .select(substr('title', 9).as('of')) - ); - expectResults(snapshot, { - of: 'of the Rings' - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .sort(field('rating').descending()) - .limit(1) - .select(field('title').substr(9).as('of')) - ); - expectResults(snapshot, { - of: 'of the Rings' - }); - } - ); - - itIf(testUnsupportedFeatures)('arrayConcat works', async () => { - const snapshot = await execute( + it('supports substring without length', async () => { + let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .select( - arrayConcat('tags', ['newTag1', 'newTag2'], field('tags'), [ - null - ]).as('modifiedTags') - ) + .sort(field('rating').descending()) .limit(1) + .select(substring('title', 9).as('of')) ); expectResults(snapshot, { - modifiedTags: [ - 'comedy', - 'space', - 'adventure', - 'newTag1', - 'newTag2', - 'comedy', - 'space', - 'adventure', - null - ] + of: 'of the Rings' + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('title').substring(9).as('of')) + ); + expectResults(snapshot, { + of: 'of the Rings' }); }); - itIf(testUnsupportedFeatures)('testToLowercase', async () => { + it('test toLower', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .select(toLower('title').as('lowercaseTitle')) + .sort(ascending('title')) + .select(toLower('author').as('lowercaseAuthor')) .limit(1) ); expectResults(snapshot, { - lowercaseTitle: "the hitchhiker's guide to the galaxy" + lowercaseAuthor: 'george orwell' }); }); - itIf(testUnsupportedFeatures)('testToUppercase', async () => { + it('test toUpper', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .sort(ascending('title')) .select(toUpper('author').as('uppercaseAuthor')) .limit(1) ); - expectResults(snapshot, { uppercaseAuthor: 'DOUGLAS ADAMS' }); + expectResults(snapshot, { uppercaseAuthor: 'GEORGE ORWELL' }); }); - itIf(testUnsupportedFeatures)('testTrim', async () => { + it('testTrim', async () => { const snapshot = await execute( firestore .pipeline() @@ -2895,20 +3789,203 @@ apiDescribe('Pipelines', persistence => { }); }); - itIf(testUnsupportedFeatures)('test reverse', async () => { + it('test reverse', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .where(eq('title', '1984')) + .where(equal('title', '1984')) .limit(1) .select(reverse('title').as('reverseTitle')) ); - expectResults(snapshot, { title: '4891' }); + expectResults(snapshot, { reverseTitle: '4891' }); }); + + it('testAbs', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(-10).as('neg10'), + constant(-22.22).as('neg22'), + constant(1).as('pos1') + ) + .select( + abs('neg10').as('10'), + abs(field('neg22')).as('22'), + field('pos1').as('1') + ) + ); + expectResults(snapshot, { + '10': 10, + '22': 22.22, + '1': 1 + }); + }); + + it('can compute the base-10 logarithm of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(field('rating').log10().as('log10Rating')) + ); + expect(snapshot.results[0]!.data().log10Rating).to.be.closeTo( + 0.672, + 0.001 + ); + }); + + it('can compute the base-10 logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(log10('rating').as('log10Rating')) + ); + expect(snapshot.results[0]!.data().log10Rating).to.be.closeTo( + 0.672, + 0.001 + ); + }); + + it('can concat fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .addFields( + concat('author', ' ', field('title')).as('display'), + field('author').concat(': ', field('title')).as('display2') + ) + .where(equal('author', 'Douglas Adams')) + .select('display', 'display2') + ); + expectResults(snapshot, { + display: "Douglas Adams The Hitchhiker's Guide to the Galaxy", + display2: "Douglas Adams: The Hitchhiker's Guide to the Galaxy" + }); + }); + + it('supports currentTimestamp', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .addFields(currentTimestamp().as('now')) + .select('now') + ); + const now = snapshot.results[0].get('now') as Timestamp; + expect(now).instanceof(Timestamp); + expect( + now.toDate().getUTCSeconds() - new Date().getUTCSeconds() + ).lessThan(5000); + }); + + // Not implemented in backend + it.skip('supports error', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(isError(error('test error')).as('error')) + ); + + expectResults(snapshot, { + 'error': true + }); + }); + + it('supports ifAbsent', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + title: 'foo' + }) + ) + .select( + ifAbsent('title', 'default title').as('title'), + field('name').ifAbsent('default name').as('name'), + field('name').ifAbsent(field('title')).as('nameOrTitle') + ) + ); + + expectResults(snapshot, { + title: 'foo', + name: 'default name', + nameOrTitle: 'foo' + }); + }); + + it('supports join', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + tags: ['foo', 'bar', 'baz'], + delimeter: '|' + }) + ) + .select(join('tags', ',').as('csv'), field('tags').join('|').as('or')) + ); + + expectResults(snapshot, { + csv: 'foo,bar,baz', + or: 'foo|bar|baz' + }); + }); + + it('can compute the sum of the elements in an array', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .addFields(array([150, 200]).as('sales')) + .select(field('sales').arraySum().as('totalSales')) + ); + expectResults(snapshot, { + totalSales: 350 + }); + }); + + it('can compute the sum of the elements in an array with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .addFields(array([150, 200]).as('sales')) + .select(arraySum('sales').as('totalSales')) + ); + expectResults(snapshot, { + totalSales: 350 + }); + }); + + // TODO(new-expression): Add new expression tests above this line }); describe('pagination', () => { + let addedDocs: DocumentReference[] = []; + /** * Adds several books to the test collection. These * additional books support pagination test scenarios @@ -2919,7 +3996,9 @@ apiDescribe('Pipelines', persistence => { async function addBooks( collectionReference: CollectionReference ): Promise { - await setDoc(doc(collectionReference, 'book11'), { + let docRef = doc(collectionReference, 'book11'); + addedDocs.push(docRef); + await setDoc(docRef, { title: 'Jonathan Strange & Mr Norrell', author: 'Susanna Clarke', genre: 'Fantasy', @@ -2928,7 +4007,9 @@ apiDescribe('Pipelines', persistence => { tags: ['historical fantasy', 'magic', 'alternate history', 'england'], awards: { hugo: false, nebula: false } }); - await setDoc(doc(collectionReference, 'book12'), { + docRef = doc(collectionReference, 'book12'); + addedDocs.push(docRef); + await setDoc(docRef, { title: 'The Master and Margarita', author: 'Mikhail Bulgakov', genre: 'Satire', @@ -2942,7 +4023,9 @@ apiDescribe('Pipelines', persistence => { ], awards: {} }); - await setDoc(doc(collectionReference, 'book13'), { + docRef = doc(collectionReference, 'book13'); + addedDocs.push(docRef); + await setDoc(docRef, { title: 'A Long Way to a Small, Angry Planet', author: 'Becky Chambers', genre: 'Science Fiction', @@ -2953,125 +4036,135 @@ apiDescribe('Pipelines', persistence => { }); } - // sort on __name__ is not working, see b/409358591 - itIf(testUnsupportedFeatures)( - 'supports pagination with filters', - async () => { - await addBooks(randomCol); - const pageSize = 2; - const pipeline = firestore - .pipeline() - .collection(randomCol.path) - .select('title', 'rating', '__name__') - .sort(field('rating').descending(), field('__name__').ascending()); + afterEach(async () => { + for (let i = 0; i < addedDocs.length; i++) { + await deleteDoc(addedDocs[i]); + } + addedDocs = []; + }); - let snapshot = await execute(pipeline.limit(pageSize)); - expectResults( - snapshot, - { title: 'The Lord of the Rings', rating: 4.7 }, - { title: 'Jonathan Strange & Mr Norrell', rating: 4.6 } - ); + it('supports pagination with filters', async () => { + await addBooks(randomCol); + const pageSize = 2; + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'rating', '__name__') + .sort(field('rating').descending(), field('__name__').ascending()); - const lastDoc = snapshot.results[snapshot.results.length - 1]; + let snapshot = await execute(pipeline.limit(pageSize)); + expectResults( + snapshot, + { title: 'The Lord of the Rings', rating: 4.7 }, + { title: 'Dune', rating: 4.6 } + ); - snapshot = await execute( - pipeline - .where( - or( - and( - field('rating').eq(lastDoc.get('rating')), - field('__path__').gt(lastDoc.ref?.id) - ), - field('rating').lt(lastDoc.get('rating')) - ) + const lastDoc = snapshot.results[snapshot.results.length - 1]; + + snapshot = await execute( + pipeline + .where( + or( + and( + field('rating').equal(lastDoc.get('rating')), + field('__name__').greaterThan(lastDoc.ref) + ), + field('rating').lessThan(lastDoc.get('rating')) ) - .limit(pageSize) - ); - expectResults( - snapshot, - { title: 'Pride and Prejudice', rating: 4.5 }, - { title: 'Crime and Punishment', rating: 4.3 } - ); - } - ); + ) + .limit(pageSize) + ); + expectResults( + snapshot, + { title: 'Jonathan Strange & Mr Norrell', rating: 4.6 }, + { title: 'The Master and Margarita', rating: 4.6 } + ); + }); - // sort on __name__ is not working, see b/409358591 - itIf(testUnsupportedFeatures)( - 'supports pagination with offsets', - async () => { - await addBooks(randomCol); + it('supports pagination with offsets', async () => { + await addBooks(randomCol); - const secondFilterField = '__path__'; + const secondFilterField = '__name__'; - const pipeline = firestore - .pipeline() - .collection(randomCol.path) - .select('title', 'rating', secondFilterField) - .sort( - field('rating').descending(), - field(secondFilterField).ascending() - ); + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'rating', secondFilterField) + .sort( + field('rating').descending(), + field(secondFilterField).ascending() + ); - const pageSize = 2; - let currPage = 0; + const pageSize = 2; + let currPage = 0; - let snapshot = await execute( - pipeline.offset(currPage++ * pageSize).limit(pageSize) - ); + let snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); - expectResults( - snapshot, - { - title: 'The Lord of the Rings', - rating: 4.7 - }, - { title: 'Dune', rating: 4.6 } - ); + expectResults( + snapshot, + { + title: 'The Lord of the Rings', + rating: 4.7 + }, + { title: 'Dune', rating: 4.6 } + ); - snapshot = await execute( - pipeline.offset(currPage++ * pageSize).limit(pageSize) - ); - expectResults( - snapshot, - { - title: 'Jonathan Strange & Mr Norrell', - rating: 4.6 - }, - { title: 'The Master and Margarita', rating: 4.6 } - ); + snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + expectResults( + snapshot, + { + title: 'Jonathan Strange & Mr Norrell', + rating: 4.6 + }, + { title: 'The Master and Margarita', rating: 4.6 } + ); - snapshot = await execute( - pipeline.offset(currPage++ * pageSize).limit(pageSize) - ); - expectResults( - snapshot, - { - title: 'A Long Way to a Small, Angry Planet', - rating: 4.6 - }, - { - title: 'Pride and Prejudice', - rating: 4.5 - } - ); - } - ); + snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + expectResults( + snapshot, + { + title: 'A Long Way to a Small, Angry Planet', + rating: 4.6 + }, + { + title: 'Pride and Prejudice', + rating: 4.5 + } + ); + }); }); - describe('console support', () => { - it('supports internal serialization to proto', async () => { - const pipeline = firestore - .pipeline() - .collection('books') - .where(eq('awards.hugo', true)) - .select( - 'title', - field('nestedField.level.1'), - mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') + describe('stage options', () => { + describe('forceIndex', () => { + // SKIP: requires pre-existing index + // eslint-disable-next-line no-restricted-properties + it.skip('Collection Stage', async () => { + const snapshot = await execute( + firestore.pipeline().collection({ + collection: randomCol, + forceIndex: 'unknown' + }) ); + expect(snapshot.results.length).to.equal(10); + }); - const proto = _internalPipelineToExecutePipelineRequestProto(pipeline); - expect(proto).not.to.be.null; + // SKIP: requires pre-existing index + // eslint-disable-next-line no-restricted-properties + it.skip('CollectionGroup Stage', async () => { + const snapshot = await execute( + firestore.pipeline().collectionGroup({ + collectionId: randomCol.id, + forceIndex: 'unknown' + }) + ); + expect(snapshot.results.length).to.equal(10); + }); }); }); }); diff --git a/packages/firestore/test/lite/pipeline.test.ts b/packages/firestore/test/lite/pipeline.test.ts index f919e1aa9de..1633492b578 100644 --- a/packages/firestore/test/lite/pipeline.test.ts +++ b/packages/firestore/test/lite/pipeline.test.ts @@ -29,47 +29,38 @@ import { field, and, array, - arrayOffset, constant, add, subtract, multiply, - avg, - bitAnd, - substr, - constantVector, - bitLeftShift, - bitNot, + average, + substring, count, mapMerge, mapRemove, - bitOr, ifError, isAbsent, isError, or, - rand, - bitRightShift, - bitXor, isNotNan, map, isNotNull, isNull, mod, documentId, - eq, - neq, - lt, + equal, + notEqual, + lessThan, countIf, - lte, - gt, + lessThanOrEqual, + greaterThan, arrayConcat, arrayContains, arrayContainsAny, - eqAny, - notEqAny, + equalAny, + notEqualAny, xor, - cond, + conditional, logicalMaximum, logicalMinimum, exists, @@ -78,7 +69,7 @@ import { like, regexContains, regexMatch, - strContains, + stringContains, startsWith, endsWith, mapGet, @@ -96,25 +87,25 @@ import { unixSecondsToTimestamp, timestampToUnixSeconds, timestampAdd, - timestampSub, + timestampSubtract, ascending, descending, - FunctionExpr, - BooleanExpr, + FunctionExpression, + BooleanExpression, AggregateFunction, sum, - strConcat, + stringConcat, arrayContainsAll, arrayLength, charLength, divide, - replaceFirst, - replaceAll, - byteLength, + abs, not, toLower, toUpper, - trim + trim, + arrayGet, + byteLength } from '../../src/lite-api/expressions'; import { documentId as documentIdFieldPath } from '../../src/lite-api/field_path'; import { vector } from '../../src/lite-api/field_value_impl'; @@ -131,7 +122,7 @@ import { doc } from '../../src/lite-api/reference'; import { addDoc, setDoc } from '../../src/lite-api/reference_impl'; -import { FindNearestOptions } from '../../src/lite-api/stage'; +import { FindNearestStageOptions } from '../../src/lite-api/stage_options'; import { Timestamp } from '../../src/lite-api/timestamp'; import { writeBatch } from '../../src/lite-api/write_batch'; import { itIf } from '../integration/util/helpers'; @@ -345,7 +336,6 @@ describe('Firestore Pipelines', () => { .sort(ascending('__name__')); const snapshot = await execute(ppl); expect(snapshot.results.length).to.equal(10); - expect(snapshot.pipeline).to.equal(ppl); expectResults( snapshot, 'book1', @@ -388,7 +378,7 @@ describe('Firestore Pipelines', () => { ); }); - it.only('returns execution time for an empty query', async () => { + it('returns execution time for an empty query', async () => { const start = new Date().valueOf(); const pipeline = firestore.pipeline().collection(randomCol.path).limit(0); @@ -445,7 +435,7 @@ describe('Firestore Pipelines', () => { const pipeline = firestore .pipeline() .collection(randomCol.path) - .aggregate(avg('rating').as('avgRating')); + .aggregate(average('rating').as('avgRating')); const snapshot = await execute(pipeline); const end = new Date().valueOf(); @@ -463,7 +453,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .aggregate({ - accumulators: [avg('rating').as('avgRating')], + accumulators: [average('rating').as('avgRating')], groups: ['genre'] }); @@ -559,7 +549,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .database() - .where(eq('randomId', randomId)) + .where(equal('randomId', randomId)) .sort(ascending('order')) ); expectResults(snapshot, doc1.id, doc2.id); @@ -582,8 +572,7 @@ describe('Firestore Pipelines', () => { Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])) ).as('bytes'), constant(doc(firestore, 'foo', 'bar')).as('documentReference'), - constantVector(vector([1, 2, 3])).as('vectorValue'), - constantVector([1, 2, 3]).as('vectorValue2'), + constant(vector([1, 2, 3])).as('vectorValue'), map({ 'number': 1, 'string': 'a string', @@ -746,7 +735,7 @@ describe('Firestore Pipelines', () => { ) .where( and( - eq('metadataArray', [ + equal('metadataArray', [ 1, 2, field('genre'), @@ -756,7 +745,7 @@ describe('Firestore Pipelines', () => { published: field('published') } ]), - eq('metadata', { + equal('metadata', { genre: field('genre'), rating: multiply('rating', 10), nestedArray: [field('title')], @@ -815,10 +804,10 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('genre', 'Science Fiction')) + .where(equal('genre', 'Science Fiction')) .aggregate( countAll().as('count'), - avg('rating').as('avgRating'), + average('rating').as('avgRating'), maximum('rating').as('maxRating'), sum('rating').as('sumRating') ) @@ -837,7 +826,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(lt('published', 1900)) + .where(lessThan('published', 1900)) .aggregate({ accumulators: [], groups: ['genre'] @@ -851,12 +840,12 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(lt(field('published'), 1984)) + .where(lessThan(field('published'), 1984)) .aggregate({ - accumulators: [avg('rating').as('avgRating')], + accumulators: [average('rating').as('avgRating')], groups: ['genre'] }) - .where(gt('avgRating', 4.3)) + .where(greaterThan('avgRating', 4.3)) .sort(field('avgRating').descending()) ); expectResults( @@ -867,7 +856,7 @@ describe('Firestore Pipelines', () => { ); }); - it('returns min, max, count, and countAll accumulations', async () => { + it('returns minimum, maximum, count, and countAll accumulations', async () => { const snapshot = await execute( firestore .pipeline() @@ -892,7 +881,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .aggregate(countIf(field('rating').gt(4.3)).as('count')) + .aggregate(countIf(field('rating').greaterThan(4.3)).as('count')) ); const expectedResults = { count: 3 @@ -903,7 +892,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .aggregate(field('rating').gt(4.3).countIf().as('count')) + .aggregate(field('rating').greaterThan(4.3).countIf().as('count')) ); expectResults(snapshot, expectedResults); }); @@ -1068,8 +1057,8 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( and( - gt('rating', 4.5), - eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) ) ) ); @@ -1082,9 +1071,9 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( and( - gt('rating', 4.5), - eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), - lt('published', 1965) + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), + lessThan('published', 1965) ) ) ); @@ -1097,9 +1086,9 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( or( - eq('genre', 'Romance'), - eq('genre', 'Dystopian'), - eq('genre', 'Fantasy') + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('genre', 'Fantasy') ) ) .sort(ascending('title')) @@ -1121,10 +1110,10 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( xor( - eq('genre', 'Romance'), - eq('genre', 'Dystopian'), - eq('genre', 'Fantasy'), - eq('published', 1949) + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('genre', 'Fantasy'), + equal('published', 1949) ) ) .select('title') @@ -1158,13 +1147,13 @@ describe('Firestore Pipelines', () => { }); }); - describe('generic stage', () => { + describe('raw stage', () => { it('can select fields', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .genericStage('select', [ + .rawStage('select', [ { title: field('title'), metadata: { @@ -1191,9 +1180,9 @@ describe('Firestore Pipelines', () => { .sort(field('author').ascending()) .limit(1) .select('title', 'author') - .genericStage('add_fields', [ + .rawStage('add_fields', [ { - display: strConcat('title', ' - ', field('author')) + display: stringConcat('title', ' - ', field('author')) } ]) ); @@ -1210,7 +1199,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select('title', 'author') - .genericStage('where', [field('author').eq('Douglas Adams')]) + .rawStage('where', [field('author').equal('Douglas Adams')]) ); expectResults(snapshot, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1224,14 +1213,14 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select('title', 'author') - .genericStage('sort', [ + .rawStage('sort', [ { direction: 'ascending', expression: field('author') } ]) - .genericStage('offset', [3]) - .genericStage('limit', [1]) + .rawStage('offset', [3]) + .rawStage('limit', [1]) ); expectResults(snapshot, { author: 'Fyodor Dostoevsky', @@ -1245,8 +1234,8 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select('title', 'author', 'rating') - .genericStage('aggregate', [ - { averageRating: field('rating').avg() }, + .rawStage('aggregate', [ + { averageRating: field('rating').average() }, {} ]) ); @@ -1261,7 +1250,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select('title', 'author', 'rating') - .genericStage('distinct', [{ rating: field('rating') }]) + .rawStage('distinct', [{ rating: field('rating') }]) .sort(field('rating').descending()) ); expectResults( @@ -1297,7 +1286,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .replaceWith('awards') ); expectResults(snapshot, { @@ -1312,7 +1301,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .replaceWith( map({ foo: 'bar', @@ -1407,7 +1396,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .unnest(field('tags').as('tag')) .select( 'title', @@ -1475,7 +1464,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .unnest(array([1, 2, 3]).as('copy')) .select( 'title', @@ -1542,7 +1531,7 @@ describe('Firestore Pipelines', () => { describe('findNearest stage', () => { it('run pipeline with findNearest', async () => { - const measures: Array = [ + const measures: Array = [ 'euclidean', 'dot_product', 'cosine' @@ -1605,7 +1594,7 @@ describe('Firestore Pipelines', () => { }); describe('function expressions', () => { - it('logical max works', async () => { + it('logical maximum works', async () => { const snapshot = await execute( firestore .pipeline() @@ -1627,7 +1616,7 @@ describe('Firestore Pipelines', () => { ); }); - it('logical min works', async () => { + it('logical minimum works', async () => { const snapshot = await execute( firestore .pipeline() @@ -1649,15 +1638,15 @@ describe('Firestore Pipelines', () => { ); }); - it('cond works', async () => { + it('conditiona works', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .select( 'title', - cond( - lt(field('published'), 1960), + conditional( + lessThan(field('published'), 1960), constant(1960), field('published') ).as('published-safe') @@ -1678,7 +1667,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eqAny('published', [1979, 1999, 1967])) + .where(equalAny('published', [1979, 1999, 1967])) .sort(descending('title')) .select('title') ); @@ -1695,7 +1684,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .where( - notEqAny( + notEqualAny( 'published', [1965, 1925, 1949, 1960, 1866, 1985, 1954, 1967, 1979] ) @@ -1751,7 +1740,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select(arrayLength('tags').as('tagsCount')) - .where(eq('tagsCount', 3)) + .where(equal('tagsCount', 3)) ); expect(snapshot.results.length).to.equal(10); }); @@ -1763,7 +1752,7 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .sort(ascending('author')) .select( - field('author').strConcat(' - ', field('title')).as('bookInfo') + field('author').stringConcat(' - ', field('title')).as('bookInfo') ) .limit(1) ); @@ -1811,7 +1800,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(strContains('title', "'s")) + .where(stringContains('title', "'s")) .select('title') .sort(field('title').ascending()) ); @@ -1828,7 +1817,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select(charLength('title').as('titleLength'), field('title')) - .where(gt('titleLength', 20)) + .where(greaterThan('titleLength', 20)) .sort(field('title').ascending()) ); @@ -1892,14 +1881,14 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', 'To Kill a Mockingbird')) + .where(equal('title', 'To Kill a Mockingbird')) .select( add(field('rating'), 1).as('ratingPlusOne'), subtract(field('published'), 1900).as('yearsSince1900'), field('rating').multiply(10).as('ratingTimesTen'), divide('rating', 2).as('ratingDividedByTwo'), - multiply('rating', 10, 2).as('ratingTimes20'), - add('rating', 1, 2).as('ratingPlus3'), + multiply('rating', 10).as('ratingTimes20'), + add('rating', 1).as('ratingPlus3'), mod('rating', 2).as('ratingMod2') ) .limit(1) @@ -1915,6 +1904,20 @@ describe('Firestore Pipelines', () => { }); }); + it('testAbs', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', 'To Kill a Mockingbird')) + .select(abs(field('rating')).as('absRating')) + .limit(1) + ); + expectResults(snapshot, { + absRating: 4.2 + }); + }); + it('testComparisonOperators', async () => { const snapshot = await execute( firestore @@ -1922,9 +1925,9 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( and( - gt('rating', 4.2), - lte(field('rating'), 4.5), - neq('genre', 'Science Fiction') + greaterThan('rating', 4.2), + lessThanOrEqual(field('rating'), 4.5), + notEqual('genre', 'Science Fiction') ) ) .select('rating', 'title') @@ -1948,8 +1951,11 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( or( - and(gt('rating', 4.5), eq('genre', 'Science Fiction')), - lt('published', 1900) + and( + greaterThan('rating', 4.5), + equal('genre', 'Science Fiction') + ), + lessThan('published', 1900) ) ) .select('title') @@ -1963,7 +1969,7 @@ describe('Firestore Pipelines', () => { ); }); - it.only('testChecks', async () => { + it('testChecks', async () => { let snapshot = await execute( firestore .pipeline() @@ -1973,10 +1979,8 @@ describe('Firestore Pipelines', () => { .select( isNull('rating').as('ratingIsNull'), isNan('rating').as('ratingIsNaN'), - isError(arrayOffset('title', 0)).as('isError'), - ifError(arrayOffset('title', 0), constant('was error')).as( - 'ifError' - ), + isError(arrayGet('title', 0)).as('isError'), + ifError(arrayGet('title', 0), constant('was error')).as('ifError'), isAbsent('foo').as('isAbsent'), isNotNull('title').as('titleIsNotNull'), isNotNan('cost').as('costIsNotNan'), @@ -2005,10 +2009,8 @@ describe('Firestore Pipelines', () => { .select( field('rating').isNull().as('ratingIsNull'), field('rating').isNan().as('ratingIsNaN'), - arrayOffset('title', 0).isError().as('isError'), - arrayOffset('title', 0) - .ifError(constant('was error')) - .as('ifError'), + arrayGet('title', 0).isError().as('isError'), + arrayGet('title', 0).ifError(constant('was error')).as('ifError'), field('foo').isAbsent().as('isAbsent'), field('title').isNotNull().as('titleIsNotNull'), field('cost').isNotNan().as('costIsNotNan') @@ -2036,7 +2038,7 @@ describe('Firestore Pipelines', () => { field('awards').mapGet('others').as('others'), field('title') ) - .where(eq('hugoAward', true)) + .where(equal('hugoAward', true)) ); expectResults( snapshot, @@ -2050,20 +2052,20 @@ describe('Firestore Pipelines', () => { }); it('testDistanceFunctions', async () => { - const sourceVector = [0.1, 0.1]; - const targetVector = [0.5, 0.8]; + const sourceVector = vector([0.1, 0.1]); + const targetVector = vector([0.5, 0.8]); let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .select( - cosineDistance(constantVector(sourceVector), targetVector).as( + cosineDistance(constant(sourceVector), targetVector).as( 'cosineDistance' ), - dotProduct(constantVector(sourceVector), targetVector).as( + dotProduct(constant(sourceVector), targetVector).as( 'dotProductDistance' ), - euclideanDistance(constantVector(sourceVector), targetVector).as( + euclideanDistance(constant(sourceVector), targetVector).as( 'euclideanDistance' ) ) @@ -2081,13 +2083,13 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select( - constantVector(sourceVector) + constant(sourceVector) .cosineDistance(targetVector) .as('cosineDistance'), - constantVector(sourceVector) + constant(sourceVector) .dotProduct(targetVector) .as('dotProductDistance'), - constantVector(sourceVector) + constant(sourceVector) .euclideanDistance(targetVector) .as('euclideanDistance') ) @@ -2107,7 +2109,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .limit(1) - .select(vectorLength(constantVector([1, 2, 3])).as('vectorLength')) + .select(vectorLength(constant(vector([1, 2, 3]))).as('vectorLength')) ); expectResults(snapshot, { vectorLength: 3 @@ -2119,7 +2121,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('awards.hugo', true)) + .where(equal('awards.hugo', true)) .sort(descending('title')) .select('title', 'awards.hugo') ); @@ -2138,7 +2140,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('awards.hugo', true)) + .where(equal('awards.hugo', true)) .select( 'title', field('nestedField.level.1'), @@ -2166,7 +2168,7 @@ describe('Firestore Pipelines', () => { .sort(descending('rating')) .limit(1) .select( - new FunctionExpr('add', [field('rating'), constant(1)]).as( + new FunctionExpression('add', [field('rating'), constant(1)]).as( 'rating' ) ) @@ -2182,9 +2184,9 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .where( - new BooleanExpr('and', [ - field('rating').gt(0), - field('title').charLength().lt(5), + new BooleanExpression('and', [ + field('rating').greaterThan(0), + field('title').charLength().lessThan(5), field('tags').arrayContains('propaganda') ]) ) @@ -2201,7 +2203,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .where( - new BooleanExpr('array_contains_any', [ + new BooleanExpression('array_contains_any', [ field('tags'), array(['politics']) ]) @@ -2219,9 +2221,9 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .aggregate( - new AggregateFunction('count_if', [field('rating').gte(4.5)]).as( - 'countOfBest' - ) + new AggregateFunction('count_if', [ + field('rating').greaterThanOrEqual(4.5) + ]).as('countOfBest') ) ); expectResults(snapshot, { @@ -2235,7 +2237,9 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .sort( - new FunctionExpr('char_length', [field('title')]).ascending(), + new FunctionExpression('char_length', [ + field('title') + ]).ascending(), descending('__name__') ) .limit(3) @@ -2256,45 +2260,6 @@ describe('Firestore Pipelines', () => { }); }); - itIf(testUnsupportedFeatures)('testReplaceFirst', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .where(eq('title', 'The Lord of the Rings')) - .limit(1) - .select(replaceFirst('title', 'o', '0').as('newName')) - ); - expectResults(snapshot, { newName: 'The L0rd of the Rings' }); - }); - - itIf(testUnsupportedFeatures)('testReplaceAll', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .where(eq('title', 'The Lord of the Rings')) - .limit(1) - .select(replaceAll('title', 'o', '0').as('newName')) - ); - expectResults(snapshot, { newName: 'The L0rd 0f the Rings' }); - }); - - it('supports Rand', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(10) - .select(rand().as('result')) - ); - expect(snapshot.results.length).to.equal(10); - snapshot.results.forEach(d => { - expect(d.get('result')).to.be.lt(1); - expect(d.get('result')).to.be.gte(0); - }); - }); - it('supports array', async () => { const snapshot = await execute( firestore @@ -2334,7 +2299,7 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(3) - .select(arrayOffset('tags', 0).as('firstTag')) + .select(arrayGet('tags', 0).as('firstTag')) ); const expectedResults = [ { @@ -2355,7 +2320,7 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(3) - .select(field('tags').arrayOffset(0).as('firstTag')) + .select(field('tags').arrayGet(0).as('firstTag')) ); expectResults(snapshot, ...expectedResults); }); @@ -2507,12 +2472,16 @@ describe('Firestore Pipelines', () => { timestampAdd('timestamp', 'second', 10).as('plus10seconds'), timestampAdd('timestamp', 'microsecond', 10).as('plus10micros'), timestampAdd('timestamp', 'millisecond', 10).as('plus10millis'), - timestampSub('timestamp', 'day', 10).as('minus10days'), - timestampSub('timestamp', 'hour', 10).as('minus10hours'), - timestampSub('timestamp', 'minute', 10).as('minus10minutes'), - timestampSub('timestamp', 'second', 10).as('minus10seconds'), - timestampSub('timestamp', 'microsecond', 10).as('minus10micros'), - timestampSub('timestamp', 'millisecond', 10).as('minus10millis') + timestampSubtract('timestamp', 'day', 10).as('minus10days'), + timestampSubtract('timestamp', 'hour', 10).as('minus10hours'), + timestampSubtract('timestamp', 'minute', 10).as('minus10minutes'), + timestampSubtract('timestamp', 'second', 10).as('minus10seconds'), + timestampSubtract('timestamp', 'microsecond', 10).as( + 'minus10micros' + ), + timestampSubtract('timestamp', 'millisecond', 10).as( + 'minus10millis' + ) ) ); expectResults(snapshot, { @@ -2529,7 +2498,7 @@ describe('Firestore Pipelines', () => { minus10micros: new Timestamp(1741380234, 999990000), minus10millis: new Timestamp(1741380234, 990000000) }); - }); + }).timeout(10000); it('supports byteLength', async () => { const snapshot = await execute( @@ -2557,7 +2526,7 @@ describe('Firestore Pipelines', () => { .collection(randomCol) .limit(1) .select(constant(true).as('trueField')) - .select('trueField', not(eq('trueField', true)).as('falseField')) + .select('trueField', not(equal('trueField', true)).as('falseField')) ); expectResults(snapshot, { @@ -2565,187 +2534,43 @@ describe('Firestore Pipelines', () => { falseField: false }); }); - }); - - describe('not yet implemented in backend', () => { - itIf(testUnsupportedFeatures)('supports Bit_and', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(bitAnd(constant(5), 12).as('result')) - ); - expectResults(snapshot, { - result: 4 - }); - }); - - itIf(testUnsupportedFeatures)('supports Bit_and', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(constant(5).bitAnd(12).as('result')) - ); - expectResults(snapshot, { - result: 4 - }); - }); - - itIf(testUnsupportedFeatures)('supports Bit_or', async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(bitOr(constant(5), 12).as('result')) - ); - expectResults(snapshot, { - result: 13 - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(constant(5).bitOr(12).as('result')) - ); - expectResults(snapshot, { - result: 13 - }); - }); - - itIf(testUnsupportedFeatures)('supports Bit_xor', async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(bitXor(constant(5), 12).as('result')) - ); - expectResults(snapshot, { - result: 9 - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(constant(5).bitXor(12).as('result')) - ); - expectResults(snapshot, { - result: 9 - }); - }); - - itIf(testUnsupportedFeatures)('supports Bit_not', async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select( - bitNot(constant(Bytes.fromUint8Array(Uint8Array.of(0xfd)))).as( - 'result' - ) - ) - ); - expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x02)) - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0xfd))) - .bitNot() - .as('result') - ) - ); - expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x02)) - }); - }); - - itIf(testUnsupportedFeatures)('supports Bit_left_shift', async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select( - bitLeftShift( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), - 2 - ).as('result') - ) - ); - expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x04)) - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) - .bitLeftShift(2) - .as('result') - ) - ); - expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x04)) - }); - }); - itIf(testUnsupportedFeatures)('supports Bit_right_shift', async () => { + it('supports Document_id', async () => { let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .sort(field('rating').descending()) .limit(1) - .select( - bitRightShift( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), - 2 - ).as('result') - ) + .select(documentId(field('__path__')).as('docId')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + docId: 'book4' }); snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .sort(field('rating').descending()) .limit(1) - .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) - .bitRightShift(2) - .as('result') - ) + .select(field('__path__').documentId().as('docId')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + docId: 'book4' }); }); - itIf(testUnsupportedFeatures)('supports Document_id', async () => { + it('supports Substr', async () => { let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .sort(field('rating').descending()) .limit(1) - .select(documentId(field('__path__')).as('docId')) + .select(substring('title', 9, 2).as('of')) ); expectResults(snapshot, { - docId: 'book4' + of: 'of' }); snapshot = await execute( firestore @@ -2753,24 +2578,24 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(1) - .select(field('__path__').documentId().as('docId')) + .select(field('title').substring(9, 2).as('of')) ); expectResults(snapshot, { - docId: 'book4' + of: 'of' }); }); - itIf(testUnsupportedFeatures)('supports Substr', async () => { + it('supports Substr without length', async () => { let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .sort(field('rating').descending()) .limit(1) - .select(substr('title', 9, 2).as('of')) + .select(substring('title', 9).as('of')) ); expectResults(snapshot, { - of: 'of' + of: 'of the Rings' }); snapshot = await execute( firestore @@ -2778,42 +2603,14 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(1) - .select(field('title').substr(9, 2).as('of')) + .select(field('title').substring(9).as('of')) ); expectResults(snapshot, { - of: 'of' + of: 'of the Rings' }); }); - itIf(testUnsupportedFeatures)( - 'supports Substr without length', - async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .sort(field('rating').descending()) - .limit(1) - .select(substr('title', 9).as('of')) - ); - expectResults(snapshot, { - of: 'of the Rings' - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .sort(field('rating').descending()) - .limit(1) - .select(field('title').substr(9).as('of')) - ); - expectResults(snapshot, { - of: 'of the Rings' - }); - } - ); - - itIf(testUnsupportedFeatures)('arrayConcat works', async () => { + it('arrayConcat works', async () => { const snapshot = await execute( firestore .pipeline() @@ -2840,7 +2637,7 @@ describe('Firestore Pipelines', () => { }); }); - itIf(testUnsupportedFeatures)('testToLowercase', async () => { + it('testToLowercase', async () => { const snapshot = await execute( firestore .pipeline() @@ -2853,7 +2650,7 @@ describe('Firestore Pipelines', () => { }); }); - itIf(testUnsupportedFeatures)('testToUppercase', async () => { + it('testToUppercase', async () => { const snapshot = await execute( firestore .pipeline() @@ -2864,7 +2661,7 @@ describe('Firestore Pipelines', () => { expectResults(snapshot, { uppercaseAuthor: 'DOUGLAS ADAMS' }); }); - itIf(testUnsupportedFeatures)('testTrim', async () => { + it('testTrim', async () => { const snapshot = await execute( firestore .pipeline() @@ -2881,12 +2678,12 @@ describe('Firestore Pipelines', () => { }); }); - itIf(testUnsupportedFeatures)('test reverse', async () => { + it('test reverse', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .where(eq('title', '1984')) + .where(equal('title', '1984')) .limit(1) .select(reverse('title').as('reverseTitle')) ); @@ -2965,10 +2762,10 @@ describe('Firestore Pipelines', () => { .where( or( and( - field('rating').eq(lastDoc.get('rating')), - field('__path__').gt(lastDoc.ref?.id) + field('rating').equal(lastDoc.get('rating')), + field('__path__').greaterThan(lastDoc.ref?.id) ), - field('rating').lt(lastDoc.get('rating')) + field('rating').lessThan(lastDoc.get('rating')) ) ) .limit(pageSize) diff --git a/packages/firestore/test/unit/api/pipeline_impl.test.ts b/packages/firestore/test/unit/api/pipeline_impl.test.ts new file mode 100644 index 00000000000..f11db5a427e --- /dev/null +++ b/packages/firestore/test/unit/api/pipeline_impl.test.ts @@ -0,0 +1,202 @@ +/** + * @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. + */ + +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { Timestamp } from '../../../src'; +import { Firestore } from '../../../src/api/database'; +import { execute } from '../../../src/api/pipeline_impl'; +import { + MemoryOfflineComponentProvider, + OnlineComponentProvider +} from '../../../src/core/component_provider'; +import { + ExecutePipelineRequest as ProtoExecutePipelineRequest, + ExecutePipelineResponse as ProtoExecutePipelineResponse +} from '../../../src/protos/firestore_proto_api'; +import { newTestFirestore } from '../../util/api_helpers'; + +const FIRST_CALL = 0; +const EXECUTE_PIPELINE_REQUEST = 3; + +function fakePipelineResponse( + firestore: Firestore, + response?: ProtoExecutePipelineResponse[] +): sinon.SinonSpy { + response = response ?? [ + { + executionTime: Timestamp.now().toDate().toISOString(), + results: [] + } + ]; + const fake = sinon.fake.resolves(response); + + firestore._componentsProvider = { + _offline: { + build: () => new MemoryOfflineComponentProvider() + }, + _online: { + build: () => { + const provider = new OnlineComponentProvider(); + const ogCreateDatastore = provider.createDatastore.bind(provider); + provider.createDatastore = config => { + const datastore = ogCreateDatastore(config); + // @ts-ignore + datastore.invokeStreamingRPC = fake; + return datastore; + }; + return provider; + } + } + }; + + return fake; +} + +describe('execute(Pipeline|PipelineOptions)', () => { + it('returns execution time with empty results', async () => { + const firestore = newTestFirestore(); + + const executeTime = Timestamp.now(); + const spy = fakePipelineResponse(firestore, [ + { + executionTime: executeTime.toDate().toISOString(), + results: [] + } + ]); + + const pipelineSnapshot = await execute( + firestore.pipeline().collection('foo') + ); + + expect(pipelineSnapshot.results.length).to.equal(0); + expect(spy.calledOnce); + + expect(pipelineSnapshot.executionTime.toJSON()).to.deep.equal( + executeTime.toJSON() + ); + }); + + it('serializes the pipeline', async () => { + const firestore = newTestFirestore(); + const spy = fakePipelineResponse(firestore); + + await execute({ + pipeline: firestore.pipeline().collection('foo') + }); + + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: 'projects/new-project/databases/(default)', + structuredPipeline: { + 'options': {}, + 'pipeline': { + 'stages': [ + { + 'args': [ + { + 'referenceValue': '/foo' + } + ], + 'name': 'collection', + 'options': {} + } + ] + } + } + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest + ); + }); + + it('serializes the pipeline options', async () => { + const firestore = newTestFirestore(); + const spy = fakePipelineResponse(firestore); + + await execute({ + pipeline: firestore.pipeline().collection('foo'), + indexMode: 'recommended' + }); + + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: 'projects/new-project/databases/(default)', + structuredPipeline: { + 'options': { + 'index_mode': { + 'stringValue': 'recommended' + } + }, + 'pipeline': { + 'stages': [ + { + 'args': [ + { + 'referenceValue': '/foo' + } + ], + 'name': 'collection', + 'options': {} + } + ] + } + } + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest + ); + }); + + it('serializes the pipeline raw options', async () => { + const firestore = newTestFirestore(); + const spy = fakePipelineResponse(firestore); + + await execute({ + pipeline: firestore.pipeline().collection('foo'), + rawOptions: { + 'foo': 'bar' + } + }); + + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: 'projects/new-project/databases/(default)', + structuredPipeline: { + 'options': { + 'foo': { + 'stringValue': 'bar' + } + }, + 'pipeline': { + 'stages': [ + { + 'args': [ + { + 'referenceValue': '/foo' + } + ], + 'name': 'collection', + 'options': {} + } + ] + } + } + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest + ); + }); +}); diff --git a/packages/firestore/test/unit/core/options_util.test.ts b/packages/firestore/test/unit/core/options_util.test.ts new file mode 100644 index 00000000000..0549c2c8a9c --- /dev/null +++ b/packages/firestore/test/unit/core/options_util.test.ts @@ -0,0 +1,224 @@ +/** + * @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. + */ + +import { expect } from 'chai'; + +import { ParseContext } from '../../../src/api/parse_context'; +import { OptionsUtil } from '../../../src/core/options_util'; +import { UserDataSource } from '../../../src/lite-api/user_data_reader'; +import { testUserDataReader } from '../../util/helpers'; + +describe('OptionsUtil', () => { + let context: ParseContext | undefined; + beforeEach(async () => { + context = testUserDataReader(false).createContext( + UserDataSource.Argument, + 'beforeEach' + ); + }); + + afterEach(async () => { + context = undefined; + }); + + it('should support known options', () => { + const optionsUtil = new OptionsUtil({ + fooBar: { + serverName: 'foo_bar' + } + }); + const proto = optionsUtil.getOptionsProto(context!, { + fooBar: 'recommended' + }); + + expect(proto).deep.equal({ + 'foo_bar': { + stringValue: 'recommended' + } + }); + }); + + it('should support unknown options', () => { + const optionsUtil = new OptionsUtil({}); + const proto = optionsUtil.getOptionsProto(context!, {}, { baz: 'foo' }); + + expect(proto).to.deep.equal({ + baz: { + stringValue: 'foo' + } + }); + }); + + it('should support unknown nested options', () => { + const optionsUtil = new OptionsUtil({}); + const proto = optionsUtil.getOptionsProto( + context!, + {}, + { 'foo.bar': 'baz' } + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: { stringValue: 'baz' } + } + } + } + }); + }); + + it('should support options override', () => { + const optionsUtil = new OptionsUtil({ + indexMode: { + serverName: 'index_mode' + } + }); + const proto = optionsUtil.getOptionsProto( + context!, + { + indexMode: 'recommended' + }, + { + 'index_mode': 'baz' + } + ); + + expect(proto).to.deep.equal({ + 'index_mode': { + stringValue: 'baz' + } + }); + }); + + it('should support options override of nested field', () => { + const optionsUtil = new OptionsUtil({ + foo: { + serverName: 'foo', + nestedOptions: { + bar: { + serverName: 'bar' + }, + waldo: { + serverName: 'waldo' + } + } + } + }); + const proto = optionsUtil.getOptionsProto( + context!, + { + foo: { bar: 'yep', waldo: 'found' } + }, + { + 'foo.bar': 123, + 'foo.baz': true + } + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: { + integerValue: '123' + }, + waldo: { + stringValue: 'found' + }, + baz: { + booleanValue: true + } + } + } + } + }); + }); + + it('will replace a nested object if given a new object', () => { + const optionsUtil = new OptionsUtil({ + foo: { + serverName: 'foo', + nestedOptions: { + bar: { + serverName: 'bar' + }, + waldo: { + serverName: 'waldo' + } + } + } + }); + const proto = optionsUtil.getOptionsProto( + context!, + { + foo: { bar: 'yep', waldo: 'found' } + }, + { + foo: { + bar: 123 + } + } + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: { + integerValue: '123' + } + } + } + } + }); + }); + + it('will replace a top level property that is not an object if given a nested field with dot notation', () => { + const optionsUtil = new OptionsUtil({ + foo: { + serverName: 'foo' + } + }); + + const proto = optionsUtil.getOptionsProto( + context!, + { + foo: 'bar' + }, + { + 'foo.bar': '123', + 'foo.waldo': true + } + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: { + stringValue: '123' + }, + waldo: { + booleanValue: true + } + } + } + } + }); + }); +}); diff --git a/packages/firestore/test/unit/core/structured_pipeline.test.ts b/packages/firestore/test/unit/core/structured_pipeline.test.ts new file mode 100644 index 00000000000..759dfecea44 --- /dev/null +++ b/packages/firestore/test/unit/core/structured_pipeline.test.ts @@ -0,0 +1,187 @@ +/** + * @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. + */ + +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { DatabaseId } from '../../../src/core/database_info'; +import { + StructuredPipeline, + StructuredPipelineOptions +} from '../../../src/core/structured_pipeline'; +import { UserDataSource } from '../../../src/lite-api/user_data_reader'; +import { Pipeline as PipelineProto } from '../../../src/protos/firestore_proto_api'; +import { + JsonProtoSerializer, + ProtoSerializable +} from '../../../src/remote/serializer'; +import { testUserDataReader } from '../../util/helpers'; + +describe('StructuredPipeline', () => { + it('should serialize the pipeline argument', () => { + const pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const structuredPipelineOptions = new StructuredPipelineOptions(); + structuredPipelineOptions._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') + ); + const structuredPipeline = new StructuredPipeline( + pipeline, + structuredPipelineOptions + ); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: {} + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it('should support known options', () => { + const pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + + const options = new StructuredPipelineOptions({ + indexMode: 'recommended' + }); + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') + ); + const structuredPipeline = new StructuredPipeline(pipeline, options); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'index_mode': { + stringValue: 'recommended' + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it('should support unknown options', () => { + const pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const options = new StructuredPipelineOptions( + {}, + { + 'foo_bar': 'baz' + } + ); + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') + ); + const structuredPipeline = new StructuredPipeline(pipeline, options); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'foo_bar': { + stringValue: 'baz' + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it('should support unknown nested options', () => { + const pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const options = new StructuredPipelineOptions( + {}, + { + 'foo.bar': 'baz' + } + ); + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') + ); + const structuredPipeline = new StructuredPipeline(pipeline, options); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'foo': { + mapValue: { + fields: { + 'bar': { stringValue: 'baz' } + } + } + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it('should support options override', () => { + const pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const options = new StructuredPipelineOptions( + { + indexMode: 'recommended' + }, + { + 'index_mode': 'baz' + } + ); + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') + ); + const structuredPipeline = new StructuredPipeline(pipeline, options); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'index_mode': { + stringValue: 'baz' + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); +});