From b17f8f1e856d655bce49921fc84f856192c64f8a Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Mon, 3 Nov 2025 14:51:14 -0800 Subject: [PATCH 01/18] add initial pipelines snippet file --- snippets/firestore/firestore_pipelines.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 snippets/firestore/firestore_pipelines.py diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py new file mode 100644 index 00000000..bedec4f7 --- /dev/null +++ b/snippets/firestore/firestore_pipelines.py @@ -0,0 +1,22 @@ +# 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 firebase_admin +from firebase_admin import firestore + +default_app = firebase_admin.initialize_app() +client = firestore.client(default_app, "your-new-enterprise-database") + +# pylint: disable=invalid-name +def init_firestore_client(): From 71c481fcbf1b46af3771bc499e80090d41c1c02e Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Tue, 4 Nov 2025 15:51:56 -0800 Subject: [PATCH 02/18] add pipeline snippets for python --- snippets/firestore/firestore_pipelines.py | 1269 ++++++++++++++++++++- 1 file changed, 1268 insertions(+), 1 deletion(-) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index bedec4f7..478236ba 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -12,6 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. +from google.cloud.firestore import Query +from google.cloud.firestore_v1.pipeline import Pipeline +from google.cloud.firestore_v1.pipeline_source import PipelineSource +from google.cloud.firestore_v1.pipeline_expressions import ( + AggregateFunction, + Constant, + Expression, + Field, + Count, +) +from google.cloud.firestore_v1.pipeline_expressions import ( + And, Conditional, Or, Not, Xor +) +from google.cloud.firestore_v1.pipeline_stages import ( + Aggregate, + FindNearestOptions, + SampleOptions, + UnnestOptions, +) +from google.cloud.firestore_v1.base_vector_query import DistanceMeasure +from google.cloud.firestore_v1.vector import Vector +from google.cloud.firestore_v1.client import Client + import firebase_admin from firebase_admin import firestore @@ -19,4 +42,1248 @@ client = firestore.client(default_app, "your-new-enterprise-database") # pylint: disable=invalid-name -def init_firestore_client(): +def pipeline_concepts(): + # [START pipeline_concepts] + pipeline = client.pipeline() \ + .collection("cities") \ + .where(Field.of("population").greater_than(100000)) \ + .sort(Field.of("name").ascending()) \ + .limit(10) + # [END pipeline_concepts] + print(pipeline) + +def basic_read(): + # [START basic_read] + pipeline = client.pipeline().collection("users") + for result in pipeline.execute(): + print(result.id + " => " + result.data) + # [END basic_read] + +def pipeline_initialization(): + # [START pipeline_initialization] + firestore_client = firestore.client(default_app, "your-new-enterprise-database") + pipeline = firestore_client.pipeline() + # [END pipeline_initialization] + print(pipeline) + +def field_vs_constants(): + # [START field_or_constant] + pipeline = client.pipeline() \ + .collection("cities") \ + .where(Field.of("name").equal(Constant("Toronto"))) + # [END field_or_constant] + print(pipeline) + +def input_stages(): + # [START input_stages] + # Return all restaurants in San Francisco + results = client.pipeline().collection("cities/sf/restaurants").execute() + + # Return all restaurants + results = client.pipeline().collection_group("restaurants").execute() + + # Return all documents across all collections in the database (the entire database) + results = client.pipeline().database().execute() + + # Batch read of 3 documents + results = client.pipeline().documents( + client.collection("cities").document("SF"), + client.collection("cities").document("DC"), + client.collection("cities").document("NY") + ).execute() + # [END input_stages] + for result in results: + print(result) + +def where_pipeline(): + # [START pipeline_where] + results = client.pipeline().collection("books") \ + .where(Field.of("rating").equal(5)) \ + .where(Field.of("published").less_than(1900)) \ + .execute() + + results = client.pipeline().collection("books") \ + .where(And( + Field.of("rating").equal(5), + Field.of("published").less_than(1900) + )) \ + .execute() + # [END pipeline_where] + for result in results: + print(result) + +def aggregate_groups(): + # [START aggregate_groups] + results = client.pipeline() \ + .collection("books") \ + .aggregate( + Field.of("rating").average().as_("avg_rating"), + groups=[Field.of("genre")] + ) \ + .execute() + # [END aggregate_groups] + for result in results: + print(result) + +def aggregate_distinct(): + # [START aggregate_distinct] + results = client.pipeline() \ + .collection("books") \ + .distinct( + Field.of("author").to_upper().as_("author"), + "genre" + ) \ + .execute() + # [END aggregate_distinct] + for result in results: + print(result) + +def sort(): + # [START sort] + results = client.pipeline() \ + .collection("books") \ + .sort( + Field.of("release_date").descending(), + Field.of("author").ascending() + ) \ + .execute() + # [END sort] + for result in results: + print(result) + +def sort_comparison(): + # [START sort_comparison] + query = client.collection("cities") \ + .order_by("state") \ + .order_by("population", direction=Query.DESCENDING) + + pipeline = client.pipeline() \ + .collection("books") \ + .sort( + Field.of("release_date").descending(), + Field.of("author").ascending() + ) + # [END sort_comparison] + print(query) + print(pipeline) + +def functions_example(): + # [START functions_example] + # Type 1: Scalar (for use in non-aggregation stages) + # Example: Return the min store price for each book. + results = client.pipeline().collection("books") \ + .select( + Field.of("current").logical_minimum(Field.of("updated")).as_("price_min") + ) \ + .execute() + + # Type 2: Aggregation (for use in aggregate stages) + # Example: Return the min price of all books. + results = client.pipeline().collection("books") \ + .aggregate(Field.of("price").minimum().as_("min_price")) \ + .execute() + # [END functions_example] + for result in results: + print(result) + +def creating_indexes(): + # [START query_example] + results = client.pipeline() \ + .collection("books") \ + .where(Field.of("published").less_than(1900)) \ + .where(Field.of("genre").equal("Science Fiction")) \ + .where(Field.of("rating").greater_than(4.3)) \ + .sort(Field.of("published").descending()) \ + .execute() + # [END query_example] + for result in results: + print(result) + +def sparse_indexes(): + # [START sparse_index_example] + results = client.pipeline() \ + .collection("books") \ + .where(Field.of("category").like("%fantasy%")) \ + .execute() + # [END sparse_index_example] + for result in results: + print(result) + +def sparse_indexes2(): + # [START sparse_index_example_2] + results = client.pipeline() \ + .collection("books") \ + .sort(Field.of("release_date").ascending()) \ + .execute() + # [END sparse_index_example_2] + for result in results: + print(result) + +def covered_query(): + # [START covered_query] + results = client.pipeline() \ + .collection("books") \ + .where(Field.of("category").like("%fantasy%")) \ + .where(Field.of("title").exists()) \ + .where(Field.of("author").exists()) \ + .select("title", "author") \ + .execute() + # [END covered_query] + for result in results: + print(result) + +def pagination(): + # [START pagination_not_supported_preview] + # Existing pagination via `start_at()` + query = client.collection("cities").order_by("population").start_at({ + "population": 1000000 + }) + + # Private preview workaround using pipelines + pipeline = client.pipeline() \ + .collection("cities") \ + .where(Field.of("population").greater_than_or_equal(1000000)) \ + .sort(Field.of("population").descending()) + # [END pagination_not_supported_preview] + print(query) + print(pipeline) + +def collection_stage(): + # [START collection_example] + results = client.pipeline() \ + .collection("users/bob/games") \ + .sort(Field.of("name").ascending()) \ + .execute() + # [END collection_example] + for result in results: + print(result) + +def collection_group_stage(): + # [START collection_group_example] + results = client.pipeline() \ + .collection_group("games") \ + .sort(Field.of("name").ascending()) \ + .execute() + # [END collection_group_example] + for result in results: + print(result) + +def database_stage(): + # [START database_example] + # Count all documents in the database + results = client.pipeline() \ + .database() \ + .aggregate(Count().as_("total")) \ + .execute() + # [END database_example] + for result in results: + print(result) + +def documents_stage(): + # [START documents_example] + results = client.pipeline().documents( + client.collection("cities").document("SF"), + client.collection("cities").document("DC"), + client.collection("cities").document("NY") + ).execute() + # [END documents_example] + for result in results: + print(result) + +def replace_with_stage(): + # [START initial_data] + client.collection("cities").document("SF").set({ + "name": "San Francisco", + "population": 800000, + "location": { + "country": "USA", + "state": "California" + } + }) + client.collection("cities").document("TO").set({ + "name": "Toronto", + "population": 3000000, + "province": "ON", + "location": { + "country": "Canada", + "province": "Ontario" + } + }) + client.collection("cities").document("NY").set({ + "name": "New York", + "location": { + "country": "USA", + "state": "New York" + } + }) + client.collection("cities").document("AT").set({ + "name": "Atlantis", + }) + # [END initial_data] + + # [START full_replace] + names = client.pipeline() \ + .collection("cities") \ + .replace_with(Field.of("location")) \ + .execute() + # [END full_replace] + + # [START map_merge_overwrite] + # unsupported in client SDKs for now + # [END map_merge_overwrite] + for name in names: + print(name) + +def sample_stage(): + # [START sample_example] + # Get a sample of 100 documents in a database + results = client.pipeline() \ + .database() \ + .sample(100) \ + .execute() + + # Randomly shuffle a list of 3 documents + results = client.pipeline() \ + .documents( + client.collection("cities").document("SF"), + client.collection("cities").document("NY"), + client.collection("cities").document("DC") + ) \ + .sample(3) \ + .execute() + # [END sample_example] + for result in results: + print(result) + +def sample_percent(): + # [START sample_percent] + # Get a sample of on average 50% of the documents in the database + results = client.pipeline() \ + .database() \ + .sample(SampleOptions.percentage(0.5)) \ + .execute() + # [END sample_percent] + for result in results: + print(result) + +def union_stage(): + # [START union_stage] + results = client.pipeline() \ + .collection("cities/SF/restaurants") \ + .where(Field.of("type").equal("Chinese")) \ + .union(client.pipeline() \ + .collection("cities/NY/restaurants") \ + .where(Field.of("type").equal("Italian"))) \ + .where(Field.of("rating").greater_than_or_equal(4.5)) \ + .sort(Field.of("__name__").descending()) \ + .execute() + # [END union_stage] + for result in results: + print(result) + +def union_stage_stable(): + # [START union_stage_stable] + results = client.pipeline() \ + .collection("cities/SF/restaurants") \ + .where(Field.of("type").equal("Chinese")) \ + .union(client.pipeline() \ + .collection("cities/NY/restaurants") \ + .where(Field.of("type").equal("Italian"))) \ + .where(Field.of("rating").greater_than_or_equal(4.5)) \ + .sort(Field.of("__name__").descending()) \ + .execute() + # [END union_stage_stable] + for result in results: + print(result) + +def unnest_stage(): + # [START unnest_stage] + results = client.pipeline() \ + .database() \ + .unnest(Field.of("arrayField").as_("unnestedArrayField"), \ + options=UnnestOptions(index_field="index")) \ + .execute() + # [END unnest_stage] + for result in results: + print(result) + +def unnest_stage_empty_or_non_array(): + # [START unnest_edge_cases] + # Input + # { "identifier" : 1, "neighbors": [ "Alice", "Cathy" ] } + # { "identifier" : 2, "neighbors": [] } + # { "identifier" : 3, "neighbors": "Bob" } + + results = client.pipeline() \ + .database() \ + .unnest(Field.of("neighbors").as_("unnestedNeighbors"), \ + options=UnnestOptions(index_field="index")) \ + .execute() + + # Output + # { "identifier": 1, "neighbors": [ "Alice", "Cathy" ], + # "unnestedNeighbors": "Alice", "index": 0 } + # { "identifier": 1, "neighbors": [ "Alice", "Cathy" ], + # "unnestedNeighbors": "Cathy", "index": 1 } + # { "identifier": 3, "neighbors": "Bob", "index": null} + # [END unnest_edge_cases] + for result in results: + print(result) + +def count_function(): + # [START count_function] + # Total number of books in the collection + count_all = client.pipeline() \ + .collection("books") \ + .aggregate(Count().as_("count")) \ + .execute() + + # Number of books with nonnull `ratings` field + count_field = client.pipeline() \ + .collection("books") \ + .aggregate(Count("ratings").as_("count")) \ + .execute() + # [END count_function] + for result in count_all: + print(result) + for result in count_field: + print(result) + +def count_if_function(): + # [START count_if] + result = client.pipeline() \ + .collection("books") \ + .aggregate( + Field.of("rating").greater_than(4).count_if().as_("filteredCount") + ) \ + .execute() + # [END count_if] + for res in result: + print(res) + +def count_distinct_function(): + # [START count_distinct] + result = client.pipeline() \ + .collection("books") \ + .aggregate(Field.of("author").count_distinct().as_("unique_authors")) \ + .execute() + # [END count_distinct] + for res in result: + print(res) + +def sum_function(): + # [START sum_function] + result = client.pipeline() \ + .collection("cities") \ + .aggregate(Field.of("population").sum().as_("totalPopulation")) \ + .execute() + # [END sum_function] + for res in result: + print(res) + +def avg_function(): + # [START avg_function] + result = client.pipeline() \ + .collection("cities") \ + .aggregate(Field.of("population").average().as_("averagePopulation")) \ + .execute() + # [END avg_function] + for res in result: + print(res) + +def min_function(): + # [START min_function] + result = client.pipeline() \ + .collection("books") \ + .aggregate(Field.of("price").minimum().as_("minimumPrice")) \ + .execute() + # [END min_function] + for res in result: + print(res) + +def max_function(): + # [START max_function] + result = client.pipeline() \ + .collection("books") \ + .aggregate(Field.of("price").maximum().as_("maximumPrice")) \ + .execute() + # [END max_function] + for res in result: + print(res) + +def add_function(): + # [START add_function] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("soldBooks").add(Field.of("unsoldBooks")).as_("totalBooks")) \ + .execute() + # [END add_function] + for res in result: + print(res) + +def subtract_function(): + # [START subtract_function] + store_credit = 7 + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("price").subtract(store_credit).as_("totalCost")) \ + .execute() + # [END subtract_function] + for res in result: + print(res) + +def multiply_function(): + # [START multiply_function] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("price").multiply(Field.of("soldBooks")).as_("revenue")) \ + .execute() + # [END multiply_function] + for res in result: + print(res) + +def divide_function(): + # [START divide_function] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("ratings").divide(Field.of("soldBooks")).as_("reviewRate")) \ + .execute() + # [END divide_function] + for res in result: + print(res) + +def mod_function(): + # [START mod_function] + display_capacity = 1000 + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("unsoldBooks").mod(display_capacity).as_("warehousedBooks")) \ + .execute() + # [END mod_function] + for res in result: + print(res) + +def ceil_function(): + # [START ceil_function] + books_per_shelf = 100 + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("unsoldBooks").divide(books_per_shelf).ceil().as_("requiredShelves") + ) \ + .execute() + # [END ceil_function] + for res in result: + print(res) + +def floor_function(): + # [START floor_function] + result = client.pipeline() \ + .collection("books") \ + .add_fields( + Field.of("wordCount").divide(Field.of("pages")).floor().as_("wordsPerPage") + ) \ + .execute() + # [END floor_function] + for res in result: + print(res) + +def round_function(): + # [START round_function] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("soldBooks").multiply(Field.of("price")).round().as_("partialRevenue")) \ + .aggregate(Field.of("partialRevenue").sum().as_("totalRevenue")) \ + .execute() + # [END round_function] + for res in result: + print(res) + +def pow_function(): + # [START pow_function] + googleplexLat = 37.4221 + googleplexLng = -122.0853 + result = client.pipeline() \ + .collection("cities") \ + .add_fields( + Field.of("lat").subtract(googleplexLat) + .multiply(111) # km per degree + .pow(2) + .as_("latitudeDifference"), + Field.of("lng").subtract(googleplexLng) + .multiply(111) # km per degree + .pow(2) + .as_("longitudeDifference") + ) \ + .select( + Field.of("latitudeDifference").add(Field.of("longitudeDifference")).sqrt() + # Inaccurate for large distances or close to poles + .as_("approximateDistanceToGoogle") + ) \ + .execute() + # [END pow_function] + for res in result: + print(res) + +def sqrt_function(): + # [START sqrt_function] + googleplexLat = 37.4221 + googleplexLng = -122.0853 + result = client.pipeline() \ + .collection("cities") \ + .add_fields( + Field.of("lat").subtract(googleplexLat) + .multiply(111) # km per degree + .pow(2) + .as_("latitudeDifference"), + Field.of("lng").subtract(googleplexLng) + .multiply(111) # km per degree + .pow(2) + .as_("longitudeDifference") + ) \ + .select( + Field.of("latitudeDifference").add(Field.of("longitudeDifference")).sqrt() + # Inaccurate for large distances or close to poles + .as_("approximateDistanceToGoogle") + ) \ + .execute() + # [END sqrt_function] + for res in result: + print(res) + +def exp_function(): + # [START exp_function] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("rating").exp().as_("expRating")) \ + .execute() + # [END exp_function] + for res in result: + print(res) + +def ln_function(): + # [START ln_function] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("rating").ln().as_("lnRating")) \ + .execute() + # [END ln_function] + for res in result: + print(res) + +def log_function(): + # [START log_function] + # Not supported on Python + # [END log_function] + pass + +def array_concat(): + # [START array_concat] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("genre").array_concat(Field.of("subGenre")).as_("allGenres")) \ + .execute() + # [END array_concat] + for res in result: + print(res) + +def array_contains(): + # [START array_contains] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("genre").array_contains("mystery").as_("isMystery")) \ + .execute() + # [END array_contains] + for res in result: + print(res) + +def array_contains_all(): + # [START array_contains_all] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("genre") + .array_contains_all(["fantasy", "adventure"]) + .as_("isFantasyAdventure") + ) \ + .execute() + # [END array_contains_all] + for res in result: + print(res) + +def array_contains_any(): + # [START array_contains_any] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("genre") + .array_contains_any(["fantasy", "nonfiction"]) + .as_("isMysteryOrFantasy") + ) \ + .execute() + # [END array_contains_any] + for res in result: + print(res) + +def array_length(): + # [START array_length] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("genre").array_length().as_("genreCount")) \ + .execute() + # [END array_length] + for res in result: + print(res) + +def array_reverse(): + # [START array_reverse] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("genre").array_reverse().as_("reversedGenres")) \ + .execute() + # [END array_reverse] + for res in result: + print(res) + +def equal_function(): + # [START equal_function] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("rating").equal(5).as_("hasPerfectRating")) \ + .execute() + # [END equal_function] + for res in result: + print(res) + +def greater_than_function(): + # [START greater_than] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("rating").greater_than(4).as_("hasHighRating")) \ + .execute() + # [END greater_than] + for res in result: + print(res) + +def greater_than_or_equal_to_function(): + # [START greater_or_equal] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("published").greater_than_or_equal(1900).as_("publishedIn20thCentury")) \ + .execute() + # [END greater_or_equal] + for res in result: + print(res) + +def less_than_function(): + # [START less_than] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("published").less_than(1923).as_("isPublicDomainProbably")) \ + .execute() + # [END less_than] + for res in result: + print(res) + +def less_than_or_equal_to_function(): + # [START less_or_equal] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("rating").less_than_or_equal(2).as_("hasBadRating")) \ + .execute() + # [END less_or_equal] + for res in result: + print(res) + +def not_equal_function(): + # [START not_equal] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("title").not_equal("1984").as_("not1984")) \ + .execute() + # [END not_equal] + for res in result: + print(res) + +def exists_function(): + # [START exists_function] + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("rating").exists().as_("hasRating")) \ + .execute() + # [END exists_function] + for res in result: + print(res) + +def and_function(): + # [START and_function] + result = client.pipeline() \ + .collection("books") \ + .select( + And( + Field.of("rating").greater_than(4), + Field.of("price").less_than(10) + ).as_("under10Recommendation") + ) \ + .execute() + # [END and_function] + for res in result: + print(res) + +def or_function(): + # [START or_function] + result = client.pipeline() \ + .collection("books") \ + .select( + Or( + Field.of("genre").equal("Fantasy"), + Field.of("tags").array_contains("adventure") + ).as_("matchesSearchFilters") + ) \ + .execute() + # [END or_function] + for res in result: + print(res) + +def xor_function(): + # [START xor_function] + result = client.pipeline() \ + .collection("books") \ + .select( + Xor([ + Field.of("tags").array_contains("magic"), + Field.of("tags").array_contains("nonfiction") + ]).as_("matchesSearchFilters") + ) \ + .execute() + # [END xor_function] + for res in result: + print(res) + +def not_function(): + # [START not_function] + result = client.pipeline() \ + .collection("books") \ + .select( + Not( + Field.of("tags").array_contains("nonfiction") + ).as_("isFiction") + ) \ + .execute() + # [END not_function] + for res in result: + print(res) + +def cond_function(): + # [START cond_function] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("tags").array_concat( + Conditional( + Field.of("pages").greater_than(100), + Constant("longRead"), + Constant("shortRead") + ) + ).as_("extendedTags") + ) \ + .execute() + # [END cond_function] + for res in result: + print(res) + +def equal_any_function(): + # [START eq_any] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("genre").equal_any(["Science Fiction", "Psychological Thriller"]) + .as_("matchesGenreFilters") + ) \ + .execute() + # [END eq_any] + for res in result: + print(res) + +def not_equal_any_function(): + # [START not_eq_any] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("author").not_equal_any(["George Orwell", "F. Scott Fitzgerald"]) + .as_("byExcludedAuthors") + ) \ + .execute() + # [END not_eq_any] + for res in result: + print(res) + +def max_logical_function(): + # [START max_logical_function] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("rating").logical_maximum(1).as_("flooredRating") + ) \ + .execute() + # [END max_logical_function] + for res in result: + print(res) + +def min_logical_function(): + # [START min_logical_function] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("rating").logical_minimum(5).as_("cappedRating") + ) \ + .execute() + # [END min_logical_function] + for res in result: + print(res) + +def map_get_function(): + # [START map_get] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("awards").map_get("pulitzer").as_("hasPulitzerAward") + ) \ + .execute() + # [END map_get] + for res in result: + print(res) + +def byte_length_function(): + # [START byte_length] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("title").byte_length().as_("titleByteLength") + ) \ + .execute() + # [END byte_length] + for res in result: + print(res) + +def char_length_function(): + # [START char_length] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("title").char_length().as_("titleCharLength") + ) \ + .execute() + # [END char_length] + for res in result: + print(res) + +def starts_with_function(): + # [START starts_with] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("title").starts_with("The") + .as_("needsSpecialAlphabeticalSort") + ) \ + .execute() + # [END starts_with] + for res in result: + print(res) + +def ends_with_function(): + # [START ends_with] + result = client.pipeline() \ + .collection("inventory/devices/laptops") \ + .select( + Field.of("name").ends_with("16 inch") + .as_("16InLaptops") + ) \ + .execute() + # [END ends_with] + for res in result: + print(res) + +def like_function(): + # [START like] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("genre").like("%Fiction") + .as_("anyFiction") + ) \ + .execute() + # [END like] + for res in result: + print(res) + +def regex_contains_function(): + # [START regex_contains] + result = client.pipeline() \ + .collection("documents") \ + .select( + Field.of("title").regex_contains("Firestore (Enterprise|Standard)") + .as_("isFirestoreRelated") + ) \ + .execute() + # [END regex_contains] + for res in result: + print(res) + +def regex_match_function(): + # [START regex_match] + result = client.pipeline() \ + .collection("documents") \ + .select( + Field.of("title").regex_match("Firestore (Enterprise|Standard)") + .as_("isFirestoreExactly") + ) \ + .execute() + # [END regex_match] + for res in result: + print(res) + +def str_concat_function(): + # [START str_concat] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("title").concat(" by ", Field.of("author")) + .as_("fullyQualifiedTitle") + ) \ + .execute() + # [END str_concat] + for res in result: + print(res) + +def str_contains_function(): + # [START string_contains] + result = client.pipeline() \ + .collection("articles") \ + .select( + Field.of("body").string_contains("Firestore") + .as_("isFirestoreRelated") + ) \ + .execute() + # [END string_contains] + for res in result: + print(res) + +def to_upper_function(): + # [START to_upper] + result = client.pipeline() \ + .collection("authors") \ + .select( + Field.of("name").to_upper() + .as_("uppercaseName") + ) \ + .execute() + # [END to_upper] + for res in result: + print(res) + +def to_lower_function(): + # [START to_lower] + result = client.pipeline() \ + .collection("authors") \ + .select( + Field.of("genre").to_lower().equal("fantasy") + .as_("isFantasy") + ) \ + .execute() + # [END to_lower] + for res in result: + print(res) + +def substr_function(): + # [START substr_function] + result = client.pipeline() \ + .collection("books") \ + .where(Field.of("title").starts_with("The ")) \ + .select( + Field.of("title").substring(4) + .as_("titleWithoutLeadingThe") + ) \ + .execute() + # [END substr_function] + for res in result: + print(res) + +def str_reverse_function(): + # [START str_reverse] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("name").string_reverse().as_("reversedName") + ) \ + .execute() + # [END str_reverse] + for res in result: + print(res) + +def str_trim_function(): + # [START trim_function] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("name").trim().as_("whitespaceTrimmedName") + ) \ + .execute() + # [END trim_function] + for res in result: + print(res) + +def str_replace_function(): + # not yet supported until GA + pass + +def str_split_function(): + # not yet supported until GA + pass + +def unix_micros_to_timestamp_function(): + # [START unix_micros_timestamp] + result = client.pipeline() \ + .collection("documents") \ + .select( + Field.of("createdAtMicros").unix_micros_to_timestamp().as_("createdAtString") + ) \ + .execute() + # [END unix_micros_timestamp] + for res in result: + print(res) + +def unix_millis_to_timestamp_function(): + # [START unix_millis_timestamp] + result = client.pipeline() \ + .collection("documents") \ + .select( + Field.of("createdAtMillis").unix_millis_to_timestamp().as_("createdAtString") + ) \ + .execute() + # [END unix_millis_timestamp] + for res in result: + print(res) + +def unix_seconds_to_timestamp_function(): + # [START unix_seconds_timestamp] + result = client.pipeline() \ + .collection("documents") \ + .select( + Field.of("createdAtSeconds").unix_seconds_to_timestamp().as_("createdAtString") + ) \ + .execute() + # [END unix_seconds_timestamp] + for res in result: + print(res) + +def timestamp_add_function(): + # [START timestamp_add] + result = client.pipeline() \ + .collection("documents") \ + .select( + Field.of("createdAt").timestamp_add("day", 3653).as_("expiresAt") + ) \ + .execute() + # [END timestamp_add] + for res in result: + print(res) + +def timestamp_sub_function(): + # [START timestamp_sub] + result = client.pipeline() \ + .collection("documents") \ + .select( + Field.of("expiresAt").timestamp_subtract("day", 14).as_("sendWarningTimestamp") + ) \ + .execute() + # [END timestamp_sub] + for res in result: + print(res) + +def timestamp_to_unix_micros_function(): + # [START timestamp_unix_micros] + result = client.pipeline() \ + .collection("documents") \ + .select( + Field.of("dateString").timestamp_to_unix_micros().as_("unixMicros") + ) \ + .execute() + # [END timestamp_unix_micros] + for res in result: + print(res) + +def timestamp_to_unix_millis_function(): + # [START timestamp_unix_millis] + result = client.pipeline() \ + .collection("documents") \ + .select( + Field.of("dateString").timestamp_to_unix_millis().as_("unixMillis") + ) \ + .execute() + # [END timestamp_unix_millis] + for res in result: + print(res) + +def timestamp_to_unix_seconds_function(): + # [START timestamp_unix_seconds] + result = client.pipeline() \ + .collection("documents") \ + .select( + Field.of("dateString").timestamp_to_unix_seconds().as_("unixSeconds") + ) \ + .execute() + # [END timestamp_unix_seconds] + for res in result: + print(res) + +def cosine_distance_function(): + # [START cosine_distance] + sample_vector = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("embedding").cosine_distance(sample_vector).as_("cosineDistance") + ) \ + .execute() + # [END cosine_distance] + for res in result: + print(res) + +def dot_product_function(): + # [START dot_product] + sample_vector = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("embedding").dot_product(sample_vector).as_("dotProduct") + ) \ + .execute() + # [END dot_product] + for res in result: + print(res) + +def euclidean_distance_function(): + # [START euclidean_distance] + sample_vector = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("embedding").euclidean_distance(sample_vector).as_("euclideanDistance") + ) \ + .execute() + # [END euclidean_distance] + for res in result: + print(res) + +def vector_length_function(): + # [START vector_length] + result = client.pipeline() \ + .collection("books") \ + .select( + Field.of("embedding").vector_length().as_("vectorLength") + ) \ + .execute() + # [END vector_length] + for res in result: + print(res) From cf8b63e485b826bc281a3832c63bf1fafc9b8f46 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Thu, 6 Nov 2025 09:57:52 -0800 Subject: [PATCH 03/18] address feedback except formatting --- snippets/firestore/firestore_pipelines.py | 361 +++++++++++++++------- 1 file changed, 251 insertions(+), 110 deletions(-) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index 478236ba..ac94cf9e 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -46,7 +46,7 @@ def pipeline_concepts(): # [START pipeline_concepts] pipeline = client.pipeline() \ .collection("cities") \ - .where(Field.of("population").greater_than(100000)) \ + .where(Field.of("population").greater_than(100_000)) \ .sort(Field.of("name").ascending()) \ .limit(10) # [END pipeline_concepts] @@ -56,7 +56,11 @@ def basic_read(): # [START basic_read] pipeline = client.pipeline().collection("users") for result in pipeline.execute(): - print(result.id + " => " + result.data) + print(f"{result.id} => {result.data()}") + # or, asynchronously + result_stream = pipeline.stream() + async for result in result_stream: + print(f"{result.id} => {result.data()}") # [END basic_read] def pipeline_initialization(): @@ -70,7 +74,7 @@ def field_vs_constants(): # [START field_or_constant] pipeline = client.pipeline() \ .collection("cities") \ - .where(Field.of("name").equal(Constant("Toronto"))) + .where(Field.of("name").equal(Constant.of("Toronto"))) # [END field_or_constant] print(pipeline) @@ -97,6 +101,8 @@ def input_stages(): def where_pipeline(): # [START pipeline_where] + from google.cloud.firestore_v1.pipeline_expressions import (And, Field) + results = client.pipeline().collection("books") \ .where(Field.of("rating").equal(5)) \ .where(Field.of("published").less_than(1900)) \ @@ -106,53 +112,57 @@ def where_pipeline(): .where(And( Field.of("rating").equal(5), Field.of("published").less_than(1900) - )) \ - .execute() + )).execute() # [END pipeline_where] for result in results: print(result) def aggregate_groups(): # [START aggregate_groups] + from google.cloud.firestore_v1.pipeline_expressions import Field + results = client.pipeline() \ .collection("books") \ .aggregate( Field.of("rating").average().as_("avg_rating"), groups=[Field.of("genre")] - ) \ - .execute() + ).execute() # [END aggregate_groups] for result in results: print(result) def aggregate_distinct(): # [START aggregate_distinct] + from google.cloud.firestore_v1.pipeline_expressions import Field + results = client.pipeline() \ .collection("books") \ .distinct( Field.of("author").to_upper().as_("author"), "genre" - ) \ - .execute() + ).execute() # [END aggregate_distinct] for result in results: print(result) def sort(): # [START sort] + from google.cloud.firestore_v1.pipeline_expressions import Field + results = client.pipeline() \ .collection("books") \ .sort( Field.of("release_date").descending(), Field.of("author").ascending() - ) \ - .execute() + ).execute() # [END sort] for result in results: print(result) def sort_comparison(): # [START sort_comparison] + from google.cloud.firestore_v1.pipeline_expressions import Field + query = client.collection("cities") \ .order_by("state") \ .order_by("population", direction=Query.DESCENDING) @@ -169,13 +179,14 @@ def sort_comparison(): def functions_example(): # [START functions_example] + from google.cloud.firestore_v1.pipeline_expressions import Field + # Type 1: Scalar (for use in non-aggregation stages) # Example: Return the min store price for each book. results = client.pipeline().collection("books") \ .select( Field.of("current").logical_minimum(Field.of("updated")).as_("price_min") - ) \ - .execute() + ).execute() # Type 2: Aggregation (for use in aggregate stages) # Example: Return the min price of all books. @@ -188,6 +199,8 @@ def functions_example(): def creating_indexes(): # [START query_example] + from google.cloud.firestore_v1.pipeline_expressions import Field + results = client.pipeline() \ .collection("books") \ .where(Field.of("published").less_than(1900)) \ @@ -201,6 +214,8 @@ def creating_indexes(): def sparse_indexes(): # [START sparse_index_example] + from google.cloud.firestore_v1.pipeline_expressions import Field + results = client.pipeline() \ .collection("books") \ .where(Field.of("category").like("%fantasy%")) \ @@ -211,6 +226,8 @@ def sparse_indexes(): def sparse_indexes2(): # [START sparse_index_example_2] + from google.cloud.firestore_v1.pipeline_expressions import Field + results = client.pipeline() \ .collection("books") \ .sort(Field.of("release_date").ascending()) \ @@ -221,6 +238,8 @@ def sparse_indexes2(): def covered_query(): # [START covered_query] + from google.cloud.firestore_v1.pipeline_expressions import Field + results = client.pipeline() \ .collection("books") \ .where(Field.of("category").like("%fantasy%")) \ @@ -234,15 +253,17 @@ def covered_query(): def pagination(): # [START pagination_not_supported_preview] + from google.cloud.firestore_v1.pipeline_expressions import Field + # Existing pagination via `start_at()` query = client.collection("cities").order_by("population").start_at({ - "population": 1000000 + "population": 1_000_000 }) # Private preview workaround using pipelines pipeline = client.pipeline() \ .collection("cities") \ - .where(Field.of("population").greater_than_or_equal(1000000)) \ + .where(Field.of("population").greater_than_or_equal(1_000_000)) \ .sort(Field.of("population").descending()) # [END pagination_not_supported_preview] print(query) @@ -250,6 +271,8 @@ def pagination(): def collection_stage(): # [START collection_example] + from google.cloud.firestore_v1.pipeline_expressions import Field + results = client.pipeline() \ .collection("users/bob/games") \ .sort(Field.of("name").ascending()) \ @@ -260,6 +283,8 @@ def collection_stage(): def collection_group_stage(): # [START collection_group_example] + from google.cloud.firestore_v1.pipeline_expressions import Field + results = client.pipeline() \ .collection_group("games") \ .sort(Field.of("name").ascending()) \ @@ -270,6 +295,8 @@ def collection_group_stage(): def database_stage(): # [START database_example] + from google.cloud.firestore_v1.pipeline_expressions import Count + # Count all documents in the database results = client.pipeline() \ .database() \ @@ -294,7 +321,7 @@ def replace_with_stage(): # [START initial_data] client.collection("cities").document("SF").set({ "name": "San Francisco", - "population": 800000, + "population": 800_000, "location": { "country": "USA", "state": "California" @@ -302,7 +329,7 @@ def replace_with_stage(): }) client.collection("cities").document("TO").set({ "name": "Toronto", - "population": 3000000, + "population": 3_000_000, "province": "ON", "location": { "country": "Canada", @@ -311,6 +338,7 @@ def replace_with_stage(): }) client.collection("cities").document("NY").set({ "name": "New York", + "population": 8_500_000, "location": { "country": "USA", "state": "New York" @@ -322,6 +350,8 @@ def replace_with_stage(): # [END initial_data] # [START full_replace] + from google.cloud.firestore_v1.pipeline_expressions import Field + names = client.pipeline() \ .collection("cities") \ .replace_with(Field.of("location")) \ @@ -357,6 +387,8 @@ def sample_stage(): def sample_percent(): # [START sample_percent] + from google.cloud.firestore_v1.pipeline_stages import SampleOptions + # Get a sample of on average 50% of the documents in the database results = client.pipeline() \ .database() \ @@ -368,6 +400,8 @@ def sample_percent(): def union_stage(): # [START union_stage] + from google.cloud.firestore_v1.pipeline_expressions import Field + results = client.pipeline() \ .collection("cities/SF/restaurants") \ .where(Field.of("type").equal("Chinese")) \ @@ -383,6 +417,8 @@ def union_stage(): def union_stage_stable(): # [START union_stage_stable] + from google.cloud.firestore_v1.pipeline_expressions import Field + results = client.pipeline() \ .collection("cities/SF/restaurants") \ .where(Field.of("type").equal("Chinese")) \ @@ -398,6 +434,9 @@ def union_stage_stable(): def unnest_stage(): # [START unnest_stage] + from google.cloud.firestore_v1.pipeline_expressions import Field + from google.cloud.firestore_v1.pipeline_stages import UnnestOptions + results = client.pipeline() \ .database() \ .unnest(Field.of("arrayField").as_("unnestedArrayField"), \ @@ -409,6 +448,9 @@ def unnest_stage(): def unnest_stage_empty_or_non_array(): # [START unnest_edge_cases] + from google.cloud.firestore_v1.pipeline_expressions import Field + from google.cloud.firestore_v1.pipeline_stages import UnnestOptions + # Input # { "identifier" : 1, "neighbors": [ "Alice", "Cathy" ] } # { "identifier" : 2, "neighbors": [] } @@ -432,6 +474,8 @@ def unnest_stage_empty_or_non_array(): def count_function(): # [START count_function] + from google.cloud.firestore_v1.pipeline_expressions import Count + # Total number of books in the collection count_all = client.pipeline() \ .collection("books") \ @@ -451,18 +495,21 @@ def count_function(): def count_if_function(): # [START count_if] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .aggregate( Field.of("rating").greater_than(4).count_if().as_("filteredCount") - ) \ - .execute() + ).execute() # [END count_if] for res in result: print(res) def count_distinct_function(): # [START count_distinct] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .aggregate(Field.of("author").count_distinct().as_("unique_authors")) \ @@ -473,6 +520,8 @@ def count_distinct_function(): def sum_function(): # [START sum_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("cities") \ .aggregate(Field.of("population").sum().as_("totalPopulation")) \ @@ -483,6 +532,8 @@ def sum_function(): def avg_function(): # [START avg_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("cities") \ .aggregate(Field.of("population").average().as_("averagePopulation")) \ @@ -493,6 +544,8 @@ def avg_function(): def min_function(): # [START min_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .aggregate(Field.of("price").minimum().as_("minimumPrice")) \ @@ -503,6 +556,8 @@ def min_function(): def max_function(): # [START max_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .aggregate(Field.of("price").maximum().as_("maximumPrice")) \ @@ -513,6 +568,8 @@ def max_function(): def add_function(): # [START add_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("soldBooks").add(Field.of("unsoldBooks")).as_("totalBooks")) \ @@ -523,6 +580,8 @@ def add_function(): def subtract_function(): # [START subtract_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + store_credit = 7 result = client.pipeline() \ .collection("books") \ @@ -534,6 +593,8 @@ def subtract_function(): def multiply_function(): # [START multiply_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("price").multiply(Field.of("soldBooks")).as_("revenue")) \ @@ -544,6 +605,8 @@ def multiply_function(): def divide_function(): # [START divide_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("ratings").divide(Field.of("soldBooks")).as_("reviewRate")) \ @@ -554,6 +617,8 @@ def divide_function(): def mod_function(): # [START mod_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + display_capacity = 1000 result = client.pipeline() \ .collection("books") \ @@ -565,31 +630,35 @@ def mod_function(): def ceil_function(): # [START ceil_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + books_per_shelf = 100 result = client.pipeline() \ .collection("books") \ .select( Field.of("unsoldBooks").divide(books_per_shelf).ceil().as_("requiredShelves") - ) \ - .execute() + ).execute() # [END ceil_function] for res in result: print(res) def floor_function(): # [START floor_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .add_fields( Field.of("wordCount").divide(Field.of("pages")).floor().as_("wordsPerPage") - ) \ - .execute() + ).execute() # [END floor_function] for res in result: print(res) def round_function(): # [START round_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("soldBooks").multiply(Field.of("price")).round().as_("partialRevenue")) \ @@ -601,6 +670,8 @@ def round_function(): def pow_function(): # [START pow_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + googleplexLat = 37.4221 googleplexLng = -122.0853 result = client.pipeline() \ @@ -619,14 +690,15 @@ def pow_function(): Field.of("latitudeDifference").add(Field.of("longitudeDifference")).sqrt() # Inaccurate for large distances or close to poles .as_("approximateDistanceToGoogle") - ) \ - .execute() + ).execute() # [END pow_function] for res in result: print(res) def sqrt_function(): # [START sqrt_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + googleplexLat = 37.4221 googleplexLng = -122.0853 result = client.pipeline() \ @@ -645,14 +717,15 @@ def sqrt_function(): Field.of("latitudeDifference").add(Field.of("longitudeDifference")).sqrt() # Inaccurate for large distances or close to poles .as_("approximateDistanceToGoogle") - ) \ - .execute() + ).execute() # [END sqrt_function] for res in result: print(res) def exp_function(): # [START exp_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("rating").exp().as_("expRating")) \ @@ -663,6 +736,8 @@ def exp_function(): def ln_function(): # [START ln_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("rating").ln().as_("lnRating")) \ @@ -673,12 +748,20 @@ def ln_function(): def log_function(): # [START log_function] - # Not supported on Python + from google.cloud.firestore_v1.pipeline_expressions import Field + + result = client.pipeline() \ + .collection("books") \ + .select(Field.of("rating").log(2).as_("log2Rating")) \ + .execute() # [END log_function] - pass + for res in result: + print(res) def array_concat(): # [START array_concat] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("genre").array_concat(Field.of("subGenre")).as_("allGenres")) \ @@ -689,6 +772,8 @@ def array_concat(): def array_contains(): # [START array_contains] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("genre").array_contains("mystery").as_("isMystery")) \ @@ -699,34 +784,38 @@ def array_contains(): def array_contains_all(): # [START array_contains_all] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("genre") .array_contains_all(["fantasy", "adventure"]) .as_("isFantasyAdventure") - ) \ - .execute() + ).execute() # [END array_contains_all] for res in result: print(res) def array_contains_any(): # [START array_contains_any] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("genre") .array_contains_any(["fantasy", "nonfiction"]) .as_("isMysteryOrFantasy") - ) \ - .execute() + ).execute() # [END array_contains_any] for res in result: print(res) def array_length(): # [START array_length] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("genre").array_length().as_("genreCount")) \ @@ -737,6 +826,8 @@ def array_length(): def array_reverse(): # [START array_reverse] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("genre").array_reverse().as_("reversedGenres")) \ @@ -747,6 +838,8 @@ def array_reverse(): def equal_function(): # [START equal_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("rating").equal(5).as_("hasPerfectRating")) \ @@ -757,6 +850,8 @@ def equal_function(): def greater_than_function(): # [START greater_than] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("rating").greater_than(4).as_("hasHighRating")) \ @@ -767,6 +862,8 @@ def greater_than_function(): def greater_than_or_equal_to_function(): # [START greater_or_equal] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("published").greater_than_or_equal(1900).as_("publishedIn20thCentury")) \ @@ -777,6 +874,8 @@ def greater_than_or_equal_to_function(): def less_than_function(): # [START less_than] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("published").less_than(1923).as_("isPublicDomainProbably")) \ @@ -787,6 +886,8 @@ def less_than_function(): def less_than_or_equal_to_function(): # [START less_or_equal] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("rating").less_than_or_equal(2).as_("hasBadRating")) \ @@ -797,6 +898,8 @@ def less_than_or_equal_to_function(): def not_equal_function(): # [START not_equal] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("title").not_equal("1984").as_("not1984")) \ @@ -807,6 +910,8 @@ def not_equal_function(): def exists_function(): # [START exists_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select(Field.of("rating").exists().as_("hasRating")) \ @@ -817,6 +922,8 @@ def exists_function(): def and_function(): # [START and_function] + from google.cloud.firestore_v1.pipeline_expressions import (Field, And) + result = client.pipeline() \ .collection("books") \ .select( @@ -824,14 +931,15 @@ def and_function(): Field.of("rating").greater_than(4), Field.of("price").less_than(10) ).as_("under10Recommendation") - ) \ - .execute() + ).execute() # [END and_function] for res in result: print(res) def or_function(): # [START or_function] + from google.cloud.firestore_v1.pipeline_expressions import (Field, And) + result = client.pipeline() \ .collection("books") \ .select( @@ -839,14 +947,15 @@ def or_function(): Field.of("genre").equal("Fantasy"), Field.of("tags").array_contains("adventure") ).as_("matchesSearchFilters") - ) \ - .execute() + ).execute() # [END or_function] for res in result: print(res) def xor_function(): # [START xor_function] + from google.cloud.firestore_v1.pipeline_expressions import (Field, Xor) + result = client.pipeline() \ .collection("books") \ .select( @@ -854,281 +963,301 @@ def xor_function(): Field.of("tags").array_contains("magic"), Field.of("tags").array_contains("nonfiction") ]).as_("matchesSearchFilters") - ) \ - .execute() + ).execute() # [END xor_function] for res in result: print(res) def not_function(): # [START not_function] + from google.cloud.firestore_v1.pipeline_expressions import (Field, Not) + result = client.pipeline() \ .collection("books") \ .select( Not( Field.of("tags").array_contains("nonfiction") ).as_("isFiction") - ) \ - .execute() + ).execute() # [END not_function] for res in result: print(res) def cond_function(): # [START cond_function] + from google.cloud.firestore_v1.pipeline_expressions import (Field, Constant, Conditional) + result = client.pipeline() \ .collection("books") \ .select( Field.of("tags").array_concat( Conditional( Field.of("pages").greater_than(100), - Constant("longRead"), - Constant("shortRead") + Constant.of("longRead"), + Constant.of("shortRead") ) ).as_("extendedTags") - ) \ - .execute() + ).execute() # [END cond_function] for res in result: print(res) def equal_any_function(): # [START eq_any] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("genre").equal_any(["Science Fiction", "Psychological Thriller"]) .as_("matchesGenreFilters") - ) \ - .execute() + ).execute() # [END eq_any] for res in result: print(res) def not_equal_any_function(): # [START not_eq_any] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("author").not_equal_any(["George Orwell", "F. Scott Fitzgerald"]) .as_("byExcludedAuthors") - ) \ - .execute() + ).execute() # [END not_eq_any] for res in result: print(res) def max_logical_function(): # [START max_logical_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("rating").logical_maximum(1).as_("flooredRating") - ) \ - .execute() + ).execute() # [END max_logical_function] for res in result: print(res) def min_logical_function(): # [START min_logical_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("rating").logical_minimum(5).as_("cappedRating") - ) \ - .execute() + ).execute() # [END min_logical_function] for res in result: print(res) def map_get_function(): # [START map_get] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("awards").map_get("pulitzer").as_("hasPulitzerAward") - ) \ - .execute() + ).execute() # [END map_get] for res in result: print(res) def byte_length_function(): # [START byte_length] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("title").byte_length().as_("titleByteLength") - ) \ - .execute() + ).execute() # [END byte_length] for res in result: print(res) def char_length_function(): # [START char_length] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("title").char_length().as_("titleCharLength") - ) \ - .execute() + ).execute() # [END char_length] for res in result: print(res) def starts_with_function(): # [START starts_with] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("title").starts_with("The") .as_("needsSpecialAlphabeticalSort") - ) \ - .execute() + ).execute() # [END starts_with] for res in result: print(res) def ends_with_function(): # [START ends_with] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("inventory/devices/laptops") \ .select( Field.of("name").ends_with("16 inch") .as_("16InLaptops") - ) \ - .execute() + ).execute() # [END ends_with] for res in result: print(res) def like_function(): # [START like] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("genre").like("%Fiction") .as_("anyFiction") - ) \ - .execute() + ).execute() # [END like] for res in result: print(res) def regex_contains_function(): # [START regex_contains] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("documents") \ .select( Field.of("title").regex_contains("Firestore (Enterprise|Standard)") .as_("isFirestoreRelated") - ) \ - .execute() + ).execute() # [END regex_contains] for res in result: print(res) def regex_match_function(): # [START regex_match] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("documents") \ .select( Field.of("title").regex_match("Firestore (Enterprise|Standard)") .as_("isFirestoreExactly") - ) \ - .execute() + ).execute() # [END regex_match] for res in result: print(res) def str_concat_function(): # [START str_concat] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("title").concat(" by ", Field.of("author")) .as_("fullyQualifiedTitle") - ) \ - .execute() + ).execute() # [END str_concat] for res in result: print(res) def str_contains_function(): # [START string_contains] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("articles") \ .select( Field.of("body").string_contains("Firestore") .as_("isFirestoreRelated") - ) \ - .execute() + ).execute() # [END string_contains] for res in result: print(res) def to_upper_function(): # [START to_upper] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("authors") \ .select( Field.of("name").to_upper() .as_("uppercaseName") - ) \ - .execute() + ).execute() # [END to_upper] for res in result: print(res) def to_lower_function(): # [START to_lower] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("authors") \ .select( Field.of("genre").to_lower().equal("fantasy") .as_("isFantasy") - ) \ - .execute() + ).execute() # [END to_lower] for res in result: print(res) def substr_function(): # [START substr_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .where(Field.of("title").starts_with("The ")) \ .select( Field.of("title").substring(4) .as_("titleWithoutLeadingThe") - ) \ - .execute() + ).execute() # [END substr_function] for res in result: print(res) def str_reverse_function(): # [START str_reverse] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("name").string_reverse().as_("reversedName") - ) \ - .execute() + ).execute() # [END str_reverse] for res in result: print(res) def str_trim_function(): # [START trim_function] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("name").trim().as_("whitespaceTrimmedName") - ) \ - .execute() + ).execute() # [END trim_function] for res in result: print(res) @@ -1143,147 +1272,159 @@ def str_split_function(): def unix_micros_to_timestamp_function(): # [START unix_micros_timestamp] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("documents") \ .select( Field.of("createdAtMicros").unix_micros_to_timestamp().as_("createdAtString") - ) \ - .execute() + ).execute() # [END unix_micros_timestamp] for res in result: print(res) def unix_millis_to_timestamp_function(): # [START unix_millis_timestamp] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("documents") \ .select( Field.of("createdAtMillis").unix_millis_to_timestamp().as_("createdAtString") - ) \ - .execute() + ).execute() # [END unix_millis_timestamp] for res in result: print(res) def unix_seconds_to_timestamp_function(): # [START unix_seconds_timestamp] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("documents") \ .select( Field.of("createdAtSeconds").unix_seconds_to_timestamp().as_("createdAtString") - ) \ - .execute() + ).execute() # [END unix_seconds_timestamp] for res in result: print(res) def timestamp_add_function(): # [START timestamp_add] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("documents") \ .select( Field.of("createdAt").timestamp_add("day", 3653).as_("expiresAt") - ) \ - .execute() + ).execute() # [END timestamp_add] for res in result: print(res) def timestamp_sub_function(): # [START timestamp_sub] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("documents") \ .select( Field.of("expiresAt").timestamp_subtract("day", 14).as_("sendWarningTimestamp") - ) \ - .execute() + ).execute() # [END timestamp_sub] for res in result: print(res) def timestamp_to_unix_micros_function(): # [START timestamp_unix_micros] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("documents") \ .select( Field.of("dateString").timestamp_to_unix_micros().as_("unixMicros") - ) \ - .execute() + ).execute() # [END timestamp_unix_micros] for res in result: print(res) def timestamp_to_unix_millis_function(): # [START timestamp_unix_millis] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("documents") \ .select( Field.of("dateString").timestamp_to_unix_millis().as_("unixMillis") - ) \ - .execute() + ).execute() # [END timestamp_unix_millis] for res in result: print(res) def timestamp_to_unix_seconds_function(): # [START timestamp_unix_seconds] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("documents") \ .select( Field.of("dateString").timestamp_to_unix_seconds().as_("unixSeconds") - ) \ - .execute() + ).execute() # [END timestamp_unix_seconds] for res in result: print(res) def cosine_distance_function(): # [START cosine_distance] - sample_vector = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] + from google.cloud.firestore_v1.pipeline_expressions import Field + + sample_vector = Vector([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) result = client.pipeline() \ .collection("books") \ .select( Field.of("embedding").cosine_distance(sample_vector).as_("cosineDistance") - ) \ - .execute() + ).execute() # [END cosine_distance] for res in result: print(res) def dot_product_function(): # [START dot_product] - sample_vector = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] + from google.cloud.firestore_v1.pipeline_expressions import Field + + sample_vector = Vector([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) result = client.pipeline() \ .collection("books") \ .select( Field.of("embedding").dot_product(sample_vector).as_("dotProduct") - ) \ - .execute() + ).execute() # [END dot_product] for res in result: print(res) def euclidean_distance_function(): # [START euclidean_distance] - sample_vector = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] + from google.cloud.firestore_v1.pipeline_expressions import Field + + sample_vector = Vector([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) result = client.pipeline() \ .collection("books") \ .select( Field.of("embedding").euclidean_distance(sample_vector).as_("euclideanDistance") - ) \ - .execute() + ).execute() # [END euclidean_distance] for res in result: print(res) def vector_length_function(): # [START vector_length] + from google.cloud.firestore_v1.pipeline_expressions import Field + result = client.pipeline() \ .collection("books") \ .select( Field.of("embedding").vector_length().as_("vectorLength") - ) \ - .execute() + ).execute() # [END vector_length] for res in result: print(res) From 45302f89cb7a2d85cdfcf916942bf32947b282cd Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Thu, 6 Nov 2025 09:58:18 -0800 Subject: [PATCH 04/18] run format --- snippets/firestore/firestore_pipelines.py | 1283 +++++++++++++-------- 1 file changed, 805 insertions(+), 478 deletions(-) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index ac94cf9e..63914095 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -23,7 +23,11 @@ Count, ) from google.cloud.firestore_v1.pipeline_expressions import ( - And, Conditional, Or, Not, Xor + And, + Conditional, + Or, + Not, + Xor, ) from google.cloud.firestore_v1.pipeline_stages import ( Aggregate, @@ -41,17 +45,21 @@ default_app = firebase_admin.initialize_app() client = firestore.client(default_app, "your-new-enterprise-database") + # pylint: disable=invalid-name def pipeline_concepts(): # [START pipeline_concepts] - pipeline = client.pipeline() \ - .collection("cities") \ - .where(Field.of("population").greater_than(100_000)) \ - .sort(Field.of("name").ascending()) \ + pipeline = ( + client.pipeline() + .collection("cities") + .where(Field.of("population").greater_than(100_000)) + .sort(Field.of("name").ascending()) .limit(10) + ) # [END pipeline_concepts] print(pipeline) + def basic_read(): # [START basic_read] pipeline = client.pipeline().collection("users") @@ -63,6 +71,7 @@ def basic_read(): print(f"{result.id} => {result.data()}") # [END basic_read] + def pipeline_initialization(): # [START pipeline_initialization] firestore_client = firestore.client(default_app, "your-new-enterprise-database") @@ -70,14 +79,18 @@ def pipeline_initialization(): # [END pipeline_initialization] print(pipeline) + def field_vs_constants(): # [START field_or_constant] - pipeline = client.pipeline() \ - .collection("cities") \ + pipeline = ( + client.pipeline() + .collection("cities") .where(Field.of("name").equal(Constant.of("Toronto"))) + ) # [END field_or_constant] print(pipeline) + def input_stages(): # [START input_stages] # Return all restaurants in San Francisco @@ -90,272 +103,324 @@ def input_stages(): results = client.pipeline().database().execute() # Batch read of 3 documents - results = client.pipeline().documents( - client.collection("cities").document("SF"), - client.collection("cities").document("DC"), - client.collection("cities").document("NY") - ).execute() + results = ( + client.pipeline() + .documents( + client.collection("cities").document("SF"), + client.collection("cities").document("DC"), + client.collection("cities").document("NY"), + ) + .execute() + ) # [END input_stages] for result in results: print(result) + def where_pipeline(): # [START pipeline_where] - from google.cloud.firestore_v1.pipeline_expressions import (And, Field) + from google.cloud.firestore_v1.pipeline_expressions import And, Field - results = client.pipeline().collection("books") \ - .where(Field.of("rating").equal(5)) \ - .where(Field.of("published").less_than(1900)) \ + results = ( + client.pipeline() + .collection("books") + .where(Field.of("rating").equal(5)) + .where(Field.of("published").less_than(1900)) .execute() + ) - results = client.pipeline().collection("books") \ - .where(And( - Field.of("rating").equal(5), - Field.of("published").less_than(1900) - )).execute() + results = ( + client.pipeline() + .collection("books") + .where(And(Field.of("rating").equal(5), Field.of("published").less_than(1900))) + .execute() + ) # [END pipeline_where] for result in results: print(result) + def aggregate_groups(): # [START aggregate_groups] from google.cloud.firestore_v1.pipeline_expressions import Field - results = client.pipeline() \ - .collection("books") \ + results = ( + client.pipeline() + .collection("books") .aggregate( - Field.of("rating").average().as_("avg_rating"), - groups=[Field.of("genre")] - ).execute() + Field.of("rating").average().as_("avg_rating"), groups=[Field.of("genre")] + ) + .execute() + ) # [END aggregate_groups] for result in results: print(result) + def aggregate_distinct(): # [START aggregate_distinct] from google.cloud.firestore_v1.pipeline_expressions import Field - results = client.pipeline() \ - .collection("books") \ - .distinct( - Field.of("author").to_upper().as_("author"), - "genre" - ).execute() + results = ( + client.pipeline() + .collection("books") + .distinct(Field.of("author").to_upper().as_("author"), "genre") + .execute() + ) # [END aggregate_distinct] for result in results: print(result) + def sort(): # [START sort] from google.cloud.firestore_v1.pipeline_expressions import Field - results = client.pipeline() \ - .collection("books") \ - .sort( - Field.of("release_date").descending(), - Field.of("author").ascending() - ).execute() + results = ( + client.pipeline() + .collection("books") + .sort(Field.of("release_date").descending(), Field.of("author").ascending()) + .execute() + ) # [END sort] for result in results: print(result) + def sort_comparison(): # [START sort_comparison] from google.cloud.firestore_v1.pipeline_expressions import Field - query = client.collection("cities") \ - .order_by("state") \ + query = ( + client.collection("cities") + .order_by("state") .order_by("population", direction=Query.DESCENDING) + ) - pipeline = client.pipeline() \ - .collection("books") \ - .sort( - Field.of("release_date").descending(), - Field.of("author").ascending() - ) + pipeline = ( + client.pipeline() + .collection("books") + .sort(Field.of("release_date").descending(), Field.of("author").ascending()) + ) # [END sort_comparison] print(query) print(pipeline) + def functions_example(): # [START functions_example] from google.cloud.firestore_v1.pipeline_expressions import Field # Type 1: Scalar (for use in non-aggregation stages) # Example: Return the min store price for each book. - results = client.pipeline().collection("books") \ + results = ( + client.pipeline() + .collection("books") .select( Field.of("current").logical_minimum(Field.of("updated")).as_("price_min") - ).execute() + ) + .execute() + ) # Type 2: Aggregation (for use in aggregate stages) # Example: Return the min price of all books. - results = client.pipeline().collection("books") \ - .aggregate(Field.of("price").minimum().as_("min_price")) \ + results = ( + client.pipeline() + .collection("books") + .aggregate(Field.of("price").minimum().as_("min_price")) .execute() + ) # [END functions_example] for result in results: print(result) + def creating_indexes(): # [START query_example] from google.cloud.firestore_v1.pipeline_expressions import Field - results = client.pipeline() \ - .collection("books") \ - .where(Field.of("published").less_than(1900)) \ - .where(Field.of("genre").equal("Science Fiction")) \ - .where(Field.of("rating").greater_than(4.3)) \ - .sort(Field.of("published").descending()) \ + results = ( + client.pipeline() + .collection("books") + .where(Field.of("published").less_than(1900)) + .where(Field.of("genre").equal("Science Fiction")) + .where(Field.of("rating").greater_than(4.3)) + .sort(Field.of("published").descending()) .execute() + ) # [END query_example] for result in results: print(result) + def sparse_indexes(): # [START sparse_index_example] from google.cloud.firestore_v1.pipeline_expressions import Field - results = client.pipeline() \ - .collection("books") \ - .where(Field.of("category").like("%fantasy%")) \ + results = ( + client.pipeline() + .collection("books") + .where(Field.of("category").like("%fantasy%")) .execute() + ) # [END sparse_index_example] for result in results: print(result) + def sparse_indexes2(): # [START sparse_index_example_2] from google.cloud.firestore_v1.pipeline_expressions import Field - results = client.pipeline() \ - .collection("books") \ - .sort(Field.of("release_date").ascending()) \ + results = ( + client.pipeline() + .collection("books") + .sort(Field.of("release_date").ascending()) .execute() + ) # [END sparse_index_example_2] for result in results: print(result) + def covered_query(): # [START covered_query] from google.cloud.firestore_v1.pipeline_expressions import Field - results = client.pipeline() \ - .collection("books") \ - .where(Field.of("category").like("%fantasy%")) \ - .where(Field.of("title").exists()) \ - .where(Field.of("author").exists()) \ - .select("title", "author") \ + results = ( + client.pipeline() + .collection("books") + .where(Field.of("category").like("%fantasy%")) + .where(Field.of("title").exists()) + .where(Field.of("author").exists()) + .select("title", "author") .execute() + ) # [END covered_query] for result in results: print(result) + def pagination(): # [START pagination_not_supported_preview] from google.cloud.firestore_v1.pipeline_expressions import Field # Existing pagination via `start_at()` - query = client.collection("cities").order_by("population").start_at({ - "population": 1_000_000 - }) + query = ( + client.collection("cities") + .order_by("population") + .start_at({"population": 1_000_000}) + ) # Private preview workaround using pipelines - pipeline = client.pipeline() \ - .collection("cities") \ - .where(Field.of("population").greater_than_or_equal(1_000_000)) \ + pipeline = ( + client.pipeline() + .collection("cities") + .where(Field.of("population").greater_than_or_equal(1_000_000)) .sort(Field.of("population").descending()) + ) # [END pagination_not_supported_preview] print(query) print(pipeline) + def collection_stage(): # [START collection_example] from google.cloud.firestore_v1.pipeline_expressions import Field - results = client.pipeline() \ - .collection("users/bob/games") \ - .sort(Field.of("name").ascending()) \ + results = ( + client.pipeline() + .collection("users/bob/games") + .sort(Field.of("name").ascending()) .execute() + ) # [END collection_example] for result in results: print(result) + def collection_group_stage(): # [START collection_group_example] from google.cloud.firestore_v1.pipeline_expressions import Field - results = client.pipeline() \ - .collection_group("games") \ - .sort(Field.of("name").ascending()) \ + results = ( + client.pipeline() + .collection_group("games") + .sort(Field.of("name").ascending()) .execute() + ) # [END collection_group_example] for result in results: print(result) + def database_stage(): # [START database_example] from google.cloud.firestore_v1.pipeline_expressions import Count # Count all documents in the database - results = client.pipeline() \ - .database() \ - .aggregate(Count().as_("total")) \ - .execute() + results = client.pipeline().database().aggregate(Count().as_("total")).execute() # [END database_example] for result in results: print(result) + def documents_stage(): # [START documents_example] - results = client.pipeline().documents( - client.collection("cities").document("SF"), - client.collection("cities").document("DC"), - client.collection("cities").document("NY") - ).execute() + results = ( + client.pipeline() + .documents( + client.collection("cities").document("SF"), + client.collection("cities").document("DC"), + client.collection("cities").document("NY"), + ) + .execute() + ) # [END documents_example] for result in results: print(result) + def replace_with_stage(): # [START initial_data] - client.collection("cities").document("SF").set({ - "name": "San Francisco", - "population": 800_000, - "location": { - "country": "USA", - "state": "California" + client.collection("cities").document("SF").set( + { + "name": "San Francisco", + "population": 800_000, + "location": {"country": "USA", "state": "California"}, + } + ) + client.collection("cities").document("TO").set( + { + "name": "Toronto", + "population": 3_000_000, + "province": "ON", + "location": {"country": "Canada", "province": "Ontario"}, } - }) - client.collection("cities").document("TO").set({ - "name": "Toronto", - "population": 3_000_000, - "province": "ON", - "location": { - "country": "Canada", - "province": "Ontario" + ) + client.collection("cities").document("NY").set( + { + "name": "New York", + "population": 8_500_000, + "location": {"country": "USA", "state": "New York"}, } - }) - client.collection("cities").document("NY").set({ - "name": "New York", - "population": 8_500_000, - "location": { - "country": "USA", - "state": "New York" + ) + client.collection("cities").document("AT").set( + { + "name": "Atlantis", } - }) - client.collection("cities").document("AT").set({ - "name": "Atlantis", - }) + ) # [END initial_data] # [START full_replace] from google.cloud.firestore_v1.pipeline_expressions import Field - names = client.pipeline() \ - .collection("cities") \ - .replace_with(Field.of("location")) \ + names = ( + client.pipeline() + .collection("cities") + .replace_with(Field.of("location")) .execute() + ) # [END full_replace] # [START map_merge_overwrite] @@ -364,88 +429,104 @@ def replace_with_stage(): for name in names: print(name) + def sample_stage(): # [START sample_example] # Get a sample of 100 documents in a database - results = client.pipeline() \ - .database() \ - .sample(100) \ - .execute() + results = client.pipeline().database().sample(100).execute() # Randomly shuffle a list of 3 documents - results = client.pipeline() \ + results = ( + client.pipeline() .documents( client.collection("cities").document("SF"), client.collection("cities").document("NY"), - client.collection("cities").document("DC") - ) \ - .sample(3) \ + client.collection("cities").document("DC"), + ) + .sample(3) .execute() + ) # [END sample_example] for result in results: print(result) + def sample_percent(): # [START sample_percent] from google.cloud.firestore_v1.pipeline_stages import SampleOptions # Get a sample of on average 50% of the documents in the database - results = client.pipeline() \ - .database() \ - .sample(SampleOptions.percentage(0.5)) \ - .execute() + results = ( + client.pipeline().database().sample(SampleOptions.percentage(0.5)).execute() + ) # [END sample_percent] for result in results: print(result) + def union_stage(): # [START union_stage] from google.cloud.firestore_v1.pipeline_expressions import Field - results = client.pipeline() \ - .collection("cities/SF/restaurants") \ - .where(Field.of("type").equal("Chinese")) \ - .union(client.pipeline() \ - .collection("cities/NY/restaurants") \ - .where(Field.of("type").equal("Italian"))) \ - .where(Field.of("rating").greater_than_or_equal(4.5)) \ - .sort(Field.of("__name__").descending()) \ + results = ( + client.pipeline() + .collection("cities/SF/restaurants") + .where(Field.of("type").equal("Chinese")) + .union( + client.pipeline() + .collection("cities/NY/restaurants") + .where(Field.of("type").equal("Italian")) + ) + .where(Field.of("rating").greater_than_or_equal(4.5)) + .sort(Field.of("__name__").descending()) .execute() + ) # [END union_stage] for result in results: print(result) + def union_stage_stable(): # [START union_stage_stable] from google.cloud.firestore_v1.pipeline_expressions import Field - results = client.pipeline() \ - .collection("cities/SF/restaurants") \ - .where(Field.of("type").equal("Chinese")) \ - .union(client.pipeline() \ - .collection("cities/NY/restaurants") \ - .where(Field.of("type").equal("Italian"))) \ - .where(Field.of("rating").greater_than_or_equal(4.5)) \ - .sort(Field.of("__name__").descending()) \ + results = ( + client.pipeline() + .collection("cities/SF/restaurants") + .where(Field.of("type").equal("Chinese")) + .union( + client.pipeline() + .collection("cities/NY/restaurants") + .where(Field.of("type").equal("Italian")) + ) + .where(Field.of("rating").greater_than_or_equal(4.5)) + .sort(Field.of("__name__").descending()) .execute() + ) # [END union_stage_stable] for result in results: print(result) + def unnest_stage(): # [START unnest_stage] from google.cloud.firestore_v1.pipeline_expressions import Field from google.cloud.firestore_v1.pipeline_stages import UnnestOptions - results = client.pipeline() \ - .database() \ - .unnest(Field.of("arrayField").as_("unnestedArrayField"), \ - options=UnnestOptions(index_field="index")) \ + results = ( + client.pipeline() + .database() + .unnest( + Field.of("arrayField").as_("unnestedArrayField"), + options=UnnestOptions(index_field="index"), + ) .execute() + ) # [END unnest_stage] for result in results: print(result) + def unnest_stage_empty_or_non_array(): # [START unnest_edge_cases] from google.cloud.firestore_v1.pipeline_expressions import Field @@ -456,11 +537,15 @@ def unnest_stage_empty_or_non_array(): # { "identifier" : 2, "neighbors": [] } # { "identifier" : 3, "neighbors": "Bob" } - results = client.pipeline() \ - .database() \ - .unnest(Field.of("neighbors").as_("unnestedNeighbors"), \ - options=UnnestOptions(index_field="index")) \ + results = ( + client.pipeline() + .database() + .unnest( + Field.of("neighbors").as_("unnestedNeighbors"), + options=UnnestOptions(index_field="index"), + ) .execute() + ) # Output # { "identifier": 1, "neighbors": [ "Alice", "Cathy" ], @@ -472,959 +557,1201 @@ def unnest_stage_empty_or_non_array(): for result in results: print(result) + def count_function(): # [START count_function] from google.cloud.firestore_v1.pipeline_expressions import Count # Total number of books in the collection - count_all = client.pipeline() \ - .collection("books") \ - .aggregate(Count().as_("count")) \ - .execute() + count_all = ( + client.pipeline().collection("books").aggregate(Count().as_("count")).execute() + ) # Number of books with nonnull `ratings` field - count_field = client.pipeline() \ - .collection("books") \ - .aggregate(Count("ratings").as_("count")) \ + count_field = ( + client.pipeline() + .collection("books") + .aggregate(Count("ratings").as_("count")) .execute() + ) # [END count_function] for result in count_all: print(result) for result in count_field: print(result) + def count_if_function(): # [START count_if] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .aggregate( - Field.of("rating").greater_than(4).count_if().as_("filteredCount") - ).execute() + result = ( + client.pipeline() + .collection("books") + .aggregate(Field.of("rating").greater_than(4).count_if().as_("filteredCount")) + .execute() + ) # [END count_if] for res in result: print(res) + def count_distinct_function(): # [START count_distinct] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .aggregate(Field.of("author").count_distinct().as_("unique_authors")) \ + result = ( + client.pipeline() + .collection("books") + .aggregate(Field.of("author").count_distinct().as_("unique_authors")) .execute() + ) # [END count_distinct] for res in result: print(res) + def sum_function(): # [START sum_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("cities") \ - .aggregate(Field.of("population").sum().as_("totalPopulation")) \ + result = ( + client.pipeline() + .collection("cities") + .aggregate(Field.of("population").sum().as_("totalPopulation")) .execute() + ) # [END sum_function] for res in result: print(res) + def avg_function(): # [START avg_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("cities") \ - .aggregate(Field.of("population").average().as_("averagePopulation")) \ + result = ( + client.pipeline() + .collection("cities") + .aggregate(Field.of("population").average().as_("averagePopulation")) .execute() + ) # [END avg_function] for res in result: print(res) + def min_function(): # [START min_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .aggregate(Field.of("price").minimum().as_("minimumPrice")) \ + result = ( + client.pipeline() + .collection("books") + .aggregate(Field.of("price").minimum().as_("minimumPrice")) .execute() + ) # [END min_function] for res in result: print(res) + def max_function(): # [START max_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .aggregate(Field.of("price").maximum().as_("maximumPrice")) \ + result = ( + client.pipeline() + .collection("books") + .aggregate(Field.of("price").maximum().as_("maximumPrice")) .execute() + ) # [END max_function] for res in result: print(res) + def add_function(): # [START add_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("soldBooks").add(Field.of("unsoldBooks")).as_("totalBooks")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("soldBooks").add(Field.of("unsoldBooks")).as_("totalBooks")) .execute() + ) # [END add_function] for res in result: print(res) + def subtract_function(): # [START subtract_function] from google.cloud.firestore_v1.pipeline_expressions import Field store_credit = 7 - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("price").subtract(store_credit).as_("totalCost")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("price").subtract(store_credit).as_("totalCost")) .execute() + ) # [END subtract_function] for res in result: print(res) + def multiply_function(): # [START multiply_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("price").multiply(Field.of("soldBooks")).as_("revenue")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("price").multiply(Field.of("soldBooks")).as_("revenue")) .execute() + ) # [END multiply_function] for res in result: print(res) + def divide_function(): # [START divide_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("ratings").divide(Field.of("soldBooks")).as_("reviewRate")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("ratings").divide(Field.of("soldBooks")).as_("reviewRate")) .execute() + ) # [END divide_function] for res in result: print(res) + def mod_function(): # [START mod_function] from google.cloud.firestore_v1.pipeline_expressions import Field display_capacity = 1000 - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("unsoldBooks").mod(display_capacity).as_("warehousedBooks")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("unsoldBooks").mod(display_capacity).as_("warehousedBooks")) .execute() + ) # [END mod_function] for res in result: print(res) + def ceil_function(): # [START ceil_function] from google.cloud.firestore_v1.pipeline_expressions import Field books_per_shelf = 100 - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( - Field.of("unsoldBooks").divide(books_per_shelf).ceil().as_("requiredShelves") - ).execute() + Field.of("unsoldBooks") + .divide(books_per_shelf) + .ceil() + .as_("requiredShelves") + ) + .execute() + ) # [END ceil_function] for res in result: print(res) + def floor_function(): # [START floor_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .add_fields( Field.of("wordCount").divide(Field.of("pages")).floor().as_("wordsPerPage") - ).execute() + ) + .execute() + ) # [END floor_function] for res in result: print(res) + def round_function(): # [START round_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("soldBooks").multiply(Field.of("price")).round().as_("partialRevenue")) \ - .aggregate(Field.of("partialRevenue").sum().as_("totalRevenue")) \ + result = ( + client.pipeline() + .collection("books") + .select( + Field.of("soldBooks") + .multiply(Field.of("price")) + .round() + .as_("partialRevenue") + ) + .aggregate(Field.of("partialRevenue").sum().as_("totalRevenue")) .execute() + ) # [END round_function] for res in result: print(res) + def pow_function(): # [START pow_function] from google.cloud.firestore_v1.pipeline_expressions import Field googleplexLat = 37.4221 googleplexLng = -122.0853 - result = client.pipeline() \ - .collection("cities") \ + result = ( + client.pipeline() + .collection("cities") .add_fields( - Field.of("lat").subtract(googleplexLat) - .multiply(111) # km per degree - .pow(2) - .as_("latitudeDifference"), - Field.of("lng").subtract(googleplexLng) - .multiply(111) # km per degree - .pow(2) - .as_("longitudeDifference") - ) \ + Field.of("lat") + .subtract(googleplexLat) + .multiply(111) # km per degree + .pow(2) + .as_("latitudeDifference"), + Field.of("lng") + .subtract(googleplexLng) + .multiply(111) # km per degree + .pow(2) + .as_("longitudeDifference"), + ) .select( - Field.of("latitudeDifference").add(Field.of("longitudeDifference")).sqrt() - # Inaccurate for large distances or close to poles - .as_("approximateDistanceToGoogle") - ).execute() + Field.of("latitudeDifference") + .add(Field.of("longitudeDifference")) + .sqrt() + # Inaccurate for large distances or close to poles + .as_("approximateDistanceToGoogle") + ) + .execute() + ) # [END pow_function] for res in result: print(res) + def sqrt_function(): # [START sqrt_function] from google.cloud.firestore_v1.pipeline_expressions import Field googleplexLat = 37.4221 googleplexLng = -122.0853 - result = client.pipeline() \ - .collection("cities") \ + result = ( + client.pipeline() + .collection("cities") .add_fields( - Field.of("lat").subtract(googleplexLat) - .multiply(111) # km per degree - .pow(2) - .as_("latitudeDifference"), - Field.of("lng").subtract(googleplexLng) - .multiply(111) # km per degree - .pow(2) - .as_("longitudeDifference") - ) \ + Field.of("lat") + .subtract(googleplexLat) + .multiply(111) # km per degree + .pow(2) + .as_("latitudeDifference"), + Field.of("lng") + .subtract(googleplexLng) + .multiply(111) # km per degree + .pow(2) + .as_("longitudeDifference"), + ) .select( - Field.of("latitudeDifference").add(Field.of("longitudeDifference")).sqrt() - # Inaccurate for large distances or close to poles - .as_("approximateDistanceToGoogle") - ).execute() + Field.of("latitudeDifference") + .add(Field.of("longitudeDifference")) + .sqrt() + # Inaccurate for large distances or close to poles + .as_("approximateDistanceToGoogle") + ) + .execute() + ) # [END sqrt_function] for res in result: print(res) + def exp_function(): # [START exp_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("rating").exp().as_("expRating")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("rating").exp().as_("expRating")) .execute() + ) # [END exp_function] for res in result: print(res) + def ln_function(): # [START ln_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("rating").ln().as_("lnRating")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("rating").ln().as_("lnRating")) .execute() + ) # [END ln_function] for res in result: print(res) + def log_function(): # [START log_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("rating").log(2).as_("log2Rating")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("rating").log(2).as_("log2Rating")) .execute() + ) # [END log_function] for res in result: print(res) + def array_concat(): # [START array_concat] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("genre").array_concat(Field.of("subGenre")).as_("allGenres")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("genre").array_concat(Field.of("subGenre")).as_("allGenres")) .execute() + ) # [END array_concat] for res in result: print(res) + def array_contains(): # [START array_contains] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("genre").array_contains("mystery").as_("isMystery")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("genre").array_contains("mystery").as_("isMystery")) .execute() + ) # [END array_contains] for res in result: print(res) + def array_contains_all(): # [START array_contains_all] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( Field.of("genre") - .array_contains_all(["fantasy", "adventure"]) - .as_("isFantasyAdventure") - ).execute() + .array_contains_all(["fantasy", "adventure"]) + .as_("isFantasyAdventure") + ) + .execute() + ) # [END array_contains_all] for res in result: print(res) + def array_contains_any(): # [START array_contains_any] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( Field.of("genre") - .array_contains_any(["fantasy", "nonfiction"]) - .as_("isMysteryOrFantasy") - ).execute() + .array_contains_any(["fantasy", "nonfiction"]) + .as_("isMysteryOrFantasy") + ) + .execute() + ) # [END array_contains_any] for res in result: print(res) + def array_length(): # [START array_length] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("genre").array_length().as_("genreCount")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("genre").array_length().as_("genreCount")) .execute() + ) # [END array_length] for res in result: print(res) + def array_reverse(): # [START array_reverse] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("genre").array_reverse().as_("reversedGenres")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("genre").array_reverse().as_("reversedGenres")) .execute() + ) # [END array_reverse] for res in result: print(res) + def equal_function(): # [START equal_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("rating").equal(5).as_("hasPerfectRating")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("rating").equal(5).as_("hasPerfectRating")) .execute() + ) # [END equal_function] for res in result: print(res) + def greater_than_function(): # [START greater_than] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("rating").greater_than(4).as_("hasHighRating")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("rating").greater_than(4).as_("hasHighRating")) .execute() + ) # [END greater_than] for res in result: print(res) + def greater_than_or_equal_to_function(): # [START greater_or_equal] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("published").greater_than_or_equal(1900).as_("publishedIn20thCentury")) \ + result = ( + client.pipeline() + .collection("books") + .select( + Field.of("published") + .greater_than_or_equal(1900) + .as_("publishedIn20thCentury") + ) .execute() + ) # [END greater_or_equal] for res in result: print(res) + def less_than_function(): # [START less_than] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("published").less_than(1923).as_("isPublicDomainProbably")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("published").less_than(1923).as_("isPublicDomainProbably")) .execute() + ) # [END less_than] for res in result: print(res) + def less_than_or_equal_to_function(): # [START less_or_equal] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("rating").less_than_or_equal(2).as_("hasBadRating")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("rating").less_than_or_equal(2).as_("hasBadRating")) .execute() + ) # [END less_or_equal] for res in result: print(res) + def not_equal_function(): # [START not_equal] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("title").not_equal("1984").as_("not1984")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("title").not_equal("1984").as_("not1984")) .execute() + ) # [END not_equal] for res in result: print(res) + def exists_function(): # [START exists_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select(Field.of("rating").exists().as_("hasRating")) \ + result = ( + client.pipeline() + .collection("books") + .select(Field.of("rating").exists().as_("hasRating")) .execute() + ) # [END exists_function] for res in result: print(res) + def and_function(): # [START and_function] - from google.cloud.firestore_v1.pipeline_expressions import (Field, And) + from google.cloud.firestore_v1.pipeline_expressions import Field, And - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( And( - Field.of("rating").greater_than(4), - Field.of("price").less_than(10) + Field.of("rating").greater_than(4), Field.of("price").less_than(10) ).as_("under10Recommendation") - ).execute() + ) + .execute() + ) # [END and_function] for res in result: print(res) + def or_function(): # [START or_function] - from google.cloud.firestore_v1.pipeline_expressions import (Field, And) + from google.cloud.firestore_v1.pipeline_expressions import Field, And - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( Or( Field.of("genre").equal("Fantasy"), - Field.of("tags").array_contains("adventure") + Field.of("tags").array_contains("adventure"), ).as_("matchesSearchFilters") - ).execute() + ) + .execute() + ) # [END or_function] for res in result: print(res) + def xor_function(): # [START xor_function] - from google.cloud.firestore_v1.pipeline_expressions import (Field, Xor) + from google.cloud.firestore_v1.pipeline_expressions import Field, Xor - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( - Xor([ - Field.of("tags").array_contains("magic"), - Field.of("tags").array_contains("nonfiction") - ]).as_("matchesSearchFilters") - ).execute() + Xor( + [ + Field.of("tags").array_contains("magic"), + Field.of("tags").array_contains("nonfiction"), + ] + ).as_("matchesSearchFilters") + ) + .execute() + ) # [END xor_function] for res in result: print(res) + def not_function(): # [START not_function] - from google.cloud.firestore_v1.pipeline_expressions import (Field, Not) + from google.cloud.firestore_v1.pipeline_expressions import Field, Not - result = client.pipeline() \ - .collection("books") \ - .select( - Not( - Field.of("tags").array_contains("nonfiction") - ).as_("isFiction") - ).execute() + result = ( + client.pipeline() + .collection("books") + .select(Not(Field.of("tags").array_contains("nonfiction")).as_("isFiction")) + .execute() + ) # [END not_function] for res in result: print(res) + def cond_function(): # [START cond_function] - from google.cloud.firestore_v1.pipeline_expressions import (Field, Constant, Conditional) - - result = client.pipeline() \ - .collection("books") \ + from google.cloud.firestore_v1.pipeline_expressions import ( + Field, + Constant, + Conditional, + ) + + result = ( + client.pipeline() + .collection("books") .select( - Field.of("tags").array_concat( + Field.of("tags") + .array_concat( Conditional( Field.of("pages").greater_than(100), Constant.of("longRead"), - Constant.of("shortRead") + Constant.of("shortRead"), ) - ).as_("extendedTags") - ).execute() + ) + .as_("extendedTags") + ) + .execute() + ) # [END cond_function] for res in result: print(res) + def equal_any_function(): # [START eq_any] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( - Field.of("genre").equal_any(["Science Fiction", "Psychological Thriller"]) - .as_("matchesGenreFilters") - ).execute() + Field.of("genre") + .equal_any(["Science Fiction", "Psychological Thriller"]) + .as_("matchesGenreFilters") + ) + .execute() + ) # [END eq_any] for res in result: print(res) + def not_equal_any_function(): # [START not_eq_any] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( - Field.of("author").not_equal_any(["George Orwell", "F. Scott Fitzgerald"]) - .as_("byExcludedAuthors") - ).execute() + Field.of("author") + .not_equal_any(["George Orwell", "F. Scott Fitzgerald"]) + .as_("byExcludedAuthors") + ) + .execute() + ) # [END not_eq_any] for res in result: print(res) + def max_logical_function(): # [START max_logical_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select( - Field.of("rating").logical_maximum(1).as_("flooredRating") - ).execute() + result = ( + client.pipeline() + .collection("books") + .select(Field.of("rating").logical_maximum(1).as_("flooredRating")) + .execute() + ) # [END max_logical_function] for res in result: print(res) + def min_logical_function(): # [START min_logical_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select( - Field.of("rating").logical_minimum(5).as_("cappedRating") - ).execute() + result = ( + client.pipeline() + .collection("books") + .select(Field.of("rating").logical_minimum(5).as_("cappedRating")) + .execute() + ) # [END min_logical_function] for res in result: print(res) + def map_get_function(): # [START map_get] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select( - Field.of("awards").map_get("pulitzer").as_("hasPulitzerAward") - ).execute() + result = ( + client.pipeline() + .collection("books") + .select(Field.of("awards").map_get("pulitzer").as_("hasPulitzerAward")) + .execute() + ) # [END map_get] for res in result: print(res) + def byte_length_function(): # [START byte_length] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select( - Field.of("title").byte_length().as_("titleByteLength") - ).execute() + result = ( + client.pipeline() + .collection("books") + .select(Field.of("title").byte_length().as_("titleByteLength")) + .execute() + ) # [END byte_length] for res in result: print(res) + def char_length_function(): # [START char_length] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select( - Field.of("title").char_length().as_("titleCharLength") - ).execute() + result = ( + client.pipeline() + .collection("books") + .select(Field.of("title").char_length().as_("titleCharLength")) + .execute() + ) # [END char_length] for res in result: print(res) + def starts_with_function(): # [START starts_with] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( - Field.of("title").starts_with("The") - .as_("needsSpecialAlphabeticalSort") - ).execute() + Field.of("title").starts_with("The").as_("needsSpecialAlphabeticalSort") + ) + .execute() + ) # [END starts_with] for res in result: print(res) + def ends_with_function(): # [START ends_with] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("inventory/devices/laptops") \ - .select( - Field.of("name").ends_with("16 inch") - .as_("16InLaptops") - ).execute() + result = ( + client.pipeline() + .collection("inventory/devices/laptops") + .select(Field.of("name").ends_with("16 inch").as_("16InLaptops")) + .execute() + ) # [END ends_with] for res in result: print(res) + def like_function(): # [START like] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select( - Field.of("genre").like("%Fiction") - .as_("anyFiction") - ).execute() + result = ( + client.pipeline() + .collection("books") + .select(Field.of("genre").like("%Fiction").as_("anyFiction")) + .execute() + ) # [END like] for res in result: print(res) + def regex_contains_function(): # [START regex_contains] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("documents") \ + result = ( + client.pipeline() + .collection("documents") .select( - Field.of("title").regex_contains("Firestore (Enterprise|Standard)") - .as_("isFirestoreRelated") - ).execute() + Field.of("title") + .regex_contains("Firestore (Enterprise|Standard)") + .as_("isFirestoreRelated") + ) + .execute() + ) # [END regex_contains] for res in result: print(res) + def regex_match_function(): # [START regex_match] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("documents") \ + result = ( + client.pipeline() + .collection("documents") .select( - Field.of("title").regex_match("Firestore (Enterprise|Standard)") - .as_("isFirestoreExactly") - ).execute() + Field.of("title") + .regex_match("Firestore (Enterprise|Standard)") + .as_("isFirestoreExactly") + ) + .execute() + ) # [END regex_match] for res in result: print(res) + def str_concat_function(): # [START str_concat] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( - Field.of("title").concat(" by ", Field.of("author")) - .as_("fullyQualifiedTitle") - ).execute() + Field.of("title") + .concat(" by ", Field.of("author")) + .as_("fullyQualifiedTitle") + ) + .execute() + ) # [END str_concat] for res in result: print(res) + def str_contains_function(): # [START string_contains] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("articles") \ - .select( - Field.of("body").string_contains("Firestore") - .as_("isFirestoreRelated") - ).execute() + result = ( + client.pipeline() + .collection("articles") + .select(Field.of("body").string_contains("Firestore").as_("isFirestoreRelated")) + .execute() + ) # [END string_contains] for res in result: print(res) + def to_upper_function(): # [START to_upper] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("authors") \ - .select( - Field.of("name").to_upper() - .as_("uppercaseName") - ).execute() + result = ( + client.pipeline() + .collection("authors") + .select(Field.of("name").to_upper().as_("uppercaseName")) + .execute() + ) # [END to_upper] for res in result: print(res) + def to_lower_function(): # [START to_lower] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("authors") \ - .select( - Field.of("genre").to_lower().equal("fantasy") - .as_("isFantasy") - ).execute() + result = ( + client.pipeline() + .collection("authors") + .select(Field.of("genre").to_lower().equal("fantasy").as_("isFantasy")) + .execute() + ) # [END to_lower] for res in result: print(res) + def substr_function(): # [START substr_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .where(Field.of("title").starts_with("The ")) \ - .select( - Field.of("title").substring(4) - .as_("titleWithoutLeadingThe") - ).execute() + result = ( + client.pipeline() + .collection("books") + .where(Field.of("title").starts_with("The ")) + .select(Field.of("title").substring(4).as_("titleWithoutLeadingThe")) + .execute() + ) # [END substr_function] for res in result: print(res) + def str_reverse_function(): # [START str_reverse] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select( - Field.of("name").string_reverse().as_("reversedName") - ).execute() + result = ( + client.pipeline() + .collection("books") + .select(Field.of("name").string_reverse().as_("reversedName")) + .execute() + ) # [END str_reverse] for res in result: print(res) + def str_trim_function(): # [START trim_function] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select( - Field.of("name").trim().as_("whitespaceTrimmedName") - ).execute() + result = ( + client.pipeline() + .collection("books") + .select(Field.of("name").trim().as_("whitespaceTrimmedName")) + .execute() + ) # [END trim_function] for res in result: print(res) + def str_replace_function(): # not yet supported until GA pass + def str_split_function(): # not yet supported until GA pass + def unix_micros_to_timestamp_function(): # [START unix_micros_timestamp] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("documents") \ + result = ( + client.pipeline() + .collection("documents") .select( - Field.of("createdAtMicros").unix_micros_to_timestamp().as_("createdAtString") - ).execute() + Field.of("createdAtMicros") + .unix_micros_to_timestamp() + .as_("createdAtString") + ) + .execute() + ) # [END unix_micros_timestamp] for res in result: print(res) + def unix_millis_to_timestamp_function(): # [START unix_millis_timestamp] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("documents") \ + result = ( + client.pipeline() + .collection("documents") .select( - Field.of("createdAtMillis").unix_millis_to_timestamp().as_("createdAtString") - ).execute() + Field.of("createdAtMillis") + .unix_millis_to_timestamp() + .as_("createdAtString") + ) + .execute() + ) # [END unix_millis_timestamp] for res in result: print(res) + def unix_seconds_to_timestamp_function(): # [START unix_seconds_timestamp] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("documents") \ + result = ( + client.pipeline() + .collection("documents") .select( - Field.of("createdAtSeconds").unix_seconds_to_timestamp().as_("createdAtString") - ).execute() + Field.of("createdAtSeconds") + .unix_seconds_to_timestamp() + .as_("createdAtString") + ) + .execute() + ) # [END unix_seconds_timestamp] for res in result: print(res) + def timestamp_add_function(): # [START timestamp_add] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("documents") \ - .select( - Field.of("createdAt").timestamp_add("day", 3653).as_("expiresAt") - ).execute() + result = ( + client.pipeline() + .collection("documents") + .select(Field.of("createdAt").timestamp_add("day", 3653).as_("expiresAt")) + .execute() + ) # [END timestamp_add] for res in result: print(res) + def timestamp_sub_function(): # [START timestamp_sub] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("documents") \ + result = ( + client.pipeline() + .collection("documents") .select( - Field.of("expiresAt").timestamp_subtract("day", 14).as_("sendWarningTimestamp") - ).execute() + Field.of("expiresAt") + .timestamp_subtract("day", 14) + .as_("sendWarningTimestamp") + ) + .execute() + ) # [END timestamp_sub] for res in result: print(res) + def timestamp_to_unix_micros_function(): # [START timestamp_unix_micros] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("documents") \ - .select( - Field.of("dateString").timestamp_to_unix_micros().as_("unixMicros") - ).execute() + result = ( + client.pipeline() + .collection("documents") + .select(Field.of("dateString").timestamp_to_unix_micros().as_("unixMicros")) + .execute() + ) # [END timestamp_unix_micros] for res in result: print(res) + def timestamp_to_unix_millis_function(): # [START timestamp_unix_millis] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("documents") \ - .select( - Field.of("dateString").timestamp_to_unix_millis().as_("unixMillis") - ).execute() + result = ( + client.pipeline() + .collection("documents") + .select(Field.of("dateString").timestamp_to_unix_millis().as_("unixMillis")) + .execute() + ) # [END timestamp_unix_millis] for res in result: print(res) + def timestamp_to_unix_seconds_function(): # [START timestamp_unix_seconds] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("documents") \ - .select( - Field.of("dateString").timestamp_to_unix_seconds().as_("unixSeconds") - ).execute() + result = ( + client.pipeline() + .collection("documents") + .select(Field.of("dateString").timestamp_to_unix_seconds().as_("unixSeconds")) + .execute() + ) # [END timestamp_unix_seconds] for res in result: print(res) + def cosine_distance_function(): # [START cosine_distance] from google.cloud.firestore_v1.pipeline_expressions import Field sample_vector = Vector([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( Field.of("embedding").cosine_distance(sample_vector).as_("cosineDistance") - ).execute() + ) + .execute() + ) # [END cosine_distance] for res in result: print(res) + def dot_product_function(): # [START dot_product] from google.cloud.firestore_v1.pipeline_expressions import Field sample_vector = Vector([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) - result = client.pipeline() \ - .collection("books") \ - .select( - Field.of("embedding").dot_product(sample_vector).as_("dotProduct") - ).execute() + result = ( + client.pipeline() + .collection("books") + .select(Field.of("embedding").dot_product(sample_vector).as_("dotProduct")) + .execute() + ) # [END dot_product] for res in result: print(res) + def euclidean_distance_function(): # [START euclidean_distance] from google.cloud.firestore_v1.pipeline_expressions import Field sample_vector = Vector([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) - result = client.pipeline() \ - .collection("books") \ + result = ( + client.pipeline() + .collection("books") .select( - Field.of("embedding").euclidean_distance(sample_vector).as_("euclideanDistance") - ).execute() + Field.of("embedding") + .euclidean_distance(sample_vector) + .as_("euclideanDistance") + ) + .execute() + ) # [END euclidean_distance] for res in result: print(res) + def vector_length_function(): # [START vector_length] from google.cloud.firestore_v1.pipeline_expressions import Field - result = client.pipeline() \ - .collection("books") \ - .select( - Field.of("embedding").vector_length().as_("vectorLength") - ).execute() + result = ( + client.pipeline() + .collection("books") + .select(Field.of("embedding").vector_length().as_("vectorLength")) + .execute() + ) # [END vector_length] for res in result: print(res) From 9df189b07653f4daee3591e5cc305f873dae61ed Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Fri, 7 Nov 2025 14:23:14 -0800 Subject: [PATCH 05/18] remove confusing async bit --- snippets/firestore/firestore_pipelines.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index 63914095..9de112c8 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -65,10 +65,6 @@ def basic_read(): pipeline = client.pipeline().collection("users") for result in pipeline.execute(): print(f"{result.id} => {result.data()}") - # or, asynchronously - result_stream = pipeline.stream() - async for result in result_stream: - print(f"{result.id} => {result.data()}") # [END basic_read] From f84c39d9823bd5d71b8fc1e41d597e2f62d66d9b Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Mon, 10 Nov 2025 16:36:29 -0800 Subject: [PATCH 06/18] Add expression and input snippets, with errors --- snippets/firestore/firestore_pipelines.py | 1163 ++++++++++++++++++++- 1 file changed, 1136 insertions(+), 27 deletions(-) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index 9de112c8..9d0e8361 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -12,32 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. -from google.cloud.firestore import Query -from google.cloud.firestore_v1.pipeline import Pipeline -from google.cloud.firestore_v1.pipeline_source import PipelineSource -from google.cloud.firestore_v1.pipeline_expressions import ( - AggregateFunction, - Constant, - Expression, - Field, - Count, -) -from google.cloud.firestore_v1.pipeline_expressions import ( - And, - Conditional, - Or, - Not, - Xor, -) -from google.cloud.firestore_v1.pipeline_stages import ( - Aggregate, - FindNearestOptions, - SampleOptions, - UnnestOptions, -) -from google.cloud.firestore_v1.base_vector_query import DistanceMeasure -from google.cloud.firestore_v1.vector import Vector -from google.cloud.firestore_v1.client import Client +# from google.cloud.firestore import Query +# from google.cloud.firestore_v1.pipeline import Pipeline +# from google.cloud.firestore_v1.pipeline_source import PipelineSource +# from google.cloud.firestore_v1.pipeline_expressions import ( +# AggregateFunction, +# Constant, +# Expression, +# Field, +# Count, +# ) +# from google.cloud.firestore_v1.pipeline_expressions import ( +# And, +# Conditional, +# Or, +# Not, +# Xor, +# ) +# from google.cloud.firestore_v1.pipeline_stages import ( +# Aggregate, +# FindNearestOptions, +# SampleOptions, +# UnnestOptions, +# ) +# from google.cloud.firestore_v1.base_vector_query import DistanceMeasure +# from google.cloud.firestore_v1.vector import Vector +# from google.cloud.firestore_v1.client import Client import firebase_admin from firebase_admin import firestore @@ -49,6 +49,8 @@ # pylint: disable=invalid-name def pipeline_concepts(): # [START pipeline_concepts] + from google.cloud.firestore_v1.pipeline_expressions import Field + pipeline = ( client.pipeline() .collection("cities") @@ -78,6 +80,8 @@ def pipeline_initialization(): def field_vs_constants(): # [START field_or_constant] + from google.cloud.firestore_v1.pipeline_expressions import Field, Constant + pipeline = ( client.pipeline() .collection("cities") @@ -185,6 +189,7 @@ def sort(): def sort_comparison(): # [START sort_comparison] + from google.cloud.firestore import Query from google.cloud.firestore_v1.pipeline_expressions import Field query = ( @@ -1146,7 +1151,7 @@ def and_function(): def or_function(): # [START or_function] - from google.cloud.firestore_v1.pipeline_expressions import Field, And + from google.cloud.firestore_v1.pipeline_expressions import Field, And, Or result = ( client.pipeline() @@ -1687,6 +1692,7 @@ def timestamp_to_unix_seconds_function(): def cosine_distance_function(): # [START cosine_distance] from google.cloud.firestore_v1.pipeline_expressions import Field + from google.cloud.firestore_v1.vector import Vector sample_vector = Vector([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) result = ( @@ -1705,6 +1711,7 @@ def cosine_distance_function(): def dot_product_function(): # [START dot_product] from google.cloud.firestore_v1.pipeline_expressions import Field + from google.cloud.firestore_v1.vector import Vector sample_vector = Vector([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) result = ( @@ -1721,6 +1728,7 @@ def dot_product_function(): def euclidean_distance_function(): # [START euclidean_distance] from google.cloud.firestore_v1.pipeline_expressions import Field + from google.cloud.firestore_v1.vector import Vector sample_vector = Vector([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) result = ( @@ -1751,3 +1759,1104 @@ def vector_length_function(): # [END vector_length] for res in result: print(res) + + +def stages_expressions_example(): + # [START stages_expressions_example] + from google.cloud.firestore_v1.pipeline_expressions import Field, Constant + from firebase_admin import firestore + + trailing_30_days = ( + Constant.of(firestore.SERVER_TIMESTAMP) + .unix_millis_to_timestamp() + .timestamp_subtract("day", 30) + ) + snapshot = ( + client.pipeline() + .collection("productViews") + .where(Field.of("viewedAt").greater_than(trailing_30_days)) + .aggregate(Field.of("productId").count_distinct().as_("uniqueProductViews")) + .execute() + ) + # [END stages_expressions_example] + for result in snapshot: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/where +def create_where_data(): + # [START create_where_data] + client.collection("cities").document("SF").set( + {"name": "San Francisco", "state": "CA", "country": "USA", "population": 870000} + ) + client.collection("cities").document("LA").set( + {"name": "Los Angeles", "state": "CA", "country": "USA", "population": 3970000} + ) + client.collection("cities").document("NY").set( + {"name": "New York", "state": "NY", "country": "USA", "population": 8530000} + ) + client.collection("cities").document("TOR").set( + {"name": "Toronto", "state": None, "country": "Canada", "population": 2930000} + ) + client.collection("cities").document("MEX").set( + {"name": "Mexico City", "state": None, "country": "Mexico", "population": 9200000} + ) + # [END create_where_data] + + +def where_equality_example(): + # [START where_equality_example] + from google.cloud.firestore_v1.pipeline_expressions import Field + + cities = ( + client.pipeline() + .collection("cities") + .where(Field.of("state").equal("CA")) + .execute() + ) + # [END where_equality_example] + for city in cities: + print(city) + + +def where_multiple_stages_example(): + # [START where_multiple_stages] + from google.cloud.firestore_v1.pipeline_expressions import Field + + cities = ( + client.pipeline() + .collection("cities") + .where(Field.of("location.country").equal("USA")) + .where(Field.of("population").greater_than(500000)) + .execute() + ) + # [END where_multiple_stages] + for city in cities: + print(city) + + +def where_complex_example(): + # [START where_complex] + from google.cloud.firestore_v1.pipeline_expressions import Field, Or, And + + cities = ( + client.pipeline() + .collection("cities") + .where( + Or( + Field.of("name").like("San%"), + And( + Field.of("location.state").char_length().greater_than(7), + Field.of("location.country").equal("USA"), + ), + ) + ) + .execute() + ) + # [END where_complex] + for city in cities: + print(city) + + +def where_stage_order_example(): + # [START where_stage_order] + from google.cloud.firestore_v1.pipeline_expressions import Field + + cities = ( + client.pipeline() + .collection("cities") + .limit(10) + .where(Field.of("location.country").equal("USA")) + .execute() + ) + # [END where_stage_order] + for city in cities: + print(city) + + +def where_having_example(): + # [START where_having_example] + from google.cloud.firestore_v1.pipeline_expressions import Field + + cities = ( + client.pipeline() + .collection("cities") + .aggregate( + Field.of("population").sum().as_("totalPopulation"), + groups=[Field.of("location.state")], + ) + .where(Field.of("totalPopulation").greater_than(10000000)) + .execute() + ) + # [END where_having_example] + for city in cities: + print(city) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/unnest +def unnest_syntax_example(): + # [START unnest_syntax] + from google.cloud.firestore_v1.pipeline_expressions import Field + from google.cloud.firestore_v1.pipeline_stages import UnnestOptions + + user_score = ( + client.pipeline() + .collection("users") + .unnest( + Field.of("scores").as_("userScore"), + options=UnnestOptions(index_field="attempt"), + ) + .execute() + ) + # [END unnest_syntax] + for score in user_score: + print(score) + + +def unnest_alias_index_data_example(): + # [START unnest_alias_index_data] + client.collection("users").add({"name": "foo", "scores": [5, 4], "userScore": 0}) + client.collection("users").add({"name": "bar", "scores": [1, 3], "attempt": 5}) + # [END unnest_alias_index_data] + + +def unnest_alias_index_example(): + # [START unnest_alias_index] + from google.cloud.firestore_v1.pipeline_expressions import Field + from google.cloud.firestore_v1.pipeline_stages import UnnestOptions + + user_score = ( + client.pipeline() + .collection("users") + .unnest( + Field.of("scores").as_("userScore"), + options=UnnestOptions(index_field="attempt"), + ) + .execute() + ) + # [END unnest_alias_index] + for score in user_score: + print(score) + + +def unnest_non_array_data_example(): + # [START unnest_nonarray_data] + client.collection("users").add({"name": "foo", "scores": 1}) + client.collection("users").add({"name": "bar", "scores": None}) + client.collection("users").add({"name": "qux", "scores": {"backupScores": 1}}) + # [END unnest_nonarray_data] + + +def unnest_non_array_example(): + # [START unnest_nonarray] + from google.cloud.firestore_v1.pipeline_expressions import Field + from google.cloud.firestore_v1.pipeline_stages import UnnestOptions + + user_score = ( + client.pipeline() + .collection("users") + .unnest( + Field.of("scores").as_("userScore"), + options=UnnestOptions(index_field="attempt"), + ) + .execute() + ) + # [END unnest_nonarray] + for score in user_score: + print(score) + + +def unnest_empty_array_data_example(): + # [START unnest_empty_array_data] + client.collection("users").add({"name": "foo", "scores": [5, 4]}) + client.collection("users").add({"name": "bar", "scores": []}) + # [END unnest_empty_array_data] + + +def unnest_empty_array_example(): + # [START unnest_empty_array] + from google.cloud.firestore_v1.pipeline_expressions import Field + from google.cloud.firestore_v1.pipeline_stages import UnnestOptions + + user_score = ( + client.pipeline() + .collection("users") + .unnest( + Field.of("scores").as_("userScore"), + options=UnnestOptions(index_field="attempt"), + ) + .execute() + ) + # [END unnest_empty_array] + for score in user_score: + print(score) + + +def unnest_preserve_empty_array_example(): + # [START unnest_preserve_empty_array] + from google.cloud.firestore_v1.pipeline_expressions import ( + Field, + Conditional, + Expression, + ) + from google.cloud.firestore_v1.pipeline_stages import UnnestOptions + + user_score = ( + client.pipeline() + .collection("users") + .unnest( + Conditional( + Field.of("scores").equal(Expression.array([])), + Expression.array([Field.of("scores")]), + Field.of("scores"), + ).as_("userScore"), + options=UnnestOptions(index_field="attempt"), + ) + .execute() + ) + # [END unnest_preserve_empty_array] + for score in user_score: + print(score) + + +def unnest_nested_data_example(): + # [START unnest_nested_data] + client.collection("users").add( + { + "name": "foo", + "record": [ + {"scores": [5, 4], "avg": 4.5}, + {"scores": [1, 3], "old_avg": 2}, + ], + } + ) + # [END unnest_nested_data] + + +def unnest_nested_example(): + # [START unnest_nested] + from google.cloud.firestore_v1.pipeline_expressions import Field + from google.cloud.firestore_v1.pipeline_stages import UnnestOptions + + user_score = ( + client.pipeline() + .collection("users") + .unnest(Field.of("record").as_("record")) + .unnest( + Field.of("record.scores").as_("userScore"), + options=UnnestOptions(index_field="attempt"), + ) + .execute() + ) + # [END unnest_nested] + for score in user_score: + print(score) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/sample +def sample_syntax_example(): + # [START sample_syntax] + from google.cloud.firestore_v1.pipeline_stages import SampleOptions + + sampled = client.pipeline().database().sample(50).execute() + + sampled = ( + client.pipeline().database().sample(options=SampleOptions.percentage(0.5)).execute() + ) + # [END sample_syntax] + for result in sampled: + print(result) + + +def sample_documents_data_example(): + # [START sample_documents_data] + client.collection("cities").document("SF").set( + {"name": "San Francisco", "state": "California"} + ) + client.collection("cities").document("NYC").set( + {"name": "New York City", "state": "New York"} + ) + client.collection("cities").document("CHI").set( + {"name": "Chicago", "state": "Illinois"} + ) + # [END sample_documents_data] + + +def sample_documents_example(): + # [START sample_documents] + sampled = client.pipeline().collection("cities").sample(1).execute() + # [END sample_documents] + for result in sampled: + print(result) + + +def sample_all_documents_example(): + # [START sample_all_documents] + sampled = client.pipeline().collection("cities").sample(5).execute() + # [END sample_all_documents] + for result in sampled: + print(result) + + +def sample_percentage_data_example(): + # [START sample_percentage_data] + client.collection("cities").document("SF").set( + {"name": "San Francsico", "state": "California"} + ) + client.collection("cities").document("NYC").set( + {"name": "New York City", "state": "New York"} + ) + client.collection("cities").document("CHI").set( + {"name": "Chicago", "state": "Illinois"} + ) + client.collection("cities").document("ATL").set( + {"name": "Atlanta", "state": "Georgia"} + ) + # [END sample_percentage_data] + + +def sample_percentage_example(): + # [START sample_percentage] + from google.cloud.firestore_v1.pipeline_stages import SampleOptions + + sampled = ( + client.pipeline() + .collection("cities") + .sample(options=SampleOptions.percentage(0.5)) + .execute() + ) + # [END sample_percentage] + for result in sampled: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/sort +def sort_syntax_example(): + # [START sort_syntax] + from google.cloud.firestore_v1.pipeline_expressions import Field + + results = ( + client.pipeline() + .collection("cities") + .sort(Field.of("population").ascending()) + .execute() + ) + # [END sort_syntax] + for result in results: + print(result) + + +def sort_syntax_example2(): + # [START sort_syntax_2] + from google.cloud.firestore_v1.pipeline_expressions import Field + + results = ( + client.pipeline() + .collection("cities") + .sort(Field.of("name").char_length().ascending()) + .execute() + ) + # [END sort_syntax_2] + for result in results: + print(result) + + +def sort_document_id_example(): + # [START sort_document_id] + from google.cloud.firestore_v1.pipeline_expressions import Field + + results = ( + client.pipeline() + .collection("cities") + .sort(Field.of("country").ascending(), Field.of("__name__").ascending()) + .execute() + ) + # [END sort_document_id] + for result in results: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/select +def select_syntax_example(): + # [START select_syntax] + from google.cloud.firestore_v1.pipeline_expressions import Field + + names = ( + client.pipeline() + .collection("cities") + .select( + Field.of("name").string_concat(", ", Field.of("location.country")).as_("name"), + "population", + ) + .execute() + ) + # [END select_syntax] + for name in names: + print(name) + + +def select_position_data_example(): + # [START select_position_data] + client.collection("cities").document("SF").set( + { + "name": "San Francisco", + "population": 800000, + "location": {"country": "USA", "state": "California"}, + } + ) + client.collection("cities").document("TO").set( + { + "name": "Toronto", + "population": 3000000, + "location": {"country": "Canada", "province": "Ontario"}, + } + ) + # [END select_position_data] + + +def select_position_example(): + # [START select_position] + from google.cloud.firestore_v1.pipeline_expressions import Field + + names = ( + client.pipeline() + .collection("cities") + .where(Field.of("location.country").equal("Canada")) + .select( + Field.of("name").string_concat(", ", Field.of("location.country")).as_("name"), + "population", + ) + .execute() + ) + # [END select_position] + for name in names: + print(name) + + +def select_bad_position_example(): + # [START select_bad_position] + from google.cloud.firestore_v1.pipeline_expressions import Field + + names = ( + client.pipeline() + .collection("cities") + .select( + Field.of("name").string_concat(", ", Field.of("location.country")).as_("name"), + "population", + ) + .where(Field.of("location.country").equal("Canada")) + .execute() + ) + # [END select_bad_position] + for name in names: + print(name) + + +def select_nested_data_example(): + # [START select_nested_data] + client.collection("cities").document("SF").set( + { + "name": "San Francisco", + "population": 800000, + "location": {"country": "USA", "state": "California"}, + "landmarks": ["Golden Gate Bridge", "Alcatraz"], + } + ) + client.collection("cities").document("TO").set( + { + "name": "Toronto", + "population": 3000000, + "province": "ON", + "location": {"country": "Canada", "province": "Ontario"}, + "landmarks": ["CN Tower", "Casa Loma"], + } + ) + client.collection("cities").document("AT").set({"name": "Atlantis", "population": None}) + # [END select_nested_data] + + +def select_nested_example(): + # [START select_nested] + from google.cloud.firestore_v1.pipeline_expressions import Field + + locations = ( + client.pipeline() + .collection("cities") + .select( + Field.of("name").as_("city"), + Field.of("location.country").as_("country"), + Field.of("landmarks").array_get(0).as_("topLandmark"), + ) + .execute() + ) + # [END select_nested] + for location in locations: + print(location) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/remove_fields +def remove_fields_syntax_example(): + # [START remove_fields_syntax] + results = ( + client.pipeline() + .collection("cities") + .remove_fields("population", "location.state") + .execute() + ) + # [END remove_fields_syntax] + for result in results: + print(result) + + +def remove_fields_nested_data_example(): + # [START remove_fields_nested_data] + client.collection("cities").document("SF").set( + {"name": "San Francisco", "location": {"country": "USA", "state": "California"}} + ) + client.collection("cities").document("TO").set( + {"name": "Toronto", "location": {"country": "Canada", "province": "Ontario"}} + ) + # [END remove_fields_nested_data] + + +def remove_fields_nested_example(): + # [START remove_fields_nested] + results = ( + client.pipeline().collection("cities").remove_fields("location.state").execute() + ) + # [END remove_fields_nested] + for result in results: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/limit +def limit_syntax_example(): + # [START limit_syntax] + results = client.pipeline().collection("cities").limit(10).execute() + # [END limit_syntax] + for result in results: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/find_nearest +def find_nearest_syntax_example(): + # [START find_nearest_syntax] + from google.cloud.firestore_v1.vector import Vector + from google.cloud.firestore_v1.base_vector_query import DistanceMeasure + + results = ( + client.pipeline() + .collection("cities") + .find_nearest( + field="embedding", + vector_value=Vector([1.5, 2.345]), + distance_measure=DistanceMeasure.EUCLIDEAN, + ) + .execute() + ) + # [END find_nearest_syntax] + for result in results: + print(result) + + +def find_nearest_limit_example(): + # [START find_nearest_limit] + from google.cloud.firestore_v1.vector import Vector + from google.cloud.firestore_v1.base_vector_query import DistanceMeasure + + results = ( + client.pipeline() + .collection("cities") + .find_nearest( + field="embedding", + vector_value=Vector([1.5, 2.345]), + distance_measure=DistanceMeasure.EUCLIDEAN, + limit=10, + ) + .execute() + ) + # [END find_nearest_limit] + for result in results: + print(result) + + +def find_nearest_distance_data_example(): + # [START find_nearest_distance_data] + from google.cloud.firestore_v1.vector import Vector + + client.collection("cities").document("SF").set( + {"name": "San Francisco", "embedding": Vector([1.0, -1.0])} + ) + client.collection("cities").document("TO").set( + {"name": "Toronto", "embedding": Vector([5.0, -10.0])} + ) + client.collection("cities").document("AT").set( + {"name": "Atlantis", "embedding": Vector([2.0, -4.0])} + ) + # [END find_nearest_distance_data] + + +def find_nearest_distance_example(): + # [START find_nearest_distance] + from google.cloud.firestore_v1.vector import Vector + from google.cloud.firestore_v1.base_vector_query import DistanceMeasure + + results = ( + client.pipeline() + .collection("cities") + .find_nearest( + field="embedding", + vector_value=Vector([1.3, 2.345]), + distance_measure=DistanceMeasure.EUCLIDEAN, + distance_field="computedDistance", + ) + .execute() + ) + # [END find_nearest_distance] + for result in results: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/offset +def offset_syntax_example(): + # [START offset_syntax] + results = client.pipeline().collection("cities").offset(10).execute() + # [END offset_syntax] + for result in results: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/add_fields +def add_fields_syntax_example(): + # [START add_fields_syntax] + from google.cloud.firestore_v1.pipeline_expressions import Field + + results = ( + client.pipeline() + .collection("users") + .add_fields( + Field.of("firstName").string_concat(" ", Field.of("lastName")).as_("fullName") + ) + .execute() + ) + # [END add_fields_syntax] + for result in results: + print(result) + + +def add_fields_overlap_example(): + # [START add_fields_overlap] + from google.cloud.firestore_v1.pipeline_expressions import Field + + results = ( + client.pipeline() + .collection("users") + .add_fields(Field.of("age").abs().as_("age")) + .add_fields(Field.of("age").add(10).as_("age")) + .execute() + ) + # [END add_fields_overlap] + for result in results: + print(result) + + +def add_fields_nesting_example(): + # [START add_fields_nesting] + from google.cloud.firestore_v1.pipeline_expressions import Field + + results = ( + client.pipeline() + .collection("users") + .add_fields(Field.of("address.city").to_lower().as_("address.city")) + .execute() + ) + # [END add_fields_nesting] + for result in results: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/input/collection +def collection_input_syntax_example(): + # [START collection_input_syntax] + results = client.pipeline().collection("cities/SF/departments").execute() + # [END collection_input_syntax] + for result in results: + print(result) + + +def collection_input_example_data(): + # [START collection_input_data] + client.collection("cities").document("SF").set( + {"name": "San Francsico", "state": "California"} + ) + client.collection("cities").document("NYC").set( + {"name": "New York City", "state": "New York"} + ) + client.collection("cities").document("CHI").set( + {"name": "Chicago", "state": "Illinois"} + ) + client.collection("states").document("CA").set({"name": "California"}) + # [END collection_input_data] + + +def collection_input_example(): + # [START collection_input] + from google.cloud.firestore_v1.pipeline_expressions import Field + + results = ( + client.pipeline().collection("cities").sort(Field.of("name").ascending()).execute() + ) + # [END collection_input] + for result in results: + print(result) + + +def subcollection_input_example_data(): + # [START subcollection_input_data] + client.collection("cities/SF/departments").document("building").set( + {"name": "SF Building Deparment", "employees": 750} + ) + client.collection("cities/NY/departments").document("building").set( + {"name": "NY Building Deparment", "employees": 1000} + ) + client.collection("cities/CHI/departments").document("building").set( + {"name": "CHI Building Deparment", "employees": 900} + ) + client.collection("cities/NY/departments").document("finance").set( + {"name": "NY Finance Deparment", "employees": 1200} + ) + # [END subcollection_input_data] + + +def subcollection_input_example(): + # [START subcollection_input] + from google.cloud.firestore_v1.pipeline_expressions import Field + + results = ( + client.pipeline() + .collection("cities/NY/departments") + .sort(Field.of("employees").ascending()) + .execute() + ) + # [END subcollection_input] + for result in results: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/input/collection_group +def collection_group_input_syntax_example(): + # [START collection_group_input_syntax] + results = client.pipeline().collection_group("departments").execute() + # [END collection_group_input_syntax] + for result in results: + print(result) + + +def collection_group_input_example_data(): + # [START collection_group_data] + client.collection("cities/SF/departments").document("building").set( + {"name": "SF Building Deparment", "employees": 750} + ) + client.collection("cities/NY/departments").document("building").set( + {"name": "NY Building Deparment", "employees": 1000} + ) + client.collection("cities/CHI/departments").document("building").set( + {"name": "CHI Building Deparment", "employees": 900} + ) + client.collection("cities/NY/departments").document("finance").set( + {"name": "NY Finance Deparment", "employees": 1200} + ) + # [END collection_group_data] + + +def collection_group_input_example(): + # [START collection_group_input] + from google.cloud.firestore_v1.pipeline_expressions import Field + + results = ( + client.pipeline() + .collection_group("departments") + .sort(Field.of("employees").ascending()) + .execute() + ) + # [END collection_group_input] + for result in results: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/input/database +def database_input_syntax_example(): + # [START database_syntax] + results = client.pipeline().database().execute() + # [END database_syntax] + for result in results: + print(result) + + +def database_input_syntax_example_data(): + # [START database_input_data] + client.collection("cities").document("SF").set( + {"name": "San Francsico", "state": "California", "population": 800000} + ) + client.collection("states").document("CA").set( + {"name": "California", "population": 39000000} + ) + client.collection("countries").document("USA").set( + {"name": "United States of America", "population": 340000000} + ) + # [END database_input_data] + + +def database_input_example(): + # [START database_input] + from google.cloud.firestore_v1.pipeline_expressions import Field + + results = ( + client.pipeline() + .database() + .sort(Field.of("population").ascending()) + .execute() + ) + # [END database_input] + for result in results: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/input/documents +def document_input_syntax_example(): + # [START document_input_syntax] + results = ( + client.pipeline() + .documents( + [ + client.collection("cities").document("SF"), + client.collection("cities").document("NY"), + ] + ) + .execute() + ) + # [END document_input_syntax] + for result in results: + print(result) + + +def document_input_example_data(): + # [START document_input_data] + client.collection("cities").document("SF").set( + {"name": "San Francsico", "state": "California"} + ) + client.collection("cities").document("NYC").set( + {"name": "New York City", "state": "New York"} + ) + client.collection("cities").document("CHI").set( + {"name": "Chicago", "state": "Illinois"} + ) + # [END document_input_data] + + +def document_input_example(): + # [START document_input] + from google.cloud.firestore_v1.pipeline_expressions import Field + + results = ( + client.pipeline() + .documents( + [ + client.collection("cities").document("SF"), + client.collection("cities").document("NYC"), + ] + ) + .sort(Field.of("name").ascending()) + .execute() + ) + # [END document_input] + for result in results: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/union +def union_syntax_example(): + # [START union_syntax] + results = ( + client.pipeline() + .collection("cities/SF/restaurants") + .union(client.pipeline().collection("cities/NYC/restaurants")) + .execute() + ) + # [END union_syntax] + for result in results: + print(result) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/aggregate +def aggregate_syntax_example(): + # [START aggregate_syntax] + from google.cloud.firestore_v1.pipeline_expressions import Count, Field + + cities = ( + client.pipeline() + .collection("cities") + .aggregate( + Count().as_("total"), + Field.of("population").average().as_("averagePopulation"), + ) + .execute() + ) + # [END aggregate_syntax] + for city in cities: + print(city) + + +def aggregate_group_syntax(): + # [START aggregate_group_syntax] + from google.cloud.firestore_v1.pipeline_expressions import Field, Count + + result = ( + client.pipeline() + .collection_group("cities") + .aggregate( + Count().as_("cities"), + Field.of("population").sum().as_("totalPopulation"), + groups=[Field.of("location.state").as_("state")], + ) + .execute() + ) + # [END aggregate_group_syntax] + for res in result: + print(res) + + +def aggregate_example_data(): + # [START aggregate_data] + client.collection("cities").document("SF").set( + {"name": "San Francisco", "state": "CA", "country": "USA", "population": 870000} + ) + client.collection("cities").document("LA").set( + {"name": "Los Angeles", "state": "CA", "country": "USA", "population": 3970000} + ) + client.collection("cities").document("NY").set( + {"name": "New York", "state": "NY", "country": "USA", "population": 8530000} + ) + client.collection("cities").document("TOR").set( + {"name": "Toronto", "state": None, "country": "Canada", "population": 2930000} + ) + client.collection("cities").document("MEX").set( + {"name": "Mexico City", "state": None, "country": "Mexico", "population": 9200000} + ) + # [END aggregate_data] + + +def aggregate_without_group_example(): + # [START aggregate_without_group] + from google.cloud.firestore_v1.pipeline_expressions import Field, Count + + cities = ( + client.pipeline() + .collection("cities") + .aggregate( + Count().as_("total"), + Field.of("population").average().as_("averagePopulation"), + ) + .execute() + ) + # [END aggregate_without_group] + for city in cities: + print(city) + + +def aggregate_group_example(): + # [START aggregate_group_example] + from google.cloud.firestore_v1.pipeline_expressions import Field, Count + + cities = ( + client.pipeline() + .collection("cities") + .aggregate( + Count().as_("numberOfCities"), + Field.of("population").maximum().as_("maxPopulation"), + groups=["country", "state"], + ) + .execute() + ) + # [END aggregate_group_example] + for city in cities: + print(city) + + +def aggregate_group_complex_example(): + # [START aggregate_group_complex] + from google.cloud.firestore_v1.pipeline_expressions import Field + + cities = ( + client.pipeline() + .collection("cities") + .aggregate( + Field.of("population").sum().as_("totalPopulation"), + groups=[Field.of("state").equal(None).as_("stateIsNull")], + ) + .execute() + ) + # [END aggregate_group_complex] + for city in cities: + print(city) + + +# https://cloud.google.com/firestore/docs/pipeline/stages/transformation/distinct +def distinct_syntax_example(): + # [START distinct_syntax] + from google.cloud.firestore_v1.pipeline_expressions import Field + + cities = client.pipeline().collection("cities").distinct("country").execute() + + cities = ( + client.pipeline() + .collection("cities") + .distinct(Field.of("state").to_lower().as_("normalizedState"), "country") + .execute() + ) + # [END distinct_syntax] + for city in cities: + print(city) + + +def distinct_example_data(): + # [START distinct_data] + client.collection("cities").document("SF").set( + {"name": "San Francisco", "state": "CA", "country": "USA"} + ) + client.collection("cities").document("LA").set( + {"name": "Los Angeles", "state": "CA", "country": "USA"} + ) + client.collection("cities").document("NY").set( + {"name": "New York", "state": "NY", "country": "USA"} + ) + client.collection("cities").document("TOR").set( + {"name": "Toronto", "state": None, "country": "Canada"} + ) + client.collection("cities").document("MEX").set( + {"name": "Mexico City", "state": None, "country": "Mexico"} + ) + # [END distinct_data] + + +def distinct_example(): + # [START distinct_example] + cities = client.pipeline().collection("cities").distinct("country").execute() + # [END distinct_example] + for city in cities: + print(city) + + +def distinct_expressions_example(): + # [START distinct_expressions] + from google.cloud.firestore_v1.pipeline_expressions import Field + + cities = ( + client.pipeline() + .collection("cities") + .distinct(Field.of("state").to_lower().as_("normalizedState"), "country") + .execute() + ) + # [END distinct_expressions] + for city in cities: + print(city) From b17342b59afdb87096e8b85f0c396aa0330daeb3 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Tue, 11 Nov 2025 15:06:29 -0800 Subject: [PATCH 07/18] add query explain snippet and fix lint errors --- snippets/firestore/firestore_pipelines.py | 49 +++++++++-------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index 9d0e8361..e00b5379 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -12,33 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# from google.cloud.firestore import Query -# from google.cloud.firestore_v1.pipeline import Pipeline -# from google.cloud.firestore_v1.pipeline_source import PipelineSource -# from google.cloud.firestore_v1.pipeline_expressions import ( -# AggregateFunction, -# Constant, -# Expression, -# Field, -# Count, -# ) -# from google.cloud.firestore_v1.pipeline_expressions import ( -# And, -# Conditional, -# Or, -# Not, -# Xor, -# ) -# from google.cloud.firestore_v1.pipeline_stages import ( -# Aggregate, -# FindNearestOptions, -# SampleOptions, -# UnnestOptions, -# ) -# from google.cloud.firestore_v1.base_vector_query import DistanceMeasure -# from google.cloud.firestore_v1.vector import Vector -# from google.cloud.firestore_v1.client import Client - import firebase_admin from firebase_admin import firestore @@ -47,6 +20,19 @@ # pylint: disable=invalid-name + +def query_explain(): + # [START query_explain] + from google.cloud.firestore import Query, FieldFilter, ExplainOptions + + results = client.collection("cities") \ + .where(filter=FieldFilter("capital", "==", True)) \ + .execute(explain_options=ExplainOptions(analyze=False)) + metrics = results.explain_metrics() + summary = metrics.plan_summary() + # [END query_explain] + + def pipeline_concepts(): # [START pipeline_concepts] from google.cloud.firestore_v1.pipeline_expressions import Field @@ -1764,7 +1750,7 @@ def vector_length_function(): def stages_expressions_example(): # [START stages_expressions_example] from google.cloud.firestore_v1.pipeline_expressions import Field, Constant - from firebase_admin import firestore + from google.cloud import firestore trailing_30_days = ( Constant.of(firestore.SERVER_TIMESTAMP) @@ -1995,9 +1981,10 @@ def unnest_empty_array_example(): def unnest_preserve_empty_array_example(): # [START unnest_preserve_empty_array] from google.cloud.firestore_v1.pipeline_expressions import ( - Field, + Array, Conditional, Expression, + Field, ) from google.cloud.firestore_v1.pipeline_stages import UnnestOptions @@ -2006,8 +1993,8 @@ def unnest_preserve_empty_array_example(): .collection("users") .unnest( Conditional( - Field.of("scores").equal(Expression.array([])), - Expression.array([Field.of("scores")]), + Field.of("scores").equal(Array([])), + Array([Field.of("scores")]), Field.of("scores"), ).as_("userScore"), options=UnnestOptions(index_field="attempt"), From 79e3d2b3f7e5cd338b947c74ba4856a38854225b Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Wed, 12 Nov 2025 14:00:50 -0800 Subject: [PATCH 08/18] fix explainoptions snippet --- snippets/firestore/firestore_pipelines.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index e00b5379..31b8149b 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -23,13 +23,14 @@ def query_explain(): # [START query_explain] - from google.cloud.firestore import Query, FieldFilter, ExplainOptions + from google.cloud.firestore import Query, FieldFilter + from google.cloud.firestore_v1.query_profile import PipelineExplainOptions results = client.collection("cities") \ .where(filter=FieldFilter("capital", "==", True)) \ - .execute(explain_options=ExplainOptions(analyze=False)) - metrics = results.explain_metrics() - summary = metrics.plan_summary() + .execute(explain_options=PipelineExplainOptions()) + stats = results.explain_stats + print(stats.get_text()) # [END query_explain] From addbcbf268bff21a405f1ec7e8e77f489f813466 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Wed, 14 Jan 2026 15:02:56 -0800 Subject: [PATCH 09/18] add type and constant functions --- snippets/firestore/firestore_pipelines.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index 31b8149b..f8f9a680 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -21,6 +21,24 @@ # pylint: disable=invalid-name +def type_generic_functions(): + from google.cloud.firestore_v1.pipeline_expressions import Field, Constant + # [START type_function] + Field.of("rating").type() + # [END type_function] + + # [START concat_function] + Constant.of("Author ID: ").concat(Field.of("authorId")) + # [END concat_function] + + # [START length_function] + Field.of("tags").length() + # [END length_function] + + # [START reverse_function] + Field.of("tags").reverse() + # [END reverse_function] + def query_explain(): # [START query_explain] from google.cloud.firestore import Query, FieldFilter From bff59b78482f7661a72e2c1c448ab23f9f073e33 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Wed, 15 Apr 2026 16:54:14 -0700 Subject: [PATCH 10/18] add snippets except forceindex (not supported) --- snippets/firestore/firestore_pipelines.py | 189 +++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index f8f9a680..7fd58d04 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -36,7 +36,7 @@ def type_generic_functions(): # [END length_function] # [START reverse_function] - Field.of("tags").reverse() + Field.of("tags").array_reverse() # [END reverse_function] def query_explain(): @@ -2866,3 +2866,190 @@ def distinct_expressions_example(): # [END distinct_expressions] for city in cities: print(city) + + +def search_basic(): + # [START search_basic] + from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches + + results = ( + client.pipeline() + .collection("restaurants") + .search(DocumentMatches("waffles")) + .execute() + ) + # [END search_basic] + for result in results: + print(result) + + +def search_exact(): + # [START search_exact] + from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches + + results = ( + client.pipeline() + .collection("restaurants") + .search(DocumentMatches('"belgian waffles"')) + .execute() + ) + # [END search_exact] + for result in results: + print(result) + + +def search_two_terms(): + # [START search_two_terms] + from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches + + results = ( + client.pipeline() + .collection("restaurants") + .search(DocumentMatches("waffles eggs")) + .execute() + ) + # [END search_two_terms] + for result in results: + print(result) + + +def search_exclude(): + # [START search_exclude] + from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches + + results = ( + client.pipeline() + .collection("restaurants") + .search(DocumentMatches("-waffles")) + .execute() + ) + # [END search_exclude] + for result in results: + print(result) + + +def search_score(): + # [START search_score] + from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches, Score + from google.cloud.firestore_v1.pipeline_stages import SearchOptions + + results = ( + client.pipeline() + .collection("restaurants") + .search( + SearchOptions( + query=DocumentMatches("menu:waffles"), + add_fields=[Score().as_("score")], + ) + ) + .execute() + ) + # [END search_score] + for result in results: + print(result) + + +def define_example(): + # [START define_example] + from google.cloud.firestore_v1.pipeline_expressions import Field, Variable + + result = ( + client.pipeline() + .collection("authors") + .define(Field.of("id").as_("currentAuthorId")) + .add_fields( + client.pipeline() + .collection("books") + .where(Field.of("author_id").equal(Variable("currentAuthorId"))) + .aggregate(Field.of("rating").average().as_("avgRating")) + .to_scalar_expression() + .as_("averageBookRating") + ) + .execute() + ) + # [END define_example] + for r in result: + print(r) + + +def to_array_example(): + # [START to_array_example] + from google.cloud.firestore_v1.pipeline_expressions import Field, Variable + + results = ( + client.pipeline() + .collection("projects") + .define(Field.of("id").as_("parentId")) + .add_fields( + client.pipeline() + .collection("tasks") + .where(Field.of("project_id").equal(Variable("parentId"))) + .select(Field.of("title")) + .to_array_expression() + .as_("taskTitles") + ) + .execute() + ) + # [END to_array_example] + for result in results: + print(result) + + +def to_scalar_example(): + # [START to_scalar_example] + from google.cloud.firestore_v1.pipeline_expressions import Field, Variable + + results = ( + client.pipeline() + .collection("authors") + .define(Field.of("id").as_("currentAuthorId")) + .add_fields( + client.pipeline() + .collection("books") + .where(Field.of("author_id").equal(Variable("currentAuthorId"))) + .aggregate(Field.of("rating").average().as_("avgRating")) + .to_scalar_expression() + .as_("averageBookRating") + ) + .execute() + ) + # [END to_scalar_example] + for result in results: + print(result) + + +def pipeline_update_example(): + # [START pipeline_update] + from google.cloud.firestore_v1.pipeline_expressions import Constant, Field, Not + + snapshot = ( + client.pipeline() + .collection_group("users") + .where(Not(Field.of("preferences.color").exists())) + .add_fields(Constant.of(None).as_("preferences.color")) + .remove_fields("color") + .update() + .execute() + ) + # [END pipeline_update] + print(snapshot) + + +def pipeline_delete_example(): + # [START pipeline_delete] + from google.cloud.firestore_v1.pipeline_expressions import CurrentTimestamp, Field + + snapshot = ( + client.pipeline() + .collection_group("users") + .where(Field.of("address.country").equal("USA")) + .where( + Field.of("__create_time__") + .timestamp_add("day", 10) + .less_than(CurrentTimestamp()) + ) + .delete() + .execute() + ) + # [END pipeline_delete] + print(snapshot) From c2db983da140d0e0a7f371d4752618e51107377b Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Fri, 17 Apr 2026 16:53:01 -0700 Subject: [PATCH 11/18] Add snippets for inserting data used by other snippets --- snippets/firestore/firestore_pipelines.py | 123 ++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index 7fd58d04..4c204898 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime import firebase_admin from firebase_admin import firestore @@ -2868,6 +2869,15 @@ def distinct_expressions_example(): print(city) + +def search_basic_query_data(): + # [START search_basic_query_data] + client.collection("Restaurants").add( + {"name": "Waffle Place", "description": "A cozy place for fresh waffles."} + ) + # [END search_basic_query_data] + + def search_basic(): # [START search_basic] from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches @@ -2883,6 +2893,14 @@ def search_basic(): print(result) +def search_exact_match_data(): + # [START search_exact_match_data] + client.collection("Restaurants").add( + {"name": "Waffle Place", "description": "A cozy place for fresh waffles."} + ) + # [END search_exact_match_data] + + def search_exact(): # [START search_exact] from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches @@ -2898,6 +2916,17 @@ def search_exact(): print(result) +def search_two_terms_data(): + # [START search_two_terms_data] + client.collection("Restaurants").add( + { + "name": "Morning Diner", + "description": "Start your day with waffles and eggs.", + } + ) + # [END search_two_terms_data] + + def search_two_terms(): # [START search_two_terms] from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches @@ -2913,6 +2942,14 @@ def search_two_terms(): print(result) +def search_exclude_term_data(): + # [START search_exclude_term_data] + client.collection("Restaurants").add( + {"name": "City Coffee", "description": "Premium coffee and pastries."} + ) + # [END search_exclude_term_data] + + def search_exclude(): # [START search_exclude] from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches @@ -2928,6 +2965,14 @@ def search_exclude(): print(result) +def search_score_data(): + # [START search_score_data] + client.collection("Restaurants").add( + {"name": "The Waffle Hub", "description": "Everything waffles!"} + ) + # [END search_score_data] + + def search_score(): # [START search_score] from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches, Score @@ -2949,6 +2994,14 @@ def search_score(): print(result) +def define_stage_data(): + # [START define_stage_data] + client.collection("Authors").document("author_123").set( + {"id": "author_123", "name": "Jane Austen"} + ) + # [END define_stage_data] + + def define_example(): # [START define_example] from google.cloud.firestore_v1.pipeline_expressions import Field, Variable @@ -2972,6 +3025,20 @@ def define_example(): print(r) +def to_array_expression_stage_data(): + # [START to_array_expression_stage_data] + client.collection("Projects").document("project_1").set( + {"id": "project_1", "name": "Alpha Build"} + ) + tasks = [ + {"project_id": "project_1", "title": "System Architecture"}, + {"project_id": "project_1", "title": "Database Schema Design"}, + ] + for task in tasks: + client.collection("Tasks").add(task) + # [END to_array_expression_stage_data] + + def to_array_example(): # [START to_array_example] from google.cloud.firestore_v1.pipeline_expressions import Field, Variable @@ -2995,6 +3062,20 @@ def to_array_example(): print(result) +def to_scalar_expression_stage_data(): + # [START to_scalar_expression_stage_data] + client.collection("Authors").document("author_202").set( + {"id": "author_202", "name": "Charles Dickens"} + ) + books = [ + {"author_id": "author_202", "title": "Great Expectations", "rating": 4.8}, + {"author_id": "author_202", "title": "Oliver Twist", "rating": 4.5}, + ] + for book in books: + client.collection("Books").add(book) + # [END to_scalar_expression_stage_data] + + def to_scalar_example(): # [START to_scalar_example] from google.cloud.firestore_v1.pipeline_expressions import Field, Variable @@ -3018,6 +3099,32 @@ def to_scalar_example(): print(result) +def force_index_example(): + # [START force_index_example] + # Note: forceIndex is not currently supported in the Python SDK. + results = client.pipeline().collection_group("customers").limit(100).execute() + # [END force_index_example] + for result in results: + print(result) + + +def force_scan_example(): + # [START force_scan_example] + # Note: forceIndex="primary" is not currently supported in the Python SDK. + results = client.pipeline().collection_group("customers").limit(100).execute() + # [END force_scan_example] + for result in results: + print(result) + + +def update_dml_example_data(): + # [START update_dml_example_data] + client.collection("Users").document("userID").set( + {"id": "userID", "preferences": {}, "color": "#FFFFFF"} + ) + # [END update_dml_example_data] + + def pipeline_update_example(): # [START pipeline_update] from google.cloud.firestore_v1.pipeline_expressions import Constant, Field, Not @@ -3035,6 +3142,22 @@ def pipeline_update_example(): print(snapshot) +def delete_dml_example_data(): + # [START delete_dml_example_data] + import datetime + + client.collection("Users").document("userID").set( + { + "id": "userID", + "address": {"country": "USA", "state": "CA"}, + "__create_time__": datetime.datetime.fromtimestamp( + 946684800000 / 1000, tz=datetime.timezone.utc + ), + } + ) + # [END delete_dml_example_data] + + def pipeline_delete_example(): # [START pipeline_delete] from google.cloud.firestore_v1.pipeline_expressions import CurrentTimestamp, Field From eb621886de1ba1f4685a7cf9093673633d568975 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Mon, 20 Apr 2026 14:03:57 -0700 Subject: [PATCH 12/18] add search and geo snippets --- snippets/firestore/firestore_pipelines.py | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index 4c204898..b4a532ff 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -2994,6 +2994,42 @@ def search_score(): print(result) +def search_score_sort(): + # [START search_score_sort] + from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches, Score + from google.cloud.firestore_v1.pipeline_stages import SearchOptions + + results = ( + client.pipeline() + .collection("restaurants") + .search( + SearchOptions( + query=DocumentMatches("waffles"), + sort=Score().descending(), + ) + ) + .execute() + ) + # [END search_score_sort] + for result in results: + print(result) + + +def search_geospatial(): + # [START search_geospatial] + from google.cloud.firestore_v1.pipeline_expressions import DocumentMatches + + results = ( + client.pipeline() + .collection("restaurants") + .search(DocumentMatches('"belgian waffles"')) + .execute() + ) + # [END search_geospatial] + for result in results: + print(result) + + def define_stage_data(): # [START define_stage_data] client.collection("Authors").document("author_123").set( From 3ab8ef214a0e85c0219112cc430881b85f277900 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Fri, 15 May 2026 15:37:43 -0700 Subject: [PATCH 13/18] code review feedback --- snippets/firestore/firestore_pipelines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snippets/firestore/firestore_pipelines.py b/snippets/firestore/firestore_pipelines.py index b4a532ff..338553a9 100644 --- a/snippets/firestore/firestore_pipelines.py +++ b/snippets/firestore/firestore_pipelines.py @@ -2896,7 +2896,7 @@ def search_basic(): def search_exact_match_data(): # [START search_exact_match_data] client.collection("Restaurants").add( - {"name": "Waffle Place", "description": "A cozy place for fresh waffles."} + {"name": "Waffle Place", "description": "A cozy place for fresh belgian waffles."} ) # [END search_exact_match_data] From 8ca4f37732d72b553637555bf56a67a51cbc6f8a Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:39:16 -0400 Subject: [PATCH 14/18] =?UTF-8?q?feat(fcm):=C2=A0Enable=20`fid`=20and=20de?= =?UTF-8?q?precate=20`token`=20for=20Send=20API=20(#951)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add fid arg and token deprecate warning to Message class and add unit tests * add fids to MulticastMessage and token deprecate warning and add unit tests * Fix lint formatting error * Position `tokens` first to guarantee backward compatibility for legacy positional arguments and add a unit test for it * Extract multicast-to-message-list conversion logic into a private helper function * Add docstring for the helper function * Add unit tests for the async function, change deprecate message and address other review comments * Fix error messages and MulticastMessage constructor and add mix types of fids and tokens * Add integration tests for fid as argument * Update integration tests error code for invalid fid target --- firebase_admin/_messaging_encoder.py | 57 +++++++--- firebase_admin/_messaging_utils.py | 3 +- firebase_admin/messaging.py | 63 ++++++----- integration/test_messaging.py | 23 ++++ tests/test_messaging.py | 155 ++++++++++++++++++++++++++- 5 files changed, 258 insertions(+), 43 deletions(-) diff --git a/firebase_admin/_messaging_encoder.py b/firebase_admin/_messaging_encoder.py index 4c0c6daa..b7c69107 100644 --- a/firebase_admin/_messaging_encoder.py +++ b/firebase_admin/_messaging_encoder.py @@ -19,6 +19,7 @@ import math import numbers import re +import warnings from firebase_admin import _messaging_utils @@ -27,7 +28,7 @@ class Message: """A message that can be sent via Firebase Cloud Messaging. Contains payload information as well as recipient information. In particular, the message must - contain exactly one of token, topic or condition fields. + contain exactly one of fid, token, topic or condition fields. Args: data: A dictionary of data fields (optional). All keys and values in the dictionary must be @@ -37,20 +38,29 @@ class Message: webpush: An instance of ``messaging.WebpushConfig`` (optional). apns: An instance of ``messaging.ApnsConfig`` (optional). fcm_options: An instance of ``messaging.FCMOptions`` (optional). - token: The registration token of the device to which the message should be sent (optional). + fid: The Firebase installation ID of an FCM registered app instance to which the + message should be sent (optional). + token: Deprecated. Use ``fid`` instead. topic: Name of the FCM topic to which the message should be sent (optional). Topic name may contain the ``/topics/`` prefix. condition: The FCM condition to which the message should be sent (optional). """ def __init__(self, data=None, notification=None, android=None, webpush=None, apns=None, - fcm_options=None, token=None, topic=None, condition=None): + fcm_options=None, token=None, topic=None, condition=None, fid=None): + if token is not None: + warnings.warn( + "Message.token is deprecated. Use Message.fid instead.", + DeprecationWarning, + stacklevel=2 + ) self.data = data self.notification = notification self.android = android self.webpush = webpush self.apns = apns self.fcm_options = fcm_options + self.fid = fid self.token = token self.topic = topic self.condition = condition @@ -60,10 +70,10 @@ def __str__(self): class MulticastMessage: - """A message that can be sent to multiple tokens via Firebase Cloud Messaging. + """A message that can be sent to multiple tokens or fids via Firebase Cloud Messaging. Args: - tokens: A list of registration tokens of targeted devices. + tokens: Deprecated. Use ``fids`` instead (optional). data: A dictionary of data fields (optional). All keys and values in the dictionary must be strings. notification: An instance of ``messaging.Notification`` (optional). @@ -71,13 +81,35 @@ class MulticastMessage: webpush: An instance of ``messaging.WebpushConfig`` (optional). apns: An instance of ``messaging.ApnsConfig`` (optional). fcm_options: An instance of ``messaging.FCMOptions`` (optional). + fids: A list of Firebase Installation IDs of targeted app instances (optional). """ - def __init__(self, tokens, data=None, notification=None, android=None, webpush=None, apns=None, - fcm_options=None): - _Validators.check_string_list('MulticastMessage.tokens', tokens) - if len(tokens) > 500: - raise ValueError('MulticastMessage.tokens must not contain more than 500 tokens.') + def __init__( + self, tokens=None, data=None, notification=None, android=None, + webpush=None, apns=None, fcm_options=None, fids=None): + if tokens is not None: + warnings.warn( + "MulticastMessage.tokens is deprecated. Use MulticastMessage.fids instead.", + DeprecationWarning, + stacklevel=2 + ) + + if tokens is None and fids is None: + raise ValueError( + "Must specify at least one of MulticastMessage.tokens or MulticastMessage.fids.") + + if tokens is not None: + _Validators.check_string_list('MulticastMessage.tokens', tokens) + if fids is not None: + _Validators.check_string_list('MulticastMessage.fids', fids) + + tokens_len = len(tokens) if tokens is not None else 0 + fids_len = len(fids) if fids is not None else 0 + if tokens_len + fids_len > 500: + raise ValueError( + 'Total number of tokens and fids must not exceed 500.') + self.tokens = tokens + self.fids = fids self.data = data self.notification = notification self.android = android @@ -695,6 +727,7 @@ def default(self, o): # pylint: disable=method-hidden 'Message.condition', o.condition, non_empty=True), 'data': _Validators.check_string_dict('Message.data', o.data), 'notification': MessageEncoder.encode_notification(o.notification), + 'fid': _Validators.check_string('Message.fid', o.fid, non_empty=True), 'token': _Validators.check_string('Message.token', o.token, non_empty=True), 'topic': _Validators.check_string('Message.topic', o.topic, non_empty=True), 'webpush': MessageEncoder.encode_webpush(o.webpush), @@ -702,9 +735,9 @@ def default(self, o): # pylint: disable=method-hidden } result['topic'] = MessageEncoder.sanitize_topic_name(result.get('topic')) result = MessageEncoder.remove_null_values(result) - target_count = sum(t in result for t in ['token', 'topic', 'condition']) + target_count = sum(t in result for t in ['fid', 'token', 'topic', 'condition']) if target_count != 1: - raise ValueError('Exactly one of token, topic or condition must be specified.') + raise ValueError('Exactly one of fid, token, topic or condition must be specified.') return result @classmethod diff --git a/firebase_admin/_messaging_utils.py b/firebase_admin/_messaging_utils.py index 773ed605..877bccdd 100644 --- a/firebase_admin/_messaging_utils.py +++ b/firebase_admin/_messaging_utils.py @@ -519,7 +519,8 @@ def __init__(self, message, cause=None, http_response=None): class UnregisteredError(exceptions.NotFoundError): """App instance was unregistered from FCM. - This usually means that the token used is no longer valid and a new one must be used.""" + This usually means that the registration token or installation ID (FID) used + is no longer valid and a new one must be used.""" def __init__(self, message, cause=None, http_response=None): exceptions.NotFoundError.__init__(self, message, cause, http_response) diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index 74904443..93ecfda1 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -20,6 +20,7 @@ import json import asyncio import logging +import warnings import requests import httpx @@ -172,13 +173,45 @@ async def send_each_async( """ return await _get_messaging_service(app).send_each_async(messages, dry_run) +def _get_messages_from_multicast(multicast_message: MulticastMessage) -> List[Message]: + """Extracts individual Message objects from a MulticastMessage.""" + if not isinstance(multicast_message, MulticastMessage): + raise ValueError('Message must be an instance of messaging.MulticastMessage class.') + + messages = [] + if multicast_message.tokens is not None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + messages.extend([Message( + data=multicast_message.data, + notification=multicast_message.notification, + android=multicast_message.android, + webpush=multicast_message.webpush, + apns=multicast_message.apns, + fcm_options=multicast_message.fcm_options, + token=token + ) for token in multicast_message.tokens]) + + if multicast_message.fids is not None: + messages.extend([Message( + data=multicast_message.data, + notification=multicast_message.notification, + android=multicast_message.android, + webpush=multicast_message.webpush, + apns=multicast_message.apns, + fcm_options=multicast_message.fcm_options, + fid=fid + ) for fid in multicast_message.fids]) + + return messages + async def send_each_for_multicast_async( multicast_message: MulticastMessage, dry_run: bool = False, app: Optional[App] = None ) -> BatchResponse: - """Sends the given mutlicast message to each token asynchronously via Firebase Cloud Messaging - (FCM). + """Sends the given multicast message to each token or fid asynchronously via + Firebase Cloud Messaging (FCM). If the ``dry_run`` mode is enabled, the message will not be actually delivered to the recipients. Instead, FCM performs all the usual validations and emulates the send operation. @@ -195,21 +228,11 @@ async def send_each_for_multicast_async( FirebaseError: If an error occurs while sending the message to the FCM service. ValueError: If the input arguments are invalid. """ - if not isinstance(multicast_message, MulticastMessage): - raise ValueError('Message must be an instance of messaging.MulticastMessage class.') - messages = [Message( - data=multicast_message.data, - notification=multicast_message.notification, - android=multicast_message.android, - webpush=multicast_message.webpush, - apns=multicast_message.apns, - fcm_options=multicast_message.fcm_options, - token=token - ) for token in multicast_message.tokens] + messages = _get_messages_from_multicast(multicast_message) return await _get_messaging_service(app).send_each_async(messages, dry_run) def send_each_for_multicast(multicast_message, dry_run=False, app=None): - """Sends the given mutlicast message to each token via Firebase Cloud Messaging (FCM). + """Sends the given multicast message to each token or fid via Firebase Cloud Messaging (FCM). If the ``dry_run`` mode is enabled, the message will not be actually delivered to the recipients. Instead, FCM performs all the usual validations and emulates the send operation. @@ -226,17 +249,7 @@ def send_each_for_multicast(multicast_message, dry_run=False, app=None): FirebaseError: If an error occurs while sending the message to the FCM service. ValueError: If the input arguments are invalid. """ - if not isinstance(multicast_message, MulticastMessage): - raise ValueError('Message must be an instance of messaging.MulticastMessage class.') - messages = [Message( - data=multicast_message.data, - notification=multicast_message.notification, - android=multicast_message.android, - webpush=multicast_message.webpush, - apns=multicast_message.apns, - fcm_options=multicast_message.fcm_options, - token=token - ) for token in multicast_message.tokens] + messages = _get_messages_from_multicast(multicast_message) return _get_messaging_service(app).send_each(messages, dry_run) def subscribe_to_topic(tokens, topic, app=None): diff --git a/integration/test_messaging.py b/integration/test_messaging.py index e7208674..e568e8a7 100644 --- a/integration/test_messaging.py +++ b/integration/test_messaging.py @@ -87,6 +87,14 @@ def test_send_malformed_token(): with pytest.raises(exceptions.InvalidArgumentError): messaging.send(msg, dry_run=True) +def test_send_invalid_fid(): + msg = messaging.Message( + fid='not-a-fid', + notification=messaging.Notification('test-title', 'test-body') + ) + with pytest.raises(messaging.UnregisteredError): + messaging.send(msg, dry_run=True) + def test_send_each(): messages = [ messaging.Message( @@ -149,6 +157,21 @@ def test_send_each_for_multicast(): assert response.exception is not None assert response.message_id is None +def test_send_each_for_multicast_fids(): + multicast = messaging.MulticastMessage( + notification=messaging.Notification('Title', 'Body'), + fids=['not-a-fid', 'also-not-a-fid']) + + batch_response = messaging.send_each_for_multicast(multicast) + + assert batch_response.success_count == 0 + assert batch_response.failure_count == 2 + assert len(batch_response.responses) == 2 + for response in batch_response.responses: + assert response.success is False + assert isinstance(response.exception, messaging.UnregisteredError) + assert response.message_id is None + def test_subscribe(): resp = messaging.subscribe_to_topic(_REGISTRATION_TOKEN, 'mock-topic') assert resp.success_count + resp.failure_count == 1 diff --git a/tests/test_messaging.py b/tests/test_messaging.py index b30790f1..749e5311 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -73,18 +73,22 @@ class TestMessageStr: messaging.Message(topic='topic', condition='condition'), messaging.Message(condition='condition', token='token'), messaging.Message(topic='topic', token='token', condition='condition'), + messaging.Message(fid='fid', token='token'), + messaging.Message(fid='fid', topic='topic'), + messaging.Message(fid='fid', condition='condition'), + messaging.Message(fid='fid', token='token', topic='topic'), ]) def test_invalid_target_message(self, msg): with pytest.raises(ValueError) as excinfo: str(msg) assert str( - excinfo.value) == 'Exactly one of token, topic or condition must be specified.' + excinfo.value) == 'Exactly one of fid, token, topic or condition must be specified.' def test_empty_message(self): assert str(messaging.Message(token='value')) == '{"token": "value"}' assert str(messaging.Message(topic='value')) == '{"topic": "value"}' - assert str(messaging.Message(condition='value') - ) == '{"condition": "value"}' + assert str(messaging.Message(condition='value')) == '{"condition": "value"}' + assert str(messaging.Message(fid='value')) == '{"fid": "value"}' def test_data_message(self): assert str(messaging.Message(topic='topic', data={}) @@ -95,6 +99,12 @@ def test_data_message(self): class TestMulticastMessage: + def test_invalid_targets(self): + with pytest.raises(ValueError) as excinfo: + messaging.MulticastMessage() + expected = "Must specify at least one of MulticastMessage.tokens or MulticastMessage.fids." + assert str(excinfo.value) == expected + @pytest.mark.parametrize('tokens', NON_LIST_ARGS) def test_invalid_tokens_type(self, tokens): with pytest.raises(ValueError) as excinfo: @@ -109,7 +119,7 @@ def test_invalid_tokens_type(self, tokens): def test_tokens_over_500(self): with pytest.raises(ValueError) as excinfo: messaging.MulticastMessage(tokens=['token' for _ in range(0, 501)]) - expected = 'MulticastMessage.tokens must not contain more than 500 tokens.' + expected = 'Total number of tokens and fids must not exceed 500.' assert str(excinfo.value) == expected def test_tokens_type(self): @@ -119,6 +129,54 @@ def test_tokens_type(self): message = messaging.MulticastMessage(tokens=['token' for _ in range(0, 500)]) assert len(message.tokens) == 500 + @pytest.mark.parametrize('fids', NON_LIST_ARGS) + def test_invalid_fids_type(self, fids): + with pytest.raises(ValueError) as excinfo: + messaging.MulticastMessage(fids=fids) + if isinstance(fids, list): + expected = 'MulticastMessage.fids must not contain non-string values.' + assert str(excinfo.value) == expected + else: + expected = 'MulticastMessage.fids must be a list of strings.' + assert str(excinfo.value) == expected + + def test_fids_over_500(self): + with pytest.raises(ValueError) as excinfo: + messaging.MulticastMessage(fids=['fid' for _ in range(0, 501)]) + expected = 'Total number of tokens and fids must not exceed 500.' + assert str(excinfo.value) == expected + + def test_fids_type(self): + message = messaging.MulticastMessage(fids=['fid']) + assert len(message.fids) == 1 + + message = messaging.MulticastMessage(fids=['fid' for _ in range(0, 500)]) + assert len(message.fids) == 500 + + def test_combined_over_500(self): + with pytest.raises(ValueError) as excinfo: + messaging.MulticastMessage( + tokens=['token' for _ in range(0, 250)], + fids=['fid' for _ in range(0, 251)] + ) + expected = 'Total number of tokens and fids must not exceed 500.' + assert str(excinfo.value) == expected + + def test_mixed_targets(self): + message = messaging.MulticastMessage(tokens=['token'], fids=['fid']) + assert len(message.tokens) == 1 + assert len(message.fids) == 1 + + def test_tokens_deprecation_warning(self): + msg = 'MulticastMessage.tokens is deprecated. Use MulticastMessage.fids instead.' + with pytest.warns(DeprecationWarning, match=msg): + messaging.MulticastMessage(tokens=['token']) + + def test_tokens_deprecation_warning_positional(self): + msg = 'MulticastMessage.tokens is deprecated. Use MulticastMessage.fids instead.' + with pytest.warns(DeprecationWarning, match=msg): + messaging.MulticastMessage(['token']) + class TestMessageEncoder: @@ -128,11 +186,16 @@ class TestMessageEncoder: messaging.Message(topic='topic', condition='condition'), messaging.Message(condition='condition', token='token'), messaging.Message(topic='topic', token='token', condition='condition'), + messaging.Message(fid='fid', token='token'), + messaging.Message(fid='fid', topic='topic'), + messaging.Message(fid='fid', condition='condition'), + messaging.Message(fid='fid', token='token', topic='topic'), ]) def test_invalid_target_message(self, msg): with pytest.raises(ValueError) as excinfo: check_encoding(msg) - assert str(excinfo.value) == 'Exactly one of token, topic or condition must be specified.' + expected = 'Exactly one of fid, token, topic or condition must be specified.' + assert str(excinfo.value) == expected @pytest.mark.parametrize('target', NON_STRING_ARGS + ['']) def test_invalid_token(self, target): @@ -140,6 +203,12 @@ def test_invalid_token(self, target): check_encoding(messaging.Message(token=target)) assert str(excinfo.value) == 'Message.token must be a non-empty string.' + @pytest.mark.parametrize('target', NON_STRING_ARGS + ['']) + def test_invalid_fid(self, target): + with pytest.raises(ValueError) as excinfo: + check_encoding(messaging.Message(fid=target)) + assert str(excinfo.value) == 'Message.fid must be a non-empty string.' + @pytest.mark.parametrize('target', NON_STRING_ARGS + ['']) def test_invalid_topic(self, target): with pytest.raises(ValueError) as excinfo: @@ -159,9 +228,15 @@ def test_malformed_topic_name(self, topic): def test_empty_message(self): check_encoding(messaging.Message(token='value'), {'token': 'value'}) + check_encoding(messaging.Message(fid='value'), {'fid': 'value'}) check_encoding(messaging.Message(topic='value'), {'topic': 'value'}) check_encoding(messaging.Message(condition='value'), {'condition': 'value'}) + def test_token_deprecation_warning(self): + msg = 'Message.token is deprecated. Use Message.fid instead.' + with pytest.warns(DeprecationWarning, match=msg): + messaging.Message(token='value') + @pytest.mark.parametrize('data', NON_DICT_ARGS) def test_invalid_data_message(self, data): with pytest.raises(ValueError): @@ -2212,6 +2287,76 @@ def test_send_each_for_multicast(self): assert all(r.success for r in batch_response.responses) assert not any(r.exception for r in batch_response.responses) + def test_send_each_for_multicast_fids(self): + payload1 = json.dumps({'name': 'message-id1'}) + payload2 = json.dumps({'name': 'message-id2'}) + _ = self._instrument_messaging_service( + response_dict={'foo1': [200, payload1], 'foo2': [200, payload2]}) + msg = messaging.MulticastMessage(fids=['foo1', 'foo2']) + batch_response = messaging.send_each_for_multicast(msg, dry_run=True) + assert batch_response.success_count == 2 + assert batch_response.failure_count == 0 + assert len(batch_response.responses) == 2 + assert [r.message_id for r in batch_response.responses] == ['message-id1', 'message-id2'] + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) + + def test_send_each_for_multicast_mixed(self): + payload1 = json.dumps({'name': 'message-id1'}) + payload2 = json.dumps({'name': 'message-id2'}) + _ = self._instrument_messaging_service( + response_dict={'foo1': [200, payload1], 'foo2': [200, payload2]}) + msg = messaging.MulticastMessage(tokens=['foo1'], fids=['foo2']) + batch_response = messaging.send_each_for_multicast(msg, dry_run=True) + assert batch_response.success_count == 2 + assert batch_response.failure_count == 0 + assert len(batch_response.responses) == 2 + assert [r.message_id for r in batch_response.responses] == ['message-id1', 'message-id2'] + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) + + @respx.mock + @pytest.mark.asyncio + async def test_send_each_for_multicast_async(self): + responses = [ + respx.MockResponse(200, http_version='HTTP/2', json={'name': 'message-id1'}), + respx.MockResponse(200, http_version='HTTP/2', json={'name': 'message-id2'}), + ] + msg = messaging.MulticastMessage(tokens=['foo1', 'foo2']) + route = respx.request( + method='POST', + url='https://fcm.googleapis.com/v1/projects/explicit-project-id/messages:send' + ) + route.side_effect = responses + batch_response = await messaging.send_each_for_multicast_async(msg, dry_run=True) + assert batch_response.success_count == 2 + assert batch_response.failure_count == 0 + assert len(batch_response.responses) == 2 + assert [r.message_id for r in batch_response.responses] == ['message-id1', 'message-id2'] + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) + + @respx.mock + @pytest.mark.asyncio + async def test_send_each_for_multicast_async_fids(self): + responses = [ + respx.MockResponse(200, http_version='HTTP/2', json={'name': 'message-id1'}), + respx.MockResponse(200, http_version='HTTP/2', json={'name': 'message-id2'}), + ] + msg = messaging.MulticastMessage(fids=['foo1', 'foo2']) + route = respx.request( + method='POST', + url='https://fcm.googleapis.com/v1/projects/explicit-project-id/messages:send' + ) + route.side_effect = responses + batch_response = await messaging.send_each_for_multicast_async(msg, dry_run=True) + assert batch_response.success_count == 2 + assert batch_response.failure_count == 0 + assert len(batch_response.responses) == 2 + assert [r.message_id for r in batch_response.responses] == ['message-id1', 'message-id2'] + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) + @pytest.mark.parametrize('status', HTTP_ERROR_CODES) def test_send_each_for_multicast_detailed_error(self, status): success_payload = json.dumps({'name': 'message-id'}) From a3f6df8d9c27fe1ad767447c8827ee2c3f2b4895 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Mon, 29 Jun 2026 11:46:25 -0400 Subject: [PATCH 15/18] fix(deps): Added universe_domain override to MockGoogleComputeEngineCredential (#954) Added a universe_domain property override to MockGoogleComputeEngineCredential in tests/testutils.py to prevent unexpected network requests to the GCE metadata server during test runs with newer versions of google-auth. --- tests/testutils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/testutils.py b/tests/testutils.py index 7546595a..0827f698 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -159,6 +159,10 @@ def __init__(self): self._service_account_email = None self._token_state = credentials.TokenState.INVALID + @property + def universe_domain(self): + return 'googleapis.com' + def refresh(self, request): self.token = 'mock-compute-engine-token' self._service_account_email = 'mock-gce-email' From ef49e31d848402a11a1794e3d73b2cfbd819c01a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:52:43 -0400 Subject: [PATCH 16/18] chore(deps): update pyjwt requirement from >=2.10.1 to >=2.12.1 (#948) Updates the requirements on [pyjwt](https://github.com/jpadilla/pyjwt) to permit the latest version. - [Release notes](https://github.com/jpadilla/pyjwt/releases) - [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst) - [Commits](https://github.com/jpadilla/pyjwt/compare/2.10.1...2.12.1) --- updated-dependencies: - dependency-name: pyjwt dependency-version: 2.12.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c97c3676..744b2ca7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,6 @@ cachecontrol >= 0.14.3 google-api-core[grpc] >= 2.25.1, < 3.0.0dev; platform.python_implementation != 'PyPy' google-cloud-firestore >= 2.21.0; platform.python_implementation != 'PyPy' google-cloud-storage >= 3.1.1 -pyjwt[crypto] >= 2.10.1 +pyjwt[crypto] >= 2.12.1 cryptography < 44.0.0; platform.python_implementation == 'PyPy' and python_version < '3.11' httpx[http2] == 0.28.1 \ No newline at end of file From f3e8ed891c35c40dcf298a708c6dca560493dbeb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:59:39 -0400 Subject: [PATCH 17/18] chore(deps): update google-cloud-firestore requirement (#947) Updates the requirements on [google-cloud-firestore](https://github.com/googleapis/google-cloud-python) to permit the latest version. - [Release notes](https://github.com/googleapis/google-cloud-python/releases) - [Changelog](https://github.com/googleapis/google-cloud-python/blob/main/packages/google-cloud-documentai/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-cloud-python/compare/google-cloud-firestore-v2.23.0...google-cloud-firestore-v2.27.0) --- updated-dependencies: - dependency-name: google-cloud-firestore dependency-version: 2.27.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 744b2ca7..7066ac31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ respx == 0.23.1 cachecontrol >= 0.14.3 google-api-core[grpc] >= 2.25.1, < 3.0.0dev; platform.python_implementation != 'PyPy' -google-cloud-firestore >= 2.21.0; platform.python_implementation != 'PyPy' +google-cloud-firestore >= 2.27.0; platform.python_implementation != 'PyPy' google-cloud-storage >= 3.1.1 pyjwt[crypto] >= 2.12.1 cryptography < 44.0.0; platform.python_implementation == 'PyPy' and python_version < '3.11' From 486597b21960e6ec038e2946908ffd73573b644a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:12:38 -0400 Subject: [PATCH 18/18] chore(deps): update pytest requirement from >=8.2.2 to >=8.4.2 (#946) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.2...8.4.2) --- updated-dependencies: - dependency-name: pytest dependency-version: 8.4.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7066ac31..3f08f38c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ astroid == 3.3.11 pylint == 3.3.9 -pytest >= 8.2.2 +pytest >= 8.4.2 pytest-cov >= 2.4.0 pytest-localserver >= 0.4.1 pytest-asyncio >= 0.26.0