firestore: add CollectionGroup method Change-Id: I9bae0101f96a7637e0d98bed9f3ec9212ed93cdf Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/39150 Reviewed-by: kokoro <noreply+kokoro@google.com> Reviewed-by: Tyler Bui-Palsulich <tbp@google.com>
diff --git a/firestore/client.go b/firestore/client.go index a3e1243..8a03126 100644 --- a/firestore/client.go +++ b/firestore/client.go
@@ -133,6 +133,19 @@ return doc } +// CollectionGroup creates a reference to a group of collections that include +// the given ID, regardless of parent document. +// +// For example, consider: +// France/Cities/Paris = {population: 100} +// Canada/Cities/Montreal = {population: 90} +// +// CollectionGroup can be used to query across all "Cities" regardless of +// its parent "Countries". See ExampleCollectionGroup for a complete example. +func (c *Client) CollectionGroup(collectionID string) *CollectionGroupRef { + return newCollectionGroupRef(c, c.path(), collectionID) +} + func (c *Client) idsToRef(IDs []string, dbPath string) (*CollectionRef, *DocumentRef) { if len(IDs) == 0 { return nil, nil
diff --git a/firestore/collgroupref.go b/firestore/collgroupref.go new file mode 100644 index 0000000..e43a7e6 --- /dev/null +++ b/firestore/collgroupref.go
@@ -0,0 +1,38 @@ +// Copyright 2019 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. + +package firestore + +// A CollectionGroupRef is a reference to a group of collections sharing the +// same ID. +type CollectionGroupRef struct { + c *Client + + // Use the methods of Query on a CollectionGroupRef to create and run queries. + Query +} + +func newCollectionGroupRef(c *Client, dbPath, collectionID string) *CollectionGroupRef { + return &CollectionGroupRef{ + c: c, + + Query: Query{ + c: c, + collectionID: collectionID, + path: dbPath, + parentPath: dbPath + "/documents", + allDescendants: true, + }, + } +}
diff --git a/firestore/examples_test.go b/firestore/examples_test.go index 234cfc9..ee03496 100644 --- a/firestore/examples_test.go +++ b/firestore/examples_test.go
@@ -607,3 +607,29 @@ } fmt.Println(wr.UpdateTime) } + +func ExampleCollectionGroup() { + // Given: + // France/Cities/Paris = {population: 100} + // Canada/Cities/Montreal = {population: 95} + + ctx := context.Background() + client, err := firestore.NewClient(ctx, "project-id") + if err != nil { + // TODO: Handle error. + } + defer client.Close() + + // Query for ANY city with >95 pop, regardless of country. + docs, err := client.CollectionGroup("Cities"). + Where("pop", ">", 95). + OrderBy("pop", firestore.Desc). + Limit(10). + Documents(ctx). + GetAll() + if err != nil { + // TODO: Handle error. + } + + _ = docs // TODO: Use docs. +}
diff --git a/firestore/integration_test.go b/firestore/integration_test.go index 04f2ae8..ad295f8 100644 --- a/firestore/integration_test.go +++ b/firestore/integration_test.go
@@ -1262,6 +1262,44 @@ } } +func TestIntegration_CollectionGroupQueries(t *testing.T) { + shouldBeFoundID := collectionIDs.New() + shouldNotBeFoundID := collectionIDs.New() + + ctx := context.Background() + h := testHelper{t} + client := integrationClient(t) + cr1 := client.Collection(shouldBeFoundID) + dr1 := cr1.Doc("should-be-found-1") + h.mustCreate(dr1, map[string]string{"some-key": "should-be-found"}) + defer h.mustDelete(dr1) + + dr1.Collection(shouldBeFoundID) + dr2 := cr1.Doc("should-be-found-2") + h.mustCreate(dr2, map[string]string{"some-key": "should-be-found"}) + defer h.mustDelete(dr2) + + cr3 := client.Collection(shouldNotBeFoundID) + dr3 := cr3.Doc("should-not-be-found") + h.mustCreate(dr3, map[string]string{"some-key": "should-NOT-be-found"}) + defer h.mustDelete(dr3) + + cg := client.CollectionGroup(shouldBeFoundID) + snaps, err := cg.Documents(ctx).GetAll() + if err != nil { + t.Fatal(err) + } + if len(snaps) != 2 { + t.Fatalf("expected 2 snapshots but got %d", len(snaps)) + } + if snaps[0].Ref.ID != "should-be-found-1" { + t.Fatalf("expected ID 'should-be-found-1', got %s", snaps[0].Ref.ID) + } + if snaps[1].Ref.ID != "should-be-found-2" { + t.Fatalf("expected ID 'should-be-found-2', got %s", snaps[1].Ref.ID) + } +} + func codeEq(t *testing.T, msg string, code codes.Code, err error) { if grpc.Code(err) != code { t.Fatalf("%s:\ngot <%v>\nwant code %s", msg, err, code)
diff --git a/firestore/query.go b/firestore/query.go index 16e258c..9aa01de 100644 --- a/firestore/query.go +++ b/firestore/query.go
@@ -47,6 +47,10 @@ startDoc, endDoc *DocumentSnapshot startBefore, endBefore bool err error + + // allDescendants indicates whether this query is for all collections + // that match the ID under the specified parentPath. + allDescendants bool } // DocumentID is the special field name representing the ID of a document @@ -120,8 +124,8 @@ ) // OrderBy returns a new Query that specifies the order in which results are -// returned. A Query can have multiple OrderBy/OrderByPath specifications. OrderBy -// appends the specification to the list of existing ones. +// returned. A Query can have multiple OrderBy/OrderByPath specifications. +// OrderBy appends the specification to the list of existing ones. // // The path argument can be a single field or a dot-separated sequence of // fields, and must not contain any of the runes "˜*/[]". @@ -251,7 +255,10 @@ } } p := &pb.StructuredQuery{ - From: []*pb.StructuredQuery_CollectionSelector{{CollectionId: q.collectionID}}, + From: []*pb.StructuredQuery_CollectionSelector{{ + CollectionId: q.collectionID, + AllDescendants: q.allDescendants, + }}, Offset: q.offset, Limit: q.limit, }