Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 108 additions & 16 deletions handwritten/firestore/api-report/firestore.api.md

Large diffs are not rendered by default.

508 changes: 508 additions & 0 deletions handwritten/firestore/dev/src/pipelines/expression.ts

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions handwritten/firestore/dev/src/pipelines/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
PipelineResult,
PipelineSnapshot,
PipelineSource,
subcollection,
} from './pipelines';

export {
Expand Down Expand Up @@ -67,6 +68,10 @@ export {
arrayMinimum,
arrayMaximumN,
arrayMinimumN,
arrayFilter,
arrayTransform,
arrayTransformWithIndex,
arraySlice,
field,
xor,
AggregateFunction,
Expand Down Expand Up @@ -156,5 +161,8 @@ export {
stringReplaceOne,
nor,
switchOn,
getField,
variable,
currentDocument,
// TODO(new-expression): Add new expression exports above this line
} from './expression';
12 changes: 11 additions & 1 deletion handwritten/firestore/dev/src/pipelines/pipeline-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ import {
lessThan,
Field,
AggregateFunction,
pipelineValue,
AliasedExpression,
} from './expression';
import {Pipeline, PipelineResult, ExplainStats} from './pipelines';
import {StructuredPipeline} from './structured-pipeline';
Expand Down Expand Up @@ -603,6 +605,12 @@ export function isBooleanExpr(
return val instanceof BooleanExpression;
}

export function isAliasedExpr(
val: unknown,
): val is firestore.Pipelines.AliasedExpression {
return val instanceof AliasedExpression;
}

export function isField(val: unknown): val is firestore.Pipelines.Field {
return val instanceof Field;
}
Expand Down Expand Up @@ -630,6 +638,9 @@ export function valueToDefaultExpr(value: unknown): Expression {
if (isFirestoreValue(value)) {
return constant(value);
}
if (isPipeline(value)) {
return pipelineValue(value);
}
if (value instanceof Expression) {
return value;
} else if (isPlainObject(value)) {
Expand All @@ -640,7 +651,6 @@ export function valueToDefaultExpr(value: unknown): Expression {
result = constant(value);
}

// TODO(pipeline) is this still used?
result._createdFromLiteral = true;
return result;
}
Expand Down
260 changes: 253 additions & 7 deletions handwritten/firestore/dev/src/pipelines/pipelines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
aliasedAggregateToMap,
fieldOrExpression,
isAliasedAggregate,
isAliasedExpr,
isBooleanExpr,
isCollectionReference,
isExpr,
Expand Down Expand Up @@ -59,6 +60,7 @@ import {
constant,
_mapValue,
field,
FunctionExpression,
} from './expression';
import {
AddFields,
Expand Down Expand Up @@ -95,6 +97,10 @@ import {
InternalDocumentsStageOptions,
InternalCollectionGroupStageOptions,
InternalCollectionStageOptions,
Define,
SubcollectionSource,
InternalDefineStageOptions,
InternalSubcollectionStageOptions,
} from './stage';
import {StructuredPipeline} from './structured-pipeline';
import Selectable = FirebaseFirestore.Pipelines.Selectable;
Expand Down Expand Up @@ -510,6 +516,216 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
return this._addStage(new RemoveFields(innerOptions));
}

/**
* @public
* Defines one or more variables in the pipeline's scope. `define` is used to bind a value to a
* variable for internal reuse within the pipeline body (accessed via the `variable()` function).
*
* This stage is useful for declaring reusable values or intermediate calculations that can be
* referenced multiple times in later parts of the pipeline.
*
* @example
* ```typescript
* db.pipeline().collection("products")
* .define(
* field("price").multiply(0.9).as("discountedPrice"),
* field("stock").add(10).as("newStock")
* )
* .where(variable("discountedPrice").lessThan(100))
* .select(field("name"), variable("newStock"));
* ```
*
* @param aliasedExpression - The first expression to bind to a variable.
* @param additionalExpressions - Optional additional expressions to bind to a variable.
* @returns A new Pipeline object with this stage appended to the stage list.
*/
define(
aliasedExpression: firestore.Pipelines.AliasedExpression,
...additionalExpressions: firestore.Pipelines.AliasedExpression[]
): Pipeline;
/**
* @public
* Defines one or more variables in the pipeline's scope. `define` is used to bind a value to a
* variable for internal reuse within the pipeline body (accessed via the `variable()` function).
*
* This stage is useful for declaring reusable values or intermediate calculations that can be
* referenced multiple times in later parts of the pipeline.
*
* @example
* ```typescript
* db.pipeline().collection("products")
* .define(
* field("price").multiply(0.9).as("discountedPrice"),
* field("stock").add(10).as("newStock")
* )
* .where(variable("discountedPrice").lessThan(100))
* .select(field("name"), variable("newStock"));
* ```
*
* @param options - An object that specifies required and optional parameters for the stage.
* @returns A new Pipeline object with this stage appended to the stage list.
*/
define(options: firestore.Pipelines.DefineStageOptions): Pipeline;
define(
aliasedExpressionOrOptions:
| firestore.Pipelines.AliasedExpression
| firestore.Pipelines.DefineStageOptions,
...additionalExpressions: firestore.Pipelines.AliasedExpression[]
): Pipeline {
const options = isAliasedExpr(aliasedExpressionOrOptions)
? {}
: aliasedExpressionOrOptions;

const aliasedExpressions: firestore.Pipelines.AliasedExpression[] =
isAliasedExpr(aliasedExpressionOrOptions)
? [aliasedExpressionOrOptions, ...additionalExpressions]
: aliasedExpressionOrOptions.variables;

const convertedExpressions: Map<string, Expression> =
selectablesToMap(aliasedExpressions);

const internalOptions: InternalDefineStageOptions = {
...options,
variables: convertedExpressions,
};

return this._addStage(new Define(internalOptions));
}

/**
* @public
* Converts this Pipeline into an expression that evaluates to an array of results.
*
* <p>Result Unwrapping:</p>
* <ul>
* <li>If the items have a single field, their values are unwrapped and returned directly in the array.</li>
* <li>If the items have multiple fields, they are returned as objects in the array.</li>
* </ul>
*
* @example
* ```typescript
* // Get a list of reviewers for each book
* db.pipeline().collection("books")
* .define(field("id").as("book_id"))
* .addFields(
* db.pipeline().collection("reviews")
* .where(field("book_id").equal(variable("book_id")))
* .select(field("reviewer"))
* .toArrayExpression()
* .as("reviewers")
* );
* ```
*
* Output:
* ```json
* [
* {
* "id": "1",
* "title": "1984",
* "reviewers": ["Alice", "Bob"]
* }
* ]
* ```
*
* Multiple Fields:
* ```typescript
* // Get a list of reviews (reviewer and rating) for each book
* db.pipeline().collection("books")
* .define(field("id").as("book_id"))
* .addFields(
* db.pipeline().collection("reviews")
* .where(field("book_id").equal(variable("book_id")))
* .select(field("reviewer"), field("rating"))
* .toArrayExpression()
* .as("reviews")
* );
* ```
*
* Output:
* ```json
* [
* {
* "id": "1",
* "title": "1984",
* "reviews": [
* { "reviewer": "Alice", "rating": 5 },
* { "reviewer": "Bob", "rating": 4 }
* ]
* }
* ]
* ```
*
* @returns An `Expression` representing the execution of this pipeline.
*/
toArrayExpression(): firestore.Pipelines.Expression {
return new FunctionExpression('array', [fieldOrExpression(this)]);
}

/**
* @public
* Converts this Pipeline into an expression that evaluates to a single scalar result.
*
* <p><b>Runtime Validation:</b> The runtime validates that the result set contains zero or one item. If
* zero items, it evaluates to `null`.</p>
*
* <p>Result Unwrapping:</p>
* <ul>
* <li>If the item has a single field, its value is unwrapped and returned directly.</li>
* <li>If the item has multiple fields, they are returned as an object.</li>
* </ul>
*
* @example
* ```typescript
* // Calculate average rating for a restaurant
* db.pipeline().collection("restaurants").addFields(
* db.pipeline().collection("reviews")
* .where(field("restaurant_id").equal(variable("rid")))
* .aggregate(average("rating").as("avg"))
* // Unwraps the single "avg" field to a scalar double
* .toScalarExpression().as("average_rating")
* );
* ```
*
* Output:
* ```json
* {
* "name": "The Burger Joint",
* "average_rating": 4.5
* }
* ```
*
* Multiple Fields:
* ```typescript
* // Calculate average rating AND count for a restaurant
* db.pipeline().collection("restaurants").addFields(
* db.pipeline().collection("reviews")
* .where(field("restaurant_id").equal(variable("rid")))
* .aggregate(
* average("rating").as("avg"),
* count().as("count")
* )
* // Returns an object with "avg" and "count" fields
* .toScalarExpression().as("stats")
* );
* ```
*
* Output:
* ```json
* {
* "name": "The Burger Joint",
* "stats": {
* "avg": 4.5,
* "count": 100
* }
* }
* ```
*
* @returns An `Expression` representing the execution of this pipeline.
*/
toScalarExpression(): firestore.Pipelines.Expression {
return new FunctionExpression('scalar', [fieldOrExpression(this)]);
}

/**
* @beta
* Selects or creates a set of fields from the outputs of previous stages.
Expand Down Expand Up @@ -1645,18 +1861,16 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
return util.stream(structuredPipeline, undefined);
}

_toProto(): api.IPipeline {
if (!this.db) {
_toProto(serializer?: Serializer): api.IPipeline {
const resolvedSerializer = serializer || this.db?._serializer;
if (!resolvedSerializer) {
throw new Error(
'This pipeline was created without a database (e.g., as a subcollection pipeline) and cannot be executed directly. It can only be used as part of another pipeline.',
);
}

const stages: IStage[] = this.stages.map(
// We use a non-null assertion here because we've already checked that
// 'db' is not null at the start of this function, but TS does not
// recognize that 'db' can no longer be undefined.
stage => stage._toProto(this.db!._serializer!),
const stages: IStage[] = this.stages.map(stage =>
stage._toProto(resolvedSerializer),
);
return {stages};
}
Expand Down Expand Up @@ -2058,3 +2272,35 @@ export class PipelineResult implements firestore.Pipelines.PipelineResult {
);
}
}

/**
* @public
* Creates a new Pipeline targeted at a subcollection relative to the current document context.
* This creates a pipeline without a database instance, suitable for embedding as a subquery.
* If executed directly, this pipeline will fail.
*
* @param path - The relative path to the subcollection.
*/
export function subcollection(path: string): Pipeline;
/**
* @public
* Creates a new Pipeline targeted at a subcollection relative to the current document context.
*
* @param options - Options defining how this SubcollectionStage is evaluated.
*/
export function subcollection(
options: firestore.Pipelines.SubcollectionStageOptions,
): Pipeline;
export function subcollection(
pathOrOptions: string | firestore.Pipelines.SubcollectionStageOptions,
): Pipeline {
const options = isString(pathOrOptions) ? {} : pathOrOptions;
const path = isString(pathOrOptions) ? pathOrOptions : pathOrOptions.path;

const internalOptions: InternalSubcollectionStageOptions = {
...options,
path,
};

return new Pipeline(undefined, [new SubcollectionSource(internalOptions)]);
}
Loading
Loading