diff --git a/firestore/app/src/main/java/com/google/example/firestore/DocSnippets.java b/firestore/app/src/main/java/com/google/example/firestore/DocSnippets.java index 56bfbc22..2a0b4562 100644 --- a/firestore/app/src/main/java/com/google/example/firestore/DocSnippets.java +++ b/firestore/app/src/main/java/com/google/example/firestore/DocSnippets.java @@ -63,6 +63,8 @@ import com.google.firebase.firestore.pipeline.FindNearestStage; import com.google.firebase.firestore.pipeline.SampleStage; import com.google.firebase.firestore.pipeline.SearchStage; +import com.google.firebase.firestore.pipeline.CollectionGroupOptions; +import com.google.firebase.firestore.pipeline.CollectionHints; import com.google.firebase.firestore.pipeline.UnnestOptions; import static com.google.firebase.firestore.pipeline.Expression.field; @@ -4204,4 +4206,330 @@ void searchPhraseMatch() { // [END search_phrase_match] } + void pipelineJoinTestData() { + // [START pipeline_join_test_data] + // Load set of cities. + CollectionReference cities = db.collection("cities"); + + Map sfData = new HashMap<>(); + sfData.put("name", "San Francisco"); + sfData.put("state", "CA"); + sfData.put("country", "USA"); + cities.document("SF").set(sfData); + + Map laData = new HashMap<>(); + laData.put("name", "Los Angeles"); + laData.put("state", "CA"); + laData.put("country", "USA"); + cities.document("LA").set(laData); + + Map dcData = new HashMap<>(); + dcData.put("name", "Washington, D.C."); + dcData.put("state", null); + dcData.put("country", "USA"); + cities.document("DC").set(dcData); + + Map tokData = new HashMap<>(); + tokData.put("name", "Tokyo"); + tokData.put("state", null); + tokData.put("country", "Japan"); + cities.document("TOK").set(tokData); + + // Load restaurants in various cities. + CollectionReference sfRestaurants = db.collection("cities").document("SF").collection("restaurants"); + CollectionReference laRestaurants = db.collection("cities").document("LA").collection("restaurants"); + CollectionReference dcRestaurants = db.collection("cities").document("DC").collection("restaurants"); + + DocumentReference rest1 = sfRestaurants.document("rest1"); + Map rest1Data = new HashMap<>(); + rest1Data.put("name", "Golden Gate Pizza"); + rest1Data.put("type", "pizza"); + rest1Data.put("owner_id", "Mario Rossi"); + rest1.set(rest1Data); + + DocumentReference rest2 = sfRestaurants.document("rest2"); + Map rest2Data = new HashMap<>(); + rest2Data.put("name", "Bay Area Burger"); + rest2Data.put("type", "burger"); + rest2Data.put("owner_id", "Sarah Jenkins"); + rest2.set(rest2Data); + + DocumentReference rest3 = sfRestaurants.document("rest3"); + Map rest3Data = new HashMap<>(); + rest3Data.put("name", "Sunset Taco"); + rest3Data.put("type", "mexican"); + rest3Data.put("owner_id", "Edward"); + rest3.set(rest3Data); + + DocumentReference rest4 = laRestaurants.document("rest4"); + Map rest4Data = new HashMap<>(); + rest4Data.put("name", "Hollywood Sushi"); + rest4Data.put("type", "sushi"); + rest4Data.put("owner_id", "Ken Kenji"); + rest4.set(rest4Data); + + DocumentReference rest5 = laRestaurants.document("rest5"); + Map rest5Data = new HashMap<>(); + rest5Data.put("name", "Venice Pizza"); + rest5Data.put("type", "pizza"); + rest5Data.put("owner_id", "Luigi Romano"); + rest5.set(rest5Data); + + DocumentReference rest6 = dcRestaurants.document("rest6"); + Map rest6Data = new HashMap<>(); + rest6Data.put("name", "Capitol Tacos"); + rest6Data.put("type", "mexican"); + rest6Data.put("owner_id", "Maria Garcia"); + rest6.set(rest6Data); + + DocumentReference rest7 = dcRestaurants.document("rest7"); + Map rest7Data = new HashMap<>(); + rest7Data.put("name", "Georgetown Coffee"); + rest7Data.put("type", "cafe"); + rest7Data.put("owner_id", "David Kim"); + rest7.set(rest7Data); + + // Load collection of reviews. + CollectionReference reviews = db.collection("reviews"); + + Map rev1 = new HashMap<>(); + rev1.put("restaurant", rest1); + rev1.put("rating", 5); + rev1.put("reviewer_id", "Alice"); + reviews.add(rev1); + + Map rev2 = new HashMap<>(); + rev2.put("restaurant", rest1); + rev2.put("rating", 4); + rev2.put("reviewer_id", "Bob"); + reviews.add(rev2); + + Map rev3 = new HashMap<>(); + rev3.put("restaurant", rest2); + rev3.put("rating", 4); + rev3.put("reviewer_id", "Charlie"); + reviews.add(rev3); + + Map rev4 = new HashMap<>(); + rev4.put("restaurant", rest3); + rev4.put("rating", 5); + rev4.put("reviewer_id", "Diana"); + reviews.add(rev4); + + Map rev5 = new HashMap<>(); + rev5.put("restaurant", rest3); + rev5.put("rating", 4); + rev5.put("reviewer_id", "Edward"); + reviews.add(rev5); + + Map rev6 = new HashMap<>(); + rev6.put("restaurant", rest3); + rev6.put("rating", 4); + rev6.put("reviewer_id", "Fiona"); + reviews.add(rev6); + + // rest4 has 0 reviews + + Map rev7 = new HashMap<>(); + rev7.put("restaurant", rest5); + rev7.put("rating", 3); + rev7.put("reviewer_id", "George"); + reviews.add(rev7); + + Map rev8 = new HashMap<>(); + rev8.put("restaurant", rest6); + rev8.put("rating", 5); + rev8.put("reviewer_id", "Hannah"); + reviews.add(rev8); + + Map rev9 = new HashMap<>(); + rev9.put("restaurant", rest6); + rev9.put("rating", 4); + rev9.put("reviewer_id", "Ian"); + reviews.add(rev9); + + Map rev10 = new HashMap<>(); + rev10.put("restaurant", rest7); + rev10.put("rating", 5); + rev10.put("reviewer_id", "Julia"); + reviews.add(rev10); + // [END pipeline_join_test_data] + } + + void pipelineJoinLookup() { + // [START pipeline_join_lookup] + Task results = db.pipeline() + .collectionGroup("reviews") + .define(field("restaurant").alias("restaurant_name")) + .addFields( + db.pipeline() + .collectionGroup("restaurants") + .where(field("__name__").equal(variable("restaurant_name"))) + .select("name", "type") + .toScalarExpression() + .alias("restaurant") + ) + .execute(); + // [END pipeline_join_lookup] + } + + void pipelineJoinArray() { + // [START pipeline_join_array] + Task results = db.pipeline() + .collectionGroup("restaurants") + .where(field("type").equal("pizza")) + .define(field("__name__").alias("restaurant_name")) + .select( + field("name"), + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("restaurant_name"))) + .select("rating", "reviewer_id") + .toArrayExpression() + .alias("reviews") + ) + .execute(); + // [END pipeline_join_array] + } + + void pipelineJoinAggregate() { + // [START pipeline_join_aggregate] + Task results = db.pipeline() + .collectionGroup("restaurants") + .where(field("type").equal("pizza")) + .define(field("__name__").alias("restaurant_name")) + .select( + field("name"), + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("restaurant_name"))) + .aggregate(average("rating").alias("avg_rating")) + .toScalarExpression() + .alias("avg_rating") + ) + .execute(); + // [END pipeline_join_aggregate] + } + + void pipelineJoinLimit() { + // [START pipeline_join_limit] + Task results = db.pipeline() + .collectionGroup("restaurants") + .define(field("__name__").alias("restaurant_name")) + .select( + field("name"), + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("restaurant_name"))) + .sort(field("rating").descending()) + .limit(2) + .select("rating", "reviewer_id") + .toArrayExpression() + .alias("top_reviews") + ) + .execute(); + // [END pipeline_join_limit] + } + + void pipelineJoinSubcollection() { + // [START pipeline_join_subcollection] + Task results = db.pipeline() + .collection("cities") + .addFields( + PipelineSource.subcollection("restaurants") + .toArrayExpression() + .length() + .alias("restaurant_count") + ) + .execute(); + // [END pipeline_join_subcollection] + } + + void pipelineJoinMultiField() { + // [START pipeline_join_multi_field] + Task results = db.pipeline() + .collectionGroup("restaurants") + .define( + field("owner_id").alias("owner_id"), + field("__name__").alias("__name__") + ) + .where( + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("__name__"))) + .where(field("reviewer_id").equal(variable("owner_id"))) + .aggregate(countAll().alias("c")) + .toScalarExpression() + .greaterThan(0) + ) + .execute(); + // [END pipeline_join_multi_field] + } + + void pipelineJoinAnti() { + // [START pipeline_join_anti] + Task results = db.pipeline() + .collectionGroup("restaurants") + .define(field("__name__").alias("restaurant_name")) + .where( + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("restaurant_name"))) + .aggregate(countAll().alias("review_count")) + .toScalarExpression() + .equal(0) + ) + .execute(); + // [END pipeline_join_anti] + } + + void pipelineJoinUnnest() { + // [START pipeline_join_unnest] + Task results = db.pipeline() + .collectionGroup("restaurants") + .where(field("type").equal("pizza")) + .define(field("__name__").alias("restaurant_name")) + .unnest( + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("restaurant_name"))) + .select("rating", "reviewer_id") + .toArrayExpression() + .alias("review") + ) + .execute(); + // [END pipeline_join_unnest] + } + + void pipelineJoinUncorrelated() { + // [START pipeline_join_uncorrelated] + Task results = db.pipeline() + .collection("reviews") + // Average review rating is 4.3 + .where( + field("rating").greaterThan( + db.pipeline() + .collection("reviews") + .aggregate(average("rating").alias("avg")) + .toScalarExpression() + ) + ) + .select("rating", "reviewer_id") + .execute(); + // [END pipeline_join_uncorrelated] + } + + void pipelineForceTableScan() { + // [START pipeline_force_table_scan] + // Force Planner to only do a Full-Table Scan + Task results = db.pipeline() + .collectionGroup( + "customers", + new CollectionGroupOptions().withHints(new CollectionHints().withForceIndex("primary")) + ) + .limit(2) + .execute(); + // [END pipeline_force_table_scan] + } + } diff --git a/firestore/app/src/main/java/com/google/example/firestore/kotlin/DocSnippets.kt b/firestore/app/src/main/java/com/google/example/firestore/kotlin/DocSnippets.kt index 7ff116bd..fa9efac5 100644 --- a/firestore/app/src/main/java/com/google/example/firestore/kotlin/DocSnippets.kt +++ b/firestore/app/src/main/java/com/google/example/firestore/kotlin/DocSnippets.kt @@ -41,6 +41,9 @@ import com.google.firebase.firestore.pipeline.FindNearestStage import com.google.firebase.firestore.pipeline.SampleStage import com.google.firebase.firestore.pipeline.SearchStage import com.google.firebase.firestore.pipeline.UnnestOptions +import com.google.firebase.firestore.PipelineSource +import com.google.firebase.firestore.pipeline.CollectionGroupOptions +import com.google.firebase.firestore.pipeline.CollectionHints import com.google.firebase.firestore.toObject import java.util.Date import java.util.concurrent.LinkedBlockingQueue @@ -3826,4 +3829,295 @@ abstract class DocSnippets(val db: FirebaseFirestore) { // [END search_phrase_match] } + fun pipelineJoinTestData() { + // [START pipeline_join_test_data] + // Load set of cities. + val cities = db.collection("cities") + + cities.document("SF").set( + mapOf( + "name" to "San Francisco", + "state" to "CA", + "country" to "USA", + ), + ) + cities.document("LA").set( + mapOf( + "name" to "Los Angeles", + "state" to "CA", + "country" to "USA", + ), + ) + cities.document("DC").set( + mapOf( + "name" to "Washington, D.C.", + "state" to null, + "country" to "USA", + ), + ) + cities.document("TOK").set( + mapOf( + "name" to "Tokyo", + "state" to null, + "country" to "Japan", + ), + ) + + // Load restaurants in various cities. + val sfRestaurants = db.collection("cities").document("SF").collection("restaurants") + val laRestaurants = db.collection("cities").document("LA").collection("restaurants") + val dcRestaurants = db.collection("cities").document("DC").collection("restaurants") + + val rest1 = sfRestaurants.document("rest1") + rest1.set( + mapOf( + "name" to "Golden Gate Pizza", + "type" to "pizza", + "owner_id" to "Mario Rossi", + ), + ) + val rest2 = sfRestaurants.document("rest2") + rest2.set( + mapOf( + "name" to "Bay Area Burger", + "type" to "burger", + "owner_id" to "Sarah Jenkins", + ), + ) + val rest3 = sfRestaurants.document("rest3") + rest3.set( + mapOf( + "name" to "Sunset Taco", + "type" to "mexican", + "owner_id" to "Edward", + ), + ) + + val rest4 = laRestaurants.document("rest4") + rest4.set( + mapOf( + "name" to "Hollywood Sushi", + "type" to "sushi", + "owner_id" to "Ken Kenji", + ), + ) + val rest5 = laRestaurants.document("rest5") + rest5.set( + mapOf( + "name" to "Venice Pizza", + "type" to "pizza", + "owner_id" to "Luigi Romano", + ), + ) + + val rest6 = dcRestaurants.document("rest6") + rest6.set( + mapOf( + "name" to "Capitol Tacos", + "type" to "mexican", + "owner_id" to "Maria Garcia", + ), + ) + val rest7 = dcRestaurants.document("rest7") + rest7.set( + mapOf( + "name" to "Georgetown Coffee", + "type" to "cafe", + "owner_id" to "David Kim", + ), + ) + + // Load collection of reviews. + val reviews = db.collection("reviews") + + reviews.add(mapOf("restaurant" to rest1, "rating" to 5, "reviewer_id" to "Alice")) + reviews.add(mapOf("restaurant" to rest1, "rating" to 4, "reviewer_id" to "Bob")) + reviews.add(mapOf("restaurant" to rest2, "rating" to 4, "reviewer_id" to "Charlie")) + reviews.add(mapOf("restaurant" to rest3, "rating" to 5, "reviewer_id" to "Diana")) + reviews.add(mapOf("restaurant" to rest3, "rating" to 4, "reviewer_id" to "Edward")) + reviews.add(mapOf("restaurant" to rest3, "rating" to 4, "reviewer_id" to "Fiona")) + // rest4 has 0 reviews + reviews.add(mapOf("restaurant" to rest5, "rating" to 3, "reviewer_id" to "George")) + reviews.add(mapOf("restaurant" to rest6, "rating" to 5, "reviewer_id" to "Hannah")) + reviews.add(mapOf("restaurant" to rest6, "rating" to 4, "reviewer_id" to "Ian")) + reviews.add(mapOf("restaurant" to rest7, "rating" to 5, "reviewer_id" to "Julia")) + // [END pipeline_join_test_data] + } + + fun pipelineJoinLookup() { + // [START pipeline_join_lookup] + val results = db.pipeline() + .collectionGroup("reviews") + .define(field("restaurant").alias("restaurant_name")) + .addFields( + db.pipeline() + .collectionGroup("restaurants") + .where(field("__name__").equal(variable("restaurant_name"))) + .select("name", "type") + .toScalarExpression() + .alias("restaurant"), + ) + .execute() + // [END pipeline_join_lookup] + } + + fun pipelineJoinArray() { + // [START pipeline_join_array] + val results = db.pipeline() + .collectionGroup("restaurants") + .where(field("type").equal("pizza")) + .define(field("__name__").alias("restaurant_name")) + .select( + field("name"), + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("restaurant_name"))) + .select("rating", "reviewer_id") + .toArrayExpression() + .alias("reviews"), + ) + .execute() + // [END pipeline_join_array] + } + + fun pipelineJoinAggregate() { + // [START pipeline_join_aggregate] + val results = db.pipeline() + .collectionGroup("restaurants") + .where(field("type").equal("pizza")) + .define(field("__name__").alias("restaurant_name")) + .select( + field("name"), + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("restaurant_name"))) + .aggregate(average("rating").alias("avg_rating")) + .toScalarExpression() + .alias("avg_rating"), + ) + .execute() + // [END pipeline_join_aggregate] + } + + fun pipelineJoinLimit() { + // [START pipeline_join_limit] + val results = db.pipeline() + .collectionGroup("restaurants") + .define(field("__name__").alias("restaurant_name")) + .select( + field("name"), + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("restaurant_name"))) + .sort(field("rating").descending()) + .limit(2) + .select("rating", "reviewer_id") + .toArrayExpression() + .alias("top_reviews"), + ) + .execute() + // [END pipeline_join_limit] + } + + fun pipelineJoinSubcollection() { + // [START pipeline_join_subcollection] + val results = db.pipeline() + .collection("cities") + .addFields( + PipelineSource.subcollection("restaurants") + .toArrayExpression() + .length() + .alias("restaurant_count"), + ) + .execute() + // [END pipeline_join_subcollection] + } + + fun pipelineJoinMultiField() { + // [START pipeline_join_multi_field] + val results = db.pipeline() + .collectionGroup("restaurants") + .define( + field("owner_id").alias("owner_id"), + field("__name__").alias("__name__"), + ) + .where( + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("__name__"))) + .where(field("reviewer_id").equal(variable("owner_id"))) + .aggregate(countAll().alias("c")) + .toScalarExpression() + .greaterThan(0), + ) + .execute() + // [END pipeline_join_multi_field] + } + + fun pipelineJoinAnti() { + // [START pipeline_join_anti] + val results = db.pipeline() + .collectionGroup("restaurants") + .define(field("__name__").alias("restaurant_name")) + .where( + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("restaurant_name"))) + .aggregate(countAll().alias("review_count")) + .toScalarExpression() + .equal(0), + ) + .execute() + // [END pipeline_join_anti] + } + + fun pipelineJoinUnnest() { + // [START pipeline_join_unnest] + val results = db.pipeline() + .collectionGroup("restaurants") + .where(field("type").equal("pizza")) + .define(field("__name__").alias("restaurant_name")) + .unnest( + db.pipeline() + .collectionGroup("reviews") + .where(field("restaurant").equal(variable("restaurant_name"))) + .select("rating", "reviewer_id") + .toArrayExpression() + .alias("review"), + ) + .execute() + // [END pipeline_join_unnest] + } + + fun pipelineJoinUncorrelated() { + // [START pipeline_join_uncorrelated] + val results = db.pipeline() + .collection("reviews") + // Average review rating is 4.3 + .where( + field("rating").greaterThan( + db.pipeline() + .collection("reviews") + .aggregate(average("rating").alias("avg")) + .toScalarExpression(), + ), + ) + .select("rating", "reviewer_id") + .execute() + // [END pipeline_join_uncorrelated] + } + + fun pipelineForceTableScan() { + // [START pipeline_force_table_scan] + // Force Planner to only do a Full-Table Scan + val results = db.pipeline() + .collectionGroup( + "customers", + CollectionGroupOptions().withHints(CollectionHints().withForceIndex("primary")), + ) + .limit(2) + .execute() + // [END pipeline_force_table_scan] + } + }