diff --git a/handwritten/firestore/api-report/firestore.api.md b/handwritten/firestore/api-report/firestore.api.md index df8cc5c85c2..e152832bc54 100644 --- a/handwritten/firestore/api-report/firestore.api.md +++ b/handwritten/firestore/api-report/firestore.api.md @@ -498,6 +498,12 @@ function charLength(fieldName: string): FunctionExpression; // @beta function charLength(stringExpression: Expression): FunctionExpression; +// @beta +function coalesce(expression: Expression, replacement: Expression | unknown, ...others: Array): FunctionExpression; + +// @beta +function coalesce(fieldName: string, replacement: Expression | unknown, ...others: Array): FunctionExpression; + // Warning: (tsdoc-undefined-tag) The TSDoc tag "@class" is not defined in this configuration // // @public @@ -1099,6 +1105,7 @@ abstract class Expression implements firestore.Pipelines.Expression, HasUserData byteLength(): FunctionExpression; ceil(): FunctionExpression; charLength(): FunctionExpression; + coalesce(replacement: Expression | unknown, ...others: Array): FunctionExpression; collectionId(): FunctionExpression; concat(second: Expression | unknown, ...others: Array): FunctionExpression; cosineDistance(vectorExpression: Expression): FunctionExpression; @@ -1135,9 +1142,11 @@ abstract class Expression implements firestore.Pipelines.Expression, HasUserData ifAbsent(elseExpression: unknown): Expression; ifError(catchExpr: Expression): FunctionExpression; ifError(catchValue: unknown): FunctionExpression; + ifNull(elseExpression: Expression): FunctionExpression; + ifNull(elseValue: unknown): FunctionExpression; isAbsent(): BooleanExpression; isError(): BooleanExpression; - isType(type: Type): BooleanExpression; + isType(type: string): BooleanExpression; join(delimiterExpression: Expression): Expression; join(delimiter: string): Expression; last(): AggregateFunction; @@ -1414,6 +1423,8 @@ class Firestore implements firestore.Firestore { // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' constructor(settings?: firestore.Settings); + // @internal + get alwaysUseImplicitOrderBy(): boolean; // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" batch(): WriteBatch; @@ -1636,6 +1647,18 @@ function ifError(tryExpr: Expression, catchExpr: Expression): FunctionExpression // @beta function ifError(tryExpr: Expression, catchValue: unknown): FunctionExpression; +// @beta +function ifNull(ifExpr: Expression, elseExpr: Expression): FunctionExpression; + +// @beta +function ifNull(ifExpr: Expression, elseValue: unknown): FunctionExpression; + +// @beta +function ifNull(ifFieldName: string, elseExpr: Expression): FunctionExpression; + +// @beta +function ifNull(ifFieldName: string, elseValue: unknown): FunctionExpression; + // @beta function isAbsent(value: Expression): BooleanExpression; @@ -1646,10 +1669,10 @@ function isAbsent(field: string): BooleanExpression; function isError(value: Expression): BooleanExpression; // @beta -function isType(fieldName: string, type: Type): BooleanExpression; +function isType(fieldName: string, type: string): BooleanExpression; // @beta -function isType(expression: Expression, type: Type): BooleanExpression; +function isType(expression: Expression, type: string): BooleanExpression; // @beta function join(arrayFieldName: string, delimiter: string): Expression; @@ -1884,7 +1907,7 @@ class Ordering implements HasUserData { // @beta class Pipeline implements firestore.Pipelines.Pipeline { // Warning: (ae-forgotten-export) The symbol "Stage" needs to be exported by the entry point index.d.ts - constructor(db: Firestore, stages: Stage[]); + constructor(db: Firestore | undefined, stages: Stage[]); addFields(field: firestore.Pipelines.Selectable, ...additionalFields: firestore.Pipelines.Selectable[]): Pipeline; // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag addFields(options: firestore.Pipelines.AddFieldsStageOptions): Pipeline; @@ -1933,8 +1956,8 @@ class Pipeline implements firestore.Pipelines.Pipeline { union(options: firestore.Pipelines.UnionStageOptions): Pipeline; unnest(selectable: firestore.Pipelines.Selectable, indexField?: string): Pipeline; unnest(options: firestore.Pipelines.UnnestStageOptions): Pipeline; - // Warning: (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration - _validateUserData | HasUserData[] | HasUserData>(_: string, val: T): T; + // (undocumented) + _validateUserData(ignoreUndefinedProperties: boolean): void; where(condition: firestore.Pipelines.BooleanExpression): Pipeline; where(options: firestore.Pipelines.WhereStageOptions): Pipeline; } @@ -2114,7 +2137,6 @@ declare namespace Pipelines { arrayConcat, type, isType, - Type, timestampTruncate, split, ltrim, @@ -2124,7 +2146,9 @@ declare namespace Pipelines { stringReplaceAll, stringReplaceOne, nor, - switchOn + switchOn, + ifNull, + coalesce } } export { Pipelines } @@ -2354,7 +2378,7 @@ export class Query + ): FunctionExpression { + const values = [replacement, ...others]; + return new FunctionExpression('coalesce', [ + this, + ...values.map(valueToDefaultExpr), + ]); + } + /** * @beta * Creates an expression that joins the elements of an array into a string. @@ -9188,6 +9261,167 @@ export function ifAbsent( ); } +/** + * @beta + * Creates an expression that returns the `elseExpr` argument if `ifExpr` is null, else + * return the result of the `ifExpr` argument evaluation. + * + * @remarks + * This function provides a fallback for both absent and explicit null values. In contrast, + * {@link Expression#ifAbsent} only triggers for missing fields. + * + * @example + * ```typescript + * // Returns the user's preferred name, or if that is null, returns their full name. + * ifNull(field("preferredName"), field("fullName")) + * ``` + * + * @param ifExpr The expression to check for null. + * @param elseExpr The expression that will be evaluated and returned if `ifExpr` is null. + * @returns A new `Expression` representing the ifNull operation. + */ +export function ifNull( + ifExpr: Expression, + elseExpr: Expression, +): FunctionExpression; + +/** + * @beta + * Creates an expression that returns the `elseValue` argument if `ifExpr` is null, else + * return the result of the `ifExpr` argument evaluation. + * + * @remarks + * This function provides a fallback for both absent and explicit null values. In contrast, + * {@link Expression#ifAbsent} only triggers for missing fields. + * + * @example + * ```typescript + * // Returns the user's display name, or returns "Anonymous" if the field is null. + * ifNull(field("displayName"), "Anonymous") + * ``` + * + * @param ifExpr The expression to check for null. + * @param elseValue The value that will be returned if `ifExpr` evaluates to null. + * @returns A new `Expression` representing the ifNull operation. + */ +export function ifNull( + ifExpr: Expression, + elseValue: unknown, +): FunctionExpression; + +/** + * @beta + * Creates an expression that returns the `elseExpr` argument if `ifFieldName` is null, else + * return the value of the field. + * + * @remarks + * This function provides a fallback for both absent and explicit null values. In contrast, + * {@link Expression#ifAbsent} only triggers for missing fields. + * + * @example + * ```typescript + * // Returns the user's preferred name, or if that is null, returns their full name. + * ifNull("preferredName", field("fullName")) + * ``` + * + * @param ifFieldName The field to check for null. + * @param elseExpr The expression that will be evaluated and returned if `ifFieldName` is + * null. + * @returns A new `Expression` representing the ifNull operation. + */ +export function ifNull( + ifFieldName: string, + elseExpr: Expression, +): FunctionExpression; + +/** + * @beta + * Creates an expression that returns the `elseValue` argument if `ifFieldName` is null, else + * return the value of the field. + * + * @remarks + * This function provides a fallback for both absent and explicit null values. In contrast, + * {@link Expression#ifAbsent} only triggers for missing fields. + * + * @example + * ```typescript + * // Returns the user's display name, or returns "Anonymous" if the field is null. + * ifNull("displayName", "Anonymous") + * ``` + * + * @param ifFieldName The field to check for null. + * @param elseValue The value that will be returned if `ifFieldName` is null. + * @returns A new `Expression` representing the ifNull operation. + */ +export function ifNull( + ifFieldName: string, + elseValue: unknown, +): FunctionExpression; +export function ifNull( + fieldNameOrExpression: string | Expression, + elseValue: Expression | unknown, +): FunctionExpression { + return fieldOrExpression(fieldNameOrExpression).ifNull( + valueToDefaultExpr(elseValue), + ); +} + +/** + * @beta + * Creates an expression that returns the first non-null, non-absent argument, without evaluating + * the rest of the arguments. When all arguments are null or absent, returns the last argument. + * + * @example + * ```typescript + * // Returns the value of the first non-null, non-absent field among 'preferredName', 'fullName', + * // or the last argument if all previous fields are null. + * coalesce(field("preferredName"), field("fullName"), constant("Anonymous")) + * ``` + * + * @param expression The first expression to check for null. + * @param replacement The fallback expression or value if the first one is null. + * @param others Optional additional expressions to check if previous ones are null. + * @returns A new `Expression` representing the coalesce operation. + */ +export function coalesce( + expression: Expression, + replacement: Expression | unknown, + ...others: Array +): FunctionExpression; + +/** + * @beta + * Creates an expression that returns the first non-null, non-absent argument, without evaluating + * the rest of the arguments. When all arguments are null or absent, returns the last argument. + * + * @example + * ```typescript + * // Returns the value of the first non-null, non-absent field among 'preferredName', 'fullName', + * // or the last argument if all previous fields are null. + * coalesce("preferredName", field("fullName"), constant("Anonymous")) + * ``` + * + * @param fieldName The name of the first field to check for null. + * @param replacement The fallback expression or value if the first one is null. + * @param others Optional additional expressions to check if previous ones are null. + * @returns A new `Expression` representing the coalesce operation. + */ +export function coalesce( + fieldName: string, + replacement: Expression | unknown, + ...others: Array +): FunctionExpression; +export function coalesce( + fieldNameOrExpression: Expression | string, + replacement: Expression | unknown, + ...others: Array +): FunctionExpression { + return fieldOrExpression(fieldNameOrExpression).coalesce( + replacement, + ...others, + ); +} + /** * @beta * Creates an expression that joins the elements of an array into a string. diff --git a/handwritten/firestore/dev/src/pipelines/index.ts b/handwritten/firestore/dev/src/pipelines/index.ts index 4dcf2694b61..95288b3232c 100644 --- a/handwritten/firestore/dev/src/pipelines/index.ts +++ b/handwritten/firestore/dev/src/pipelines/index.ts @@ -156,5 +156,7 @@ export { stringReplaceOne, nor, switchOn, + ifNull, + coalesce, // TODO(new-expression): Add new expression exports above this line } from './expression'; diff --git a/handwritten/firestore/dev/system-test/pipeline.ts b/handwritten/firestore/dev/system-test/pipeline.ts index 8377424c8d2..018866bdadf 100644 --- a/handwritten/firestore/dev/system-test/pipeline.ts +++ b/handwritten/firestore/dev/system-test/pipeline.ts @@ -140,6 +140,8 @@ import { log10, concat, ifAbsent, + ifNull, + coalesce, join, arraySum, currentTimestamp, @@ -4984,6 +4986,87 @@ describe.skipClassic('Pipeline class', () => { }); }); + it('supports ifNull', async () => { + const snapshot = await firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + title: 'foo', + name: null, + }), + ) + .select( + ifNull('title', 'default title').as('staticMethod'), + field('title').ifNull('default title').as('instanceMethod'), + field('name').ifNull(field('title')).as('nameOrTitle'), + field('name').ifNull('default name').as('fieldIsNull'), + field('absent').ifNull('default name').as('fieldIsAbsent'), + ) + .execute(); + + expectResults(snapshot, { + staticMethod: 'foo', + instanceMethod: 'foo', + nameOrTitle: 'foo', + fieldIsNull: 'default name', + fieldIsAbsent: 'default name', + }); + }); + + it('supports coalesce', async () => { + const snapshot = await firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + numberValue: 1, + stringValue: 'hello', + booleanValue: false, + nullValue: null, + nullValue2: null, + }), + ) + .select( + coalesce(field('numberValue'), field('stringValue')).as( + 'staticMethod', + ), + field('numberValue') + .coalesce(field('stringValue')) + .as('instanceMethod'), + coalesce(field('nullValue'), field('stringValue')).as('firstIsNull'), + coalesce( + field('nullValue'), + field('nullValue2'), + field('booleanValue'), + ).as('lastIsNotNull'), + coalesce(field('nullValue'), field('nullValue2')).as('allFieldsNull'), + coalesce( + field('nullValue'), + field('nullValue2'), + constant('default'), + ).as('allFieldsNullWithDefault'), + coalesce( + field('absentField'), + field('numberValue'), + constant('default'), + ).as('withAbsentField'), + ) + .execute(); + + expectResults(snapshot, { + staticMethod: 1, + instanceMethod: 1, + firstIsNull: 'hello', + lastIsNotNull: false, + allFieldsNull: null, + allFieldsNullWithDefault: 'default', + withAbsentField: 1, + }); + }); + it('supports join', async () => { const snapshot = await firestore .pipeline() diff --git a/handwritten/firestore/types/firestore.d.ts b/handwritten/firestore/types/firestore.d.ts index 6b88278d4e4..29a149f132e 100644 --- a/handwritten/firestore/types/firestore.d.ts +++ b/handwritten/firestore/types/firestore.d.ts @@ -5454,6 +5454,67 @@ declare namespace FirebaseFirestore { ifAbsent(elseValueOrExpression: Expression | unknown): Expression; + /** + * @beta + * Creates an expression that returns the `elseValue` argument if this expression evaluates to null, else + * return the result of this expression evaluation. + * + * @remarks + * This function provides a fallback for both absent and explicit null values. In contrast, {@link ifAbsent} + * only triggers for missing fields. + * + * @example + * ```typescript + * // Returns the user's display name, or returns "Anonymous" if the field is null. + * field("displayName").ifNull("Anonymous") + * ``` + * + * @param elseValue The value that will be returned if this Expression evaluates to null. + * @returns A new `Expression` representing the ifNull operation. + */ + ifNull(elseValue: unknown): FunctionExpression; + + /** + * @beta + * Creates an expression that returns the `elseValue` argument if this expression evaluates to null, else + * return the result of this expression evaluation. + * + * @remarks + * This function provides a fallback for both absent and explicit null values. In contrast, {@link ifAbsent} + * only triggers for missing fields. + * + * @example + * ```typescript + * // Returns the user's preferred name, or if that is null, returns their full name. + * field("preferredName").ifNull(field("fullName")) + * ``` + * + * @param elseExpression The Expression that will be evaluated if this Expression evaluates to null. + * @returns A new `Expression` representing the ifNull operation. + */ + ifNull(elseExpression: Expression): FunctionExpression; + + /** + * @beta + * Creates an expression that returns the first non-null, non-absent argument, without evaluating + * the rest of the arguments. When all arguments are null or absent, returns the last argument. + * + * @example + * ```typescript + * // Returns the value of the first non-null, non-absent field among 'preferredName', 'fullName', + * // or the last argument if all previous fields are null. + * field("preferredName").coalesce(field("fullName"), "Anonymous"); + * ``` + * + * @param replacement The next expression or literal to evaluate. + * @param others Additional expressions or literals to evaluate. + * @returns A new [Expression] representing the coalesce operation. + */ + coalesce( + replacement: Expression | unknown, + ...others: Array + ): FunctionExpression; + /** * @beta * Creates an expression that joins the elements of an array into a string. @@ -10886,6 +10947,149 @@ declare namespace FirebaseFirestore { elseExpr: Expression, ): Expression; + /** + * @beta + * Creates an expression that returns the `elseExpr` argument if `ifExpr` is null, else + * return the result of the `ifExpr` argument evaluation. + * + * @remarks + * This function provides a fallback for both absent and explicit null values. In contrast, + * {@link ifAbsent} only triggers for missing fields. + * + * @example + * ```typescript + * // Returns the user's preferred name, or if that is null, returns their full name. + * ifNull(field("preferredName"), field("fullName")) + * ``` + * + * @param ifExpr The expression to check for null. + * @param elseExpr The value that will be returned if `ifExpr` evaluates to null. + * @returns A new {@code Expression} representing the ifNull operation. + */ + export function ifNull( + ifExpr: Expression, + elseExpr: Expression, + ): FunctionExpression; + + /** + * @beta + * Creates an expression that returns the `elseValue` argument if `ifExpr` is null, else + * return the result of the `ifExpr` argument evaluation. + * + * @remarks + * This function provides a fallback for both absent and explicit null values. In contrast, + * {@link ifAbsent} only triggers for missing fields. + * + * @example + * ```typescript + * // Returns the user's display name, or returns "Anonymous" if the field is null. + * ifNull(field("displayName"), "Anonymous") + * ``` + * + * @param ifExpr The expression to check for null. + * @param elseValue The value that will be returned if `ifExpr` evaluates to null. + * @returns A new {@code Expression} representing the ifNull operation. + */ + export function ifNull( + ifExpr: Expression, + elseValue: unknown, + ): FunctionExpression; + + /** + * @beta + * Creates an expression that returns the `elseExpr` argument if `ifFieldName` is null, else + * return the value of the field. + * + * @remarks + * This function provides a fallback for both absent and explicit null values. In contrast, + * {@link ifAbsent} only triggers for missing fields. + * + * @example + * ```typescript + * // Returns the user's preferred name, or if that is null, returns their full name. + * ifNull("preferredName", field("fullName")) + * ``` + * + * @param ifFieldName The field to check for null. + * @param elseExpr The expression that will be evaluated and returned if `ifFieldName` is + * null. + * @returns A new {@code Expression} representing the ifNull operation. + */ + export function ifNull( + ifFieldName: string, + elseExpr: Expression, + ): FunctionExpression; + + /** + * @beta + * Creates an expression that returns the `elseValue` argument if `ifFieldName` is null, else + * return the value of the field. + * + * @remarks + * This function provides a fallback for both absent and explicit null values. In contrast, + * {@link ifAbsent} only triggers for missing fields. + * + * @example + * ```typescript + * // Returns the user's display name, or returns "Anonymous" if the field is null. + * ifNull("displayName", "Anonymous") + * ``` + * + * @param ifFieldName The field to check for null. + * @param elseValue The value that will be returned if `ifFieldName` is null. + * @returns A new {@code Expression} representing the ifNull operation. + */ + export function ifNull( + ifFieldName: string, + elseValue: unknown, + ): FunctionExpression; + + /** + * @beta + * Creates an expression that returns the first non-null, non-absent argument, without evaluating + * the rest of the arguments. When all arguments are null or absent, returns the last argument. + * + * @example + * ```typescript + * // Returns the value of the first non-null, non-absent field among 'preferredName', 'fullName', + * // or the last argument if all previous fields are null. + * coalesce(field("preferredName"), field("fullName"), constant("Anonymous")) + * ``` + * + * @param expression The first expression to evaluate. + * @param replacement The next expression or literal to evaluate. + * @param others Additional expressions or literals to evaluate. + * @returns A new [Expression] representing the coalesce operation. + */ + export function coalesce( + expression: Expression, + replacement: Expression | unknown, + ...others: Array + ): FunctionExpression; + + /** + * @beta + * Creates an expression that returns the first non-null, non-absent argument, without evaluating + * the rest of the arguments. When all arguments are null or absent, returns the last argument. + * + * @example + * ```typescript + * // Returns the value of the first non-null, non-absent field among 'preferredName', 'fullName', + * // or the last argument if all previous fields are null. + * coalesce("preferredName", field("fullName"), constant("Anonymous")) + * ``` + * + * @param fieldName The first field name to evaluate. + * @param replacement The next expression or literal to evaluate. + * @param others Additional expressions or literals to evaluate. + * @returns A new [Expression] representing the coalesce operation. + */ + export function coalesce( + fieldName: string, + replacement: Expression | unknown, + ...others: Array + ): FunctionExpression; + /** * @beta * Creates an expression that joins the elements of an array into a string.