Skip to content
This repository was archived by the owner on May 8, 2026. It is now read-only.

Commit 7acdb11

Browse files
committed
add documentation and overload for getField()
1 parent ed82cd4 commit 7acdb11

3 files changed

Lines changed: 207 additions & 22 deletions

File tree

google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java

Lines changed: 129 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -275,13 +275,32 @@ public static Pipeline subcollection(String path) {
275275
}
276276

277277
/**
278-
* Adds new fields or redefines existing fields in the output documents by
279-
* evaluating given expressions.
278+
* Defines one or more variables in the pipeline's scope. `define` is used to
279+
* bind a value to a
280+
* variable for internal reuse within the pipeline body (accessed via the {@link
281+
* Expression#variable(String)} function).
280282
*
281283
* <p>
282-
* This stage is useful for binding a value to a variable for internal reuse
283-
* within the pipeline
284-
* body (accessed via the {@link Expression#variable(String)} function).
284+
* This stage is useful for declaring reusable values or intermediate
285+
* calculations that can be
286+
* referenced multiple times in later parts of the pipeline, improving
287+
* readability and maintainability.
288+
*
289+
* <p>
290+
* Each variable is defined using an {@link AliasedExpression}, which pairs an
291+
* expression with a name (alias).
292+
*
293+
* <p>
294+
* Example:
295+
*
296+
* <pre>{@code
297+
* firestore.pipeline().collection("products")
298+
* .define(
299+
* multiply(field("price"), 0.9).as("discountedPrice"),
300+
* add(field("stock"), 10).as("newStock"))
301+
* .where(lessThan(variable("discountedPrice"), 100))
302+
* .select(field("name"), variable("newStock"));
303+
* }</pre>
285304
*
286305
* @param expression The expression to define using
287306
* {@link AliasedExpression}.
@@ -312,7 +331,110 @@ public Expression toArrayExpression() {
312331
}
313332

314333
/**
315-
* Converts the pipeline into a scalar expression.
334+
* Converts this Pipeline into an expression that evaluates to a single scalar
335+
* result. Used for
336+
* 1:1 lookups or Aggregations when the subquery is expected to return a single
337+
* value or object.
338+
*
339+
* <p>
340+
* <b>Runtime Validation:</b> The runtime will validate that the result set
341+
* contains exactly
342+
* one item. It throws a runtime error if the result has more than one item, and
343+
* evaluates to
344+
* {@code null} if the pipeline has zero results.
345+
*
346+
* <p>
347+
* <b>Result Unwrapping:</b> For simpler access, scalar subqueries producing a
348+
* single field
349+
* automatically unwrap that value to the top level, ignoring the inner alias.
350+
* If the subquery
351+
* returns multiple fields, they are preserved as a map.
352+
*
353+
* <p>
354+
* <b>Example 1: Single field unwrapping</b>
355+
*
356+
* <pre>{@code
357+
* // Calculate average rating for each restaurant using a subquery
358+
* db.pipeline().collection("restaurants")
359+
* .define(field("id").as("rid"))
360+
* .addFields(
361+
* db.pipeline().collection("reviews")
362+
* .where(field("restaurant_id").equal(variable("rid")))
363+
* // Inner aggregation returns a single document
364+
* .aggregate(AggregateFunction.average("rating").as("value"))
365+
* // Convert Pipeline -> Scalar Expression (validates result is 1 item)
366+
* .toScalarExpression()
367+
* .as("average_rating"))
368+
* }</pre>
369+
*
370+
* <p>
371+
* <i>The result set is unwrapped twice: from {@code "average_rating": [{
372+
* "value": 4.5 }]}
373+
* to {@code "average_rating": { "value": 4.5 }}, and finally to
374+
* {@code "average_rating": 4.5}.</i>
375+
*
376+
* <pre>{@code
377+
* // Output Document:
378+
* [
379+
* {
380+
* "id": "123",
381+
* "name": "The Burger Joint",
382+
* "cuisine": "American",
383+
* "average_rating": 4.5
384+
* },
385+
* {
386+
* "id": "456",
387+
* "name": "Sushi World",
388+
* "cuisine": "Japanese",
389+
* "average_rating": 4.8
390+
* }
391+
* ]
392+
* }</pre>
393+
*
394+
* <p>
395+
* <b>Example 2: Multiple fields (Map)</b>
396+
*
397+
* <pre>{@code
398+
* // For each restaurant, calculate review statistics (average rating AND total
399+
* // count)
400+
* db.pipeline().collection("restaurants")
401+
* .define(field("id").as("rid"))
402+
* .addFields(
403+
* db.pipeline().collection("reviews")
404+
* .where(field("restaurant_id").equal(variable("rid")))
405+
* .aggregate(
406+
* AggregateFunction.average("rating").as("avg_score"),
407+
* AggregateFunction.countAll().as("review_count"))
408+
* .toScalarExpression()
409+
* .as("stats"))
410+
* }</pre>
411+
*
412+
* <p>
413+
* <i>When the subquery produces multiple fields, they are wrapped in a map:</i>
414+
*
415+
* <pre>{@code
416+
* // Output Document:
417+
* [
418+
* {
419+
* "id": "123",
420+
* "name": "The Burger Joint",
421+
* "cuisine": "American",
422+
* "stats": {
423+
* "avg_score": 4.0,
424+
* "review_count": 3
425+
* }
426+
* },
427+
* {
428+
* "id": "456",
429+
* "name": "Sushi World",
430+
* "cuisine": "Japanese",
431+
* "stats": {
432+
* "avg_score": 4.8,
433+
* "review_count": 120
434+
* }
435+
* }
436+
* ]
437+
* }</pre>
316438
*
317439
* @return A new {@link Expression} representing the pipeline as a scalar.
318440
*/
@@ -1396,8 +1518,7 @@ public void onComplete() {
13961518
}
13971519
};
13981520

1399-
// logger.log(Level.FINEST, "Sending pipeline request: " +
1400-
// request.getStructuredPipeline());
1521+
logger.log(Level.FINEST, "Sending pipeline request: " + request.getStructuredPipeline());
14011522

14021523
rpcContext.streamRequest(request, observer, rpcContext.getClient().executePipelineCallable());
14031524
}

google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ public static Field field(FieldPath fieldPath) {
218218
return Field.ofUserPath(fieldPath.toString());
219219
}
220220

221+
221222
/**
222223
* Creates an expression that returns the current timestamp.
223224
*
@@ -4810,8 +4811,8 @@ public static Expression currentDocument() {
48104811
}
48114812

48124813
/**
4813-
* Creates an expression that retrieves the value of a variable bound via
4814-
* pipeline definitions.
4814+
* Creates an expression that retrieves the value of a variable bound via {@link
4815+
* Pipeline#define(AliasedExpression, AliasedExpression...)}.
48154816
*
48164817
* @param name The name of the variable to retrieve.
48174818
* @return An {@link Expression} representing the variable's value.
@@ -4821,28 +4822,91 @@ public static Expression variable(String name) {
48214822
return new Variable(name);
48224823
}
48234824

4825+
/**
4826+
* Creates an expression that evaluates to the provided pipeline.
4827+
*
4828+
* @param pipeline The pipeline to use as an expression.
4829+
* @return A new {@link Expression} representing the pipeline value.
4830+
*/
4831+
@InternalApi
4832+
public static Expression pipeline(Pipeline pipeline) {
4833+
return new PipelineValueExpression(pipeline);
4834+
}
4835+
48244836
/**
48254837
* Accesses a field/property of the expression (useful when the expression
4826-
* evaluates to a Map or Document).
4838+
* evaluates to a Map or
4839+
* Document).
48274840
*
4828-
* @param expression The expression evaluating to a map/document.
4829-
* @param key The key of the field to access.
4841+
* @param key The key of the field to access.
48304842
* @return An {@link Expression} representing the value of the field.
48314843
*/
48324844
@BetaApi
4833-
public static Expression field(Expression expression, String key) {
4834-
return new FunctionExpression("field", ImmutableList.of(expression, constant(key)));
4845+
public Expression getField(String key) {
4846+
return new FunctionExpression("field", ImmutableList.of(this, constant(key)));
48354847
}
48364848

48374849
/**
4838-
* Creates an expression that evaluates to the provided pipeline.
4850+
* Retrieves the value of a specific field from the document evaluated by this
4851+
* expression.
48394852
*
4840-
* @param pipeline The pipeline to use as an expression.
4841-
* @return A new {@link Expression} representing the pipeline value.
4853+
* @param keyExpression The expression evaluating to the key to access.
4854+
* @return A new {@link Expression} representing the field value.
48424855
*/
4843-
@InternalApi
4844-
public static Expression pipeline(Pipeline pipeline) {
4845-
return new PipelineValueExpression(pipeline);
4856+
@BetaApi
4857+
public Expression getField(Expression keyExpression) {
4858+
return new FunctionExpression("field", ImmutableList.of(this, keyExpression));
4859+
}
4860+
4861+
/**
4862+
* Accesses a field/property of a document field using the provided {@code key}.
4863+
*
4864+
* @param fieldName The field name of the map or document field.
4865+
* @param key The key of the field to access.
4866+
* @return An {@link Expression} representing the value of the field.
4867+
*/
4868+
@BetaApi
4869+
public static Expression getField(String fieldName, String key) {
4870+
return field(fieldName).getField(key);
4871+
}
4872+
4873+
/**
4874+
* Accesses a field/property of the expression using the provided
4875+
* {@code keyExpression}.
4876+
*
4877+
* @param expression The expression evaluating to a Map or Document.
4878+
* @param keyExpression The expression evaluating to the key.
4879+
* @return A new {@link Expression} representing the value of the field.
4880+
*/
4881+
@BetaApi
4882+
public static Expression getField(Expression expression, Expression keyExpression) {
4883+
return expression.getField(keyExpression);
4884+
}
4885+
4886+
/**
4887+
* Accesses a field/property of a document field using the provided
4888+
* {@code keyExpression}.
4889+
*
4890+
* @param fieldName The field name of the map or document field.
4891+
* @param keyExpression The expression evaluating to the key.
4892+
* @return A new {@link Expression} representing the value of the field.
4893+
*/
4894+
@BetaApi
4895+
public static Expression getField(String fieldName, Expression keyExpression) {
4896+
return field(fieldName).getField(keyExpression);
4897+
}
4898+
4899+
/**
4900+
* Accesses a field/property of the expression (useful when the expression
4901+
* evaluates to a Map or Document).
4902+
*
4903+
* @param expression The expression evaluating to a map/document.
4904+
* @param key The key of the field to access.
4905+
* @return An {@link Expression} representing the value of the field.
4906+
*/
4907+
@BetaApi
4908+
public static Expression getField(Expression expression, String key) {
4909+
return new FunctionExpression("field", ImmutableList.of(expression, constant(key)));
48464910
}
48474911

48484912
/**

google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2888,7 +2888,7 @@ public void testSubqueryWithCorrelatedField() throws Exception {
28882888
Pipeline sub = firestore.pipeline().collection(
28892889
collection.document("doc1").collection("some_subcollection").getPath())
28902890
// Using field access on a variable simulating a correlated query
2891-
.select(com.google.cloud.firestore.pipeline.expressions.Expression.field(
2891+
.select(com.google.cloud.firestore.pipeline.expressions.Expression.getField(
28922892
com.google.cloud.firestore.pipeline.expressions.Expression.variable("p"), "a").as("parent_a"));
28932893

28942894
List<PipelineResult> results = firestore

0 commit comments

Comments
 (0)