firestore: support retrieval of missing documents

DO NOT SUBMIT before the Firestore team approves the surface.

Add two methods:

- CollectionRef.DocumentRefs
- Transaction.DocumentRefs

These return an iterator over all document references in a collection
including those for "missing documents," which have been deleted but
have documents in subcollections.

Fixes #1080.

Change-Id: I804248fcde7d6f8a7401ec80ab471f637f822232
Reviewed-on: https://code-review.googlesource.com/c/32970
Reviewed-by: Jean de Klerk <deklerk@google.com>
diff --git a/firestore/collref.go b/firestore/collref.go
index 0ccea3b..c2e2f5c 100644
--- a/firestore/collref.go
+++ b/firestore/collref.go
@@ -96,6 +96,16 @@
 	return d, wr, nil
 }
 
+// DocumentRefs returns references to all the documents in the collection, including
+// missing documents. A missing document is a document that does not exist but has
+// sub-documents.
+func (c *CollectionRef) DocumentRefs(ctx context.Context) *DocumentRefIterator {
+	if err := checkTransaction(ctx); err != nil {
+		return &DocumentRefIterator{err: err}
+	}
+	return newDocumentRefIterator(ctx, c, nil)
+}
+
 const alphanum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
 
 var (
diff --git a/firestore/integration_test.go b/firestore/integration_test.go
index 5f418c3a..00432b8 100644
--- a/firestore/integration_test.go
+++ b/firestore/integration_test.go
@@ -1230,6 +1230,38 @@
 	codeEq(t, "after cancel", codes.Canceled, err)
 }
 
+func TestIntegration_MissingDocs(t *testing.T) {
+	ctx := context.Background()
+	h := testHelper{t}
+	client := integrationClient(t)
+	coll := client.Collection(collectionIDs.New())
+	dr1 := coll.NewDoc()
+	dr2 := coll.NewDoc()
+	dr3 := dr2.Collection("sub").NewDoc()
+	h.mustCreate(dr1, integrationTestMap)
+	defer h.mustDelete(dr1)
+	h.mustCreate(dr3, integrationTestMap)
+	defer h.mustDelete(dr3)
+
+	// dr1 is a document in coll. dr2 was never created, but there are documents in
+	// its sub-collections. It is "missing".
+	// The Collection.DocumentRefs method includes missing document refs.
+	want := []string{dr1.Path, dr2.Path}
+	drs, err := coll.DocumentRefs(ctx).GetAll()
+	if err != nil {
+		t.Fatal(err)
+	}
+	var got []string
+	for _, dr := range drs {
+		got = append(got, dr.Path)
+	}
+	sort.Strings(want)
+	sort.Strings(got)
+	if !testutil.Equal(got, want) {
+		t.Errorf("got %v, want %v", got, want)
+	}
+}
+
 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/list_documents.go b/firestore/list_documents.go
new file mode 100644
index 0000000..5ba70fd
--- /dev/null
+++ b/firestore/list_documents.go
@@ -0,0 +1,103 @@
+// Copyright 2018 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
+
+import (
+	vkit "cloud.google.com/go/firestore/apiv1beta1"
+	"golang.org/x/net/context"
+	"google.golang.org/api/iterator"
+	pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
+)
+
+// DocumentRefIterator is an interator over DocumentRefs.
+type DocumentRefIterator struct {
+	client   *Client
+	it       *vkit.DocumentIterator
+	pageInfo *iterator.PageInfo
+	nextFunc func() error
+	items    []*DocumentRef
+	err      error
+}
+
+func newDocumentRefIterator(ctx context.Context, cr *CollectionRef, tid []byte) *DocumentRefIterator {
+	client := cr.c
+	req := &pb.ListDocumentsRequest{
+		Parent:       cr.parentPath,
+		CollectionId: cr.ID,
+		ShowMissing:  true,
+		Mask:         &pb.DocumentMask{}, // empty mask: we want only the ref
+	}
+	if tid != nil {
+		req.ConsistencySelector = &pb.ListDocumentsRequest_Transaction{tid}
+	}
+	it := &DocumentRefIterator{
+		client: client,
+		it:     client.c.ListDocuments(withResourceHeader(ctx, client.path()), req),
+	}
+	it.pageInfo, it.nextFunc = iterator.NewPageInfo(
+		it.fetch,
+		func() int { return len(it.items) },
+		func() interface{} { b := it.items; it.items = nil; return b })
+	return it
+}
+
+// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
+func (it *DocumentRefIterator) PageInfo() *iterator.PageInfo { return it.pageInfo }
+
+// Next returns the next result. Its second return value is iterator.Done if there
+// are no more results. Once Next returns Done, all subsequent calls will return
+// Done.
+func (it *DocumentRefIterator) Next() (*DocumentRef, error) {
+	if err := it.nextFunc(); err != nil {
+		return nil, err
+	}
+	item := it.items[0]
+	it.items = it.items[1:]
+	return item, nil
+}
+
+func (it *DocumentRefIterator) fetch(pageSize int, pageToken string) (string, error) {
+	if it.err != nil {
+		return "", it.err
+	}
+	return iterFetch(pageSize, pageToken, it.it.PageInfo(), func() error {
+		docProto, err := it.it.Next()
+		if err != nil {
+			return err
+		}
+		docRef, err := pathToDoc(docProto.Name, it.client)
+		if err != nil {
+			return err
+		}
+		it.items = append(it.items, docRef)
+		return nil
+	})
+}
+
+// GetAll returns all the DocumentRefs remaining from the iterator.
+func (it *DocumentRefIterator) GetAll() ([]*DocumentRef, error) {
+	var drs []*DocumentRef
+	for {
+		dr, err := it.Next()
+		if err == iterator.Done {
+			break
+		}
+		if err != nil {
+			return nil, err
+		}
+		drs = append(drs, dr)
+	}
+	return drs, nil
+}
diff --git a/firestore/transaction.go b/firestore/transaction.go
index efaf106..e273799 100644
--- a/firestore/transaction.go
+++ b/firestore/transaction.go
@@ -235,6 +235,17 @@
 	}
 }
 
+// DocumentRefs returns references to all the documents in the collection, including
+// missing documents. A missing document is a document that does not exist but has
+// sub-documents.
+func (t *Transaction) DocumentRefs(cr *CollectionRef) *DocumentRefIterator {
+	if len(t.writes) > 0 {
+		t.readAfterWrite = true
+		return &DocumentRefIterator{err: errReadAfterWrite}
+	}
+	return newDocumentRefIterator(t.ctx, cr, t.id)
+}
+
 // Create adds a Create operation to the Transaction.
 // See DocumentRef.Create for details.
 func (t *Transaction) Create(dr *DocumentRef, data interface{}) error {